필터에서 인증 처리를 하여도 권한이 없다는 문제가 발생한다?

문제 상황

UsernamePasswordAuthenticationFilter 를 아래와 같이 구현했다. 인증시도 메소드와 인증 성공 시 메소드를 구현하도록 했다.

public class SessionAuthenticationFilter extends UsernamePasswordAuthenticationFilter {
    private static final Logger logger = LoggerFactory.getLogger(SessionAuthenticationFilter.class);

    @Override
    public Authentication attemptAuthentication(HttpServletRequest request, HttpServletResponse response) throws AuthenticationException {
        String userId = request.getParameter("loginId");
        String userPw = request.getParameter("loginPw");
        String loginType = request.getParameter("loginType");
        logger.info("-------------------------------------------");
        logger.info("userId -> " + userId);
        logger.info("userPw -> " + userPw);
        logger.info("loginType -> " + loginType);
        logger.info("-------------------------------------------");
        return getAuthenticationManager().authenticate(new SessionAuthenticationToken(userId, userPw, loginType));
    }

    @Override
    protected void successfulAuthentication(HttpServletRequest request, HttpServletResponse response, FilterChain chain, Authentication authResult) throws IOException, ServletException {
        SecurityContextHolder.getContext().setAuthentication(authResult);
        logger.info("--------- successfulAuthentication ---------");
        System.out.println(authResult.getPrincipal());
        System.out.println(authResult.getDetails());
        System.out.println(authResult.isAuthenticated());
        logger.info("--------- successfulAuthentication ---------");
        chain.doFilter(request, response);
    }

}

문제는 이렇게 구현해서 필터를 등록한 뒤, 해당 필터를 거쳐서 인증 시도를 하고 2개 메소드를 통과하였으나 결과적으로는 로그인 처리가 되지 않았고, Security Context에 인증 정보가 등록되지 않았다.

필터 등록은 아래와 같이 했다.

.addFilterAt(sessionAuthenticationFilter(), UsernamePasswordAuthenticationFilter.class)

addFilterAt은 마치 해당 필터를 대신하여 동작하게끔 설정하고 대체된 필터를 동작이 안하는 것 처럼 보이지만 실제로는 해당 필터 전에 실행하고 다음에 해당 필터를 실행하도록 되어있다, 단 적용한 필터에서 인증이 성공하면 실행을 하지 않고 넘어갈 뿐이다.

여기를 참고

해결 과정

우선, 필터에서 인증 요청을 하게되면 어떤 순서로 메소드가 진행되는지 살펴보았다. 코어 라이브러리의 UsernamePasswordAuthenticationFilterAbstractAuthenticationProcessingFilter 를 확장한다.

내부에는 인증 시도 메소드가 아래와 같이 존재한다. 나는 이 메소드를 재정의하려고 했다.

public Authentication attemptAuthentication(HttpServletRequest request, HttpServletResponse response) throws AuthenticationException {
        if (this.postOnly && !request.getMethod().equals("POST")) {
            throw new AuthenticationServiceException("Authentication method not supported: " + request.getMethod());
        } else {
            String username = this.obtainUsername(request);
            username = username != null ? username.trim() : "";
            String password = this.obtainPassword(request);
            password = password != null ? password : "";
            UsernamePasswordAuthenticationToken authRequest = UsernamePasswordAuthenticationToken.unauthenticated(username, password);
            this.setDetails(request, authRequest);
            return this.getAuthenticationManager().authenticate(authRequest);
        }
    }

인증시도 메소드는 별 다를게 없어서, 확장 메소드의 원래 클래스로 이동하여 doFilter 구조를 살펴보았다.

AbstractAuthenticationProcessingFilterdoFilter 구조는 다음과 같다.

private void doFilter(HttpServletRequest request, HttpServletResponse response, FilterChain chain) throws IOException, ServletException {
        if (!this.requiresAuthentication(request, response)) {
            chain.doFilter(request, response);
        } else {
            try {
                // 내가 재정의하려고 했던 메소드인 인증 시도 메소드
                Authentication authenticationResult = this.attemptAuthentication(request, response);
                // 인증 시도 후 결과에 대한 NULL Check
                if (authenticationResult == null) {
                    return;
                }

                // 세션 정책에 따른 처리 같다
                this.sessionStrategy.onAuthentication(authenticationResult, request, response);
                // 이전 단계에서 인증이 이미 된 경우, 다음 필터 진행.
                if (this.continueChainBeforeSuccessfulAuthentication) {
                    chain.doFilter(request, response);
                }

                // 여기까지 문제 없으면 successfulAuthentication를 실행한다.
                this.successfulAuthentication(request, response, chain, authenticationResult);
            } catch (InternalAuthenticationServiceException var5) {
                InternalAuthenticationServiceException failed = var5;
                this.logger.error("An internal error occurred while trying to authenticate the user.", failed);
                this.unsuccessfulAuthentication(request, response, failed);
            } catch (AuthenticationException var6) {
                AuthenticationException ex = var6;
                this.unsuccessfulAuthentication(request, response, ex);
            }

        }
    }

