문제 원인
카카오 로그인 페이지로 이동할 때, 다음과 같은 요구사항이 추가되었다.
요구사항
권한이 없는 페이지에서 접근하였을 때 로그인 페이지로 이동
로그인 페이지로 이동을 하면서, 이전 페이지에 대한 정보를 같이 보냄
로그인 성공시 이전 페이지를 다시 받아내고 싶음.
위와 같이 쿼리 파라미터 로 전달한 정보를 로그인 성공 후 리다이렉트시, 다시 전송을 원하는 상황이었습니다.
당시 상황
OAuth2 로그인 구현 방법
Spring Security OAuth2 를 사용하여 로그인 기능을 구현
단순히 로그인 처리에 대한 엔드 포인트(/api/v1/auth/oauth2/kakao
)를 설정
즉, baseUri 로 요청시 카카오 로그인 페이지로 리다이렉트 시키는 방식으로 작동
문제 접근
해당 요구사항에 따라 baseUri에 요청할 때 쿼리 파라미터로 넘어온 값에 대한 처리가 필요하다고 판단하였습니다.
oauth2Login에서 authorizationEndpoint
를 커스터마이징하는 방법은 없나? 라는 생각으로 설정할 수 있는 클래스를 검색
authorizationRequestResolver : 클라이언트가 인증 요청을 할 때 해당 요청을 변환하거나 수정하는 역할
authorizationRequestRepository : 클라이언트가 인증 요청을 보내고 나서 그 요청 정보를 저장하고 관리하는 역할
authorizationRedirectStrategy : 인증 요청 후, OAuth2 인증 서버로 리다이렉트할 때의 전략을 정의
위 3가지는 OAuth2 인증 과정의 주요 컴포넌트로서 필요에 따라 커스터마이징이 가능합니다.
이 중, authorizationRequestResolver 를 통해 로그인 요청 엔드포인트에 클라이언트가 요청을 보낼 때 쿼리 파라미터를 파싱하고 세션에 저장하는 방식으로 접근하면 좋을 것 같아 ChatGPT 를 활용해 보았습니다.
문제 해결
DefaultRequestResolver
의 방식대로 구현을 하되, 요청에서 redirect
쿼리 파라미터에 대한 정보만 세션에 저장
로그인 성공시 동작하는 OAuth2SuccessHandler
에서 세션에서 redirect
정보를 가져온 후 클라이언트에게 전달
위의 두 방식을 통해 변경된 요구사항에 맞게 커스터마이징을 진행
DeafultOAuth2AuthorizationRequestResolver 방식을 따라 기존에 환경 변수를 통해 Spring Security가 처리해주던 동작을 그대로 구현하기 위해서 사용하였습니다.
CustomAuthorizationRequestResolver.java
@Component
public class CustomAuthorizationRequestResolver implements OAuth2AuthorizationRequestResolver {
private static final String AUTHORIZATION_REQUEST_BASE_URI = "/api/v1/auth/oauth2";
private static final String QUERY_PARAM = "redirect";
private final OAuth2AuthorizationRequestResolver defaultAuthorizationRequestResolver;
public CustomAuthorizationRequestResolver(ClientRegistrationRepository clientRegistrationRepository) {
this.defaultAuthorizationRequestResolver =
new DefaultOAuth2AuthorizationRequestResolver(clientRegistrationRepository, AUTHORIZATION_REQUEST_BASE_URI);
}
@Override
public OAuth2AuthorizationRequest resolve(HttpServletRequest request) {
OAuth2AuthorizationRequest authorizationRequest = defaultAuthorizationRequestResolver.resolve(request);
return customizeAuthorizationRequest(request, authorizationRequest);
}
@Override
public OAuth2AuthorizationRequest resolve(HttpServletRequest request, String clientRegistrationId) {
OAuth2AuthorizationRequest authorizationRequest = defaultAuthorizationRequestResolver.resolve(request, clientRegistrationId);
return customizeAuthorizationRequest(request, authorizationRequest);
}
/**
* 쿼리 파라미터에서 redirect 값을 추출하여 세션에 저장하는 메서드입니다.
*
* @param request HttpServletRequest
* @param authorizationRequest OAuth2AuthorizationRequest
* @return OAuth2AuthorizationRequest
*/
private OAuth2AuthorizationRequest customizeAuthorizationRequest(HttpServletRequest request, OAuth2AuthorizationRequest authorizationRequest) {
if (authorizationRequest == null) {
return null;
}
String redirect = request.getParameter(QUERY_PARAM);
if (redirect != null) {
request.getSession().setAttribute(QUERY_PARAM, redirect);
}
return authorizationRequest;
}
}
OAuth2SuccessHandler.java
@Component
@RequiredArgsConstructor
public class OAuth2SuccessHandler extends SimpleUrlAuthenticationSuccessHandler {
private static final String REFRESH_TOKEN_NAME = "refresh_token";
private static final String QUERY_PARAM = "redirect";
private final JjakkakProperties jjakkakProperties;
private final JwtProvider jwtProvider;
@Override
public void onAuthenticationSuccess(HttpServletRequest request, HttpServletResponse response, Authentication authentication) throws IOException, ServletException {
Member oauth2User = (Member) authentication.getPrincipal();
String kakaoId = Long.toString(oauth2User.getKakaoId());
String refreshToken = jwtProvider.createRefreshToken(kakaoId);
ResponseCookie refreshCookie = createCookie(REFRESH_TOKEN_NAME, refreshToken, 60 * 60 * 24 * 7);
response.addHeader(HttpHeaders.SET_COOKIE, refreshCookie.toString());
String redirectUrl = getRedirectUrl(request);
response.sendRedirect(redirectUrl);
}
/**
* 리다이렉트 URL 을 반환하는 메서드입니다.
*
* @param request HttpServletRequest
* @return 리다이렉트 URL
*/
public String getRedirectUrl(HttpServletRequest request) {
String baseUrl = jjakkakProperties.getFrontUrl() + "/login/success";
HttpSession session = request.getSession();
String redirectParam = (String) session.getAttribute(QUERY_PARAM);
session.removeAttribute(QUERY_PARAM);
return (redirectParam != null)
? baseUrl + "?redirect=" + redirectParam
: baseUrl;
}
/**
* 쿠키를 생성하는 메서드입니다.
*
* @param name 쿠키 이름
* @param value 쿠키 값
* @param maxAge 쿠키 만료 시간
* @return 생성된 쿠키
*/
private ResponseCookie createCookie(String name, String value, int maxAge) {
return ResponseCookie.from(name, value)
.secure(true)
.sameSite("None")
.httpOnly(true)
.path("/")
.maxAge(maxAge)
.build();
}
}
로그인 실패시 동작하는 OAuth2FailureHandler
에서도 세션에 저장된 정보를 제거해줘야 함!
생각해볼 내용
요구사항을 만족하기 위해 Session을 사용하였지만, 로그인 페이지만 이동시킨 후 사용하지 않을 경우에 발생할 문제는 없을까?
다른 방식으로 처리할 수 없는지 궁금함이 남아있음