주의
이 책은 Spring Security 5 버전을 기준으로 작성되었으므로, Spring Boot 3.X 버전에서는 일부 클래스가 더 이상 사용되지(deprecated) 않을 수 있습니다.
서론
- 스프링 시큐리티의 HTTP 필터는 일반적으로 요청에 적용해야 하는 각 책임을 관리하며 책임의 체인을 형성함
- 필터는 요청을 수신하고 그 논리를 실행하며 최종적으로 체임의 다음 필터에 요청을 위임함

- 스프링 시큐리티는 맞춤 구성을 통해 필터 체인에 추가할 수 있는 필터 구현을 제공하지만 맞춤형 필터도 정의할 수 있음

1. 스프링 시큐리티 아키텍처의 필터 구현
- 인증 필터는 요청을 가로채고 인증 책임을 AuthenticationManager에 위임함
- 인증 이전에 특정 논리를 실행하려면 인증 필터 앞에 필터를 추가하면 됨
- 스프링 시큐리티 아키텍처의 필터는 일반적인 HTTP 필터이며 필터를 생성하기 위해 javax.servlet 패키지의 Filter 인터페이스를 구현하면 됨
- 다른 HTTP 필터와 마찬가지로 doFilter() 메서드를 재정의해 논리를 구현해야 함
- 해당 메서드는 ServletRequest, ServletResponse, 그리고 FilterChain 매개변수를 받음
- 필터 체인은 필터가 작동하는 순서가 정의된 필터의 모음
- 스프링 시큐리티에는 몇 가지 필터 구현과 순서가 있으며 이들 중 몇 가지를 소개하면 다음과 같음
- BasicAuthenticationFilter는 HTTP Basic 인증을 처리
- CsrfFilter는 CSRF (사이트 간 요청 위조)를 처리
- CorsFilter는 CORS (교차 출처 리소스 공유) 권한 부여 규칙을 처리
- 애플리케이션이 필터 체인에 이러한 모든 필터의 인스턴스를 반드시 가질 필요는 없음
- 필터 체인은 애플리케이션을 구성하는 방법에 따라 더 길어지거나 짧아질 수도 있음
- 개발자가 작성하는 구성에 따라 필터 체인의 정의가 영향을 받음
- i.g. HTTP Basic 인증 방식을 이용하려면 HttpSecurity 클래스의 httpBasic() 메서드를 호출해야 하는데 해당 메서드를 호출하면 필터 체인에 BasicAuthenticationFilter가 추가됨
2. 체인에서 기존 필터 앞에 필터 추가
- 모든 요청에 Request-Id 헤더가 있고 해당 헤더로 요청을 추적하여 인증을 수행하기 전에 헤더가 있는지 검증한다고 가정
- 인증 프로세스에는 DB 쿼리나 다른 리소스를 소비하는 작업이 포함될 수 있으므로 요청의 형식이 유효하지 않으면 이런 작업을 실행할 필요가 없음
- 위 요구 사항을 두 개의 단계로 해결할 수 있으며 최종 필터 체인 형상은 다음과 같음
- 필터를 구현: 요청에 필요한 헤더가 있는지 확인하는 RequestValidationFilter 클래스 생성
- 필터 체인에 필터 추가: 구성 클래스에서 configure() 메서드를 재정의해 필터 체인에 필터 추가
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
public class RequestValidationFilter implements Filter { | |
@Override | |
public void doFilter(ServletRequest request, ServletResponse response, FilterChain filterChain) throws IOException, ServletException { | |
var httpRequest = (HttpServletRequest) request; | |
var httpResponse = (HttpServletResponse) response; | |
String requestId = httpRequest.getHeader("Request-Id"); | |
if (requestId == null || requestId.isBlank()) { | |
httpResponse.setStatus(HttpServletResponse.SC_BAD_REQUEST); | |
return; | |
} | |
filterChain.doFilter(request, response); | |
} | |
} | |
@Configuration | |
public class ProjectConfig extends WebSecurityConfigurerAdapter { | |
@Override | |
protected void configure(HttpSecurity http) throws Exception { | |
// 필터 체인에서 인증 필터 앞에 맞춤형 필터의 인스턴스 추가 | |
http.addFilterBefore( | |
new RequestValidationFilter(), | |
BasicAuthenticationFilter.class) | |
.authorizeRequests() | |
.anyRequest() | |
.permitAll(); | |
} | |
} |

