커스텀 Login + OAuth2 (구글, 네이버, 카카오) API 구현 [2] - OAuth2 로그인
OAuth2 활성화
Spring에서 OAuth2를 사용하는 방법, 구현되는 작동원리에 대해서 간략하게 알아보자. 실제로 써먹어야 하니 중요한 부분이다. 당장 구현에 급급하여 복사붙여넣기를 반복하면 결국 남는게 없어질 뿐이다.
implementation 'org.springframework.boot:spring-boot-starter-oauth2-client'
해당 dependency를 추가한 후에, WebSecurityConfigurerAdapter를 사용하여 설정할 수 있다. 그러나 우리는 커스텀 config를 사용하며, Security Config를 이용하기 때문에, 해당 클래스를 상속받아 사용하는 SecurityConfig를 필수적으로 사용한다. 해당 클래스에서 설정해주자.
public SecurityFilterChain filterChain(HttpSecurity http) throws Exception {
http.oauth2Login();
}
http.oauth2Login(); 해당 코드를 사용해서 로그인을 활성화할 수 있다. 해당 기능이 활성화 되면 OAuth2LoginConfigurer 를 사용할 수 있다.해당 메소드와 필터를 등록하여 실제 사비스를 구현할때 사용이 가능하다.
oauth2가 요청이 들어올 경우 어떤식으로 작동하는지는 아래와 같다.
/oauth2/authorization/{} 해당 주소로 들어오는 요청에 대해서 필터를 작동시킨다. 해당 요청에 필터가 등록되는 메소드는 OAuth2AuthorizationRequestRedirectFilter 해당 메소드를 들여다볼때, 중요한 부분들을 나열해봤다.
public class OAuth2AuthorizationRequestRedirectFilter extends OncePerRequestFilter {
/**
* The default base {@code URI} used for authorization requests.
*/
public static final String DEFAULT_AUTHORIZATION_REQUEST_BASE_URI = "/oauth2/authorization";
해당 주소로 요청이 들어올 경우에 이 필터가 작동된다. :: doFilterInternal()
@Override
protected void doFilterInternal(HttpServletRequest request, HttpServletResponse response, FilterChain filterChain)
throws ServletException, IOException {
try {
OAuth2AuthorizationRequest authorizationRequest = this.authorizationRequestResolver.resolve(request);
if (authorizationRequest != null) {
this.sendRedirectForAuthorization(request, response, authorizationRequest);
return;
}
}
catch (Exception ex) {
this.unsuccessfulRedirectForAuthorization(request, response, ex);
return;
}
try {
filterChain.doFilter(request, response);
}
catch (IOException ex) {
throw ex;
}
catch (Exception ex) {
// Check to see if we need to handle ClientAuthorizationRequiredException
Throwable[] causeChain = this.throwableAnalyzer.determineCauseChain(ex);
ClientAuthorizationRequiredException authzEx = (ClientAuthorizationRequiredException) this.throwableAnalyzer
.getFirstThrowableOfType(ClientAuthorizationRequiredException.class, causeChain);
if (authzEx != null) {
try {
OAuth2AuthorizationRequest authorizationRequest = this.authorizationRequestResolver.resolve(request,
authzEx.getClientRegistrationId());
if (authorizationRequest == null) {
throw authzEx;
}
this.requestCache.saveRequest(request, response);
this.sendRedirectForAuthorization(request, response, authorizationRequest);
}
catch (Exception failed) {
this.unsuccessfulRedirectForAuthorization(request, response, failed);
}
return;
}
if (ex instanceof ServletException) {
throw (ServletException) ex;
}
if (ex instanceof RuntimeException) {
throw (RuntimeException) ex;
}
throw new RuntimeException(ex);
}
}
즉, /oauth2/authorization/{clientRegisrationId} url로 요청이 보낼 경우 DefaultOAuth2AuthorizationRequestResolver 코드가 실행된다.
{clientRegisrationId} 는 클라이언트 어플리케이션을 식별하는데 사용하는 값이다. 구글, 카카오, 네이버 같은 인증이 필요한 공급자에 대한 정보들이다.
private String resolveRegistrationId(HttpServletRequest request) {
if (this.authorizationRequestMatcher.matches(request)) {
return this.authorizationRequestMatcher.matcher(request).getVariables()
.get(REGISTRATION_ID_URI_VARIABLE_NAME);
}
return null;
}
코드는 authorizationRequestMatcher를 세팅한다. 이 Security 설정은 특정 URL 패턴에 대해 요청을 처리할 때에 어떤 규칙을 적용해야 되는지를 세팅한다. 즉, 해당 URL로 들어오면 OAuth 2.0 방식으로 작동한다. 라는 뜻이다.
clientRegisrationId는 Security OAuth에서 기본적으로 제공하는 아이디도 있다. Google, GitHub, aceBook, OKTA 가 아니라면 application 세팅에서 알맞은 클라이언트 아이디를 저장해서 사용해야한다.
/oauth2/authorization/{clientRegisrationId} 이 주소가 실행되서 결국 {clientRegistrationId} 를 추출해서 resolve 함수가 실행된다. reslove에서는 application 설정파일에서 세팅한 registration 정보가 있다면, 로직을 진행하고 없다면 예외를 발생한다.
정상적인 주소, 정상적인 정보가 있을 경우에는 DefaultOAuth2UserService에서 DefaultOAuth2User가 반환되는데
public class DefaultOAuth2User implements OAuth2User, Serializable {
private static final long serialVersionUID = SpringSecurityCoreVersion.SERIAL_VERSION_UID;
private final Set<GrantedAuthority> authorities;
private final Map<String, Object> attributes;
private final String nameAttributeKey;
위와 같은 필드들이 포함된다. 권한, API 반환 값, Primary Key와 같은 값들이 들어간다. 해당 값들이 오류없이 계속해서 진행된다면 successfulAuthentication 메서드가 작동하여 권한설정을 해준 후 successHadler를 호출한다. (성공처리)
위와 같이 작동하는 OAuth2, Security 의 작동을 보통 커스텀해서 사용한다. OAuth2UserService<OAuth2UserRequest, OAuth2User> 를 자신의 프로젝트에 맞게 리팩토링 하여 사용하도록 하자.
@RequiredArgsConstructor
@EnableWebSecurity
public class SecurityConfig extends WebSecurityConfigurerAdapter {
{...}
}
Security를 사용한다면 거의 필수적으로 사용되는 SecurityConfig 파일 내부에서 커스텀을 사용해 기본 제공하지 않는 KAKAO, NAVER를 구현할 수 있도록 세팅하자
1. Http 요청
2. OAuth2AuthorizationRequestRedirectFilter가 작동함
3. DefaultOAuth2AuthorizationRequestResolver 작동 : {clientRegisrationId} 반환
4. Application.yml (설정파일) 에 등록된 clientRegisrationId 로 보내진 요청 여부 확인
5. OAuth2LoginAuthenticationProvider 의 authenticate()가 실행
6. userService의 loadUser()가 실행
7. DefaultOAuth2User 반환
8. AbstractAuthenticationProcessingFilter 접근, 성공적이라면 successfulAuthentication 동작
9. SecurityContextHolder 에 Authentication 저장
10. successHandler.onAuthenticationSuccess가 호출된다.