SecurityContextHolderFilter
SecurityContextHolderFilter는 SecurityFilterChain의 Security Filter로 SecurityContext를 HTTP 요청 간에 관리하는 역할을 수행한다. 이를 위해 SecurityContextRepository를 사용한다.
해당 필터의 동작 방식은 다음과 같다.
- SecurityContextHolderFilter는 SecurityContext를 SecurityContextRepository에서 로드하고 SecurityContextHolder에 설정한다.
주의할 점은 다음과 같다.
- SecurityContextHolderFilter는 SecurityContext를 로드하지만 이를 SecurityContextRepository에 다시 저장하지 않는다.
- 즉, 요청 처리 중에 SecurityContext에 대한 변경 사항이 자동으로 지속되지 않는다.
- 따라서 SecurityContextHolderFilter를 사용할 때는 수정된 SecurityContext를 명시적으로 SecurityContextRepository에 저장해야 한다.
public class SecurityContextHolderFilter extends GenericFilterBean {
private static final String FILTER_APPLIED = SecurityContextHolderFilter.class.getName() + ".APPLIED";
private final SecurityContextRepository securityContextRepository;
private SecurityContextHolderStrategy securityContextHolderStrategy = SecurityContextHolder
.getContextHolderStrategy();
/**
* Creates a new instance.
* @param securityContextRepository the repository to use. Cannot be null.
*/
public SecurityContextHolderFilter(SecurityContextRepository securityContextRepository) {
Assert.notNull(securityContextRepository, "securityContextRepository cannot be null");
this.securityContextRepository = securityContextRepository;
}
@Override
public void doFilter(ServletRequest request, ServletResponse response, FilterChain chain)
throws IOException, ServletException {
doFilter((HttpServletRequest) request, (HttpServletResponse) response, chain);
}
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);
}
}
/**
* Sets the {@link SecurityContextHolderStrategy} to use. The default action is to use
* the {@link SecurityContextHolderStrategy} stored in {@link SecurityContextHolder}.
*
* @since 5.8
*/
public void setSecurityContextHolderStrategy(SecurityContextHolderStrategy securityContextHolderStrategy) {
Assert.notNull(securityContextHolderStrategy, "securityContextHolderStrategy cannot be null");
this.securityContextHolderStrategy = securityContextHolderStrategy;
}
}
- SecurityContextRepository의 구현체 중 하나인 HttpSessionSecurityContextRepository는 세션 기반의 인증 방식에서 사용된다.
- 사용자가 세션에 인증되어 있는 경우, HttpSessionSecurityContextRepository는 세션에 저장된 SecurityContext를 반환하여 사용자의 인증 정보를 유지한다.
- 그러나 사용자가 세션에 인증되어 있지 않은 경우, 새로운 SecurityContext를 생성하여 반환한다.
- SecurityContextHolderStrategy의 구현체 중 하나인 ThreadLocalSecurityContextHolderStrategy를 통해 SecurityContext를 SecurityContextHolder에 저장한다.
SecurityContextHolder
SecurityContextHolder는 Spring Security 인증 모델의 핵심으로, 현재 실행 중인 스레드의 SecurityContext를 제어한다. 이 SecurityContext에는 현재 인증된 사용자의 인증 객체가 포함되어 있다. 일반적으로 이는 Authentication 객체이다.
즉, 요청을 처리하는 동안에 인증된 사용자의 인증 정보에 접근해야 할 때마다 SecurityContextHolder를 사용하여 현재 SecurityContext를 가져올 수 있다. 이를 통해 인증된 사용자의 세부 정보와 권한을 확인할 수 있다.
- SecurityContextHolder에 SecurityContext를 설정하는 방법
SecurityContext context = SecurityContextHolder.createEmptyContext(); Authentication authentication = new TestingAuthenticationToken("username", "password", "ROLE_USER"); context.setAuthentication(authentication); SecurityContextHolder.setContext(context);
새로운 SecurityContext 인스턴스를 생성하여 SecurityContextHolder에 설정한다 이때, 새로운 Authentication 객체를 생성하여 SecurityContext에 설정한다.
이렇게 함으로써 현재 사용자의 인증 및 권한 정보를 포함하는 SecurityContext를 해당 요청을 처리하는 동안 전역적으로 사용할 수 있다.
- SecurityContextHolder에서 SecurityContext를 꺼내오는 방법
SecurityContext context = SecurityContextHolder.getContext(); Authentication authentication = context.getAuthentication(); String username = authentication.getName(); Object principal = authentication.getPrincipal(); Collection<? extends GrantedAuthority> authorities = authentication.getAuthorities();
더 간편한 방법으로 현재 인증된 사용자에 대한 정보가 필요할 때 SecurityContextHolder.getContext().getAuthentication()을 호출할 수 있다.
해당 메서드는 현재 스레드의 SecurityContext에서 인증 객체를 반환한다. 따라서 이 객체를 통해 사용자의 상세 정보 및 권한을 액세스할 수 있다.
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";
public static final String SYSTEM_PROPERTY = "spring.security.strategy";
private static String strategyName = System.getProperty(SYSTEM_PROPERTY);
private static SecurityContextHolderStrategy strategy;
private static int initializeCount = 0;
static {
initialize();
}
private static void initialize() {
if (!StringUtils.hasText(strategyName)) {
// Set default
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 to load a custom strategy
try {
Class<?> clazz = Class.forName(strategyName);
Constructor<?> customStrategy = clazz.getConstructor();
strategy = (SecurityContextHolderStrategy) customStrategy.newInstance();
}
catch (Exception ex) {
ReflectionUtils.handleReflectionException(ex);
}
}
initializeCount++;
}
/**
* Explicitly clears the context value from the current thread.
*/
public static void clearContext() {
strategy.clearContext();
}
/**
* Obtain the current <code>SecurityContext</code>.
* @return the security context (never <code>null</code>)
*/
public static SecurityContext getContext() {
return strategy.getContext();
}
/**
* Primarily for troubleshooting purposes, this method shows how many times the class
* has re-initialized its <code>SecurityContextHolderStrategy</code>.
* @return the count (should be one unless you've called
* {@link #setStrategyName(String)} to switch to an alternate strategy.
*/
public static int getInitializeCount() {
return initializeCount;
}
/**
* Associates a new <code>SecurityContext</code> with the current thread of execution.
* @param context the new <code>SecurityContext</code> (may not be <code>null</code>)
*/
public static void setContext(SecurityContext context) {
strategy.setContext(context);
}
/**
* Changes the preferred strategy. Do <em>NOT</em> call this method more than once for
* a given JVM, as it will re-initialize the strategy and adversely affect any
* existing threads using the old strategy.
* @param strategyName the fully qualified class name of the strategy that should be
* used.
*/
public static void setStrategyName(String strategyName) {
SecurityContextHolder.strategyName = strategyName;
initialize();
}
/**
* Allows retrieval of the context strategy. See SEC-1188.
* @return the configured strategy for storing the security context.
*/
public static SecurityContextHolderStrategy getContextHolderStrategy() {
return strategy;
}
/**
* Delegates the creation of a new, empty context to the configured strategy.
*/
public static SecurityContext createEmptyContext() {
return strategy.createEmptyContext();
}
@Override
public String toString() {
return "SecurityContextHolder[strategy='" + strategyName + "'; initializeCount=" + initializeCount + "]";
}
}
SecurityContextHolder는 SecurityContextHolderStrategy라는 필드를 내부적으로 가지고 있다. 이 필드는 실제로 SecurityContext의 저장을 담당한다.
initialize 메서드를 SecurityContextHolder가 SecurityContext를 저장하고 관리하는 방식이 결정된다.
MODE_THREADLOCAL: 각 스레드마다 별도의 SecurityContext를 유지. 스레드 간에는 공유되지 않으며, 각 스레드에서는 독립적으로 보안 컨텍스트를 유지. MODE_INHERITABLETHREADLOCAL: 현재 스레드의 SecurityContext가 자식 스레드에 상속. 따라서 스레드 간에는 동일한 보안 컨텍스트가 유지. MODE_GLOBAL: 전역적으로 단 하나의 SecurityContext를 유지. 따라서 응용 프로그램 전체에서 동일한 보안 컨텍스트를 사용.
- 일반적으로 변경이 없으면 기본적으로 ThreadLocalSecurityContextHolderStrategy가 사용된다.
- 이 전략은 각 스레드마다 SecurityContext를 별도로 관리하며, 스레드 간에는 공유되지 않는다. 이를 통해 각 스레드가 독립적으로 보안 SecurityContext를 유지할 수 있다.
- 이와 달리 InheritableThreadLocalSecurityContextHolderStrategy는 현재 스레드의 SecurityContext를 자식 스레드에 상속할 수 있다.
final class ThreadLocalSecurityContextHolderStrategy implements SecurityContextHolderStrategy {
private static final ThreadLocal<SecurityContext> contextHolder = new ThreadLocal<>();
@Override
public void clearContext() {
contextHolder.remove();
}
@Override
public SecurityContext getContext() {
SecurityContext ctx = contextHolder.get();
if (ctx == null) {
ctx = createEmptyContext();
contextHolder.set(ctx);
}
return ctx;
}
@Override
public void setContext(SecurityContext context) {
Assert.notNull(context, "Only non-null SecurityContext instances are permitted");
contextHolder.set(context);
}
@Override
public SecurityContext createEmptyContext() {
return new SecurityContextImpl();
}
}
- SecurityContextHolder는 주로 ThreadLocal을 사용하여 현재 스레드의 SecurityContext를 저장하고 관리한다.
- ThreadLocal은 멀티스레드 환경에서 각 스레드마다 독립적으로 값을 유지하고 접근할 수 있도록 해주는 클래스다.
- Thread가 수행하는 호출 스택의 어디에 있던 Thread 본인이 수행하고 있다면 어디에서든 접근할 수 있다.
SecurityContext
SecurityContext는 SecurityContextHolder에서 얻을 수 있다. SecurityContext에는 Authentication 객체가 포함되어 있다.
public class SecurityContextImpl implements SecurityContext {
private static final long serialVersionUID = SpringSecurityCoreVersion.SERIAL_VERSION_UID;
private Authentication authentication;
public SecurityContextImpl() {
}
public SecurityContextImpl(Authentication authentication) {
this.authentication = authentication;
}
@Override
public boolean equals(Object obj) {
if (obj instanceof SecurityContextImpl other) {
if ((this.getAuthentication() == null) && (other.getAuthentication() == null)) {
return true;
}
if ((this.getAuthentication() != null) && (other.getAuthentication() != null)
&& this.getAuthentication().equals(other.getAuthentication())) {
return true;
}
}
return false;
}
@Override
public Authentication getAuthentication() {
return this.authentication;
}
@Override
public int hashCode() {
return ObjectUtils.nullSafeHashCode(this.authentication);
}
@Override
public void setAuthentication(Authentication authentication) {
this.authentication = authentication;
}
@Override
public String toString() {
StringBuilder sb = new StringBuilder();
sb.append(getClass().getSimpleName()).append(" [");
if (this.authentication == null) {
sb.append("Null authentication");
}
else {
sb.append("Authentication=").append(this.authentication);
}
sb.append("]");
return sb.toString();
}
}
- SecurityContext의 실제 구현체에 해당하는 클래스로 Authentication이라는 필드를 갖고 있어 SecurityContextHolder에 Authentication을 담기 위한 목적이 있다.
Authentication
Authentication 인터페이스는 Spring Security 내에서 두 가지 주요 목적을 제공한다.
- AuthenticationManager에게 사용자의 자격 증명을 제공하여 사용자를 인증하는 데 사용된다.
- 현재 인증된 사용자를 나타낸다. SecurityContext에서 현재 Authentication을 얻을 수 있다.
Authentication에는 다음이 포함된다.
- Principal: 사용자의 식별 정보를 나타낸다. 주로 UserDetails 인터페이스의 구현체이다. 아이디 및 사용자의 고유 식별자 정보를 포함한다.
- Credentials: 사용자의 자격 증명을 나타낸다. 주로 암호 또는 인증 토큰과 같은 비밀 정보를 의미한다.
- Authorities: 사용자에게 부여된 권한을 나타낸다. GrantedAuthority 인터페이스를 구현한 객체의 집합으로 표현된다. 주로 역할(Role)이나 스코프(scope)와 같은 것을 나타낸다.