복잡한뇌구조마냥

[Spring] Spring Security + OAuth2 로그인 본문

BE/Spring

[Spring] Spring Security + OAuth2 로그인

지금해냥 2025. 10. 15. 14:57

스프링 시큐리티를 사용하면 OAuth2 로그인 기능을 매우 간단하게 붙일 수 있지만,
실무에서는 기본 설정만으로는 부족하고 커스텀해야 하는 영역이 꽤 많다.

이번 글에서는 내가 실제로 프로젝트에서 사용한 구조를 기반으로

  • OAuth2 로그인 동작 방식
  • Spring Security OAuth2 흐름
  • AuthorizationRequestResolver 커스텀
  • OAuth2SuccessHandler
  • Redirect 처리(state 커스텀)
  • 액세스/리프레시 토큰 생성 구조

까지 정리해본다.


1. OAuth2 로그인 전체 흐름 이해하기

OAuth2 로그인은 간단히 말하면

"서비스가 직접 비밀번호를 받아 인증하지 않고, 구글/카카오 같은 외부 인증 서버를 통해 사용자 정보를 받아오는 방식"

이다.

전체 흐름은 다음과 같다:

  1. 사용자가 /oauth2/authorization/google 으로 이동
  2. Spring Security → AuthorizationRequest 생성
  3. 구글/카카오 로그인 화면으로 redirect
  4. 사용자 로그인 완료 후 callback URL로 code 전달
  5. Spring Security가 code → access token, 사용자 정보 요청
  6. OAuth2UserService → 사용자 정보 매핑
  7. OAuth2SuccessHandler → JWT 발급/redirect 처리
  8. 서비스 자체 인증(JWT)으로 전환

핵심 키워드

  • Authorization Code Grant
  • OAuth2User
  • OAuth2UserService
  • OAuth2SuccessHandler
  • AuthorizedRedirectURI
  • state 파라미터 커스텀 (중요!)

2. Spring OAuth2 기본 설정

1) Gradle 의존성

implementation("org.springframework.boot:spring-boot-starter-oauth2-client")

Spring Security OAuth2 클라이언트 사용을 위한 기본 스타터다.
이거 하나 추가하면 oauth2Login() DSL과 spring.security.oauth2.client.* 설정을 사용할 수 있다.

2) Kakao 설정 (application.yml)

spring:
  security:
    oauth2:
      client:
        registration:
          kakao:
            client-id: ${SPRING__SECURITY__OAUTH2__CLIENT__REGISTRATION__KAKAO__CLIENT_ID}
            client-secret: ${SPRING__SECURITY__OAUTH2__CLIENT__REGISTRATION__KAKAO__CLIENT_SECRET}
            redirect-uri: ${SPRING__SECURITY__OAUTH2__CLIENT__REGISTRATION__KAKAO__REDIRECT_URI}
            client-name: Kakao
            authorization-grant-type: authorization_code
            client-authentication-method: client_secret_post
            scope:
              - profile_nickname
              - profile_image
              - account_email
        provider:
          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

