🌀
f1v3-log
  • Welcome
  • 개발
    • SecurityContext를 새로 만들어야 할까?
    • OAuth2AuthorizationRequestResolver 커스터마이징
    • 동시성 문제를 해결해보자
    • MySQL은 어떻게 ID 값을 순차적으로 넣어주는 것일까? (Feat. Auto Increment Lock)
    • 외부 API 호출에 대한 고찰
      • HTTP Clients in Spring Boot
      • I/O와 트랜잭션 분리하기
      • 처리율 제한 장치 (Rate Limiter) 도입
      • 외부 API 의존성을 줄여보자
      • 캐시 레이어를 구성해보자 (Local Cache)
    • JPA Deep Dive
      • 결제 및 정산 시스템 기능 요구사항 분석
      • 글로벌 서비스를 고려할 때, 타임존 이슈를 어떻게 처리해야 할까?
      • Spring Data JPA - ID 생성 전략과 채번은 어떻게 되는걸까?
  • 회고
    • NHN Academy 인증과정 회고
    • DND 11기 회고
  • 독서
    • Effective Java 3/E
      • Item 1. 생성자 대신 정적 팩터리 메서드를 고려하라
      • Item 2. 생성자에 매개변수가 많다면 빌더를 고려하라
      • Item 3. private 생성자나 열거 타입으로 싱글턴임을 보증하라
    • 객체지향의 사실과 오해
      • 1장. 협력하는 객체들의 공동체
      • 2장. 이상한 나라의 객체
      • 3장. 타입과 추상화
      • 4장. 역할, 책임, 협력
      • 5장. 책임과 메시지
      • 6장. 객체 지도
      • 7장. 함께 모으기
  • Real MySQL 8.0
    • 04. 아키텍처
    • 05. 트랜잭션과 잠금
    • 08. 인덱스
    • 09. 옵티마이저와 힌트
  • 생각정리
    • 기술에 매몰되지 말자.
  • 공부
    • 객체지향 5원칙(SOLID)
      • SRP (Single Responsibility Principle)
      • OCP (Open Closed Principle)
Powered by GitBook
On this page
  • 문제 원인
  • 당시 상황
  • 문제 접근
  • 문제 해결
  • 생각해볼 내용

Was this helpful?

  1. 개발

OAuth2AuthorizationRequestResolver 커스터마이징

PreviousSecurityContext를 새로 만들어야 할까?Next동시성 문제를 해결해보자

Last updated 6 months ago

Was this helpful?

문제 원인

카카오 로그인 페이지로 이동할 때, 다음과 같은 요구사항이 추가되었다.

요구사항

  • 권한이 없는 페이지에서 접근하였을 때 로그인 페이지로 이동

  • 로그인 페이지로 이동을 하면서, 이전 페이지에 대한 정보를 같이 보냄

  • 로그인 성공시 이전 페이지를 다시 받아내고 싶음.

위와 같이 쿼리 파라미터 로 전달한 정보를 로그인 성공 후 리다이렉트시, 다시 전송을 원하는 상황이었습니다.

당시 상황

OAuth2 로그인 구현 방법

  • Spring Security OAuth2 를 사용하여 로그인 기능을 구현

  • 단순히 로그인 처리에 대한 엔드 포인트(/api/v1/auth/oauth2/kakao)를 설정

즉, baseUri 로 요청시 카카오 로그인 페이지로 리다이렉트 시키는 방식으로 작동

image

문제 접근

해당 요구사항에 따라 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을 사용하였지만, 로그인 페이지만 이동시킨 후 사용하지 않을 경우에 발생할 문제는 없을까?

  • 다른 방식으로 처리할 수 없는지 궁금함이 남아있음