로그인 페이지 서버사이드 mvc 구현에서 endpoint 호출로 수정

 

문제

많은 블로그, 유튜브 레퍼런스들이 OAuth2 + JWT를 활용한 로그인 구현에 spring 서버 사이드(mvc) 형태로 해결하고 있다.

나도 프로젝트 초반에 레퍼런스를 따라 진행했기 때문에 로그인 플로우를 웹서버에서 백엔드로 넘어오게 설정하였다.

 

이러한 구조의 문제는 다음과 같다.

  • 백엔드로 불필요한 요청이 넘어온다
  • 확장성이 떨어지고 다양한 클라이언트를 지원할 수 없다.
  • 프론트에서 CDN을 활용해 페이지를 렌더링할 수 없다.

 

해결

@RestController
public class LoginController {

    @GetMapping("/api/login")
    public void login(HttpServletResponse response, @RequestParam String provider) throws IOException {
        String redirectUrl =  DEFAULT_AUTHORIZATION_REQUEST_BASE_URI + "/" + provider;
        response.sendRedirect(redirectUrl);
    }
}

Login 컨트롤러를 생성하고 구글, 카카오, 네이버와 같은 provider를 파라미터로 받는다.

스프링 시큐리티는 OAuth2ClientProperties 로 provider의 공개된 API로 요청을 보낼 때 필요한 값들을 설정한다.

설정된 기본값을 사용하기 위해서 정해진 url로 redirect한다.

 

DEFAULT_AUTHORIZATION_REQUEST_BASE_URI 는

"/oauth2/authorization" 값을 가지는 OAuth2AuthorizationRequestRedirectFilter에서 정의한 상수 값이다.

 

@RequiredArgsConstructor
@Configuration
@EnableWebSecurity
public class OAuth2LoginConfig {

    private final CustomOAuth2UserService oAuth2UserService;
    private final OAuth2SuccessHandler oAuth2SuccessHandler;
    private final JwtAuthenticationFilter jwtAuthenticationFilter;

    @Bean
    public SecurityFilterChain filterChain(HttpSecurity http) throws Exception {
        http.oauth2Login(oauth2Login ->
                oauth2Login
                        .successHandler(oAuth2SuccessHandler)
                        .userInfoEndpoint(userInfoEndpointConfig ->
                                userInfoEndpointConfig.userService(oAuth2UserService)));

        http.sessionManagement((sessionManagement) ->
                sessionManagement.sessionCreationPolicy(SessionCreationPolicy.STATELESS));

        http.addFilterAfter(jwtAuthenticationFilter, OAuth2LoginAuthenticationFilter.class);

        http.authorizeHttpRequests(a ->
                a.requestMatchers("/api/login").permitAll()
                        .anyRequest().authenticated());

        return http.build();
    }
}

만약 모든 엔드포인트를 인증된 채로 접근할 수 있도록 설정했다면 프론트에서 "/api/login"으로 요청을 보낼 수 없기 때문에

스프링 시큐리티 설정을 위처럼 수정한다.

 

번외

 

HttpServletResponse.sendRedirect()의 IOException

 

단순한 url을 받는 sendRedirect가 왜 IOException 예외 처리를 해야하는지 궁금했다.

 

gpt의 답변을 정리하면

HttpServletResponse는 클라이언트로 응답을 보내기 전에 요청을 처리한 데이터를 한번에 보내기 위해서 버퍼에 데이터를 저장하는데

리다이렉션을 수행하기 전에 버퍼를 지우는 과정에서 IOException이 발생할 수 있다는 것이다.

 

다른 해결 시도

처음에는 프론트에서 권한 서버에 요청을 보내고 승인 코드 grant 값을 백엔드로 전달하는 방식으로 해결하려 했다.

하지만 프론트에서 권한 서버로 요청을 직접 보내게 되면 spring에서 [authorization_request_not_found] 에러 메세지로

OAuth2UserService의 loadUser가 실행되기 전에 요청이 종료되는 문제가 있었다.

 

그리고 결정적으로 프론트에서 직접 요청을 보내기 위해서는 client registration에 등록된 secret이 노출되는 문제가 있다.

따라서 로그인 플로우의 화면 렌더링을 제외한 다른 부분은 spring security 기능을 활용하는 것이 가장 편하고 신뢰도가 높다.