3. 체인에서 기존 필터 뒤에 필터 추가
- 간단한 로깅과 추적 목적을 위해 특정 인증 이벤트 이후 다른 시스템에 알림을 전달하는 사례가 있다고 가정
- 다음은 인증 필터 뒤에 성공한 인증 이벤트를 모두 추가하는 필터 예제
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
public class AuthenticationLoggingFilter implements Filter { | |
private final Logger logger = | |
Logger.getLogger(AuthenticationLoggingFilter.class.getName()); | |
@Override | |
public void doFilter(ServletRequest request, ServletResponse response, FilterChain filterChain) throws IOException, ServletException { | |
var httpRequest = (HttpServletRequest) request; | |
String requestId = httpRequest.getHeader("Request-Id"); | |
logger.info("Successfully authenticated request with id " + requestId); | |
filterChain.doFilter(request, response); | |
} | |
} | |
@Configuration | |
public class ProjectConfig extends WebSecurityConfigurerAdapter { | |
@Override | |
protected void configure(HttpSecurity http) throws Exception { | |
http.addFilterBefore( | |
new RequestValidationFilter(), | |
BasicAuthenticationFilter.class) | |
// 필터 체인에서 인증 필터 뒤에 맞춤형 필터의 인스턴스 추가 | |
.addFilterAfter( | |
new AuthenticationLoggingFilter(), | |
BasicAuthenticationFilter.class) | |
.authorizeRequests() | |
.anyRequest() | |
.permitAll(); | |
} | |
} |

4. 필터 체인의 다른 필터 위치에 필터 추가
- 스프링 시큐리티의 기존 필터가 수행하는 책임에 대해 다른 구현을 제공할 때 필터 체인의 다른 필터 위치에 필터를 추가하는 과정이 적합함
- HTTP Basic 인증 흐름 대신 조금 다른 인증을 구현해서 애플리케이션이 사용자를 인증하기 위한 자격 증명으로 사용자 이름과 암호 대신 다른 접근법을 적용하기를 원한다고 가정했을 때 가능성이 있는 시나리오의 예는 다음과 같음
- 인증을 위한 정적 헤더 값에 기반을 둔 식별
- 대칭 키를 이용해 인증 요청 서명
- 인증 프로세스에 OTP 이용
4.1 인증을 위한 정적 헤더 값에 기반을 둔 식별
- 클라이언트는 HTTP 요청의 헤더에 항상 동일한 문자열 하나를 앱으로 전달
- 애플리케이션은 이러한 값을 예를 들어 DB나 비밀 볼트에 저장
- 애플리케이션은 해당 정적 값을 바탕으로 클라이언트를 식별
- 인증의 보안 수준은 낮지만 단순하다는 장점이 있어 설계자와 개발자는 백엔드 애플리케이션 간의 호출에 이를 자주 선택
- 암호와 서명을 적용하는 것처럼 복잡한 계산을 수행할 필요가 없기 때문에 빠르게 실행됨
- 정적 키를 인증에 이용하는 방식은 개발자가 보안 측면에서 인프라 수준에 더 의존하면서도 엔드포인트를 완전한 비보호 상태로 두지 않는 절충안

4.2 대칭 키를 이용해 인증 요청 서명
- 클라이언트와 서버가 모두 키의 값을 공유하므로 서로가 키 값을 아는 상태
- 클라이언트는 해당 키로 요청의 일부에 서명하고 서버는 같은 키로 서명이 유효한지 확인
- 서버는 각 클라이언트의 개별 키를 DB나 비밀 볼트에 저장할 수 있으며 비슷하게 비대칭 키 쌍을 이용할 수도 있음

4.3 인증 프로세스에 OTP 이용
- 문자 메시지를 통해 또는 Google Authenticator와 같은 인증 공급자 앱으로 OTP를 받음

