필터에서 인증 처리를 하여도 권한이 없다는 문제가 발생한다?
문제 상황
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은 마치 해당 필터를 대신하여 동작하게끔 설정하고 대체된 필터를 동작이 안하는 것 처럼 보이지만 실제로는 해당 필터 전에 실행하고 다음에 해당 필터를 실행하도록 되어있다, 단 적용한 필터에서 인증이 성공하면 실행을 하지 않고 넘어갈 뿐이다.
해결 과정
우선, 필터에서 인증 요청을 하게되면 어떤 순서로 메소드가 진행되는지 살펴보았다. 코어 라이브러리의 UsernamePasswordAuthenticationFilter
는 AbstractAuthenticationProcessingFilter
를 확장한다.
내부에는 인증 시도 메소드가 아래와 같이 존재한다. 나는 이 메소드를 재정의하려고 했다.
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
구조를 살펴보았다.
AbstractAuthenticationProcessingFilter
의 doFilter
구조는 다음과 같다.
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가 동작 안하니까)
실제로 구현할 필요도 없어졌다.
구글링의 파편화 된 정보 검색과 그에 따른 결과다 :(
추가적으로 알게 된 사실
- 운영 상에서는 보안 상 인증 정보에 인증이 완료되면 credential에 패스워드를 저장하지않고 null로 세팅한다.
- 각 인증 방식(form Login, http)에 따라 매니저가 동작한다. 따라서 인증 과정 내 다른 인증 방식이 끼어들 여지가 있는 URL이 있고 그게 동작하면 다른 매니저가 인증 시도를 한다.
- 인증이 완료된 후, 인증 필터에서 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;
}