| 일 | 월 | 화 | 수 | 목 | 금 | 토 |
|---|---|---|---|---|---|---|
| 1 | 2 | 3 | ||||
| 4 | 5 | 6 | 7 | 8 | 9 | 10 |
| 11 | 12 | 13 | 14 | 15 | 16 | 17 |
| 18 | 19 | 20 | 21 | 22 | 23 | 24 |
| 25 | 26 | 27 | 28 | 29 | 30 | 31 |
- Websocket
- Strict
- font-size
- 10px
- 0.25px border
- TS
- 1px border
- Props
- angular
- ES5
- 전역변수
- es6
- jwt
- 문서번호
- 클론코딩
- 당근마켓
- 타입스크립트
- 서버리스 #
- github
- entity
- TypeScript
- npm
- literal
- 0.75px border
- 데이터베이스 #try #이중
- 컴포넌튼
- 으
- 0.5px border
- &연산
- ZOOM
- Today
- Total
복잡한뇌구조마냥
[Spring] Spring Security + OAuth2 로그인 본문
스프링 시큐리티를 사용하면 OAuth2 로그인 기능을 매우 간단하게 붙일 수 있지만,
실무에서는 기본 설정만으로는 부족하고 커스텀해야 하는 영역이 꽤 많다.
이번 글에서는 내가 실제로 프로젝트에서 사용한 구조를 기반으로
- OAuth2 로그인 동작 방식
- Spring Security OAuth2 흐름
- AuthorizationRequestResolver 커스텀
- OAuth2SuccessHandler
- Redirect 처리(state 커스텀)
- 액세스/리프레시 토큰 생성 구조
까지 정리해본다.
1. OAuth2 로그인 전체 흐름 이해하기
OAuth2 로그인은 간단히 말하면
"서비스가 직접 비밀번호를 받아 인증하지 않고, 구글/카카오 같은 외부 인증 서버를 통해 사용자 정보를 받아오는 방식"
이다.
전체 흐름은 다음과 같다:
- 사용자가 /oauth2/authorization/google 으로 이동
- Spring Security → AuthorizationRequest 생성
- 구글/카카오 로그인 화면으로 redirect
- 사용자 로그인 완료 후 callback URL로 code 전달
- Spring Security가 code → access token, 사용자 정보 요청
- OAuth2UserService → 사용자 정보 매핑
- OAuth2SuccessHandler → JWT 발급/redirect 처리
- 서비스 자체 인증(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
→ 어떤 사용자 정보에 접근할지 정의 (콘솔에서 동의 항목도 맞춰야 함)
- 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));
}
}
성공 핸들러에서 하는 일 정리
- OAuth2 로그인 성공
- 플랫폼별 사용자 정보 추출
- 우리 서비스의 회원 DB에 존재하는지 확인 → 신규면 가입 처리
- JWT Access + Refresh 토큰 생성
- redirectUrl에 토큰 붙여서 redirect
→ 이 구조는 모바일/웹 모두 대응 가능하며,
현대 OAuth2 기반 서비스에서 가장 많이 쓰는 패턴이다.
7. 전체 플로우 다시 보기


- /oauth2/authorization/google?redirectUrl=/mypage
- CustomAuthorizationRequestResolver → state에 redirectUrl 인코딩
- 구글 로그인 페이지로 이동
- 로그인 완료 → callback URL로 code 반환
- Spring Security가 code → access token 교환
- user-info 요청 → OAuth2UserService 실행
- OAuth2SuccessHandler 실행
- 회원가입/로그인 처리
- JWT 생성
- redirectUrl 디코딩하여 이동
📋 마무리
OAuth2 로그인은 Spring Security 기본 흐름 위에
- AuthorizationRequestResolver
- OAuth2UserService
- OAuth2SuccessHandler
- JWT 발급 구조
- redirectUrl state 관리
웹/앱 모두 대응 가능한 안정적인 OAuth2 구조를 만들 수 있다.
'BE > Spring' 카테고리의 다른 글
| [Spring] JOOQ ( + QueryDSL vs JOOQ ) (0) | 2025.11.21 |
|---|---|
| [Spring] Spring AI ( OpenAI + Rag + MariaDB Vector ) (0) | 2025.11.18 |
| [Spring] JPA + QueryDSL 정리 (0) | 2025.10.02 |
| [Spring] MyBatis 구조 정리 (0) | 2025.08.29 |
| [Spring] Spring Security + JWT 인증 구조 (0) | 2025.08.28 |