4.4 맞춤형 필터 적용 예제
- 모든 요청에 대해 같은 정적 키 값을 이용한다고 가정
- 필터 클래스인 StaticKeyAuthenticationFilter는 속성 파일에서 정저 키 값을 읽고 Authorization 헤더 값과 같은지 확인
- 값이 같으면 필터는 요청을 필터 체인의 다음 구성 요소에 전달
- 그렇지 않으면 요청을 필터 체인에 전달하지 않고 응답의 HTTP 상태를 401 권한 없음으로 설정
- 중요한 점은 특정 위치에 필터를 추가해도 스프링 시큐리티는 해당 위치에 필터가 하나라고 가정하지 않음
- 필터 체인의 같은 위치에 필터를 더 추가할 수 있으며 이 경우 스프링 시큐리티는 필터가 실행되는 순서를 보장하지 않음
- 기존 필터의 위치에 다른 필터를 적용하면 필터가 대체된다고 생각하는 경우가 많은데 그렇지 않으며 필터 체인에 필요 없는 필터는 아예 추가하지 않아야 함
- 아래 예제에서는 필터 체인에 BasicAuthenticationFilter 인스턴스가 추가되는 것은 원하지 않으므로 HttpSecurity 클래스의 httpBasic() 메서드를 호출하지 않음
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
@Component | |
public class StaticKeyAuthenticationFilter implements Filter { | |
@Value("${authorization.key}") | |
private String authorizationKey; | |
@Override | |
public void doFilter(ServletRequest request, ServletResponse response, FilterChain filterChain) throws IOException, ServletException { | |
var httpRequest = (HttpServletRequest) request; | |
var httpResponse = (HttpServletResponse) response; | |
String authentication = httpRequest.getHeader("Authorization"); | |
if (authorizationKey.equals(authentication)) { | |
filterChain.doFilter(request, response); | |
} else { | |
httpResponse.setStatus(HttpServletResponse.SC_UNAUTHORIZED); | |
} | |
} | |
} | |
@Configuration | |
public class ProjectConfig extends WebSecurityConfigurerAdapter { | |
@Autowired | |
private StaticKeyAuthenticationFilter filter; | |
@Override | |
protected void configure(HttpSecurity http) throws Exception { | |
// 필터 체인에서 기본 인증 필터의 위치에 필터 추가 | |
http.addFilterAt(filter, | |
BasicAuthenticationFilter.class) | |
.authorizeRequests() | |
.anyRequest() | |
.permitAll(); | |
} | |
} |

5. 스프링 시큐리티가 제공하는 필터 구현
- 스프링 시큐리티에는 Filter 인터페이스를 구현하는 여러 추상 클래스가 있으며, 이를 위해 필터 정의를 확장할 수 있음
- 또한 이러한 클래스는 구현을 확장할 때 이점을 얻을 수 있는 기능을 추가함
- OncePerRequestFilter는 GenericFilterBean을 확장하는 유용한 클래스
- 프레임워크는 필터 체인에 추가한 필터를 요청당 한 번만 실행하도록 보장하지 않음
- OncePerRequestFilter는 이름이 의미하듯이 필터의 doFilter() 메서드가 요청당 한 번만 실행되도록 논리를 구현
- OncePerRequestFilter 클래스에 관해 몇 가지 알아둘 사항은 다음과 같음
- 해당 클래스의 장점은 형식을 형 변환하여 HttpServletRequest 및 HttpServletResponse로 직접 요청을 수신한다는 것, Filter 인터페이스의 경우에는 요청과 응답을 형 변환해야 함
- 필터 체인에 추가한 필터가 특정 요청에는 적용되지 않는다고 결정할 수 있음, 이 경우 shouldNotFilter(HttpServletRequest) 메서드를 재정의하면 됨, 기본적으로 필터는 모든 요청에 적용됨
- OncePerRequestFilter는 기본적으로 비동기 요청이나 오류 발송 요청에는 적용되지 않음: 해당 동작을 변경하려면 shouldNotFilterAsyncDispatch() 및 shouldNotFilterErrorDispatch() 메서드를 재정의하면 됨
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
public class AuthenticationLoggingFilter extends OncePerRequestFilter { | |
private final Logger logger = | |
Logger.getLogger(AuthenticationLoggingFilter.class.getName()); | |
@Override | |
protected void doFilterInternal(HttpServletRequest request, | |
HttpServletResponse response, | |
FilterChain filterChain) throws ServletException, IOException { | |
String requestId = request.getHeader("Request-Id"); | |
logger.info("Successfully authenticated request with id " + requestId); | |
filterChain.doFilter(request, response); | |
} | |
} | |
@Configuration | |
public class ProjectConfig extends WebSecurityConfigurerAdapter { | |
@Override | |
protected void configure(HttpSecurity http) throws Exception { | |
http.addFilterAfter( | |
new AuthenticationLoggingFilter(), | |
BasicAuthenticationFilter.class) | |
.authorizeRequests() | |
.anyRequest() | |
.permitAll(); | |
} | |
} |
참고
스프링 시큐리티 인 액션
반응형
'Spring > Spring Security' 카테고리의 다른 글
[Spring Security] 전역 메서드 보안 (0) | 2024.10.09 |
---|---|
[Spring Security] CSRF & CORS (0) | 2024.10.08 |
[Spring Security] 필터 체인과 커스텀 필터 (0) | 2024.10.08 |
[Spring Security] 액세스 제한과 권한 (0) | 2024.10.07 |
[Spring Security] 아키텍처 간단 정리 (0) | 2024.09.13 |