스프링 시큐리티 + 소셜로그인 인증 구현 방법

은찬·2023년 12월 12일
6

Java, Spring

목록 보기
6/7

이전에 사용하던 시큐리티 + 소셜로그인 방식

이전에는 시큐리티를 통해 소셜로그인을 구현할 때 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 카카오가 되겠다.

순서를 설명하자면

  1. 사용자가 소셜로그인을 백엔드 서버에 요청함
  2. 백엔드 서버는 소셜로그인 제공사에 맞는 로그인 페이지로 사용자를 리다이렉트 시킨다
  3. 사용자는 소셜로그인을 통해 인증을 한다
  4. Authorization ServerAuthorization code 를 발급해서 백엔드 서버에 전달한다
  5. 백엔드 서버는 받은 Authroization code 를 이용해서 Authorization Server 로 부터 Access Token 을 발급받는다
  6. 백엔드 서버는 발급받은 Accesss Token 을 이용해서 Resource Server 로 부터 사용자 정보를 가져온다

소셜로그인을 직접 구현한다면 해당 과정을 모두 직접 구현해야겠지만, 스프링 시큐리티는 이 모든 과정을 모두 수행해준다 👍


구현

위 과정은 시큐리티가 수행해주기에 우리가 개발할 내용도 많이 줄어든다.
우리가 해야할 일은 아래와 같다

  1. 소셜 로그인 인증 성공 후 처리
  2. AuthorizationRequestRepository 인터페이스를 Cookie 를 활용하는 방식으로 새롭게 구현해서 등록한다(optional)
  3. OAuth2AuthorizedClientRepository 인터페이스 구현체를 JdbcOAuth2AuthorizedClientService 를 사용한 구현체로 대체(optional)

이 과정 중에서 1번 과정에 대해서만 다뤄보도록 하겠다 🫡

소셜로그인 인증 성공 후 처리

OAuth2 인증은 OAuth2LoginAuthenticationFilter 에서 이루어지는데 attemptAuthentication 메소드에서 작업이 수행된다.

소셜로그인 페이지에서 로그인을 통한 인증이 수행되고 설정한 RedirectUri 로 리다이렉트 되는 요청을 시큐리티에서 잡아서 OAuth2LoginAuthenticationFilterattemptAuthentication 메소드를 통해 발급 받은 code 를 통해 Token 을 발급받고 사용자 정보를 가져와서 최종적으로 OAuth2AuthenticationToken 을 만들어내는 과정을 해당 필터에서 수행한다

attemptAuthentication 메소드가 수행된 후에는 다시 AbstractAuthenticationProcessingFilter 필터의 doFilter 메소드로 넘어오는데 인증이 성공했다면 마지막에 아래 successfulAuthentication 메소드를 호출한다

코드 마지막 줄을 보면 successHandleronAuthenticaitonSuccess 를 호출한다

💡 여기서 사용되는 SuccessHandler 를 커스텀해서 우리가 원하는 처리를 만들어내면 된다!!
SuccessHandler 를 커스텀해서 인증 후 처리에 필요한 로직을 추가한다면 앞선 코드처럼 OAuth2UserService 를 구현하지 않아도 된다

AuthenticationSuccessHandler 커스텀

AuthenticationSuccessHandler 인터페이스를 구현해도 되지만 나는 소셜로그인 성공 후 처리를 리다이렉트를 사용할 것이기 때문에 SimpleUrlAuthenticationSuccessHandler 를 상속해서 구현했다.
SimpleUrlAuthenticationSuccessHandlerAuthenticationSuccessHandler 구현체다.

@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;
	}
}

코드가 하는 일은 다음과 같다

  1. 소셜로그인한 사용자가 이미 회원인 사용자면 소셜로그인 정보를 통해서 사용자 정보를 업데이트한다
  2. 회원이 아닌 사용자라면 회원정보를 저장한다
  3. acceess-token, refresh-token 을 발급해서 리다이렉트와 함께 제공한다

💡 1번 2번 과정은 비즈니스에 따라 구성을 다르게 가져가면 된다


커스텀 AuthenticationSuccessHandler 적용

적용하는건 세상에서 제일 간단하다

@Bean
	public SecurityFilterChain filterChain(HttpSecurity http) throws Exception {
		return http
			...
			.oauth2Login(customizer -> customizer.successHandler(authenticationSuccessHandler))
			...
			.build();
	}

위처럼 설정에 체인으로 등록해주면 된다. 나는 주입을 받아서 등록했다.

마무리

이상 소셜로그인 포스팅은 마쳤는데, 생각보다 간단하다. OAuth2LoginService 를 직접 커스텀해서 구현하는 경우를 많이 봤는데 나는 인증 과정에 특별한 로직이 필요한게 아니라면 직접 구현할 이유가 없다고 생각한다.

그래서 나는 SuccessHandler 만 커스텀해서 사용하며 필요한 경우에만 OAuth2LoginService 도 커스텀해서 사용하는게 좋은 것 같다!! 👍

profile
`강한` 백엔드 개발자라고 해두겠습니다

0개의 댓글