이전에는 시큐리티를 통해 소셜로그인을 구현할 때 OAuth2UserService
를 커스텀해서 인증과 함께 회원가입 처리 같은 애플리케이션에 필요한 로직을 추가해서 사용했다
근데 이 방식은 단점이 있다. 아래를 코드를 보며 설명해보겠다
@RequiredArgsConstructor
public class CustomOAuth2LoginService implements OAuth2UserService<OAuth2UserRequest, OAuth2User> {
private final MemberRepository memberRepository;
private final OAuth2UserService<OAuth2UserRequest, OAuth2User> delegate;
@Override
public OAuth2User loadUser(OAuth2UserRequest userRequest) throws OAuth2AuthenticationException {
OAuth2User oauth2User = delegate.loadUser(userRequest);
...
...
}
위 코드를 보면 DefaultOAuth2UserService
를 통해 loaduser
를 호출하는데, 이 과정에서 access-token 을 통해 사용자 정보를 가져온다.
이 과정에 아래에 코드를 추가해서 회원가입 같은 별도의 기능을 추가해서 사용하는 방식이다
해당 코드는 테스트 코드를 작성할 때 OAuth2UserService, OAuth2UserRequest, OAuth2User 같은 친구들은 모킹 지옥 열차에 빠지해주며 인증과정에 부가로직을 넣었기 때문에 성공 처리에 대한 핸들러까지 직접 만들어줄게 많아진다 😢
그래서 나는 이런 부가로직을 해당 단계에서 넣어야될까? 🤔 라는 생각을 했고 다른 구현방법에 대해서 천천히 소셜로그인에 대해서 설명하면서 소개해보겠다 👋
소셜로그인에는 여러 인증 방식이 있지만 나는 가장 보편적이고 중요한 Authorization Code Grant
방식의 인증 방식을 이용한다
흐름은 위 사진과 같다.
Resource Owner
가 사용자이고 Application
이 백엔드 서버, Authorization Server
, Resource Server
는 모두 소셜로그인을 지원하는 제공사다. 예를 들면 구글 or 카카오가 되겠다.
순서를 설명하자면
Authorization Server
는 Authorization code
를 발급해서 백엔드 서버에 전달한다Authroization code
를 이용해서 Authorization Server
로 부터 Access Token
을 발급받는다Accesss Token
을 이용해서 Resource Server
로 부터 사용자 정보를 가져온다소셜로그인을 직접 구현한다면 해당 과정을 모두 직접 구현해야겠지만, 스프링 시큐리티는 이 모든 과정을 모두 수행해준다 👍
위 과정은 시큐리티가 수행해주기에 우리가 개발할 내용도 많이 줄어든다.
우리가 해야할 일은 아래와 같다
AuthorizationRequestRepository
인터페이스를 Cookie
를 활용하는 방식으로 새롭게 구현해서 등록한다(optional)OAuth2AuthorizedClientRepository
인터페이스 구현체를 JdbcOAuth2AuthorizedClientService
를 사용한 구현체로 대체(optional)이 과정 중에서 1번 과정에 대해서만 다뤄보도록 하겠다 🫡
OAuth2 인증은 OAuth2LoginAuthenticationFilter
에서 이루어지는데 attemptAuthentication
메소드에서 작업이 수행된다.
소셜로그인 페이지에서 로그인을 통한 인증이 수행되고 설정한 RedirectUri
로 리다이렉트 되는 요청을 시큐리티에서 잡아서 OAuth2LoginAuthenticationFilter
의 attemptAuthentication
메소드를 통해 발급 받은 code 를 통해 Token
을 발급받고 사용자 정보를 가져와서 최종적으로 OAuth2AuthenticationToken
을 만들어내는 과정을 해당 필터에서 수행한다
attemptAuthentication
메소드가 수행된 후에는 다시 AbstractAuthenticationProcessingFilter
필터의 doFilter
메소드로 넘어오는데 인증이 성공했다면 마지막에 아래 successfulAuthentication
메소드를 호출한다
코드 마지막 줄을 보면 successHandler
의 onAuthenticaitonSuccess
를 호출한다
💡 여기서 사용되는 SuccessHandler 를 커스텀해서 우리가 원하는 처리를 만들어내면 된다!!
SuccessHandler 를 커스텀해서 인증 후 처리에 필요한 로직을 추가한다면 앞선 코드처럼OAuth2UserService
를 구현하지 않아도 된다
AuthenticationSuccessHandler
인터페이스를 구현해도 되지만 나는 소셜로그인 성공 후 처리를 리다이렉트를 사용할 것이기 때문에 SimpleUrlAuthenticationSuccessHandler
를 상속해서 구현했다.
SimpleUrlAuthenticationSuccessHandler
는 AuthenticationSuccessHandler
구현체다.
@Slf4j
@Component
@RequiredArgsConstructor
public class OAuth2LoginSuccessHandler extends SimpleUrlAuthenticationSuccessHandler {
private final JwtTokenProvider tokenProvider;
private final UserService userService;
@Override
public void onAuthenticationSuccess(
HttpServletRequest request,
HttpServletResponse response,
Authentication authentication
) throws IOException {
log.info("OAuth Login Success!!");
if (authentication instanceof OAuth2AuthenticationToken authenticationToken) {
String provider = authenticationToken.getAuthorizedClientRegistrationId();
OAuth2User oAuth2User = authenticationToken.getPrincipal();
userService.join(oAuth2User, provider);
log.info("oAuthUser's role : {}", authenticationToken.getAuthorities());
String accessToken = tokenProvider.createAccessToken(authenticationToken);
String refreshToken = tokenProvider.createRefreshToken(authenticationToken);
String url = UriComponentsBuilder.fromUriString("https://mydomain" + "/welcome")
.queryParam("access-token", accessToken)
.queryParam("refresh-token", refreshToken)
.queryParam("provider", provider)
.build()
.toUri()
.toString();
getRedirectStrategy().sendRedirect(request, response, url);
}
}
}
@Service
@RequiredArgsConstructor
public class UserService {
private final UserRepository userRepository;
public User join(OAuth2User oAuth2User, String provider) {
User user = OAuthUserMappers.mapToUser(provider, oAuth2User);
userRepository.findByProviderAndProviderId(provider, oAuth2User.getName())
.ifPresentOrElse(
findUser -> findUser.updateByOAuth(user.getName()),
() -> userRepository.save(user)
);
return user;
}
}
코드가 하는 일은 다음과 같다
acceess-token
, refresh-token
을 발급해서 리다이렉트와 함께 제공한다💡 1번 2번 과정은 비즈니스에 따라 구성을 다르게 가져가면 된다
적용하는건 세상에서 제일 간단하다
@Bean
public SecurityFilterChain filterChain(HttpSecurity http) throws Exception {
return http
...
.oauth2Login(customizer -> customizer.successHandler(authenticationSuccessHandler))
...
.build();
}
위처럼 설정에 체인으로 등록해주면 된다. 나는 주입을 받아서 등록했다.
이상 소셜로그인 포스팅은 마쳤는데, 생각보다 간단하다. OAuth2LoginService
를 직접 커스텀해서 구현하는 경우를 많이 봤는데 나는 인증 과정에 특별한 로직이 필요한게 아니라면 직접 구현할 이유가 없다고 생각한다.
그래서 나는 SuccessHandler
만 커스텀해서 사용하며 필요한 경우에만 OAuth2LoginService
도 커스텀해서 사용하는게 좋은 것 같다!! 👍