[Spring Security] Authentication 이후 첫 요청에서 원래 보내야 하는 응답 보내기

lsjbh45·2022년 8월 28일
3

이전 글에서 Spring Security의 기본 설정에 따라 인증에 성공하게 되면 메인 페이지로의 redirect가 발생하게 됨을 알 수 있다. 이번 글에서는 이어서 Spring Security의 설정을 변경해서 메인 페이지로의 redirect를 강제하지 않고 요청에 대해 인증 이후 원래 보내야 하는 response를 보내는 방법을 찾아보고자 한다.

Custom SuccessHandler class 만들어 등록하기

public class OriginalRequestAuthenticationSuccessHandler extends SimpleUrlAuthenticationSuccessHandler {
	@Override
    public void onAuthenticationSuccess(HttpServletRequest request, HttpServletResponse response,
    		Authentication authentication) throws IOException, ServletException {
    	/* Implementation */
    }
}
@Configuration
@EnableWebSecurity
public class SecurityConfiguration extends WebSecurityConfigurerAdapter {
	/* ... */
    
    @Bean
    public OriginalRequestAuthenticationSuccessHandler originalRequestAuthenticationSuccessHandler() {
	    return new OriginalRequestAuthenicationSuccessHandler();
    }

	@Bean
    public SSOAuthenticationFilter ssoAuthFilter() {
    	SSOAuthenticationFilter ssoAuthFilter = new SSOAuthenticationFilter(ssoServiceProviders());
        ssoAuthFilter.setAuthenticationManager(authenticationManager());
        
        ssoAuthFilter.setAuthenticationSuccessHandler(original RequestAuthenticationSuccessHandler());
        
    	return ssoAuthFilter;
    }
}

원래 security configuration class에서는 Spring Security의 인증 과정을 구현하기 위해서 abstract class인 AbstractAuthenticationProcessingFilter를 상속받아 내부적인 동작을 구현한 구현체인 SSOAuthenticationFilter class를 bean으로 등록해서 Spring Security의 filter chain에 연결하여 사용하고 있었다.
당연히 AuthenticationSuccessHandler를 따로 지정하지 않았기 때문에 로 default인 SavedRequestAwareAuthenticationSuccessHandler instance가 사용되면서 이전 글의 문제가 발생하는 상황이다.

이전 글에서 분석해 보았듯이 filter에서 호출되는AuthenticationSuccessHandler class의 onAuthenticationSuccess method가 인증 성공 후의 작업을 수행하므로, custom successHandler class를 만들어onAuthenticationSuccess method를 구현, filter에서 호출하도록 지정해주면 authentication 이후의 작업을 통제할 수 있다. 먼저 SimpleUrlAuthenticationSuccessHandler class를 상속받아 onAuthenticationSuccess method를 구현할 예정인 custom success handler class인 OriginalRequestAuthenticationSuccessHandler를 작성해 준 뒤 Security Configuration class에 bean으로 등록해준다. 그리고 SSOAuthenticationFilter class를 bean으로 등록할 때 이 설정을 변경해서 setAuthenticationSuccessHandler method를 호출하도록 해주면 SSOAuthenticationFilter가 사용하는 AuthenticationSuccessHandler instance를 custom success handler instance로 변경할 수 있다.

Empty onAuthenticationSuccess Method

public class OriginalRequestAuthenticationSuccessHandler extends SimpleUrlAuthenticationSuccessHandler {
    @Override
	public void onAuthenticationSuccess(HttpServletRequest request, HttpServletResponse response,
			Authentication authentication) throws IOException, ServletException {

	}
}

만약 custom successHandler class의 onAuthenticationSuccess method를 구현하지 않은 빈 상태로 두면 인증 후에 어떤 일이 발생할까? 인증 후에 default url로의 redirect을 수행하지 않기 때문에 원래 요청이 그대로 진행된다고 생각할 수도 있겠으나, 실제로 실행해 보면 그렇지 않다는 사실을 확인할 수 있다.

