@RequiredArgsConstructor
@Component
public class OAuth2SuccessHandler extends SimpleUrlAuthenticationSuccessHandler {
public static final String REFRESH_TOKEN_COOKIE_NAME = "refresh_token";
public static final Duration REFRESH_TOKEN_DURATION = Duration.ofDays(14);
public static final Duration ACCESS_TOKEN_DURATION = Duration.ofDays(1);
public static final String REDIRECT_PATH = "/articles";
private final TokenProvider tokenProvider;
private final RefreshTokenRepository refreshTokenRepository;
private final OAuth2AuthorizationRequestBasedOnCookieRepository authorizationRequestRepository;
private final UserService userService;
@Override
public void onAuthenticationSuccess(HttpServletRequest request, HttpServletResponse response, Authentication authentication) throws IOException {
// 인증된 유저 객체 추출.
OAuth2User oAuth2User = (OAuth2User) authentication.getPrincipal();
// 우리 DB의 사용자 조회
User user = userService.findByEmail((String) oAuth2User.getAttributes().get("email"));
// 리프레시 토큰 생성 -> 저장 -> 쿠키에 저장
String refreshToken = tokenProvider.generateToken(user, REFRESH_TOKEN_DURATION);
saveRefreshToken(user.getId(), refreshToken);
addRefreshTokenToCookie(request, response, refreshToken);
// 엑세스 토큰 생성 -> path 에 엑세스 토큰 추가
String accessToken = tokenProvider.generateToken(user, ACCESS_TOKEN_DURATION);
String targetUrl = getTargetUrl(accessToken);
// 인증 관련 설정값, 쿠키 제거
clearAuthenticationAttributes(request, response);
// 리다이렉트
getRedirectStrategy().sendRedirect(request, response, targetUrl);
}
// 생성된 리프레시 토큰을 전달받아 DB에 저장
private void saveRefreshToken(Long userId, String newRefreshToken) {
RefreshToken refreshToken = refreshTokenRepository.findByUserId(userId)
.map(entity -> entity.update(newRefreshToken))
.orElse(new RefreshToken(userId, newRefreshToken));
refreshTokenRepository.save(refreshToken);
}
// 생성된 리프레시 토큰을 쿠키에 저장
private void addRefreshTokenToCookie(HttpServletRequest request, HttpServletResponse response, String refreshToken) {
int cookieMaxAge = (int) REFRESH_TOKEN_DURATION.toSeconds();
CookieUtil.deleteCookie(request, response, REFRESH_TOKEN_COOKIE_NAME);
CookieUtil.addCookie(response, REFRESH_TOKEN_COOKIE_NAME, refreshToken, cookieMaxAge);
}
// 인증 관련 설정값, 쿠키 제거
private void clearAuthenticationAttributes(HttpServletRequest request, HttpServletResponse response) {
super.clearAuthenticationAttributes(request);
authorizationRequestRepository.removeAuthorizationRequestCookie(request, response);
}
private String getTargetUrl(String token) {
return UriComponentsBuilder.fromUriString(REDIRECT_PATH)
.queryParam("token", token)
.build()
.toUriString();
}
}
우선 이 SuccessHandler가 언제 쓰이는지부터 보면
Spring Security가 인증 성공 후,
이 클래스의 onAuthenticationSuccess()가 호출됨.
즉,
카카오한테 access_token받은 다음, 다시 카카오한테 사용자 정보를 요청
-> OAuth2UserService가 받은 유저 정보를 가지고 OAuth2User 객체를 만듦
-> 이 시점에 인증 성공으로 판단하고
-> OAuth2AuthenticationToken객체를 만듦
---> 그 결과 Spring은 "이 사람은 인증된 사용자야"라고 판단,
AuthenticationSuccessHandler 호출!
우리 서버에서 JWT accessToken 생성하는건
OAuth2SuccessHandler에서 직접함.
String accessToken = tokenProvider.generateToken(user, ACCESS_TOKEN_DURATION);
그리고 이 토큰을 프론트한테 보내기 위해서
URL에 붙임.
String targetUrl = getTargetUrl(accessToken); // → /articles?token=xxx
accessToken은 URL쿼리 파라미터에
Location: /articles?token=xxx
이렇게 담기고
refreshToken은 HttpOnly 쿠키로 전달되고
위치는 Set-Cookie헤더 안의 refresh_token=...
++
쿠키는, Http헤더임.
HTTP는 기본적으로 헤더 + 바디 구조.
쿠키는 헤더에 실려서 주고받아지는 값임.
즉
access token은 Location 헤더에 쿼리 파라미터로
refresh token은 Set-Cookie 헤더에 쿠키로
--> 둘 다 HttpServletResponse 객체로 클라이언트에게 전송됨.
HTTP/1.1 302 Found
Location: https://frontend.com/articles?token=eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9... <-- access token
Set-Cookie: refresh_token=eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9...;
Max-Age=1209600;
Path=/;
HttpOnly;
Secure;
SameSite=Lax
Content-Type: application/json
Content-Length: 123
근데 보통 AccessToken은 HTTP 헤더의 Authorization Bearer~ 이렇게 담고
refreshToken은 HttpOnly 속성 붙여서 쿠키에 넣어서 보냄.
HTTP/1.1 200 OK
Content-Type: application/json
Set-Cookie: refresh_token=eyJhbGciOiJIUzI1NiIsInR5cCI6...; Max-Age=1209600; Path=/; HttpOnly; Secure; SameSite=Lax
Content-Length: 202
{
"accessToken": "eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9...",
"user": {
"id": 123,
"email": "user@example.com",
"nickname": "john_doe"
}
}
'Spring Security' 카테고리의 다른 글
| [OAuth2] 내가 보려고 만든 OAuth2 흐름 정리 (1) | 2025.05.16 |
|---|---|
| [Spring Security] 쿠키 vs 헤더로 JWT 보내기 (0) | 2025.05.15 |
| [Spring Security] Refresh Token 기반으로 new AccessToken받는 API 구현하기 (1) | 2025.05.05 |
| [Spring Security] Token Filter 구현하기 (0) | 2025.05.05 |
| [Spring Security] SecurityContextHolder란? (0) | 2025.05.05 |