- filter chain은 연쇄적으로 일어나며, 앞선 filter의 결과물이 뒤의 filter에 injection되어 적용되는 방식이다.
- 다음과 같이 filterchainproxy에서 가상의 filterchain을 만들어서 차례로 실행한다.
실습
- 실습을 위해 다음과 같이 debug모드로설정한다.
- 참고로 debug모드에서는 유저의 session id 등 중요 정보가 콘솔에 노출될 수 있으니, 실제로는 절대로 사용하면 안된다.
- application.properties
custom filter만들기
- 다음과 같이 filter 인터페이스를 구현해 filter class를 생성한다.
package com.eazybytes.filter;
import static org.springframework.http.HttpHeaders.AUTHORIZATION;
import java.io.IOException;
import java.nio.charset.Charset;
import java.nio.charset.StandardCharsets;
import java.util.Base64;
import org.springframework.security.authentication.BadCredentialsException;
import org.springframework.util.StringUtils;
import jakarta.servlet.Filter;
import jakarta.servlet.FilterChain;
import jakarta.servlet.ServletException;
import jakarta.servlet.ServletRequest;
import jakarta.servlet.ServletResponse;
import jakarta.servlet.http.HttpServletRequest;
import jakarta.servlet.http.HttpServletResponse;
public class RequestValidationBeforeFilter implements Filter {
public static final String AUTHENTICATION_SCHEME_BASIC= "Basic";
private Charset credentialsCharset = StandardCharsets.UTF_8;
@Override
public void doFilter(ServletRequest request, ServletResponse response, FilterChain chain)
throws IOException, ServletException {
HttpServletRequest req = (HttpServletRequest) request;
HttpServletResponse res = (HttpServletResponse) response;
String header = req.getHeader(AUTHORIZATION);
if(header != null) {
header = header.trim();
if(StringUtils.startsWithIgnoreCase(header, AUTHENTICATION_SCHEME_BASIC)) {
byte[] base64Token = header.substring(6).getBytes(StandardCharsets.UTF_8);
byte[] decoded;
// basic 무시하기 위해서 6자리부터 시작
try {
decoded = Base64.getDecoder().decode(base64Token);
String token = new String(decoded, credentialsCharset);
int delim = token.indexOf(":");
if(delim == -1) {
throw new BadCredentialsException("Invalid basic authentication token");
}
String email = token.substring(0, delim);
if(email.toLowerCase().contains("test")) {
res.setStatus(HttpServletResponse.SC_BAD_REQUEST);
return;
}
} catch(IllegalArgumentException e) {
throw new BadCredentialsException("Failed to decode basic authentication token");
}
}
}
chain.doFilter(request, response);
}
}
- 보안에 활용되는 AUTHORIZATION 헤더를 가져온 뒤, basic으로 시작하는지 즉, basic authentication token인지를 검사한다.
- 맞을 경우 해당 token을 추출해서 decode하는데, base authentication은 기본적으로 user:password 형태로 저장되므로 해당 형식에 맞게 token을 잘라서 해당 값을 확인한다.
- 이 형식이 적용되기 위해서는 유저가 header에 다음과 같은 형태로 전달할 필요가 있다.
httpHeaders.append('Authorization', 'Basic' + window.btoa(this.user.email + ':' + this.user.password));
- chain.doFilter(request, response)를 통해서 다음 filterchain과 연결한다.
- 다음과 같이 filter들을 추가로 생성한다.
package com.eazybytes.filter;
import java.io.IOException;
import java.util.logging.Logger;
import org.springframework.security.core.Authentication;
import org.springframework.security.core.context.SecurityContextHolder;
import jakarta.servlet.Filter;
import jakarta.servlet.FilterChain;
import jakarta.servlet.ServletException;
import jakarta.servlet.ServletRequest;
import jakarta.servlet.ServletResponse;
public class AuthoritiesLoggingAfterFilter implements Filter {
private final Logger LOG = Logger.getLogger(AuthoritiesLoggingAfterFilter.class.getName());
@Override
public void doFilter(ServletRequest request, ServletResponse response, FilterChain chain)
throws IOException, ServletException {
Authentication authentication = SecurityContextHolder.getContext().getAuthentication();
if(null != authentication) {
LOG.info("User " + authentication.getName() + " is successfully authenticated and "
+ "has the authorities " + authentication.getAuthorities().toString());
}
chain.doFilter(request, response);
}
}
package com.eazybytes.filter;
import java.io.IOException;
import java.util.logging.Logger;
import jakarta.servlet.Filter;
import jakarta.servlet.FilterChain;
import jakarta.servlet.ServletException;
import jakarta.servlet.ServletRequest;
import jakarta.servlet.ServletResponse;
public class AuthoritiesLoggingAtFilter implements Filter {
private final Logger LOG = Logger.getLogger(AuthoritiesLoggingAtFilter.class.getName());
@Override
public void doFilter(ServletRequest request, ServletResponse response, FilterChain chain)
throws IOException, ServletException {
LOG.info("Authentication Validation is in progress");
chain.doFilter(request, response);
}
}
custom filter 주입하기
- security config class에 다음과 같이 설정한다.
- requestValidationBeforeFilter -> basicAuthenticationFilter -> AuthroitiesLoggingAfterFilter순으로 적용된다.
- basicAuthenticationFilter 앞이나 뒤에 AuthroitiesLoggingAtFilter가 적용되는데 앞에 적용될지 뒤에 적용될지 모른다.(그래서 이 메소드는 잘 사용하지 않는다)
(참고) spring security의 filter chain
- spring security의 filter chain은 기본적으로 다른 포트에서 접근했을 때 적용된다.
- 따라서 localhost8080등으로 접근했을 때는 spring security filter가 안 돌 수 있다.
- spring security filter chain은 security에서 만들어오는 default filter가 모두 돌고 난 후에 유저가 만든 custom filter가 도는 구조이다.
- 필터들 간 적용 순서는 다음과 같다.
- SecurityContextPersistenceFilter : 보안컨텍스트 설정, 검색
- UsernamePasswordAuthenticationFilter : 사용자 자격증명 기반으로 인증 시도
- AuthenticationProvider : 실제 인증 수행 후 Authentication 객체 생성
- SecurityContextHolderAwareRequestFilter : Spring Security에 의해 보호되는 리소스에 대한 요청을 판별하고, 요청을 알맞은 보안 컨텍스트와 연결
- FilterSecurityInterceptor : 권한 검사 및 권한 부여 수행(Authentication객체 사용)
- 그 외 유저의 custom filter
- 따라서 다음과 같은 사진에서 UsernamePasswordAuthenticationToken을 사용할 수 있다.(UsernamePasswordAuthenticationFilter에 의해 이미 생성되었기 때문)
- 그리고 default filter를 사용할 수도 있지만 AuthenticationProvider를 구현한 것 처럼 위의 ault필터라도 유저가 custom할 수 있다.
- 내가 생성한 custom filter가 default filter다음에 돌기 때문에 다음과 같이 SecurityContextHolder에 있는 Authentication객체를 가져와서 사용할 수 있다.