



package org.springframework.boot.autoconfigure.security.servlet;
import jakarta.servlet.DispatcherType;
import java.util.EnumSet;
import java.util.stream.Collectors;
import org.springframework.boot.autoconfigure.AutoConfiguration;
import org.springframework.boot.autoconfigure.condition.ConditionalOnBean;
import org.springframework.boot.autoconfigure.condition.ConditionalOnClass;
import org.springframework.boot.autoconfigure.condition.ConditionalOnWebApplication;
import org.springframework.boot.autoconfigure.condition.ConditionalOnWebApplication.Type;
import org.springframework.boot.autoconfigure.security.SecurityProperties;
import org.springframework.boot.context.properties.EnableConfigurationProperties;
import org.springframework.boot.web.servlet.DelegatingFilterProxyRegistrationBean;
import org.springframework.boot.web.servlet.ServletRegistrationBean;
import org.springframework.context.annotation.Bean;
import org.springframework.security.config.http.SessionCreationPolicy;
import org.springframework.security.web.context.AbstractSecurityWebApplicationInitializer;
@AutoConfiguration(
after = {SecurityAutoConfiguration.class}
)
@ConditionalOnWebApplication(
type = Type.SERVLET
)
@EnableConfigurationProperties({SecurityProperties.class})
@ConditionalOnClass({AbstractSecurityWebApplicationInitializer.class, SessionCreationPolicy.class})
public class SecurityFilterAutoConfiguration {
private static final String DEFAULT_FILTER_NAME = "springSecurityFilterChain";
public SecurityFilterAutoConfiguration() {
}
@Bean
@ConditionalOnBean(
name = {"springSecurityFilterChain"}
)
public DelegatingFilterProxyRegistrationBean securityFilterChainRegistration(SecurityProperties securityProperties) {
DelegatingFilterProxyRegistrationBean registration = new DelegatingFilterProxyRegistrationBean("springSecurityFilterChain", new ServletRegistrationBean[0]);
registration.setOrder(securityProperties.getFilter().getOrder());
registration.setDispatcherTypes(this.getDispatcherTypes(securityProperties));
return registration;
}
private EnumSet<DispatcherType> getDispatcherTypes(SecurityProperties securityProperties) {
return securityProperties.getFilter().getDispatcherTypes() == null ? null : (EnumSet)securityProperties.getFilter().getDispatcherTypes().stream().map((type) -> {
return DispatcherType.valueOf(type.name());
}).collect(Collectors.toCollection(() -> {
return EnumSet.noneOf(DispatcherType.class);
}));
}
}
DelegatingFilterProxyRegistrationBean registration = new DelegatingFilterProxyRegistrationBean("springSecurityFilterChain", new ServletRegistrationBean[0]);package org.springframework.web.filter;
import jakarta.servlet.Filter;
import jakarta.servlet.FilterChain;
import jakarta.servlet.ServletException;
import jakarta.servlet.ServletRequest;
import jakarta.servlet.ServletResponse;
import java.io.IOException;
import java.util.concurrent.locks.Lock;
import java.util.concurrent.locks.ReentrantLock;
import org.springframework.context.ConfigurableApplicationContext;
import org.springframework.lang.Nullable;
import org.springframework.util.Assert;
import org.springframework.web.context.WebApplicationContext;
import org.springframework.web.context.support.WebApplicationContextUtils;
public class DelegatingFilterProxy extends GenericFilterBean {
@Nullable
private String contextAttribute;
@Nullable
private WebApplicationContext webApplicationContext;
@Nullable
private String targetBeanName;
private boolean targetFilterLifecycle;
@Nullable
private volatile Filter delegate;
private final Lock delegateLock;
....
protected void invokeDelegate(Filter delegate, ServletRequest request, ServletResponse response, FilterChain filterChain) throws ServletException, IOException {
delegate.doFilter(request, response, filterChain);
}

package org.springframework.security.web;
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;
import java.io.IOException;
import java.util.Arrays;
import java.util.Collections;
import java.util.Iterator;
import java.util.List;
import org.apache.commons.logging.Log;
import org.apache.commons.logging.LogFactory;
import org.springframework.core.log.LogMessage;
import org.springframework.security.core.context.SecurityContextHolder;
import org.springframework.security.core.context.SecurityContextHolderStrategy;
import org.springframework.security.web.firewall.FirewalledRequest;
import org.springframework.security.web.firewall.HttpFirewall;
import org.springframework.security.web.firewall.HttpStatusRequestRejectedHandler;
import org.springframework.security.web.firewall.RequestRejectedException;
import org.springframework.security.web.firewall.RequestRejectedHandler;
import org.springframework.security.web.firewall.StrictHttpFirewall;
import org.springframework.security.web.util.ThrowableAnalyzer;
import org.springframework.security.web.util.UrlUtils;
import org.springframework.util.Assert;
import org.springframework.web.filter.GenericFilterBean;
public class FilterChainProxy extends GenericFilterBean {
private static final Log logger = LogFactory.getLog(FilterChainProxy.class);
private static final String FILTER_APPLIED = FilterChainProxy.class.getName().concat(".APPLIED");
private SecurityContextHolderStrategy securityContextHolderStrategy;
private List<SecurityFilterChain> filterChains;
private FilterChainValidator filterChainValidator;
private HttpFirewall firewall;
private RequestRejectedHandler requestRejectedHandler;
private ThrowableAnalyzer throwableAnalyzer;
private FilterChainDecorator filterChainDecorator;
public FilterChainProxy() {
this.securityContextHolderStrategy = SecurityContextHolder.getContextHolderStrategy();
this.filterChainValidator = new NullFilterChainValidator();
this.firewall = new StrictHttpFirewall();
this.requestRejectedHandler = new HttpStatusRequestRejectedHandler();
this.throwableAnalyzer = new ThrowableAnalyzer();
this.filterChainDecorator = new VirtualFilterChainDecorator();
}
public FilterChainProxy(SecurityFilterChain chain) {
this(Arrays.asList(chain));
}
public FilterChainProxy(List<SecurityFilterChain> filterChains) {
this.securityContextHolderStrategy = SecurityContextHolder.getContextHolderStrategy();
this.filterChainValidator = new NullFilterChainValidator();
this.firewall = new StrictHttpFirewall();
this.requestRejectedHandler = new HttpStatusRequestRejectedHandler();
this.throwableAnalyzer = new ThrowableAnalyzer();
this.filterChainDecorator = new VirtualFilterChainDecorator();
this.filterChains = filterChains;
}
.....
private List<Filter> getFilters(HttpServletRequest request) {
int count = 0;
Iterator var3 = this.filterChains.iterator();
SecurityFilterChain chain;
do {
if (!var3.hasNext()) {
return null;
}
chain = (SecurityFilterChain)var3.next();
if (logger.isTraceEnabled()) {
++count;
logger.trace(LogMessage.format("Trying to match request against %s (%d/%d)", chain, count, this.filterChains.size()));
}
} while(!chain.matches(request));
return chain.getFilters();
}

SecurityFilterChain 한 개만 등록.
@Configuration
@EnableWebSecurity
public class SecurityConfig {
@Bean
public SecurityFilterChain filterChain(HttpSecurity httpSecurity) throws Exception{
return httpSecurity.build();
}
}
SecurityFilterChain 두 개 이상 등록.
@Configuration
@EnableWebSecurity
public class SecurityConfig {
@Bean
public SecurityFilterChain filterChain(HttpSecurity httpSecurity) throws Exception {
httpSecurity.securityMatcher("/")
.authorizeHttpRequests((authorize) -> authorize
.requestMatchers("/").permitAll());
return httpSecurity.build();
}
@Bean
public SecurityFilterChain filterChain2(HttpSecurity httpSecurity) throws Exception{
httpSecurity.securityMatcher("/admin")
.authorizeHttpRequests((authorize) -> authorize
.requestMatchers("/admin").authenticated());
return httpSecurity.build();
}
filterChainProxy에서 SecurityFilterChain이 제대로 등록되어 있는 지 확인.
securityMatcher
httpSecurity.securityMatcher("/api/**") 하게 되면 클라이언트의 요청에 대해 /api/** 경로로만 시큐리티가 보안작동을 진행 시키겠다는 것./api/** 이외의 요청이 오게 되면 무시해 버림.requestMatcher
httpSecurity.requestMatcher("/api/**")/api/** 경로로 오는 것에 대해 보안 심사를 하겠다는 것./api/** 요청과 이외의 요청도 포함.그래서 securityMatcher 로 특정한 경로로만 보안기능을 작동하도록 한 다음 이 경로안에서 보안심사를 하고자 할 때 requestMatcher 를 사용한다고 생각하면 됨.
@Configuration
@EnableWebSecurity
public class SecurityConfig {
@Bean
@Order(1)
public SecurityFilterChain filterChain(HttpSecurity httpSecurity) throws Exception {
httpSecurity.securityMatcher("/")
.authorizeHttpRequests((authorize) -> authorize
.requestMatchers("/").permitAll());
return httpSecurity.build();
}
@Bean
@Order(2)
public SecurityFilterChain filterChain2(HttpSecurity httpSecurity) throws Exception{
httpSecurity.securityMatcher("/admin")
.authorizeHttpRequests((authorize) -> authorize
.requestMatchers("/admin").authenticated());
return httpSecurity.build();
}
SecurityFilterChain을 거치게 된다면 내부적으로 여러 가지 필터를 거치게 되는데 이때 서버의 자원을 사용하고 상주 시간이 발생함.
보통 정적 리소스(이미지, CSS)의 경우 필터를 통과하지 않도록 아래 코드를 통해 설정할 수 있음.
@Bean
public WebSecurityCustomizer webSecurityCustomizer() {
return web -> web.ignoring().requestMatchers("/img/**");
}
Interface WebSecurityCustomizer
@EnableWebSecurity(debug = true)
debug = true로 설정하면 됨.false임.@Configuration
@EnableWebSecurity(debug = true)
public class SecurityConfig {
}

어떠한 설정도 없이 그저 의존성 추가와 어노테이션만 했을 경우 위와 같은 필터로 구성되어 있음.
WebAsyncManagerIntegrationFilter
UsernamePasswordAuthenticationFilter
DefaultLoginPageGeneratingFilter
DefaultLogoutPageGeneratingFilter
Ex) 사용자 "Aladdin"이 비밀번호 "open sesame"으로 인증하려는 경우 다음과 같은 헤더가 표시됨.Authorization: Basic QWxhZGRpbjpvcGVuIHNlc2FtZQ==SecurityContextHolderAwareRequestFilter
@Configuration
@EnableWebSecurity(debug = true)
public class SecurityConfig {
@Bean
public SecurityFilterChain filterChain(HttpSecurity httpSecurity) throws Exception {
return httpSecurity.build();
}
}

@Configuration
@EnableWebSecurity(debug = true)
public class SecurityConfig {
@Bean
public SecurityFilterChain filterChain(HttpSecurity httpSecurity) throws Exception {
httpSecurity
.formLogin(withDefaults());
return httpSecurity.build();
}
}

@Configuration
@EnableWebSecurity(debug = true)
public class SecurityConfig {
@Bean
public SecurityFilterChain filterChain(HttpSecurity httpSecurity) throws Exception {
httpSecurity
.authorizeHttpRequests(authorize -> authorize
.requestMatchers("/user").permitAll()
.anyRequest().authenticated()
)
.formLogin(form -> form
.loginPage("/login")
.permitAll()
);
return httpSecurity.build();
}
}

@Configuration
@EnableWebSecurity(debug = true)
public class SecurityConfig {
@Bean
public SecurityFilterChain filterChain(HttpSecurity httpSecurity) throws Exception {
httpSecurity
.httpBasic(Customizer.withDefaults());
return httpSecurity.build();
}
}

httpSecurity.addFilterBefore(Filter(추가할 필터), Class<?>(기존 필터))
Class<?>) 앞에 새로운 필터(Filter)를 추가.httpSecurity.addFilterAfter(Filter(추가할 필터), Class<?>(기존 필터))
Class<?>) 필터 뒤에 새로운 필터(Filter)를 추가.httpSecurity.addFilterAt(Filter(추가할 필터), Class<?>(기존 필터))
Class<?>)를 새로운 필터(Filter)로 교체.
package org.springframework.security.core;
import java.io.Serializable;
import java.security.Principal;
import java.util.Collection;
public interface Authentication extends Principal, Serializable {
Collection<? extends GrantedAuthority> getAuthorities();
Object getCredentials();
Object getDetails();
Object getPrincipal();
boolean isAuthenticated();
void setAuthenticated(boolean isAuthenticated) throws IllegalArgumentException;
}
package org.springframework.security.core.context;
import java.lang.reflect.Constructor;
import java.util.function.Supplier;
import org.springframework.util.Assert;
import org.springframework.util.ReflectionUtils;
import org.springframework.util.StringUtils;
public class SecurityContextHolder {
public static final String MODE_THREADLOCAL = "MODE_THREADLOCAL";
public static final String MODE_INHERITABLETHREADLOCAL = "MODE_INHERITABLETHREADLOCAL";
public static final String MODE_GLOBAL = "MODE_GLOBAL";
private static final String MODE_PRE_INITIALIZED = "MODE_PRE_INITIALIZED";
public static final String SYSTEM_PROPERTY = "spring.security.strategy";
private static String strategyName = System.getProperty("spring.security.strategy");
private static SecurityContextHolderStrategy strategy;
private static int initializeCount = 0;
public SecurityContextHolder() {
}
private static void initialize() {
initializeStrategy();
++initializeCount;
}
private static void initializeStrategy() {
if ("MODE_PRE_INITIALIZED".equals(strategyName)) {
Assert.state(strategy != null, "When using MODE_PRE_INITIALIZED, setContextHolderStrategy must be called with the fully constructed strategy");
} else {
if (!StringUtils.hasText(strategyName)) {
strategyName = "MODE_THREADLOCAL";
}
if (strategyName.equals("MODE_THREADLOCAL")) {
strategy = new ThreadLocalSecurityContextHolderStrategy();
} else if (strategyName.equals("MODE_INHERITABLETHREADLOCAL")) {
strategy = new InheritableThreadLocalSecurityContextHolderStrategy();
} else if (strategyName.equals("MODE_GLOBAL")) {
strategy = new GlobalSecurityContextHolderStrategy();
} else {
try {
Class<?> clazz = Class.forName(strategyName);
Constructor<?> customStrategy = clazz.getConstructor();
strategy = (SecurityContextHolderStrategy)customStrategy.newInstance();
} catch (Exception var2) {
ReflectionUtils.handleReflectionException(var2);
}
}
}
}
public static void clearContext() {
strategy.clearContext();
}
public static SecurityContext getContext() {
return strategy.getContext();
}
public static Supplier<SecurityContext> getDeferredContext() {
return strategy.getDeferredContext();
}
public static int getInitializeCount() {
return initializeCount;
}
public static void setContext(SecurityContext context) {
strategy.setContext(context);
}
public static void setDeferredContext(Supplier<SecurityContext> deferredContext) {
strategy.setDeferredContext(deferredContext);
}
public static void setStrategyName(String strategyName) {
SecurityContextHolder.strategyName = strategyName;
initialize();
}
public static void setContextHolderStrategy(SecurityContextHolderStrategy strategy) {
Assert.notNull(strategy, "securityContextHolderStrategy cannot be null");
strategyName = "MODE_PRE_INITIALIZED";
SecurityContextHolder.strategy = strategy;
initialize();
}
public static SecurityContextHolderStrategy getContextHolderStrategy() {
return strategy;
}
public static SecurityContext createEmptyContext() {
return strategy.createEmptyContext();
}
public String toString() {
String var10000 = strategy.getClass().getSimpleName();
return "SecurityContextHolder[strategy='" + var10000 + "'; initializeCount=" + initializeCount + "]";
}
static {
initialize();
}
}
접근 방법.
SecurityContextHolder.getContext().getAuthentication()
SecurityContextHolder의 메소드는 static으로 선언되기 때문에 어디서든 접근할 수 있음.
SecurityContextHolder는 SecurityContext를 관리하지만, 실제 저장 및 조회 작업은 SecurityContextHolderStrategy에 위임함.
Spring Security의 기본 전략은 ThreadLocalSecurityContextHolderStrategy로, 쓰레드별로 SecurityContext를 저장하여 멀티쓰레드 환경에서 인증 정보가 충돌하지 않도록 함.
package org.springframework.security.core.context;
import java.util.function.Supplier;
import org.springframework.util.Assert;
final class ThreadLocalSecurityContextHolderStrategy implements SecurityContextHolderStrategy {
private static final ThreadLocal<Supplier<SecurityContext>> contextHolder = new ThreadLocal();
ThreadLocalSecurityContextHolderStrategy() {
}
public void clearContext() {
contextHolder.remove();
}
public SecurityContext getContext() {
return (SecurityContext)this.getDeferredContext().get();
}
public Supplier<SecurityContext> getDeferredContext() {
Supplier<SecurityContext> result = (Supplier)contextHolder.get();
if (result == null) {
SecurityContext context = this.createEmptyContext();
result = () -> {
return context;
};
contextHolder.set(result);
}
return result;
}
public void setContext(SecurityContext context) {
Assert.notNull(context, "Only non-null SecurityContext instances are permitted");
contextHolder.set(() -> {
return context;
});
}
public void setDeferredContext(Supplier<SecurityContext> deferredContext) {
Assert.notNull(deferredContext, "Only non-null Supplier instances are permitted");
Supplier<SecurityContext> notNullDeferredContext = () -> {
SecurityContext result = (SecurityContext)deferredContext.get();
Assert.notNull(result, "A Supplier<SecurityContext> returned null and is not allowed.");
return result;
};
contextHolder.set(notNullDeferredContext);
}
public SecurityContext createEmptyContext() {
return new SecurityContextImpl();
}
}
SecurityContextHolder 은 SecurityContext를 ThreadLocal에 저장.
SecurityContext의 생명 주기
접근 쓰레드별 SecurityContext 배분
Spring Security는 SecurityContextHolder를 통해 사용자별 인증 정보를 관리하는데, 이를 위해 ThreadLocal을 활용함.