인증 객체 - Authentication
이전에 기본 인증 API에 대해 다뤘을 때 `Authentication`의 구현체가 자주 등장했었다. 이제 이 인터페이스를 상세하게 살펴보자.
`Authentication`은 이름 그대로 Spring Security 전역에서 사용자의 인증 정보를 다룰 때 사용하기 위한 역할들을 추상화 해놓았다.
인증 시 `Authentication`을 구현한 객체를 `SecurityContext`에 저장하고, 인증 정보가 필요할 경우 `SecurityContext`에서 `Authentication` 객체를 꺼내어 각종 정보를 토대로 보안 로직을 수행하게 된다.
6개의 추상 메서드가 정의되어 있는데 한번 살펴보자.
package org.springframework.security.core;
public interface Authentication extends Principal, Serializable {
// 인증된 사용자의 권한 목록
Collection<? extends GrantedAuthority> getAuthorities();
// 사용자의 비밀번호
Object getCredentials();
// 사용자의 부가 정보들
Object getDetails();
// 사용자 사이디 혹은 인증된 사용자 정보
Object getPrincipal();
// 인증 여부
boolean isAuthenticated();
// 인증 여부 설정
void setAuthenticated(boolean isAuthenticated) throws IllegalArgumentException;
}
코드에 주석으로 각 추상 메서드들의 역할이 무엇인지 적어놓았다. 그렇다면 Spring Security에서 Form Login 활성 시 어떤 `Authentication` 구현체가 어떻게 저장되고 사용되는지 과정을 살펴보며 이해해보자.
UsernamePasswordAuthenticationToken 구현체
Spring Security에서 Form Login 기능을 사용하면 기본적으로 `UsernamePasswordAuthenticationToken` 구현체를 사용하여 인증을 처리한다. Form Login시에는 `AbstractAuthenticationProcessingFilter`가 인증 작업을 수행한다는 것은 이전에 알아보았기 때문에 한번 여기에만 초점을 두어 다시 살펴보자.
package org.springframework.security.web.authentication;
public abstract class AbstractAuthenticationProcessingFilter extends GenericFilterBean
implements ApplicationEventPublisherAware, MessageSourceAware {
private void doFilter(HttpServletRequest request, HttpServletResponse response, FilterChain chain)
throws IOException, ServletException {
...
try {
Authentication authenticationResult = attemptAuthentication(request, response);
if (authenticationResult == null) {
// return immediately as subclass has indicated that it hasn't completed
return;
}
this.sessionStrategy.onAuthentication(authenticationResult, request, response);
// Authentication success
if (this.continueChainBeforeSuccessfulAuthentication) {
chain.doFilter(request, response);
}
successfulAuthentication(request, response, chain, authenticationResult);
}
catch (InternalAuthenticationServiceException failed) {
this.logger.error("An internal error occurred while trying to authenticate the user.", failed);
unsuccessfulAuthentication(request, response, failed);
}
catch (AuthenticationException ex) {
// Authentication failed
unsuccessfulAuthentication(request, response, ex);
}
}
protected void successfulAuthentication(HttpServletRequest request, HttpServletResponse response, FilterChain chain,
Authentication authResult) throws IOException, ServletException {
SecurityContext context = SecurityContextHolder.createEmptyContext();
context.setAuthentication(authResult);
SecurityContextHolder.setContext(context);
this.securityContextRepository.saveContext(context, request, response);
if (this.logger.isDebugEnabled()) {
this.logger.debug(LogMessage.format("Set SecurityContextHolder to %s", authResult));
}
this.rememberMeServices.loginSuccess(request, response, authResult);
if (this.eventPublisher != null) {
this.eventPublisher.publishEvent(new InteractiveAuthenticationSuccessEvent(authResult, this.getClass()));
}
this.successHandler.onAuthenticationSuccess(request, response, authResult);
}
}
우선 `attemptAuthentication()`을 통해 `Authentication` 객체를 얻고 있다.
`attemptAuthentication()`은 추상 메서드로 정의되어 있고, 역시 `UsernamePasswordAuthenticationFilter`가 이를 구현하고 있다. 한번 살펴보자.
package org.springframework.security.web.authentication;
public class UsernamePasswordAuthenticationFilter extends AbstractAuthenticationProcessingFilter {
...
@Override
public Authentication attemptAuthentication(HttpServletRequest request, HttpServletResponse response)
throws AuthenticationException {
if (this.postOnly && !request.getMethod().equals("POST")) {
throw new AuthenticationServiceException("Authentication method not supported: " + request.getMethod());
}
String username = obtainUsername(request);
username = (username != null) ? username.trim() : "";
String password = obtainPassword(request);
password = (password != null) ? password : "";
UsernamePasswordAuthenticationToken authRequest = UsernamePasswordAuthenticationToken.unauthenticated(username,
password);
// Allow subclasses to set the "details" property
setDetails(request, authRequest);
return this.getAuthenticationManager().authenticate(authRequest);
}
}
보면 `UsernamePasswordAuthenticationToken`.`unauthenticated()`를 통해 객체를 생성하고 있다.
`UsernamePasswordAuthenticationToken`는 `Authentication`을 구현하는 `AbstractAuthenticationToken` 추상 클래스의 구현체이다. 한번 살펴보자.
package org.springframework.security.authentication;
public class UsernamePasswordAuthenticationToken extends AbstractAuthenticationToken {
private final Object principal;
private Object credentials;
public UsernamePasswordAuthenticationToken(Object principal, Object credentials) {
super(null);
this.principal = principal;
this.credentials = credentials;
setAuthenticated(false);
}
public UsernamePasswordAuthenticationToken(Object principal, Object credentials,
Collection<? extends GrantedAuthority> authorities) {
super(authorities);
this.principal = principal;
this.credentials = credentials;
super.setAuthenticated(true); // must use super, as we override
}
public static UsernamePasswordAuthenticationToken unauthenticated(Object principal, Object credentials) {
return new UsernamePasswordAuthenticationToken(principal, credentials);
}
public static UsernamePasswordAuthenticationToken authenticated(Object principal, Object credentials,
Collection<? extends GrantedAuthority> authorities) {
return new UsernamePasswordAuthenticationToken(principal, credentials, authorities);
}
@Override
public Object getCredentials() {
return this.credentials;
}
@Override
public Object getPrincipal() {
return this.principal;
}
@Override
public void setAuthenticated(boolean isAuthenticated) throws IllegalArgumentException {
Assert.isTrue(!isAuthenticated,
"Cannot set this token to trusted - use constructor which takes a GrantedAuthority list instead");
super.setAuthenticated(false);
}
@Override
public void eraseCredentials() {
super.eraseCredentials();
this.credentials = null;
}
}
`unauthenticated()` 팩토리 메서드를 살펴보면, `principal`과 `credentials`를 인자로 받아 `authorities`가 없는 생성자를 호출하고 있다. 생성자를 살펴보면 `setAuthenticated()`에 false로 인자를 주어 생성하고 있음을 확인할 수 있다.
따라서 결론적으로 `attemptAuthenticataion()`은 사용자 이름과 비밀번호를 인자로 넣은 아직 인증되지 않은`UsernamePasswordAuthenticationToken` 객체를 생성한 후 부모로 부터 `AuthenticationManager` 객체를 얻어서 인증을 시도하게 된다.
부모인 `AbstractAuthenticationProcessingFilter`의 `authenticationManager`에는 `ProviderManager`객체가 초기화 되어 있다. 이 클래스는 `AuthenticationProvider`를 관리하는 클래스인데, `AuthenticationProvider` 구현체는 인증 작업을 수행한 후 `UsernamePasswordAuthenticationToken`의 `authenticated()` 팩토리 메서드를 호출하여 인증에 성공한 `Authentication` 객체를 리턴해주게 된다. (다음에 자세히 다룬다)
따라서 `AbstractAuthenticationProcessingFilter`의 `successfulAuthentication()`이 호출되어 인증에 성공한 `Authentication` 객체가 `SecurityContext`에 저장되게 된다.
코드와 함께 살펴보느라 굉장히 복잡하게 설명을 한 거 같아 간단하게 정리해보자.
1. Form Login 인증 시 UsernamePasswordAuthenticationToken 구현체를 생성하여 인증을 수행
2. 인증 로직을 수행하기 위해 principal, credentials에 username, password 값을 넣고 인증 여부를 false로 하여 객체 초기화 3. AuthenticationManger를 통해 인증, 내부에서 AuthenticationProvider를 통해 인증 수행
4. 인증에 성공한 경우 principal, credentials, authorities에 User, password, 권한들 값을 넣고 인증 여부를 true로 하여 UsernamePasswordAuthenticationToken 구현체를 생성
5. SecurityContext에 해당 Authentication 객체 저장
Spring Security에서 제공하는 기본 Form Login은 `Authentication` 객체를 위와 같이 다룬다.
이는 우리가 `Authentication` 객체를 커스터마이징 하여 다르게 인증 과정을 수행할 수도 있다.
인증 객체 저장소 - SecurityContext
이전에 내용들을 배우면서 `SecurityContext`에 `Authentication` 객체를 저장한다는 말을 굉장히 많이 했다. 여기서 `SecurityContext`가 뭔지 이제 한번 자세히 알아보자.
`SecurityContext`는 `Authentication` 객체가 저장되는 보관소이며, 필요하면 어디서는 객체를 꺼낼 수 있도록 제공되는 클래스이다. 기본적으로 `ThreadLocal`에 저장되며, 인증이 완료된 경우 `Session`에 저장하기 때문에 아무 곳에서 참조가 가능하다.
`SecurityContext`는 `SecurityContextHolder` 클래스를 통해 저장하고, 얻고, 초기화할 수가 있다. 이 클래스를 한번 살펴보자.
SecurityContextHolder
package org.springframework.security.core.context;
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();
}
public static int getInitializeCount() {
return initializeCount;
}
public static void setContext(SecurityContext context) {
strategy.setContext(context);
}
public static void setStrategyName(String strategyName) {
SecurityContextHolder.strategyName = strategyName;
initialize();
}
public static void setContextHolderStrategy(SecurityContextHolderStrategy strategy) {
Assert.notNull(strategy, "securityContextHolderStrategy cannot be null");
SecurityContextHolder.strategyName = MODE_PRE_INITIALIZED;
SecurityContextHolder.strategy = strategy;
initialize();
}
public static SecurityContextHolderStrategy getContextHolderStrategy() {
return strategy;
}
public static SecurityContext createEmptyContext() {
return strategy.createEmptyContext();
}
}
모든 멤버가 정적으로 선언되어 있으며, `SecurityContextHolderStrategy`를 통해서 `SecurityContext`를 설정하고 얻을 수 있다. `SecurityContextHolderStrategy`는 인터페이스로, `SecurityContext`를 관리하는 역할을 정의해 놓았다. 따라서 구현체의 전략에 따라` SecurityContext`를 관리하는 방법이 달라진다.
`setContextHolderStrategy()` 메서드를 통해 전략을 동적으로 지정할 수 있으며, 모드에 따라 관리되는 방식을 아래의 표로 정리해 놓았다.
| 이름 | 설명 |
| `MODE_THREADLOCAL` | 스레드 당 SecurityContext를 할당한다. 기본 값이다 |
| `MODE_INHERITABLETHREADLOCAL` | 메인 스레드와 자식 스레드 당 SecurityContext를 할당한다. |
| `MODE_GLOBAL` | 애플리케이션에서 단 하나의 SecurityContext를 할당한다. |
기본적으로 요청이 들어오면 스프링에서는 하나의 스레드를 할당한다. 따라서 MODE_THREADLOCAL를 사용하면 `ThreadLocal`에 해당 `SecurityContext`를 저장하게 되고, 애플리케이션 전역에서 스레드에 따라 항상 동일한 `SecurityContext`를 설정하고 얻을 수 있다.
MODE_THREADLOCAL일 경우 `ThreadLocalSecurityContextHolderStrategy` 구현체가 `strategy` 멤버에 초기화 되는데 한번 이 클래스를 살펴보자.
package org.springframework.security.core.context;
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();
}
}
보면 `ThreadLocal` 객체를 생성한 다음 이 객체를 통해 `SecurityContext`를 관리하는 것을 볼 수 있다.
SecurityContextPersistenceFilter
지금까지 사용자 인증 정보를 관리하는 `Authentication`, 이 `Authentication` 객체를 관리하는 `SecurityContext`, `SecurityContext`를 관리하는 `SecurityContextHolder`에 대해서 다뤘다. 그리고 이전에 인증 필터, 익명 사용자 필터 등을 배우면서 `Authentication` 객체를 `SecurityContext`에 저장하는 과정까지 배웠었다. 그렇다면 이제는 `SecurityContext`를 어느 부분에서 초기화, 생성하고 관리하고 제거하는지에 대해 배워보자.
`SecurityContextPersistenceFilter`가 이 역할을 수행한다. 이 필터는 Spring Security 필터 두 번째에 위치해있는 필터이다. 각종 인증, 인가 등의 보안 필터 로직을 수행하기 전에 `SecurityContext`를 생성하여 `SecurityContextHolder`에 초기화 하고, 종료 후에는 `Session`에 `SecurityContext` 저장 및 `SecurityContextHolder`에 `SecurityContext`를 삭제한다. 한번 코드로 살펴보자.
package org.springframework.security.web.context;
public class SecurityContextPersistenceFilter extends GenericFilterBean {
...
private SecurityContextRepository repo;
...
private void doFilter(HttpServletRequest request, HttpServletResponse response, FilterChain chain)
throws IOException, ServletException {
...
SecurityContext contextBeforeChainExecution = this.repo.loadContext(holder);
try {
SecurityContextHolder.setContext(contextBeforeChainExecution);
...
chain.doFilter(holder.getRequest(), holder.getResponse());
}
finally {
SecurityContext contextAfterChainExecution = SecurityContextHolder.getContext();
// Crucial removal of SecurityContextHolder contents before anything else.
SecurityContextHolder.clearContext();
this.repo.saveContext(contextAfterChainExecution, holder.getRequest(), holder.getResponse());
request.removeAttribute(FILTER_APPLIED);
this.logger.debug("Cleared SecurityContextHolder to complete request");
}
}
...
}
보면 `SecurityContextRepository`에서 `SecurityContext`를 가져온 다음, `SecurityContextHolder`에 해당 `SecurityContext`를 설정한다. 그리고 다음 필터 로직들을 호출한 다음에 최종적으로 `SecurityContextHolder`로 부터 `SecurityContext`를 가져온 다음 클리어한다. 그 다음 `saveContext()`를 통해 `Session`에 `SecurityContext`를 저장한다.
`SecurityContextRepository`는 인터페이스로, 구현체는 `HttpSessionSecurityContextRepository`가 초기화 되어 있다. 한번 핵심 메서드들만 살펴보자.
package org.springframework.security.web.context;
public class HttpSessionSecurityContextRepository implements SecurityContextRepository {
private AuthenticationTrustResolver trustResolver = new AuthenticationTrustResolverImpl();
@Override
public SecurityContext loadContext(HttpRequestResponseHolder requestResponseHolder) {
HttpServletRequest request = requestResponseHolder.getRequest();
HttpServletResponse response = requestResponseHolder.getResponse();
HttpSession httpSession = request.getSession(false);
SecurityContext context = readSecurityContextFromSession(httpSession);
if (context == null) {
context = generateNewContext();
if (this.logger.isTraceEnabled()) {
this.logger.trace(LogMessage.format("Created %s", context));
}
}
if (response != null) {
SaveToSessionResponseWrapper wrappedResponse = new SaveToSessionResponseWrapper(response, request,
httpSession != null, context);
requestResponseHolder.setResponse(wrappedResponse);
requestResponseHolder.setRequest(new SaveToSessionRequestWrapper(request, wrappedResponse));
}
return context;
}
@Override
public void saveContext(SecurityContext context, HttpServletRequest request, HttpServletResponse response) {
SaveContextOnUpdateOrErrorResponseWrapper responseWrapper = WebUtils.getNativeResponse(response,
SaveContextOnUpdateOrErrorResponseWrapper.class);
if (responseWrapper == null) {
saveContextInHttpSession(context, request);
return;
}
responseWrapper.saveContext(context);
}
...
}
`loadContext()`를 살펴보면 우선 세션을 얻은 다음, 세션으로 부터 `SecurityContext`가 이미 있는지 찾아본다. 이는 만약 인증된 사용자라면, `SecurityContextPersistenceFilter` 에서 마지막에 세션에 `SecurityContext`를 저장하기 때문에 세션에 `SecurityContext`가 존재할 것이다. , 인증된 사용자인 지 구분하는 역할을 하는 셈이다. 따라서 세션에 이미 `SecurityContext`가 있는 인증된 사용자라면 해당 객체를 리턴하게 되고, 이후 필터들은 이 인증된 사용자 정보가 담긴 `SecurityContext`에 대해 로직을 수행하게 된다. 만약 없다면 빈 `SecurityContext`를 생성해서 리턴해주게 된다.
`saveContext()`를 살펴보면 세션에 `SecurityContext`를 저장하는 역할을 수행한다.
결국 `SecurityContextPersistenceFilter`와 `SecurityContextRepository`, 이전에 배운 필터들을 조합하여 정리하자면 아래와 같이 정리할 수 있다.
인증 시
1. 빈 SecurityContext 객체를 생성하여 SecurityContextHolder에 저장
2-1. 익명 사용자인 경우 AnonymousAuthenticationFilter에서 AnonymousAuthenticationToken을 SecurityContext에 저장
2-2. 인증 사용자인 경우 UsernamePasswordAuthenticationFilter에서 UsernamePasswordAuthenticationToken을 SecurityContext에 저장
3. Session에 SecurityContext 저장
인증 후
1. Session에서 SecurityContext 객체를 꺼낸 후 SecurityContextHolder에 저장
2. 다른 필터 로직 수행
3. Session에 SecurityContext 저장