토큰 기반 인증 방식으로 소셜 로그인 및 회원가입 기능을 구현했다.
여러 레퍼런스를 참고하며, 토큰을 세션에 저장하고 세션에서 해당 토큰을 꺼내서
현재 로그인한 사용자를 가져오도록 구현을 했다.
그런데 세션 기반 인증방식의 문제점을 해결하기 위해 토큰 기반 인증방식이 등장했는데,
두 방식을 혼용해서 사용하고 있는게 이상하다는 생각이 들었다.
세션 기반 인증 방식에서의 가장 큰 문제점 중 하나는 http의 stateless
를 위배한다는 것이다.
stateless
란 서버는 클라이언트의 상태를 저장하지 않는다는 의미이다.
하지만 세션 저장소라는 곳에서 클라이언트의 상태를 저장하게 되므로 stateful
하게 된다.
이게 무슨 문제야? 라고 생각할 수 있는데 간단한 예를 들어보면
서버를 scale-out한 경우 1번 서버에 로그인한 사용자가 다른 2번 서버로
요청을 보내게 된다면, 2번 서버에는 로그인 상태가 남아있지 않기 때문에 다시 로그인해야
하는 상황이 발생한다. 즉 확장성에 제약이 생긴다.
filter에서 클라이언트로부터 넘어온 토큰을 꺼내서 세션에 저장함으로 인해, stateful
하게 되면서 JWT를 도입한 의미를 퇴색시킬 뿐 아니라, 위에서 이야기한
세션을 사용함으로써 발생하는 문제점을 그대로 가져간다는 문제점이 존재한다고 생각했다.
물론 현재는 단일 서버이므로 문제 되지는 않는다. 하지만 실제 서비스는 웬만해선 서버
한대로 운영 하지 않는다. 설령 초기에는 한대로 운영하더라도 사용자가 늘어나고
트래픽이 증가할수록 scale-out을 피할수는 없을 것 이다.
언제나 확장 가능하도록 유연하게 설계해야 하므로, Sessionless
를 통해 stateless
하도록
만들어보고자 한다.
떠오른 방법은 아래와 같다.
각 API에서 헤더에 있는 토큰을 꺼내고, 토큰에서 식별자를 추출해 사용자 정보를 가져오는 방법
커스텀 어노테이션을 통해 사용자 정보를 가져오는 방법
커스텀 어노테이션을 만들어보기 전에, Argument Resolver가 뭔지 알아보자.
Argument Resolver는 API 엔드포인트로 인입된 데이터를 가공 및 바인딩 할 때 사용하는 객체이다.
http body 또는 url 파라미터로 넘어오는 데이터들은, @ReqeustBody와 @RequestParam 등으로 바인딩 할 수 있지만 http 헤더, 쿠키, 세션 등으로 전달되는 데이터인 경우에는 Argument Resolver를 이용할 수 있다.
대표적으로 세션에서 로그인한 사용자의 정보를 얻거나, 헤더로 전달되는 토큰에서 사용자의 정보를 얻을 때 사용한다.
Spring MVC Flow이다.
여기서 Argument Resolver가 호출되는 시기는
handler adapot를 찾은 이후 Argument Resolover가 처리되고, 이후 Handler를 실행한다.
@Target(ElementType.PARAMETER)
@Retention(RetentionPolicy.RUNTIME)
public @interface AuthAccount {
}
이름 | 설명 |
---|---|
PACKAGE | 패키지 선언 시 |
TYPE | 타입(클래스, 인터페이스, enum) 선언 시 |
CONSTUCTOR | 생성자 선언 시 |
FIELD | enum 상수를 포함한 멤버변수 선언 시 |
METHOD | 메소드 선언시 |
ANNOTATION_TYPE | 어노테이션 타입 선언 시 |
LOCAL_VARIABLE | 지역변수 선언 시 |
PARAMETER | 파라미터 선언 시 |
TYPE_PARAMETER | 파라미터 타입 선언 시 |
이름 | 설명 |
---|---|
RUNTIME | 컴파일 이후에도 참조 가능 |
CLASS | 클래스를 참조할 때 까지 유효 |
SOURCE | 컴파일 이후 어노테이션 정보 소멸 |
@Component
@RequiredArgsConstructor
@Slf4j
public class AuthAccountResolver implements HandlerMethodArgumentResolver {
private final JwtService jwtService;
private final TokenRepository tokenRepository;
@Override
public boolean supportsParameter(MethodParameter parameter) {
boolean hasAnnotation = parameter.hasParameterAnnotation(AuthAccount.class);
boolean isAccountType = Account.class.isAssignableFrom(parameter.getParameterType());
return hasAnnotation && isAccountType;
}
@Override // JwtFilter에서 모두 검증하므로, 검증 로직은 추가하지 않음
public Account resolveArgument(MethodParameter parameter, ModelAndViewContainer mavContainer,
NativeWebRequest webRequest, WebDataBinderFactory binderFactory) throws Exception {
String jwt = JwtUtils.resolveTokenByWebRequest(webRequest);
String sub = jwtService.getSubject(jwt);
Token token = tokenRepository.findByUniqueIdBySocial(sub).orElseThrow(AccountNotFoundException::new);
return token.getAccount();
}
}
supportsParameter()
컨트롤러의 메서드에 존재하는 파라미터들에 대해서 검사하여, 적용 여부를 판단한다.
AuthAccount.class (커스텀 어노테이션이) 있는지
파라미터 타입이 Account.class 인지
resloveArgument()
supportsParameter()
가 true값을 return하면 실행되는 메서드이다.
파라미터에 전달할 객체를 return한다.
토큰에 대한 검증은 filter에서 처리하므로, 검증 로직은 제외했다.
@RequiredArgsConstructor
@Configuration
public class WebConfig implements WebMvcConfigurer {
private final AuthAccountResolver authAccountResolver;
@Override
public void addArgumentResolvers(List<HandlerMethodArgumentResolver> resolvers) {
resolvers.add(authAccountResolver);
}
}
당현히 구현한 Reslover를 등록해줘야 동작한다.
WebMvcConfigurer
의 구현체를 만들고, addArgumentResolvers
메서드를 오버라이딩 하여
List에 구현체를 추가해주면 된다.
@PostMapping("/test")
public String test(@AuthAccount Account account){
log.info(account.getId())
return "ok";
}
컨트롤러 메서드 파라미터에 적용하면 된다.
relover에서 헤더로 전달되는 토큰의 고유 id값을 추출하여, DB에서 조회 후
Account 객체를 리턴해준다.
refresh token 개념을 사용하게 되면, refresh token을 어딘가에 저장해야 하므로
statueful
하게 된다. 그래도 refresh token을 서버가 아닌 DB에 저장하므로
서버를 scale-out 하는 경우에는 제약이 되지 않는다는 세션과의 차이점이 있다.
그리고 DB는 서버보다는 확장 가능성이 적고?, 클러스터링을 통해 어느정도 해결할 수 있다.
세션에서 꺼내오는게 아니라, DB에 있는 refresh token으로 사용자의 정보를 조회해오므로
DB를 조회해야 한다는 단점이 있다.
단일 서버로 운영할 것이고, 추후에도 확장 가능성이 확실히 없다면(백오피스 서버?) 토큰과 세션을 함께 사용하는게 더 나을수도 있겠다는 생각이 든다.
하지만 서버의 확장 가능성이 존재한다면, 세션을 제거하고 토큰 기반 인증 방식을 사용하는게 조금 더 나은 방법이 아닐까 생각한다.
스프링 MVC 2편 - 백엔드 웹 개발 활용 기술 - 인프런 | 강의