AbstractAuthenticationProcessingFilter의 구현을 보면, doFilter method에서 requiresAuthentication을 확인한 뒤에 인증이 필요한 경우 attemptAuthentication method를 호출해 인증을 수행하는데, 인증 수행 이후에 더 이상 filter chain을 넘어가지 않는다.
확인한 requiresAuthentication 값이 false라면 chain.doFilter method를 호출하지 않기 때문이다. 따라서 이 경우에는 기존의 request가 security filter chain의 처리 과정을 끝까지 지나지 못해 spring context의 dispatcher servlet에 전달되지 않는다. 다시 말해 인증만 수행 완료된 상태로 empty response 200 OK를 반환하게 되는 것이다. 실제로 debugging을 통해 dispatcher servlet class의 doService method에 대해 break point를 잡아 보아도 해당 method가 실행되지 않는 것을 확인할 수 있다.

만약 인증 직후의 API call에 대해서는 request의 내역에 관계 없이 인증 자체가 제대로 되었는지 여부만을 response로 확인하길 원한다면 이 상태로도 사용할 수 있고, 실제로도 인증 방식을 어떻게 합의했는지에 따라 이 방식을 구현해 사용하는 경우도 존재한다. 하지만 인증 이후에 실제로 API call이 진행되기를 원한다면, AuthenticationProcessingFilter 단에서 forwarding 또는 redirect 처리를 구현해서 dispatcher servlet에 request 정보를 전달해 주어야 한다. 개발하던 웹 어플리케이션의 사용자 인증은 여타 방식처럼 따로 로그인 페이지를 통해 로그인을 진행하는 것이 아니라 SSO 인증 방식을 기반으로 했기 때문에 인증되지 않은 상태에서 resource에 바로 접근하더라도 인증 후에 접근 허용이 이루어져야 한다고 합의했기 때문에 forwarding 또는 redirect 처리를 구현해 보아야 했다.

Forwarding 구현

public class OriginalRequestAuthenticationSuccessHandler extends SimpleUrlAuthenticationSuccessHandler {
    protected void handle(HttpServletRequest request, HttpServletResponse response, Authentication authentication)
			throws IOException, ServletException {
        String targetUrl = request.getServletPath();
        
        RequestDispatcher requestDispatcher = request.getRequestDispatcher(targetUrl);
    	requestDispatcher.forward(request, response);
    }
    
    @Override
	public void onAuthenticationSuccess(HttpServletRequest request, HttpServletResponse response,
			Authentication authentication) throws IOException, ServletException {
		this.handle(request, response, authentication);
		super.clearAuthenticationAttributes(request);
	}
}

ServletRequest에서 forwarding할 servlet path를 지정해 추출한 RequestDispatcherforward method로 forwarding을 구현할 수 있다. 언뜻 확인해 본다면 잘 작동하는 것처럼 보이겠지만, 이 방식에는 인증 이후에 forwarding된 request를 대상으로 한 권한 확인이 이루어지지 않는다는 문제가 존재한다. 일반적인 HTTP request나 redirect 방식에서의 재요청과 달리, forwarding 방식은 client와의 통신 과정 없이 서버 내부에서 동일한 컨테이너 차원의 처리가 이루어진다. forwarding된 url로의 요청 과정에서는 기본적으로 Spring Security의 filter chain이 작동하지 않는 것이다. 요청 대상이 될 수 있는 resource에 접근 제한이 필요한 상황이라면 별도의 설정 없이는 사용하기 어려운 방식이다.

redirect 구현

public class OriginalRequestAuthenticationSuccessHandler extends SimpleUrlAuthenticationSuccessHandler {
	RedirectStrategy redirectStrategy = new DefaultRedirectStrategy();
    
    protected void handle(HttpServletRequest request, HttpServletResponse response, Authentication authentication)
			throws IOException, ServletException {
        String targetUrl = request.getRequestURI();
        
    	if (response.isCommitted()) {
        	super.logger.debug("Response has already been committed. Unable to redirect to " + targetUrl);
        } else {
        	redirectStrategy.sendRedirect(request, response, targetUrl);
        }
    }
    
