프로젝트를 진행하던 중, AOP를 이용해 Access Token을 검증하는 로직을 구현했다. (스프링 시큐리티없이)
AuthAspect 클래스 (Around Advice)
@Around("@annotation(Auth)")
public Object around(final ProceedingJoinPoint pjp) throws Throwable {
try {
String token = httpServletRequest.getHeader(AUTHORIZATION);
JwtPayload payload = jwtService.getPayload(token);
Optional<UserDto> user = userMapper.findByUserId(payload.getId());
if (user.isEmpty()) {
throw new UserNotFoundException("존재하지 않는 유저입니다.");
}
return pjp.proceed(new Object[] { user.get().getId() });
} catch (SignatureException | ExpiredJwtException | MalformedJwtException | UnsupportedJwtException e) {
throw new IllegalArgumentException(e.getMessage());
}
}
초기에 이렇게 구현해보았다. (쌩 초반 코드)
하지만 Controller에서 파라미터가 많아지면 각 메서드마다 해결해야해서 좋은 방법이 없을까 고민했다.
그때 팀원분이 ThreadLocal을 이용한 방법을 알려주셨다!
코드를 보니까 Spring Security를 이용했을 때 SecurityContextHolder가 떠올랐다. (이건 맨 밑 참고)
소스 전체에서 사용할 수 있는 전역 변수처럼 데이터를 할당하고 싶을 때 쓰는 클래스라 할 수 있다.
ThreadLocal 클래스는 하나의 쓰레드에 의해 읽고 쓸 수 있는 변수를 생성한다.
2개의 쓰레드가 같은 코드를 실행할 때 각각의 쓰레드는 서로의 ThreadLocal 변수를 확인할 수 없다.
👉 Thread 단위 Local 변수를 할당하는 기능이다.
한 스레드에서 실행되는 코드가 동일한 객체를 사용할 수 있도록 해주기 때문에 관련된 코드에서 파라미터를 사용하지 않고 객체를 각자가 가져다 쓸 때 사용된다.
(출처 : 최범균님 블로그)
ThreadLocal은 메모리 누수의 주범이 될 수 있어 주의해서 사용해야한다.
ThreadLocal
객체를 생성한다.ThreadLocal.set()
메서드를 이용해서 현재 쓰레드의 로컬 변수에 값을 저장한다.ThreadLocal.get()
메서드를 이용해서 현재 쓰레드의 로컬 변수 값을 읽어온다.ThreadLocal.remove()
메서드를 이용해서 현재 쓰레드의 로컬 변수 값을 삭제한다.// 현재 쓰레드와 관련된 로컬 변수를 하나 생성한다.
ThreadLocal<UserInfo> local = new ThreadLocal<UserInfo>();
// 로컬 변수에 값 할당
local.set(currentUser);
// 이후 실행되는 코드는 쓰레드 로컬 변수 값을 사용
UserInfo userInfo = local.get();
(출처 : 최범균님 블로그)
쓰레드 풀 환경에서 ThreadLocal을 사용하는 경우 ThreadLocal 변수에 보관된 데이터의 사용이 끝나면 반드시 해당 데이터를 삭제해 주어야 한다. 그렇지 않을 경우 재사용되는 쓰레드가 올바르지 않은 데이터를 참조할 수 있다고 한다!
public class UserContext {
public static final ThreadLocal<JwtPayload> USER_CONTEXT = new ThreadLocal<>();
public static Long getCurrentUserId() {
if (UserContext.USER_CONTEXT.get() != null) {
return UserContext.USER_CONTEXT.get().getId();
}
throw new Exception("null");
}
}
set
토큰을 통해 사용자 인증 과정을 거친 후,
UserContext에 현재 user의 id를 저장
UserContext.USER_CONTEXT.set(new JwtPayload(user.getId()));
get
Long userId = UserContext.getCurrentUserId();
이상 ThreadLocal을 이용한 인증된 사용자 정보 가져오기였다!
cf) Spring Security에서는 ThreadLocal을 이용해서 사용자 인증 정보를 전파한다.
👉 어떻게 구현된 건지 궁금해서 스프링 시큐리티 관련해서 찾아보았다.
Authentication authentication = SecurityContextHolder.getContext().getAuthentication();
User user = (User)authentication.getPrincipal();
이렇게 인증된 사용자를 가져오니
SecurityContextHolder 클래스를 찾아봤다.
해당 클래스에서 초기화 메서드를 호출할 때
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();
}
...
이런 식으로 ThreadLocal 객체를 생성했다.
ThreadLocalSecurityContextHolderStrategy 클래스
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();
}
}
위에서 작성한 UserContext 클래스와 비슷한 게 보인다. 매우 신기
스프링 시큐리티없이 구현하니까 스프링 시큐리티를 알아가는 느낌