각 항목 설명

  • registration.kakao.client-id
    → 카카오 개발자 콘솔에서 발급받은 REST API 키
  • client-secret
    → 비공개용 시크릿 키 (보통 서버용 앱에서 사용)
  • redirect-uri
    → 카카오 설정에 등록해둔 Redirect URI와 완전히 동일해야 한다
    (로컬: http://localhost:8080/login/oauth2/code/kakao 같은 형태)
  • authorization-grant-type: authorization_code
    → 우리가 흔히 쓰는 Authorization Code 방식
  • client-authentication-method: client_secret_post
    → 카카오는 기본적으로 client_secret_post 방식 사용
    (토큰 요청 시 body에 client-id/secret을 실어서 보냄)
  • scope
    • profile_nickname, profile_image, account_email
      → 어떤 사용자 정보에 접근할지 정의 (콘솔에서 동의 항목도 맞춰야 함)
  • provider.kakao.*
    • authorization-uri : 인가코드 요청 URI
    • token-uri : code → access token 교환 URI
    • user-info-uri : 사용자 정보 조회 API
    • user-name-attribute: id : 응답 JSON에서 “고유 식별자”로 사용할 필드

여기까지 설정하면, 기본적으로는
/oauth2/authorization/kakao를 호출해서 카카오 로그인 화면으로 넘어갈 수 있다.


3. SecurityConfig에서의 OAuth2 설정

Spring Security는 oauth2Login()을 활성화하면 자동으로 OAuth2 로그인 흐름을 만든다.
여기서 우리가 커스텀할 수 있는 포인트는 다음 두 가지다:

  • AuthorizationRequestResolver (authorization URL 생성 단계 커스텀)
  • OAuth2SuccessHandler (로그인 성공 후 JWT 발급 + redirect 처리)

아래는 실제 구조 예시다.

 
.oauth2Login(oauth2 -> oauth2
        .authorizationEndpoint(a -> a
                .authorizationRequestResolver(customOAuth2AuthorizationRequestResolver)
        )
        .userInfoEndpoint(userInfo -> userInfo
                .userService(customOAuth2UserService)
        )
        .successHandler(oAuth2SuccessHandler)
)

4. CustomOAuth2AuthorizationRequestResolver
— redirectUrl 관리의 핵심

문제점

기본 OAuth2 로그인은 state 파라미터를 자동 생성한다.
하지만 실무에서는 로그인 이후 돌아갈 redirectUrl을 동적으로 관리해야 할 때가 많다.

예:

  • 웹 → 로그인 후 홈으로
  • 모바일 앱 → 로그인 후 딥링크로
  • 특정 기능 → 로그인 후 그 기능 상세 페이지로 이동

그래서 우리는 state 값에 redirectUrl을 암호화/인코딩하여 담는다.

구현 예시

 
@Component
@RequiredArgsConstructor
public class CustomOAuth2AuthorizationRequestResolver implements OAuth2AuthorizationRequestResolver {

    private final ClientRegistrationRepository repo;

    private OAuth2AuthorizationRequestResolver defaultResolver() {
        return new DefaultOAuth2AuthorizationRequestResolver(
                repo,
                OAuth2AuthorizationRequestRedirectFilter.DEFAULT_AUTHORIZATION_REQUEST_BASE_URI
        );
    }

    @Override
    public OAuth2AuthorizationRequest resolve(HttpServletRequest request) {
        OAuth2AuthorizationRequest req = defaultResolver().resolve(request);
        return customize(req, request);
    }

    @Override
    public OAuth2AuthorizationRequest resolve(HttpServletRequest request, String clientRegistrationId) {
        OAuth2AuthorizationRequest req = defaultResolver().resolve(request, clientRegistrationId);
        return customize(req, request);
    }

    private OAuth2AuthorizationRequest customize(OAuth2AuthorizationRequest req, HttpServletRequest request) {
        if (req == null) return null;

        String redirectUrl = request.getParameter("redirectUrl");
        if (redirectUrl == null) redirectUrl = "/";

        String state = Base64.getUrlEncoder().encodeToString(redirectUrl.getBytes());
        
        return OAuth2AuthorizationRequest.from(req)
                .state(state)
                .build();
    }
}

왜 state에 redirectURL을 넣는가?

OAuth2는 CSRF 보호 목적으로 state를 사용하지만
애플리케이션 레벨에서는 state를 로그인 이후 이동할 경로로 재활용할 수 있다.


5. CustomOAuth2UserService — 플랫폼별 값 매핑

카카오, 구글, 네이버 등은 모두 사용자 정보 형태가 다르다.
그래서 이를 “우리 서비스의 User 객체/DTO 형태로 표준화”하는 작업이 필요하다.

예시:

 
@Service
@RequiredArgsConstructor
public class CustomOAuth2UserService extends DefaultOAuth2UserService {

    @Override
    public OAuth2User loadUser(OAuth2UserRequest req) {
        OAuth2User oAuth2User = super.loadUser(req);

        String registrationId = req.getClientRegistration().getRegistrationId();

        OAuth2UserInfo userInfo = OAuth2UserInfoFactory.of(registrationId, oAuth2User.getAttributes());

        return new CustomOAuth2User(userInfo);
    }
}

구글/카카오 등을 분기 처리하고 공통 UserInfo 객체로 묶는 방식.


6. OAuth2SuccessHandler — JWT 발급 + Redirect 처리

실제 서비스 인증은 결국 우리 서버의 JWT로 관리한다.
그래서 OAuth2 로그인이 성공하면 AccessToken / RefreshToken 발급 후 redirect 시켜야 한다.

 
@Component
@RequiredArgsConstructor
public class OAuth2SuccessHandler extends SimpleUrlAuthenticationSuccessHandler {

    private final JwtProvider jwtProvider;
    private final MemberService memberService;

    @Override
    public void onAuthenticationSuccess(HttpServletRequest request, HttpServletResponse response, Authentication authentication) throws IOException {

        CustomOAuth2User user = (CustomOAuth2User) authentication.getPrincipal();

        Long memberId = memberService.registerOrLogin(user.toDto());

        String access = jwtProvider.generateAccessToken(memberId);
        String refresh = jwtProvider.generateRefreshToken(memberId);

        String redirectUrl = decodeState(request.getParameter("state"));

        getRedirectStrategy().sendRedirect(
                request, response,
                redirectUrl + "?access=" + access + "&refresh=" + refresh
        );
    }

    private String decodeState(String state) {
        return new String(Base64.getUrlDecoder().decode(state));
    }
}

성공 핸들러에서 하는 일 정리

  1. OAuth2 로그인 성공
  2. 플랫폼별 사용자 정보 추출
  3. 우리 서비스의 회원 DB에 존재하는지 확인 → 신규면 가입 처리
  4. JWT Access + Refresh 토큰 생성
  5. redirectUrl에 토큰 붙여서 redirect

→ 이 구조는 모바일/웹 모두 대응 가능하며,
현대 OAuth2 기반 서비스에서 가장 많이 쓰는 패턴이다.


7. 전체 플로우 다시 보기

 

 

 
➡️플로우
  1. /oauth2/authorization/google?redirectUrl=/mypage
  2. CustomAuthorizationRequestResolver → state에 redirectUrl 인코딩
  3. 구글 로그인 페이지로 이동
  4. 로그인 완료 → callback URL로 code 반환
  5. Spring Security가 code → access token 교환
  6. user-info 요청 → OAuth2UserService 실행
  7. OAuth2SuccessHandler 실행
    • 회원가입/로그인 처리
    • JWT 생성
    • redirectUrl 디코딩하여 이동

📋 마무리

OAuth2 로그인은 Spring Security 기본 흐름 위에

  • AuthorizationRequestResolver
  • OAuth2UserService
  • OAuth2SuccessHandler
  • JWT 발급 구조
  • redirectUrl state 관리

웹/앱 모두 대응 가능한 안정적인 OAuth2 구조를 만들 수 있다.

LIST