클라이언트에서 서버로 요청을 보내면 해당 요청을 컨트롤러로 보내기 전에 인증필터, 인증 관리자, 인증 공급자 등 몇가지 과정을 거치게 된다. 이번에는 인증관리자로 정보를 넘기기 전의 과정인 인증필터를 내 맘대로 커스텀해서 사용해보려고 한다.
필터를 커스텀해서 사용하는 방법은 크게 어렵지 않다. 나의 필터를 만들고, 이를 SecurityFilterChain 설정에서 적용시켜주면 된다. 일단 필터를 만들어 보자.
public class CustomFilter implements Filter {
@Override
public void doFilter(ServletRequest request, ServletResponse response, FilterChain chain)
throws IOException, ServletException {
HttpServletRequest httpServletRequest = (HttpServletRequest) request;
HttpServletResponse httpServletResponse = (HttpServletResponse) response;
String value = httpServletRequest.getHeader("Auth_ID");
if (value != null && "asdf1234".equals(value)) {
chain.doFilter(httpServletRequest, httpServletResponse);
}
else httpServletResponse.setStatus(HttpServletResponse.SC_UNAUTHORIZED);
}
}
커스텀 필터를 만드려면 Filter 인터페이스를 직접 구현하면 된다. Filter를 사용하려고 하면 동명의 인터페이스가 꽤 많이 나올텐데 jakarta.servlet.Filter를 이용하면 된다.
실행 과정은 다음과 같다.
1. 파라미터로는 ServletRequest, ServletResponse, FilterChain를 받는데 요청과 응답을 HttpServletRequest와 HttpServletResponse로 형변환하여 사용한다.
2. 그리고 해당 요청에서 Auth_ID라는 헤더의 값을 받아와서 임의로 지정한 asdf1234라는 값과 동일한지 검사를 한다. 헤더에 해당 정보가 들어있지 않을 수도 있으니 null인지 검사를 먼저 수행한다.
3. 원하는 정보와 일치할 시, 다음 필터로 요청과 응답을 전달한다.
4. 원하는 정보와 다르면 응답 객체에 401 응답을 설정한다.
HTTP Basic 방식과 비슷하게 동작을 하는 필터이다. HTTP 요청 헤더에 Auth_ID의 값이 asdf1234인지 확인한다는 점이 다르다.
이제 위에서 만든 필터를 실제로 적용시켜보자. 위의 필터를 사용하려면 설정을 해주어야 한다.
@Bean
public SecurityFilterChain securityFilterChain(HttpSecurity http) throws Exception {
http.addFilterAt(new CustomFilter(), BasicAuthenticationFilter.class)
.authorizeHttpRequests(auth -> auth
.anyRequest().permitAll()
)
.csrf(c -> c.disable());
return http.build();
}
SecurityFilterChain을 반환하는 메서드에서 설정을 해주자. Filter들은 말그대로 서로 쭉 엮여서 계속 다음 필터로 요청을 넘겨서 처리하도록 되어 있는데 그래서 이 FilterChain에서 내가 만든 필터가 어디에 위치할 것인지를 직접 지정해주어야 한다. 내가 지정하지 않았어도 시큐리티에서 자동으로 동작하는 필터들이 있는데 이 필터들은 우선순위에 따라서 실행할 순서가 정해진다. 그렇기 때문에 필터를 어느 곳에 위치시킬지를 정하는 과정이 필요하다.

위 스크린샷처럼 필터를 어느 한 필터 기준으로
1. 해당 필터의 앞에 위치시킬 것인지
2. 해당 필터의 위치와 동일한 곳에 위치시킬 것인지
3. 해당 필터의 뒤에 위치시킬 것인지
를 선택해야 한다. 단순히 addFilter() 메서드를 사용해서 실행해보았더니 위치를 정하는 것이 좋다는 메시지가 떠서 위의 세 가지 메서드 중에 고르는 것이 좋겠다. 필터의 예시는 addFilter() 메서드의 설명을 보면 알 수 있다.

나는 HTTPBasic 필터와 동일한 우선순위로 동작하도록 설정했다. 그런데 여기서 여러 필터가 같은 우선순위이면 어느 필터가 먼저 실행될지 동작 순서는 보장되지 않는다는 점을 주의해야 한다.
필터 설정 이후로는 모든 요청을 허용하고 postman으로 테스트해볼 것이기 때문에 csrf를 비활성화 시켰다. 이제 잘 작동하는지 테스트 해보자.
@RestController
public class TestController {
@PostMapping("/test/hello")
public String test1() {
return "hello";
}
@GetMapping("/test")
public String test2() { return "hi"; }
}
컨트롤러의 매핑 설정은 위와 같다. /test/hello로 요청해보자.

401 Unauthrized 응답이 반환되었다. 직접 설정한 필터에서 필요한 헤더 정보가 없어서 요청을 거부한 것이다.
이제 헤더에 Auth_ID를 포함시켜서 요청해보자.

이번에는 Auth_ID를 포함시켰지만 그래도 401 Unauthorized가 반환되었다. 그 이유는 직접 설정한 asdf1234라는 비밀번호와 다르기 때문이다.
맞는 비밀번호를 설정해서 다시 요청해보자.

이번에는 제대로 hello라는 메시지가 200 OK 응답과 함께 반환되었다!
위 과정으로 직접 작성한 필터가 제대로 작동하는 것을 알 수 있었다.
그런데 여기서 Filter 인터페이스를 구현할 때 주의할 점이 있다고 한다. 한 요청에 필터가 한번만 호출되도록 보장하지 않는다는 점이다. 그래서 한 요청당 한 번만 실행될 수 있도록 OncePerRequestFilter 추상 클래스를 상속받아 사용한다.
public class CustomOnceFilter extends OncePerRequestFilter {
@Override
protected void doFilterInternal(HttpServletRequest request, HttpServletResponse response, FilterChain filterChain)
throws ServletException, IOException {
String value = request.getHeader("Auth_ID");
if (value != null && "asdf1234".equals(value)) {
filterChain.doFilter(request, response);
}
else response.setStatus(HttpServletResponse.SC_UNAUTHORIZED);
}
}
위 코드처럼 만들어주면 된다. 파라미터가 HttpServletRequest와 HttpServletResponse이기 때문에 형변환을 하지 않아도 바로 사용할 수 있다.
그리고 모든 코드에서 동일하지만 실제로 애플리케이션을 운영할 때 비밀번호를 코드상에 노출시키는 것은 상당히 좋지 않기 때문에 환경변수, Vault 등을 활용하여 숨기도록 하는 것이 좋다.
'공부 > Spring Security' 카테고리의 다른 글
| [Spring Security] Filter 인증 구조에 대해서 깨달은 점 (0) | 2024.01.17 |
|---|---|
| [Spring Security] CSRF 설정하기 (0) | 2024.01.10 |
| [Spring Security] mvcMatchers, antMatchers, regexMatchers 대신 requestMatchers (0) | 2024.01.08 |
| [Spring Security] AuthenticationProvider 직접 구현하기 (0) | 2024.01.04 |
| [Spring Security] 기본 동작 과정 및 UserDetailsService (0) | 2024.01.03 |