implementation 'org.springframework.boot:spring-boot-starter-security'
security.web.session.DisableEncodeUrlFilter
security.web.context.request.async.WebAsyncManagerIntegrationFilter
security.web.context.SecurityContextHolderFilter
security.web.header.HeaderWriterFilter
web.filter.CorsFilter
security.web.csrf.CsrfFilter
security.web.authentication.logout.LogoutFilter
security.web.authentication.UsernamePasswordAuthenticationFilter
security.web.authentication.ui.DefaultLoginPageGeneratingFilter
security.web.authentication.ui.DefaultLogoutPageGeneratingFilter
security.web.authentication.www.BasicAuthenticationFilter
security.web.savedrequest.RequestCacheAwareFilter
security.web.servletapi.SecurityContextHolderAwareRequestFilter
security.web.authentication.AnonymousAuthenticationFilter
security.web.access.ExceptionTranslationFilter
security.web.access.intercept.AuthorizationFilter
기본 필터들을 알아보기 전에 먼저 기본 필터들이 상속하는 OncePerRequestFilter와 GenericFilterBean을 살펴본다.
public abstract class OncePerRequestFilter extends GenericFilterBean {
...
@Override
public final void doFilter(ServletRequest request, ServletResponse response, FilterChain filterChain)
throws ServletException, IOException {
if (!((request instanceof HttpServletRequest httpRequest) && (response instanceof HttpServletResponse httpResponse))) {
throw new ServletException("OncePerRequestFilter only supports HTTP requests");
}
String alreadyFilteredAttributeName = getAlreadyFilteredAttributeName();
boolean hasAlreadyFilteredAttribute = request.getAttribute(alreadyFilteredAttributeName) != null;
if (skipDispatch(httpRequest) || shouldNotFilter(httpRequest)) {
// Proceed without invoking this filter...
filterChain.doFilter(request, response);
}
else if (hasAlreadyFilteredAttribute) {
if (DispatcherType.ERROR.equals(request.getDispatcherType())) {
doFilterNestedErrorDispatch(httpRequest, httpResponse, filterChain);
return;
}
else {
// Do invoke this filter...
}
}
getAlreadyFilteredAttributeName(), skipDispatch(httpRequest)
를 통해 해당 필터를 적용하지 않는다. protected boolean shouldNotFilterAsyncDispatch() {
return true;
}
기본적으로 OncePerRequestFilter는 요청이 비동기 처리가 시작되어도 필터링 하지 않도록 설정되어 있다. false를 반환하면 각 비동기 디스패치에 대해 정확히 스레드당 한 번씩 필터가 호출된다. 비동기 작업 결과를 처리하거나 비동기 처리 전후로 특정 작업을 해야할 때 유용하다고 한다.
중요한 것은 하나의 요청이 한 번만 실행되는 것을 보장하는 필터이고, 어떤 필터가 처리되었을 때 OncePerRequest를 상속하는 다른 필터에서 위와 같은 방법으로 필터링 되지 않도록 할 수 있다는 것이다.
기본 필터 중에 GenericFilter를 상속하는 필터들은 다음과 같다.
SecurityContextHolderFilter LogoutFilter DefaultLoginPageGeneratingFilter RequestCacheAwareFilter SecurityContextHolderAwareRequestFilter AnonymousAuthenticationFilter ExceptionTranslationFilter AuthorizationFilter
의문이 드는 점은 LogoutFilter는 요청 당 단 한번만 처리해야 되는 거 아닌가?
DefaultLogoutPageGeneratingFilter는 OncePerRequestFilter를 상속한다. LogoutFilter는 왜 GenericBeanFilter을 상속하는 것인지?
public class DisableEncodeUrlFilter extends OncePerRequestFilter {
@Override
protected void doFilterInternal(HttpServletRequest request, HttpServletResponse response, FilterChain filterChain)
throws ServletException, IOException {
filterChain.doFilter(request, new DisableEncodeUrlResponseWrapper(response));
}
}
public final class WebAsyncManagerIntegrationFilter extends OncePerRequestFilter {
...
@Override
protected void doFilterInternal(HttpServletRequest request, HttpServletResponse response, FilterChain filterChain)
throws ServletException, IOException {
WebAsyncManager asyncManager = WebAsyncUtils.getAsyncManager(request);
SecurityContextCallableProcessingInterceptor securityProcessingInterceptor = (SecurityContextCallableProcessingInterceptor) asyncManager
.getCallableInterceptor(CALLABLE_INTERCEPTOR_KEY);
if (securityProcessingInterceptor == null) {
SecurityContextCallableProcessingInterceptor interceptor = new SecurityContextCallableProcessingInterceptor();
interceptor.setSecurityContextHolderStrategy(this.securityContextHolderStrategy);
asyncManager.registerCallableInterceptor(CALLABLE_INTERCEPTOR_KEY, interceptor);
}
filterChain.doFilter(request, response);
}
}
public final class WebAsyncManager {
...
private volatile Object[] concurrentResultContext;
}
public class SecurityContextHolderFilter extends GenericFilterBean {
...
private void doFilter(HttpServletRequest request, HttpServletResponse response, FilterChain chain)
throws ServletException, IOException {
if (request.getAttribute(FILTER_APPLIED) != null) {
chain.doFilter(request, response);
return;
}
request.setAttribute(FILTER_APPLIED, Boolean.TRUE);
Supplier<SecurityContext> deferredContext = this.securityContextRepository.loadDeferredContext(request);
try {
this.securityContextHolderStrategy.setDeferredContext(deferredContext);
chain.doFilter(request, response);
}
finally {
this.securityContextHolderStrategy.clearContext();
request.removeAttribute(FILTER_APPLIED);
}
}
}
default DeferredSecurityContext loadDeferredContext(HttpServletRequest request) {
Supplier<SecurityContext> supplier = () -> loadContext(new HttpRequestResponseHolder(request, null));
return new SupplierDeferredSecurityContext(SingletonSupplier.of(supplier),
SecurityContextHolder.getContextHolderStrategy());
}
public class HeaderWriterFilter extends OncePerRequestFilter {
private final List<HeaderWriter> headerWriters;
private boolean shouldWriteHeadersEagerly = false;
public HeaderWriterFilter(List<HeaderWriter> headerWriters) {
Assert.notEmpty(headerWriters, "headerWriters cannot be null or empty");
this.headerWriters = headerWriters;
}
@Override
protected void doFilterInternal(HttpServletRequest request, HttpServletResponse response, FilterChain filterChain)
throws ServletException, IOException {
if (this.shouldWriteHeadersEagerly) {
doHeadersBefore(request, response, filterChain);
}
else {
doHeadersAfter(request, response, filterChain);
}
}
...
}
public final class XFrameOptionsHeaderWriter implements HeaderWriter {}
public final class XXssProtectionHeaderWriter implements HeaderWriter {}
public final class XContentTypeOptionsHeaderWriter extends StaticHeadersWriter {}
public class StaticHeadersWriter implements HeaderWriter {}
public class CorsFilter extends OncePerRequestFilter {
private final CorsConfigurationSource configSource;
private CorsProcessor processor = new DefaultCorsProcessor();
public CorsFilter(CorsConfigurationSource configSource) {
Assert.notNull(configSource, "CorsConfigurationSource must not be null");
this.configSource = configSource;
}
public void setCorsProcessor(CorsProcessor processor) {
Assert.notNull(processor, "CorsProcessor must not be null");
this.processor = processor;
}
@Override
protected void doFilterInternal(HttpServletRequest request, HttpServletResponse response,
FilterChain filterChain) throws ServletException, IOException {
CorsConfiguration corsConfiguration = this.configSource.getCorsConfiguration(request);
boolean isValid = this.processor.processRequest(corsConfiguration, request, response);
if (!isValid || CorsUtils.isPreFlightRequest(request)) {
return;
}
filterChain.doFilter(request, response);
}
}
public class DefaultCorsProcessor implements CorsProcessor {
Collection<String> varyHeaders = response.getHeaders(HttpHeaders.VARY);
if (!varyHeaders.contains(HttpHeaders.ORIGIN)) {
response.addHeader(HttpHeaders.VARY, HttpHeaders.ORIGIN);
}
if (!varyHeaders.contains(HttpHeaders.ACCESS_CONTROL_REQUEST_METHOD)) {
response.addHeader(HttpHeaders.VARY, HttpHeaders.ACCESS_CONTROL_REQUEST_METHOD);
}
if (!varyHeaders.contains(HttpHeaders.ACCESS_CONTROL_REQUEST_HEADERS)) {
response.addHeader(HttpHeaders.VARY, HttpHeaders.ACCESS_CONTROL_REQUEST_HEADERS);
}
if (!CorsUtils.isCorsRequest(request)) {
return true;
}
if (response.getHeader(HttpHeaders.ACCESS_CONTROL_ALLOW_ORIGIN) != null) {
logger.trace("Skip: response already contains \"Access-Control-Allow-Origin\"");
return true;
}
boolean preFlightRequest = CorsUtils.isPreFlightRequest(request);
if (config == null) {
if (preFlightRequest) {
rejectRequest(new ServletServerHttpResponse(response));
return false;
}
else {
return true;
}
}
return handleInternal(new ServletServerHttpRequest(request), new ServletServerHttpResponse(response), config, preFlightRequest);
}
processRequest를 처리하는 부분을 살펴보면, VaryHeader를 설정한다. 이는 요청 출처(ORIGIN), 사용할 메소드(ACCESS_CONTROL_REQUEST_METHOD), 사용할 헤더(ACCESS_CONTROL_REQUEST_HEADERS)를 지정하여 요청마다 서버가 다양하게 응답할 수 있게한다.
CORS 설정을 하기 위해선 먼저 동일 출처 요청인지 아닌지부터 검사한다. 먼저 서버와 요청의 스킴(http, https)을 비교하고, 호스트(도메인)를 검사한다. 스킴과 도메인 그리고 포트가 같다면 동일 출처로 판단하여 cors를 적용하지 않는다.
return !(ObjectUtils.nullSafeEquals(scheme, originUrl.getScheme()) &&
ObjectUtils.nullSafeEquals(host, originUrl.getHost()) &&
getPort(scheme, port) == getPort(originUrl.getScheme(), originUrl.getPort()));
public static boolean isPreFlightRequest(HttpServletRequest request) {
return (HttpMethod.OPTIONS.matches(request.getMethod()) &&
request.getHeader(HttpHeaders.ORIGIN) != null &&
request.getHeader(HttpHeaders.ACCESS_CONTROL_REQUEST_METHOD) != null);
}
public final class CsrfFilter extends OncePerRequestFilter {
...
@Override
protected void doFilterInternal(HttpServletRequest request, HttpServletResponse response, FilterChain filterChain)
throws ServletException, IOException {
DeferredCsrfToken deferredCsrfToken = this.tokenRepository.loadDeferredToken(request, response);
request.setAttribute(DeferredCsrfToken.class.getName(), deferredCsrfToken);
this.requestHandler.handle(request, response, deferredCsrfToken::get);
if (!this.requireCsrfProtectionMatcher.matches(request)) {
if (this.logger.isTraceEnabled()) {
this.logger.trace("Did not protect against CSRF since request did not match "
+ this.requireCsrfProtectionMatcher);
}
filterChain.doFilter(request, response);
return;
}
CsrfToken csrfToken = deferredCsrfToken.get();
String actualToken = this.requestHandler.resolveCsrfTokenValue(request, csrfToken);
if (!equalsConstantTime(csrfToken.getToken(), actualToken)) {
boolean missingToken = deferredCsrfToken.isGenerated();
this.logger
.debug(LogMessage.of(() -> "Invalid CSRF token found for " + UrlUtils.buildFullRequestUrl(request)));
AccessDeniedException exception = (!missingToken) ? new InvalidCsrfTokenException(csrfToken, actualToken)
: new MissingCsrfTokenException(actualToken);
this.accessDeniedHandler.handle(request, response, exception);
return;
}
filterChain.doFilter(request, response);
}
}
private final HashSet<String> allowedMethods = new HashSet<>(Arrays.asList("GET", "HEAD", "TRACE", "OPTIONS"));