본 블로그의 모든 Spring Security 포스팅은 Spring Boot 3 이상 버전을 기준으로 작성됩니다
Authentication
Spring Security에서의 Authentication은 인증 및 인가 프로세스 전역적으로 사용되는 토큰의 개념이라고 생각하면 된다
- 인증 프로세스 → 여러 인증 절차를 거친 후 AuthenticationToken을 통해서 최종적으로 인증이 되었는지 판별
- 인가 프로세스 → 인증된 AuthenticationToken & FilterInvocation & GrantedAuthority 기반 자원에 대한 인가 처리
public interface Authentication extends Principal, Serializable {
/**
* Application에서 부여한 권한 정보들
*/
Collection<? extends GrantedAuthority> getAuthorities();
/**
* Authentication Request의 메타 정보 (AuthenticationDetailsSource)
* -> IP Address
* -> Session ID
* -> ...
*/
Object getDetails();
/**
* Authentication 처리된 Identity Object
* -> Principal
* -> UserDetails
* -> ...
*/
Object getPrincipal();
/**
* AuthenticationToken을 활용한 인증 프로세스 시 사용할 자격증명
* -> 일반적으로 계정의 비밀번호
*/
Object getCredentials();
/**
* 현재 요청에 대해서 AuthenticationToken이 인증되었는지 여부
*/
boolean isAuthenticated();
/**
* 인증 여부 설정
*/
void setAuthenticated(boolean isAuthenticated) throws IllegalArgumentException;
}
간략한 인증 프로세스
- 인증 프로세스가 진행되는 Basic한 흐름
- 가장 기본적인 Form Login Based UsernamePasswordAuthenticationFilter
- isAuthenticated = false인 UsernamePasswordAuthenticationToken을 통해서 인증 프로세스 진행
- 인증 완료 후 successfulAuthentication 로직 흐름
- 인증된 AuthenticationToken을 SecurityContext로 감싸고 SecurityContext를 SecurityContextHolder에 담아두기
SecurityContext & SecurityContextHolder
SecurityContext → 인증된 AuthenticationToken을 감싸는 상자 개념
SecurityContextHolder → SecurityContext를 Strategy에 따라 담아두는 Holder
// SecurityContext
public interface SecurityContext extends Serializable {
Authentication getAuthentication();
void setAuthentication(Authentication authentication);
}
// SecurityContextHolder
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(SYSTEM_PROPERTY);
private static SecurityContextHolderStrategy strategy;
private static int initializeCount = 0;
static {
initialize();
}
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");
return;
}
if (!StringUtils.hasText(strategyName)) {
// Set default
strategyName = MODE_THREADLOCAL;
}
if (strategyName.equals(MODE_THREADLOCAL)) {
strategy = new ThreadLocalSecurityContextHolderStrategy();
return;
}
if (strategyName.equals(MODE_INHERITABLETHREADLOCAL)) {
strategy = new InheritableThreadLocalSecurityContextHolderStrategy();
return;
}
if (strategyName.equals(MODE_GLOBAL)) {
strategy = new GlobalSecurityContextHolderStrategy();
return;
}
// 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);
}
}
public static void clearContext() {
strategy.clearContext();
}
public static SecurityContext getContext() {
return strategy.getContext();
}
...
...
}
이렇게 Strategy에 따라 SecurityContext를 SecurityContextHolder에 담아두고 securityContextHolderStrategy.getContext()를 통해서 필요한 어느곳에서든 꺼내서 활용할 수 있는 것이다
3가지 SecurityContextHolder 전략
1. MODE_THREADLOCAL
각각의 요청에 대한 SecurityContext를 ThreadLocal로 관리하는 방식
2. MODE_INHERITABLETHREADLOCAL
Parent Thread & Created Child Thread간 InheritableThreadLocal을 활용해서 관리하는 방식
3. MODE_GLOBAL
전역적으로 1개의 SecurityContext를 관리하고 공용으로 활용하는 방식
SecurityContext를 관리하는 방식 (SecurityContextRepository)
1. HttpSessionSecurityContextRepository
HttpSession에 SecurityContext를 저장하는 방식
2. NullSecurityContextRepository
SecurityContext를 어디에도 저장하지 않는 방식
- 어디에도 SecurityContext를 저장하지 않는 방식이기 때문에 saveContext의 로직은 비어있고 loadContext또한 저장이 안되어 있기 때문에 emptyContext를 받아오게 된다
SessionManagement → SessionCreationPolicy
어떤 상황에서 HttpSession에 저장하고 어떤 상황에서 NullRepository를 통해서 SecurityContext를 저장하지 않는지는 SecurityFilterChain에서 SecurityConfig를 진행할 때 SessionManagement의 SessionCreationPolicy를 어떻게 설정하냐에 따라 다르다
@Bean
public SecurityFilterChain filterChain(HttpSecurity http) {
http
// ...
.sessionManagement((session) -> session
.sessionCreationPolicy(SessionCreationPolicy.{Specific Policy...})
);
return http.build();
}
- ALWAYS → 항상 HttpSession 활용
- NEVER → HttpSession이 없으면 굳이 만들지는 않지만 있으면 있는거 활용
- IF_REQUIRED → 필요하면 HttpSession 활용
- STATELESS → HttpSession을 절대 사용 X
1 ~ 3 = HttpSessionSecurityContextRepository
4 = NullSecurityContextRepository
- 어떠한 설정도 없는 경우 기본값 = IF_REQUIRED
SecurityContextHolderFilter
FilterChainProxy에서 Chaining되는 여러 필터중에 꽤 앞단에 위치한 SecurityContextHolderFilter의 역할 및 로직을 알아보자
SecurityContextHolderFilter는 SecurityContextRepository를 통해서
→ 1. 요청이 들어오는 시점에 SecurityContextHolder에 SecurityContext를 load해서 넣어주기
→ 2. 요청이 마무리되는 시점에 SecurityConextHolder 비우기