Spring Security와 OAuth2를 활용하여 Google, Naver, Kakao 소셜 로그인을 구현하는 전체 과정을 다룹니다. YML 설정부터 핵심 컴포넌트 구현, 그리고 JWT 발급 연동까지의 흐름을 정리
application.yml 설정가장 먼저 각 소셜 미디어 플랫폼에서 발급받은 클라이언트 정보를 application.yml에 등록
spring:
security:
oauth2:
client:
# 1. 각 소셜 미디어의 공통 설정 (provider)
provider:
naver:
authorization-uri: https://nid.naver.com/oauth2.0/authorize
token-uri: https://nid.naver.com/oauth2.0/token
user-info-uri: https://openapi.naver.com/v1/nid/me
user-name-attribute: response # Naver의 경우 사용자 정보가 'response' 객체 안에 포함됨
kakao:
authorization-uri: https://kauth.kakao.com/oauth/authorize
token-uri: https://kauth.kakao.com/oauth/token
user-info-uri: https://kapi.kakao.com/v2/user/me
user-name-attribute: id
# 2. provider를 기반으로 한 client 상세 정보 (registration)
registration:
google:
provider: google # Google은 기본 provider 설정이 내장되어 있음
client-id: YOUR_GOOGLE_CLIENT_ID
client-secret: YOUR_GOOGLE_CLIENT_SECRET
scope:
- profile
- email
naver:
provider: naver
client-id: YOUR_NAVER_CLIENT_ID
client-secret: YOUR_NAVER_CLIENT_SECRET
# 사용자가 동의 후 리디렉션될 URI
redirect-uri: http://localhost:8080/login/oauth2/code/naver
authorization-grant-type: authorization_code
client-name: Naver
kakao:
provider: kakao
client-id: YOUR_KAKAO_CLIENT_ID
client-secret: YOUR_KAKAO_CLIENT_SECRET
redirect-uri: http://localhost:8080/login/oauth2/code/kakao
authorization-grant-type: authorization_code
client-name: Kakao
# Kakao는 POST 방식으로 client-secret을 전달해야 함
client-authentication-method: POST
scope:
- profile_image
- account_email
Spring Security가 처리하는 OAuth2 인증의 전체적인 흐름은 다음과 같음
http://localhost:8080/oauth2/authorization/kakao로 이동.redirect-uri로 다시 리디렉션.user-info-uri로 사용자 정보를 요청.OAuth2UserService 실행: 사용자 정보를 성공적으로 가져오면, 우리가 직접 구현할 커스텀 OAuth2UserService의 loadUser 메소드가 실행됨.SuccessHandler 실행: loadUser가 성공적으로 완료되면, SecurityConfig에 등록된 OAuth2SuccessHandler가 실행됨.SuccessHandler는 우리 서비스의 JWT를 생성하고, 이 토큰을 쿼리 파라미터에 담아 최종적으로 React 애플리케이션으로 리디렉션.oauth2Login()을 사용하여 OAuth2 관련 설정을 활성화하고, 커스텀 서비스와 핸들러를 연결합니다.
@Configuration
@EnableWebSecurity
public class SecurityConfig {
@Autowired
private PrincipalOauth2UserService principalOauth2UserService;
@Autowired
private OAuth2SuccessHandler oAuth2SuccessHandler;
@Bean
public SecurityFilterChain filterChain(HttpSecurity http) throws Exception {
http.csrf().disable();
http.authorizeRequests()
// ... 다른 인가 설정
.anyRequest().permitAll() // 예시로 모든 요청 허용
.and()
.oauth2Login() // OAuth2 로그인 설정 시작
.successHandler(oAuth2SuccessHandler) // 로그인 성공 시 핸들러
.userInfoEndpoint() // 로그인 성공 후 사용자 정보를 가져올 때의 설정
.userService(principalOauth2UserService); // 사용자 정보를 처리할 서비스
return http.build();
}
}
DefaultOAuth2UserService를 상속받아 loadUser 메소드를 오버라이드합니다. 여기서 각 소셜 미디어로부터 받은 사용자 정보를 우리 서비스에 맞게 가공
SimpleUrlAuthenticationSuccessHandler를 상속받아 onAuthenticationSuccess 메소드를 오버라이드합니다. 이 핸들러는 OAuth2 인증의 종착역이자, 우리 서비스의 JWT 인증 시스템으로 넘어가는 다리 역할을 함
* 역할:
1. Authentication 객체에서 가공된 사용자 정보(PrincipalUser)를 꺼냄.
2. JwtProvider를 사용하여 우리 서비스의 Access Token을 생성.
3. 생성된 토큰을 포함한 URL로 React 앱에 리디렉션. (예: http://localhost:3000/auth/oauth?token=${token})
OAuth2 과정에서도 다양한 예외가 발생할 수 있으며, 이에 대한 처리가 필요
회원가입 중 에러: Oauth2UserService에서 DB에 사용자 정보를 저장하다 실패할 경우, @Transactional(rollbackFor = Exception.class)을 통해 롤백 처리.
토큰 관련 에러: JwtProvider 또는 JwtFilter에서 토큰 유효성 검증 실패 시 JwtException 발생. 이는 AuthenticationEntryPoint 또는 @RestControllerAdvice에서 처리.
* 인증 실패 공통 처리: 로그인 과정에서 발생하는 대부분의 인증 관련 예외는 AuthenticationFailureHandler를 커스텀하여 처리하거나, SecurityConfig에 등록된 AuthenticationEntryPoint에서 일관된 에러 응답을 보내도록 설정.
토큰 수신: SuccessHandler가 리디렉션한 URL(http://localhost:3000/auth/oauth?token=...)의 쿼리 파라미터에서 토큰을 추출.
토큰 저장: 추출한 토큰을 로컬 스토리지에 저장.
전역 상태 업데이트: React-Query, Zustand 등을 사용하여 로그인 상태를 전역적으로 업데이트.
API 요청: 이후 모든 API 요청 시 axios 인터셉터 등을 활용하여 Authorization 헤더에 로컬 스토리지의 토큰을 담아 전송.