본문 바로가기
Spring Security

[OAuth2] OAuth2 로그인 후 SuccessHandler 구현

by 우주물고기 2025. 5. 17.
반응형

 

@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"
  }
}

 

 

 

 

 

 

 

 

 

 

 

 

 

 

반응형