Oauth 2.0에 대해서는 아래 강좌에 설명이 잘 되어 있으니 아래 강좌를 들어보자.
Oauth에 대한 설명이 기가막히네
Spring Boot는 Oauth-client
라는 dependency를 지원한다. 이 라이브러리를 사용하면 Oauth를 통해 Google, Facebook 등 소셜 로그인을 편리하게 할 수 있다.
일반적으로 Oauth를 통한 로그인 방법은
1. Code를 받아서 인증을 완료한다.
2. Code를 통해 AccessToken을 받아 사용자 정보에 접근할 권한을 받는다.
3. AccessToken을 통해 사용자의 프로필 정보를 가져올 수 있다.
4-1. 프로필 정보를 토대로 회원가입을 진행하거나
4-2. 프로필 정보 + 추가 정보를 통해 회원가입을 진행한다.
이제 구글 로그인을 통해 회원가입을 진행해보겠다.
먼저 Google api console
에서 Oauth 클라이언트 아이디와 비밀번호를 생성한다. (구글에 검색)
그 다음 Oauth2-client
dependency를 추가한다. (구글에 검색)
그 다음 application
파일에 다음과 같이 추가한다.
spring:
security:
oauth2:
client:
registration:
google:
client-id: 발급받은 아이디
client-secret: 발급받은 비밀번호
scope: 어떤 정보가 필요한지
- email
- profile
로그인 폼에 <a href="/oauth2/authorization/google">구글 로그인</a>
이 코드를 추가한다.
/oauth2/authorization/google
는 고정 값이기 때문에 똑같이 써야 구글 로그인 창으로 이동된다.
그 다음 SecurityConfig
파일에 구글 로그인을 위한 코드를 더 추가해준다.
@Bean
public SecurityFilterChain filterChain(HttpSecurity http) throws Exception {
http.csrf().disable();
http.authorizeRequests()
...
.and()
.oauth2Login()
.loginPage("/loginForm")
return http.build();
}
다음 3줄의 코드를 추가하면 a태그를 통해 구글 로그인 창으로 이동이 되지만 로그인을 해도 그 다음으로 진행이 되지 않는다.
왜냐하면 구글 로그인이 완료된 뒤에 후처리가 필요하기 때문이다.
@Bean
public SecurityFilterChain filterChain(HttpSecurity http) throws Exception {
http.csrf().disable();
http.authorizeRequests()
...
.and()
.oauth2Login()
.loginPage("/loginForm")
.userInfoEndpoint()
.userService(principalOauth2UserService);
return http.build();
}
다음 2줄을 추가한다. principalOauth2UserService
에서 후처리를 해준다.
@Service
public class PrincipalOauth2UserService extends DefaultOAuth2UserService {
@Override
public OAuth2User loadUser(OAuth2UserRequest userRequest) throws OAuth2AuthenticationException {
System.out.println("userRequest.getClientRegistration() = " + userRequest.getClientRegistration());
System.out.println("userRequest.getAccessToken().getTokenValue() = " + userRequest.getAccessToken().getTokenValue());
System.out.println("super.loadUser(userRequest).getAttributes() = " + super.loadUser(userRequest).getAttributes());
return super.loadUser(userRequest);
}
}
DefaultOAuth2UserService
를 상속받은 PrincipalOauth2UserService
는 구글 로그인을 한 뒤에 loadUser
가 구글로부터 받은 OAuth2UserRequest
를 통해 후처리를 해준다.
OAuth의 동작 방식을 정리하자면,
1. 구글 로그인 버튼을 클릭하면 구글 로그인창으로 이동되고 로그인을 완료한다.
2. 로그인을 완료하면 'OAuth-Client'가 'code'를 받아서 리턴해준다.
3. 'code'를 통해 'AccessToken'을 요청하여 받는다.
4. 'AccessToken'은 위 코드에 'userRequest'에 들어가 있다.
5. 'loadUser' 메소드를 호출하여 구글로부터 회원 프로필을 받는다.
이 과정을 거쳐서 우리는 프로필 정보를 받을 수 있다.
IndexController
클래스에 다음과 같은 코드를 추가하자.
@ResponseBody
@GetMapping("/test/login")
public String testLogin(Authentication authentication,
@AuthenticationPrincipal PrincipalDetails principalDetail) {
System.out.println("/test/login =================");
PrincipalDetails principalDetails = (PrincipalDetails) authentication.getPrincipal();
System.out.println("principalDetails = " + principalDetails.getUser());
System.out.println("userDetails = " + principalDetail.getUser());
return "세션 정보 확인하기";
}
이전에 쓴 포스트에서 시큐리티 세션은 Authentication
타입의 객체만 저장할 수 있다는 것을 배웠다.
그래서 우리는 매개변수로 Authentication
타입의 객체를 주입받고(DI),
우리가 구현한 PrincipalDetails
타입으로 다운 캐스팅하면, 그 안에 로그인하여 저장된 유저의 정보에 접근할 수 있다.
다른 방법으로는 @AuthenticationPrincipal
어노테이션을 통해서 접근하는 방법이다.
이 방법은 다운캐스팅 없이 Authentication
타입안에 들어가 있는 PrincipalDetails
타입으로 받아주면 된다.
여기서 주의할 점은 지금 받은 유저의 정보는 OAuth를 통해서 로그인한 유저의 정보가 아닌 홈페이지에서 회원가입을 한 다음 로그인한 유저의 정보이다.
그렇다면 OAuth를 통해 로그인한 유저의 정보는 어떻게 접근할 수 있을까?
그 방법은 다음과 같다.
IndexController
클래스에 다음과 같은 코드를 추가하자.
@ResponseBody
@GetMapping("/test/oauth/login")
public String testOauthLogin(Authentication authentication,
@AuthenticationPrincipal OAuth2User oauth) {
System.out.println("/test/oauth/login =================");
OAuth2User oAuth2User = (OAuth2User) authentication.getPrincipal();
System.out.println("oAuth2User = " + oAuth2User.getAttributes());
System.out.println("oauth = " + oauth.getAttributes());
return "Oauth 세션 정보 확인하기";
}
방금 추가한 코드와 이전에 추가한 코드를 비교하면 하나 빼고는 다 똑같을 것이다.
(방금 추가한 코드를 2번 코드라 하고, 이전에 추가한 코드를 1번 코드라 하자.)
1번 코드와 2번 코드의 차이점은 주입받는 객체의 타입이다.
1번 코드에서는 UserDetails
를 구현한 PrincipalDetails
타입으로 받았지만,
2번 코드에서는 OAuth2User
타입으로 받았다.
일반 로그인을 할 때는 Spring Security가 UserDetails
타입으로 Authentication
에 저장하지만,
OAuth 로그인을 할 때는 OAuth2User
타입으로 저장한다.
Authentication
에 저장되는 타입이 제각각이면 개발자는 타입마다 컨트롤러를 만들어야 한다.
그러면 코드 양이 늘어나서 보기에도 좋지않다.
이를 해결하기 위해 UserDetails
와 OAuth2User
를 동시에 구현한 어떠한 클래스를 만들면 될 것이다.
그것이 바로 PrincipalDetails
이다!!
PrincipalDetails
클래스에 다음과 같이 추가하자!
public class PrincipalDetails implements UserDetails, OAuth2User {
(생략)
@Override
public Map<String, Object> getAttributes() {
return null;
}
@Override
public String getName() {
return null;
}
}
두 메소드는 implement 한 메소드이다.
이제 구글로그인을 했을 때, DB에 구글 프로필 정보를 통해 회원가입을 하는 코드를 작성해 보자.
먼저 PrincipalDetailsService
와 PrincipalOauth2UserService
클래스를 만든 이유를 살펴보자.
Authentication
타입은 UserDetails
와 OAuth2User
타입만 받는다고 하였다.
만약 우리가 둘 중 한개만으로 로그인을 진행했다면 PrincipalDetails
클래스를 만들지 않고, 그냥 Authentication
에 둘 중 하나가 들어가도록 했을 것이다.
그렇지만 우리는 일반 로그인도 필요하고, OAuth 로그인도 필요했기 때문에 PrincipalDetails
를 만들어서 두 타입을 구현하게 한 것이다.
이제 구글 로그인을 통해서 회원가입을 진행해보자.
OAuth2User 타입은 Attributes를 필요로 한다. 이것이 있어야 정상적인 로그인이 가능하다.
Attributes가 무엇이냐면, PrincipalOauth2UserService
클래스에서 loadUser
메소드는 userRequest
를 받게 된다. 이 안에 다음과 같은 Attributes가 있다.
// userRequest.getAttributes()
{
sub=147490145755120067183,
name=이정수,
given_name=정수,
family_name=이,
picture=https://lh3.googleusercontent.com/DmcxfJD7pfJ2-cxl6=s96-c,
email=1996dododog@gmail.com,
email_verified=true,
locale=ko
}
이 정보들을 통해 User객체를 만들고 회원가입을 진행할 것이다.
우선 우리는 PrincipalDetails
타입의 객체를 만들어야 로그인이 정상적으로 되기 때문에
PrincipalDetails
클래스에 생성자를 추가할 것이다.
// OAuth 로그인
public PrincipalDetails(User user, Map<String, Object> attributes) {
this.user = user;
this.attributes = attributes;
}
그 다음 OAuth2User
를 implement하면서 같이 받은 메소드들도 수정해 줄것이다.
@Override
public Map<String, Object> getAttributes() {
return attributes;
}
@Override
public String getName() {
return null;
}
getName()
은 사용하지 않기 때문에, 수정하지 않았다.
다음으로 User
클래스를 수정해줄 것이다.
@Entity
@Data
@NoArgsConstructor
public class User {
@Id
@GeneratedValue(strategy = GenerationType.IDENTITY)
private int id;
private String username;
private String password;
private String email;
private String role; // ROLE_USER, ROLE_MANAGER, ROLE_ADMIN
private String provider;
private String providerId;
@CreationTimestamp
private Timestamp createDate;
@Builder
public User(String username, String password, String email, String role, String provider, String providerId, Timestamp createDate) {
this.username = username;
this.password = password;
this.email = email;
this.role = role;
this.provider = provider;
this.providerId = providerId;
this.createDate = createDate;
}
}
이제 구글 로그인을 하면 받은 회원의 프로필 정보로 회원가입을 하는 코드를 작성해보자.
구글 로그인의 후처리는 PrincipalOauth2UserService
클래스에서 한다고 하였다.
회원가입도 후처리니까 이 클래스에서 진행하면 된다.
@Service
@RequiredArgsConstructor
public class PrincipalOauth2UserService extends DefaultOAuth2UserService {
(생략)
OAuth2User oAuth2User = super.loadUser(userRequest);
// 회원가입을 강제로 진행
String provider = userRequest.getClientRegistration().getRegistrationId(); // google
String providerId = oAuth2User.getAttribute("sub");
String username = provider + "_" + providerId; // google_숫자
String password = bCryptPasswordEncoder.encode("겟인데어");
String email = oAuth2User.getAttribute("email");
String role = "ROLE_USER";
User userEntity = userRepository.findByUsername(username);
if (userEntity == null) {
userEntity = User.builder()
.username(username)
.password(password)
.email(email)
.role(role)
.provider(provider)
.providerId(providerId)
.build();
userRepository.save(userEntity);
}
return new PrincipalDetails(userEntity, oAuth2User.getAttributes());
}
}
위와 같이 코딩하였다.
userRequest.getClientRegistration().getRegistrationId()
의 값은 "google"인데 나중에 OAuth로그인을 추가할 때, 사용자 아이디가 겹칠 수 있으니 앞에 "google"을 추가하여 헷갈리지 않도록 한것이다.
oAuth2User.getAttribute("sub")
는 구글에서 제공해주는 사용자 식별값이며 고유값이다.
bCryptPasswordEncoder.encode("겟인데어");
의 "겟인데어"는 아무 의미없다. 비밀번호를 아무렇게나 지정해도 된다. 왜냐면 일반 로그인을 하지 않을 것이기 때문이다.
return new PrincipalDetails(userEntity, oAuth2User.getAttributes())
을 보면 PrincipalDetails
로 반환이 가능하다. 그 이유는 OAuth2User
구현한 클래스이기 때문이다.
이렇게 OAuth 로그인을 통해 회원가입을 강제로 해주는 코드를 작성하였다.
우리가 시큐리티 세션에 쉽게 접근할 수 있도록 도와주는 @AuthenticationPrincipal
어노테이션은 언제 생성이 될까?
PrincipalDetailsService
클래스의 loadUserByUsername
메소드가 끝날 때,
PrincipalOauth2UserService
클래스의 loadUser
메소드가 끝날 때 생성된다.