Authentication authenticationResult = this.attemptAuthentication(request, response); 에서 실제 인증 시도를 하고 authenticationResult 의 null 체크와, 이전 필터에서 이미 인증이 성공 처리되면 다음 필터를 진행하는 구문이 있다.

이후, 문제가 없으면 successfulAuthentication를 수행한다. 따라가보자.

protected void successfulAuthentication(HttpServletRequest request, HttpServletResponse response, FilterChain chain, Authentication authResult) throws IOException, ServletException {
    // 인증 정보 저장을 위한 공간 확보
    SecurityContext context = SecurityContextHolder.createEmptyContext();
    // 인증 결과를 세팅함
    context.setAuthentication(authResult);
    SecurityContextHolder.setContext(context);
    this.securityContextRepository.saveContext(context, request, response);
    // 로그 찍고 ...
    if (this.logger.isDebugEnabled()) {
        this.logger.debug(LogMessage.format("Set SecurityContextHolder to %s", authResult));
    }
    // RememberMe 서비스 호출 하고 ...
    this.rememberMeServices.loginSuccess(request, response, authResult);
    if (this.eventPublisher != null) {
        this.eventPublisher.publishEvent(new InteractiveAuthenticationSuccessEvent(authResult, this.getClass()));
    }
    // 성공 핸들러에 의해 다음이 진행된다.
    this.successHandler.onAuthenticationSuccess(request, response, authResult);
}

위 내용을 보면 내가 원래 필터에서 구현한 successfulAuthentication의 동작과는 다르다. 즉, 인증이 정상 종료되었다면 해당 필터에서 다음 필터로 진행되지 않도록 doFilter가 아닌, 기본 구현체에서 성공 시 동작을 하는 함수를 실행 시키거나 SecurityContext에 해당 인증 정보를 저장하고 리다이렉트를 시켜야 한다.

또한 이 때문에 계속 오류가 발생했던 userDetailService가 호출되던 것도 더 이상 호출되지 않았고 (다음 필터인 UsernamePasswordAuthenticationFilter가 동작 안하니까)

실제로 구현할 필요도 없어졌다.

구글링의 파편화 된 정보 검색과 그에 따른 결과다 :(

추가적으로 알게 된 사실

  1. 운영 상에서는 보안 상 인증 정보에 인증이 완료되면 credential에 패스워드를 저장하지않고 null로 세팅한다.
  2. 각 인증 방식(form Login, http)에 따라 매니저가 동작한다. 따라서 인증 과정 내 다른 인증 방식이 끼어들 여지가 있는 URL이 있고 그게 동작하면 다른 매니저가 인증 시도를 한다.
  3. 인증이 완료된 후, 인증 필터에서 Token을 리턴하는데 이 떄 그냥 만들고 리턴하면 되는게 아니라, 실제 인증 후처리 과정 및 인증 처리를 위한 작업을 해주어야 한다.

예를 들어, 기존 인증 필터에서는 인증 완료 후 아래와 같이 리턴했는데, 인증 처리가 되지 않았다.

// 인증 완료!
return new SessionAuthenticationToken(authUserDM.getUid(), null, authUserDM.getUserType(), true);

해당 필터는 UsernamePasswordAuthenticationFilter를 구현했기 때문에 UsernamePasswordAuthenticationToken을 리턴해야 했다. 그래서 토큰 쪽을 살펴보니. 아래와 같은 생성자를 통해 생성하는 경우, 인증 플래그가 true로 세팅된다.

public UsernamePasswordAuthenticationToken(Object principal, Object credentials,  
       Collection<? extends GrantedAuthority> authorities) {  
    super(authorities);  
    this.principal = principal;  
    this.credentials = credentials;  
    super.setAuthenticated(true); // must use super, as we override  
}

그래서 권한까지 세팅하는 생성자를 만들어서, 위 생성자를 실행 하도록 했다.

public SessionAuthenticationToken(String userId, String userPw, String loginType, boolean isSuccessAuthentication) {  
    super(userId, null, null);  // 이 상위 호출을 통해 인증 완료된 객체로 생성한다.
    super.setDetails(loginType);  
    this.adminId = userId;  
    this.adminPw = userPw;  
    this.loginType = loginType;  
}

© 2024. Chiptune93 All rights reserved.