🍃프로그래머스 백엔드 데브코스 4기 교육과정을 듣고 정리한 글입니다.🍃
Thread Per Request, ThreadLocal
Thread Per Request 모델
- WAS는 ThradPool을 생성하고, Tomcat에서 Thread 수는 기본값이 200
- HTTP 요청이 들어오면 Queue에 적재되고, ThreadPool 내의 특정 Thread가 Queue에서 요청을 가져와 처리하게됨
- HTTP 요청은 처음부터 끝까지 동일한 Thread에서 처리됨
- HTTP 요청 처리가 끝나면 Thread는 다시 ThreadPool에 반납됨
- 즉, WAS의 최대 동시 처리 HTTP 요청의 개수는 ThreadPool의 개수와 같음
- Thead 갯수를 늘리면 동시 처리 갯수가 늘어나지만, Thread Context 스위칭에 의한 오버헤드도 커지기 때문에 성능이 선형적으로 증가하지는 않음
- Spring MVC는 Thread Per Request 모델을 사용
ThreadLocal
- Thread 범위안의 변수이며, 동일 Thread 내에서는 언제든 ThreadLocal 변수에 접근 가능
- 즉, 동일 Thread내에서 실행되는 Controller, Service, Repository, 도메인 모델 어디에서든 명시적인 파라미터 전달 필요없이 ThreadLocal 변수에 접근할 수 있음
- ThreadPool과 함께 사용하는 경우 Thread가 ThreadPool에 반환되기 직전 ThreadLocal 변수 값을 반드시 제거해야함
- 다른 요청을 처리하기 위해 ThreadPool에서 Thread를 하나 가져왔는데 이전 요청 처리에 사용된 ThreadLocal 변수가 남아있고, 이를 참조하여 잘못된 동작을 수행할 수 있음
SecurityContextHolder
그림 참조
public class SecurityContextHolder {
private static SecurityContextHolderStrategy strategy;
public static void clearContext() {
strategy.clearContext();
}
public static SecurityContext getContext() {
return strategy.getContext();
}
public static void setContext(SecurityContext context) {
strategy.setContext(context);
}
}
- SecurityContext 데이터를 쓰거나 읽을 수 있는 API 제공
- 기본적으로 ThreadLocalSecurityContextHolderStrategy을 구현체로 갖는 strategy 필드를 갖고, 이를 사용해서 로직 수행
- ThreadLocal을 사용한다는 것은 Thread Per Request 모델을 고려했음을 의미
- 또한 위에서 언급한 Thread Per Request 모델의 주의점을 해결
- FilterChainProxy의 doFilter() 메서드에서 finally 부분에 SecurityContextHolder.clearContext() 메소드를 활용
- 따라서 strategy는 ThreadLocal이므로 Spring MVC환경에서 Controller, Service, Repository 어느 부분에서든지 SecurityContext 조회 가능
코드 어느 부분에서든지 SecurityContext에 접근할 수 있다는데 SecurityContext이 뭘까?
SecurityContext
public interface SecurityContext extends Serializable {
Authentication getAuthentication();
void setAuthentication(Authentication authentication);
}
- 단순히 Authentication 객체를 조회하거나 설정밖에 안함
Authentication
- 사용자를 표현하는 인증 토큰 인터페이스며, 인증 주체를 표현하는 Principal과 사용자의 권한을 의미하는 GrantedAuthority 목록을 포함
- AnonymousAuthenticationToken 클래스는 익명 사용자를 표현하기 위한 Authentication 인터페이스 구현체
- UsernamePasswordAuthenticationToken 클래스는 로그인 아이디/비밀번호 기반 Authentication 인터페이스 구현체
- RememberMeAuthenticationToken 클래스는 remember-me 기반 Authentication 인터페이스 구현체
- 인증이 완료되거나 혹은 인증되지 않은 사용자 모두를 포괄적으로 표현하며, 인증 여부를 확인할 수 있음
- 사용자의 인증 완료 여부에 따라 Principal 값이 달라짐
- 로그인 전 Principal: 로그인 아이디 (String)
- 로그인 후 Principal: org.springframework.security.core.userdetails.User 객체
인증(Authentication)처리 흐름
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);
setDetails(request, authRequest);
return this.getAuthenticationManager().authenticate(authRequest);
}
- 사용자 인증을 처리하기 위한 필터로 가장 대표적인 것으로 UsernamePasswordAuthenticationFilter 구현체가 있음
- 사용자 인증을 위한 정보를 취합하고, Authentication 객체를 생성함
- UsernamePasswordAuthenticationFilter 구현에서는 로그인 아이디/비밀번호를 취합하고, Authentication 인터페이스 구현체중 하나인UsernamePasswordAuthenticationToken 객체를 생성함
- 아직, 인증이 완료되지 않은 Authentication 객체가 AuthenticationManager 객체로 전달됨
- AuthenticationManager에 의해 인증이 정상적으로 완료된다면 새롭게 만들어진 Authentication 객체를 반환함
- 여기서 새롭게 만들어진 Authentication 객체는 인증이 완료된 상태이고, GrantedAuthority 권한 목록을 포함하고 있음
AuthenticationManager
public interface AuthenticationManager {
Authentication authenticate(Authentication authentication) throws AuthenticationException;
}
- AuthenticationManager 인터페이스는 사용자 인증을 위한 API를 제공함
- 기본 구현체로 org.springframework.security.authentication.ProviderManager 클래스가 있음
ProviderManager
public interface AuthenticationProvider {
Authentication authenticate(Authentication authentication) throws AuthenticationException;
boolean supports(Class<?> authentication);
}
public class ProviderManager implements AuthenticationManager, MessageSourceAware, InitializingBean {
private List<AuthenticationProvider> providers = Collections.emptyList();
@Override
public Authentication authenticate(Authentication authentication) throws AuthenticationException {
Authentication result = null;
for (AuthenticationProvider provider : getProviders()) {
if (!provider.supports(toTest)) {
continue;
}
try {
result = provider.authenticate(authentication);
if (result != null) {
copyDetails(authentication, result);
break;
}
}
}
}
}
- AuthenticationProvider 인터페이스 구현체가 실제 사용자 인증을 처리하게 됨
- AuthenticationProvider를 List로 갖고 있음
- List중 어떤 AuthenticationProvider가 실제 인증을 처리할지 결정할 수 있음
- 주어진 Authentication 객체에 대해 supports() 메소드가 true 를 반환하는 AuthenticationProvider 객체가 인증을 처리함
- 예를 들어 UsernamePasswordAuthenticationToken 타입의 인증 요청은 DaoAuthenticationProvider가 처리함
- 결국 실질적으로 인증 처리를 하는것은 ProviderManager가 아니라 AuthenticationProvider임
RememberMeAuthenticationFilter
- 인증되지 않은 사용자의 HTTP 요청이 remember-me(쿠키 파라미터명) 쿠키(Cookie)를 갖고 있다면, 사용자를 자동으로 인증처리함
- 실제 사용자 인증은 RememberMeServices 인터페이스 구현체를 통해 처리됨
- TokenBasedRememberMeServices: MD5 해시 알고리즘 기반 쿠키 검증
- PersistentTokenBasedRememberMeServices: 외부 데이터베이스에서 인증에 필요한 데이터를 가져오고 검증함
- 사용자마다 고유의 Series 식별자가 생성되고, 인증 시 마다 매번 갱신되는 임의의 토큰 값을 사용하여 보다 높은 보안성을 제공함
- remember-me 기능을 활성화하고 로그인을 하면, 사용자의 정보를 갖고 쿠키를 생성하는 역할도 함
- RememberMeAuthenticationToken
- remember-me 기반 Authentication 인터페이스 구현체
- RememberMeAuthenticationToken 객체는 언제나 인증이 완료된 상태만 존재함
- RememberMeAuthenticationProvider
- RememberMeAuthenticationToken 기반 인증 처리를 위한 AuthenticationProvider
- 앞서 remember-me 설정 시 입력한 key 값을 검증함
- 그외 과정은 일반적인 로그인 처리 과정과 동일
명시적인 로그인 아이디/비밀번호 기반 인증 사용와 권한 구분
- remember-me 기반 인증 결과: RememberMeAuthenticationToken
- 로그인 아이디/비밀번호 기반 인증 결과: UsernamePasswordAuthenticationToken
- remember-me 기반 인증은 로그인 기반 인증 보다 보안상 약한 인증이기 때문에 isFullyAuthenticated() 사용