Spring Security & OAuth2

gyeorrr·2025년 7월 14일

Spring

목록 보기
3/3

🌐 Spring Security로 OAuth2 소셜 로그인 구현하기

Spring Security와 OAuth2를 활용하여 Google, Naver, Kakao 소셜 로그인을 구현하는 전체 과정을 다룹니다. YML 설정부터 핵심 컴포넌트 구현, 그리고 JWT 발급 연동까지의 흐름을 정리

사전지식: Spring Security, JWT, React (LocalStorage, React-Query)에 대한 기본적인 이해가 필요

⚙️ 1. 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

🔑 2. OAuth2 로그인 흐름 이해하기

Spring Security가 처리하는 OAuth2 인증의 전체적인 흐름은 다음과 같음

  1. 사용자 로그인 시도: 클라이언트(React)에서 "카카오 로그인" 버튼 클릭. http://localhost:8080/oauth2/authorization/kakao로 이동.
  2. 소셜 미디어로 리디렉션: Spring Security가 요청을 받아 카카오 인증 페이지로 사용자를 리디렉션.
  3. 사용자 동의 및 인가 코드 발급: 사용자가 카카오 계정으로 로그인하고 정보 제공에 동의하면, 카카오 서버는 인가 코드(Authorization Code)를 발급하여 redirect-uri로 다시 리디렉션.
  4. 액세스 토큰 교환: Spring Security는 백그라운드에서 인가 코드를 카카오 서버로 보내 액세스 토큰(Access Token)으로 교환.
  5. 사용자 정보 요청: 발급받은 액세스 토큰을 이용해 user-info-uri로 사용자 정보를 요청.
  6. OAuth2UserService 실행: 사용자 정보를 성공적으로 가져오면, 우리가 직접 구현할 커스텀 OAuth2UserServiceloadUser 메소드가 실행됨.
  7. SuccessHandler 실행: loadUser가 성공적으로 완료되면, SecurityConfig에 등록된 OAuth2SuccessHandler가 실행됨.
  8. JWT 발급 및 최종 리디렉션: SuccessHandler는 우리 서비스의 JWT를 생성하고, 이 토큰을 쿼리 파라미터에 담아 최종적으로 React 애플리케이션으로 리디렉션.

🛠️ 3. 핵심 컴포넌트 구현

SecurityConfig

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();
    }
}

PrincipalOauth2UserService

DefaultOAuth2UserService를 상속받아 loadUser 메소드를 오버라이드합니다. 여기서 각 소셜 미디어로부터 받은 사용자 정보를 우리 서비스에 맞게 가공

💡 OAuthAttributes 패턴
Naver, Kakao, Google이 제공하는 사용자 정보의 JSON 구조가 모두 다릅니다. 이 정보를 일관되게 처리하기 위해 `OAuthAttributes`라는 DTO 클래스를 만들어 데이터를 정규화하는 패턴을 사용하는 것이 좋음 `loadUser` 내부에서는 provider 종류를 확인하여 `OAuthAttributes`로 변환하고, 이를 기반으로 우리 DB에 회원을 등록하거나 조회

OAuth2SuccessHandler

SimpleUrlAuthenticationSuccessHandler를 상속받아 onAuthenticationSuccess 메소드를 오버라이드합니다. 이 핸들러는 OAuth2 인증의 종착역이자, 우리 서비스의 JWT 인증 시스템으로 넘어가는 다리 역할을 함

  * 역할:
1. Authentication 객체에서 가공된 사용자 정보(PrincipalUser)를 꺼냄.
2. JwtProvider를 사용하여 우리 서비스의 Access Token을 생성.
3. 생성된 토큰을 포함한 URL로 React 앱에 리디렉션. (예: http://localhost:3000/auth/oauth?token=${token})


🚨 4. 주요 에러 및 처리

OAuth2 과정에서도 다양한 예외가 발생할 수 있으며, 이에 대한 처리가 필요

  회원가입 중 에러: Oauth2UserService에서 DB에 사용자 정보를 저장하다 실패할 경우, @Transactional(rollbackFor = Exception.class)을 통해 롤백 처리.
토큰 관련 에러: JwtProvider 또는 JwtFilter에서 토큰 유효성 검증 실패 시 JwtException 발생. 이는 AuthenticationEntryPoint 또는 @RestControllerAdvice에서 처리.
* 인증 실패 공통 처리: 로그인 과정에서 발생하는 대부분의 인증 관련 예외는 AuthenticationFailureHandler를 커스텀하여 처리하거나, SecurityConfig에 등록된 AuthenticationEntryPoint에서 일관된 에러 응답을 보내도록 설정.


💻 5. React 클라이언트 연동

  토큰 수신: SuccessHandler가 리디렉션한 URL(http://localhost:3000/auth/oauth?token=...)의 쿼리 파라미터에서 토큰을 추출.
토큰 저장: 추출한 토큰을 로컬 스토리지에 저장.
전역 상태 업데이트: React-Query, Zustand 등을 사용하여 로그인 상태를 전역적으로 업데이트.
API 요청: 이후 모든 API 요청 시 axios 인터셉터 등을 활용하여 Authorization 헤더에 로컬 스토리지의 토큰을 담아 전송.

0개의 댓글