이 글은 앞서 포스팅한 아래의 글과 이어지는 글 입니다.
https://codenme.tistory.com/58
[OAuth]
서비스 제공 업체들이 각자 다른 방식으로 로그인하지 않도록 제공하는 공통의 인증 방식.
기존에 사용자와 관리자, 2가지의 ROLE로 유저가 구분되었지만, OAuth를 사용하면, 구글, 네이버 등의 소셜 로그인 서비스를 제공하는 제 3의 인물을 포함해야한다. 나는 Google의 소셜 로그인 서비스를 사용할 것이기에, 구글로 이 제 3자를 칭하겠다.
Security
문제점: 소셜 로그인 처리를 통해 로그인시, ClubAuthMember 객체를 이용하지 않기에, 소셜 로그인한 유저의 정보를 알 수 없다.
따라서 소셜 로그인시, 다음과 같은 요구사항을 만족하게 설계해보자.
1) 소셜 로그인 시, 사용자의 이메일 정보 추출 ( 비밀번호 등은 따로 빼서 저장하는 것에 고민의 여지가 있다)
2) 데이터베이스와 소셜 로그인 정보를 연동하여 사용자 정보를 관리하게한다.
3) 기존 방식, 소셜 로그인 두가지 방식이 올바르게 동작되어야한다.
OAuth에서는, 기존 스프링 시큐리티에서의 UserDetailsService 인터페이스와 유사한 기능을 가진
OAuth2UserService 인터페이스가 있다.
이 인터페이스에는 여러가지 구현 클래스가 이미 구현되어 있기에,
이 중 하나를 상속받아서 구현하면 더욱 편리하게 구현이 가능하다.
아래는 구현 클래스 중 하나인, DefaultOAuth2UserService 클래스를 상속받아 작성한 클래스이다.
우선, 앞서 UserDetailsService를 상속받아 구현한 클래스와 마찬가지로, @Service 어노테이션을 붙여 스프링 빈에 자동 등록되게하면, 해당 클래스를 OAuth2UserService로 인식하고 사용하게 된다.
loadUser
DefaultOAuth2UserService 클래스에는 UserDetailsService의 LoadUserByName() 메소드와 유사하게
유저의 리퀘스트를 전달받으면 OAuth2User타입으로 리턴하는 리턴하는 loadUser() 메소드가 있기에, Override하여 사용하는 것으로 인증 작업을 진행할 수 있다.
이 OAuth2User 타입의 경우, 위와 같이 .getClientRegistration() 과 같은메소드들을 통해 필요한 정보를 추출해서 사용
가능하다.
결과 로그창
이제 OAuth2User 타입을 통해 회원정보를 추출할 수 있게 되었으니, 데이터베이스에 이를 저장하는 영역을 구현해보자.
아래는 loadUser() 메소드에 추가된 코드이다.
email 정보를 추출하고, saveSocialMember() 메소드에 넘겨준다.
아래는 받은 email 정보를 기반으로, 데이터베이스에 해당 유저가 존재하는지 찾고, 없다면 데이터베이스에 추가한 뒤 리턴해주는 saveSocialMember() 메소드의 구현이다.
이제 다시 Google을 통해 로그인을 하면, 아래와 같이 데이터베이스 내에 소셜 로그인 사용자의 정보가 들어가는 것을 확인할 수 있다.
이제 저장한 로그인 정보를 전에 했던 것처럼 View, Controller에 처리해보자.
우선 View, 그리고 Controller는 기존의 일반 로그인( 소셜 로그인이 아닌, 사이트 회원) 사용자들의 정보를
ClubAuthMemberDTO라는 인가, 인증에 더불어 DTO의 역할까지 수행하는 DTO 객체 타입을 전달받아 사용하기에
loadUser() 메소드가 반환하는 OAuth2User 타입을 ClubAuthMemberDTO 타입으로 변환해줄 필요가 있다.
( DefaultOAuth2UserService의 loadUser() 에선 OAuth2User 객체에 유저정보를 반환해 리턴하고
UserDetailService 인터페이스의 loadUserByUsername( name ) 에서는 UserDetail( User 객체의 구현체 ) 에 유저 정보를 담아 리턴한다.
여기서 재밌는 점이 하나 나온다.
ClubAuthMemberDTO는 현재 아래와 같이 "User" 클래스를 상속받고 있다.
그렇기에, 보통의 DTO가 Entity로 변환될 때, dtoToEntity() 같은 변환 메소드를 사용하는 것과 다르게, 변환해줄 필요가 없다.
아래 ClubMemberDetailService의 loadUserByUsername 메소드를 다시 한번 보자.
리턴 타입은 분명 UserDetails 라는 회원 정보를 담은 객체이다.
하지만, 우리는 구현할 때, 별도의 변환 과정없이, 아래와 같이 ClubAuthMemberDTO 객체를 리턴했다.
왜 이렇게 처리가 가능한 것 일까? 그 이유는 ClubAuthMemberDTO 클래스가 상속하는 User 클래스를 보면 알 수 있다.
User를 보면, UserDetails 인터페이스를 상속하는 것을 알 수 있다.
그렇기에, ClubAuthMemberDTO -> User -> UserDetails 이렇게 타입 캐스트가 되서 올바르게 정보가 리턴 될 수 있는 것이다.
하지만 우리는 이제 OAuth2User 객체를 ClubAuthMemberDTO로 치환해야 되고, 위에서 나온 과정처럼 마법같은 타입캐스트가 발생하기 위해선, ClubAuthMemberDTO가 OAuth2User 객체를 상속받아야만 한다.
만약 OAuth2User가 클래스라면, 2가지 클래스를 상속받지 못하는 자바 구조상 번거로운 작업이 수행되어야겠지만,
다행히도 인터페이스기에, 아래와 같이 OAuth2User를 상속하는 것으로 쉽게 타입캐스트가 가능해진다.
그리고 OAuth2User의 정보들 의 데이터를 저장한 attr 라는 변수를 만들고, 기존의 4개의 파라미터 + OAuth2User의
getAttribute()를 통해 얻을 수 있는 부가 정보를 저장하는 1개의 파라미터까지해서 5개의 파라미터를 통한 생성자를 추가로 만들어주자.
OAuth2User.getAttributes() 의 리턴 타입은 Map<String,Object> 이기에 이에 맞는 변수를 생성했다.
이제 모든 선처리 작업을 완료했으니,
loadUser() 메소드가 OAuth2User를 통해 ClubAuthMemberDTO를 만들고 반환하게 만들자.
앞서 말했듯이, clubAuthMemberDTO를 반환해주어도 알아서 타입 캐스트가 되기에 문제없다.
여기까지 성공했다면, 이제 소셜 로그인 사용자의 정보도 일반 로그인 사용자 처럼 ClubAuthMemberDTO를 통해 처리할 수 있기에, 두 사용자 모두 해당 DTO를 통해 동일한 기능을 수행할 수 있다.
다시 컨트롤러 쪽으로 가보자.
여기서, 기존에는 OAuth를 통한 소셜 로그인 사용자가 clubAuthMemberDTO에 호환되지 않아 null이 출력되었지만,
이제 아래와 같이 올바른 정보를 출력한다.
View 영역도 마찬가지로, OAuth를 통한 로그인 정보가 올바르게 DTO에 호환되며 제대로 정보가 나타난다.
여기까지 왔다면 일반 로그인, 소셜 로그인한 사용자 모두 View와 Controller 영역에서 동일하게 처리할 수 있게된다.
하지만, 소셜 로그인을 통해 로그인 시, 비밀번호가 1111로 고정되어 데이터베이스에 들어가는 문제점, 사용자의 이름이 이메일로 고정되는 점 등 문제가 있다.
clubMember에는 FromSocial이라는 소셜 사용자를 식별하는 boolean이 있기에, 이러한 정보를 가지고 제 3자가 로그인하는 것은 불가능하기에 보안적으로는 상관없지만, 불편한 사항이기에 최초에 소셜 로그인 시, 비밀번호를 수정할 수 있도록 기능을 추가해보자.
그 전에, 스프링 시큐리티의 로그인 관련 처리를 돕는 2가지의 인터페이스에 대해 알아보자.
1) AuthenticationSuccessHandler
이름 그대로, 인증 성공시의 상황에 대한 핸들러이다.
2) AuthenticationFailHandler
역시 이름 그대로, 인증 실패시의 상황에 대한 핸들러이다.
이를 사용하여 아래처럼 로그만 출력하는 간단한 구현 클래스를 만들 수 있다.
그리고 생성한 핸들러를 사용하도록 SecurityConfig에 아래와 같이 작성해주자
successHandler 메소드
그리고 기존 OAuth 로그인 설정에서 successHandler를 추가한다.
그리고 실행 해보면 아직 화면처리를 SuccessHandler에서 해주지 않았기에 빈 화면이 나오지만
log에 적은 문장이 출력되어 로그인 성공시에 Handler가 올바르게 동작함을 확인할 수 있다.
이제 아래와 같이, 소셜 로그인시 정보 수정을 제공하는 로직을 핸들러에서 구현하면된다.
만약 소셜 로그인 이용자이고, 비밀번호가 디폴트인 "1111" 이라면 "/sample/modify" 로 리다이렉트하게 구현하였다.
여기서 편의를 위한 문법 하나를 소개하겠다.
기존에는 이런식으로 권한처리를 진행했지만, @EnableGlobalMethodSecurity 어노테이션을 사용하면
컨트롤러 영역에서 권한 설정을 진행할 수 있다.
SecurityConfig
이렇게 Config 파일에서 설정을 해둔 뒤, 컨트롤러에서 @PreAuthorize( "hasRole(~~~~)" ) 어노테이션을 붙이면,
해당 어노테이션이 붙은 건 hasRole() 메소드를 통한 권한 체크를 진행하고, 어노테이션이 없다면 모든 접근을 허락한다.
또한 "특정 이름을 가진 사람만 접근 가능" 과 같은 특수한 케이스도 처리 가능하다.
이메일이 내 이메일과 같아야만, Mapping이 수행되게 구현한 컨트롤러이다.
아래와 같이, 내 계정의 이름을 가진 사용자만 해당 컨트롤러를 통해 이동할 수 있게 구현할 수 있다.
'Spring boot' 카테고리의 다른 글
API 서버 구성과 JWT를 통한 인증, 인가 (1) | 2023.01.30 |
---|---|
Spring Boot 컨트롤러와 Client간의 관계 정리 (0) | 2023.01.27 |
Spring security란? (0) | 2023.01.25 |
자바 Stream 정리 [Stream, Map, Filtering, Sorted, Collect] (0) | 2023.01.15 |
[toyProject] 게시물 사이트 Part 3 (0) | 2023.01.10 |