    @Override
	public void onAuthenticationSuccess(HttpServletRequest request, HttpServletResponse response,
			Authentication authentication) throws IOException, ServletException {
		this.handle(request, response, authentication);
		super.clearAuthenticationAttributes(request);
	}
}

한편, redirectStrategysendRedirect method를 호출하는 방식으로는 redirect를 구현할 수 있다. 이 방식으로도 인증 후의 resource 접근이 잘 되는지 확인을 해 보았는데, GET 방식의 요청은 잘 이루어지지만, POSTGET 이외의 방식의 요청은 제대로 된 응답을 얻지 못하는 문제가 있었다. 이는 내부적으로 redirction의 구현이 response의 status로 302 Found를 사용하도록 되어있기 때문이다. 302 방식의 경우 client에서는 서버에서 응답한 location에 대해 이전 http method와 무관하게 GET 방식으로 변경해서 재요청하도록 정해져 있다. 다른 http method 요청이 GET 방식으로 강제로 변경되면서 잘못된 응답이 response로 반환되는 문제가 발생하는 것이다. 일반적으로 인증이 이루어지는 시점의 요청은 GET 방식인 경우가 대부분이기 때문에 GET 방식의 요청에 대해서만 redirect를 통해 resource를 반환하고, POSTPUT, DELETE 등의 resource 수정 요청에 대해서는 메인 페이지로의 redirect를 제공하는 경우도 있다.

307 Temporary Redirect 사용

public class OriginalRequestAuthenticationSuccessHandler extends SimpleUrlAuthenticationSuccessHandler {
    protected void handle(HttpServletRequest request, HttpServletResponse response, Authentication authentication)
			throws IOException, ServletException {
        HttpStatus targetStatus = HttpStatus.TEMPORARY_REDIRECT;
        String targetUrl = request.getRequestURI();
        String queryString = Optional.ofNullable(request.getQueryString()).orElseGet(String::new);

    	if (response.isCommitted()) {
        	super.logger.debug("Response has already been committed. Unable to redirect to " + targetUrl);
        } else {
            response.setStatus(targetStatus.value());
            response.setHeader("Location", targetUrl + queryString);
        }
    }
    
    @Override
	public void onAuthenticationSuccess(HttpServletRequest request, HttpServletResponse response,
			Authentication authentication) throws IOException, ServletException {
		this.handle(request, response, authentication);
		super.clearAuthenticationAttributes(request);
	}
}

Http status code 중 302 Found307 Temporary Redirect 간에는 307 Temporary Redirect의 경우 redirect된 request를 보낼 때 method와 body 정보가 유지됨을 보장한다는 차이만이 존재한다. 내부적으로 구현에 사용된 302 Found 대신 307 Temporary Redirect 방식을 사용해 앞서 발생한 문제를 해결할 수 있는 것이다. 구현에는 response 자체에 대해 status와 location header를 지정하는 방식을 사용했다. 이때 redirectStrategy를 이용하는 방식과 달리 query parameter들이 자동으로 포함되지 않는다는 점을 해결하기 위해 queryString을 포함해 location을 지정해야 했다.

이 글에서는 인증 성공 이후 모든 요청을 그대로 진행하기 위한 구현 방식을 다루었지만, custom successHandler class의 구현을 통해 인증 성공 시점의 요청에 대한 response를 개발하고자 하는 웹 어플리케이션의 상황에 맞게 지정할 수 있다. 따로 다루지는 않았지만, 참조한 글 중에는 로그인 페이지를 사용하는 경우 인증 이후 로그인 페이지에 접근하기 이전 페이지로 redirect하도록 구현한 경우도 다수 있었다. 아직 부족해서 response를 직접 control하는 방식으로 구현했는데, 기회가 된다면 내부 구현을 참조해서 더 깔끔한 pattern으로 구현해 보면 좋을 것 같다.

profile
개발을 공부하며 깊게 고민했던 트러블슈팅 과정을 공유하고자 합니다.

0개의 댓글