인터셉터를 통한 JWT 검증
요청이 들어오면 필터 -> 인터셉터 -> 컨트롤러 -> 응답 순으로 진행이 되는데
이때 인터셉터에서 검증을 처리한다
why? 인터셉터에 들어와야 어느 컨트롤러에서 해당 요청이 처리될지 알 수 있기 때문
@Slf4j
@RequiredArgsConstructor
@Component
public class AuthorizationInterceptor implements HandlerInterceptor {
private final TokenBusiness tokenBusiness;
@Override //사전 검증
public boolean preHandle(HttpServletRequest request, HttpServletResponse response, Object handler) throws Exception {
log.info("Authorization Interceptor url: ", request.getRequestURI());
// WEB, Chrom의 경우 get, post api를 요청하기 전에 options api를 요청하여 해당 메소드를 지원하는지 확인하는 메소드 존재
if(HttpMethod.OPTIONS.matches(request.getMethod())){
return true;
}
// API요청이 아니라 리소스(js, html, png) 을 요청하는 경우 pass
if(handler instanceof ResourceHttpRequestHandler){
return true;
}
var accessToken=request.getHeader("authorization-token"); // 토큰 가져와서
if(accessToken==null){ //없으면 예외처리
throw new ApiException(TokenErrorCode.AUTHORIZATION_TOKEN_NOT_FOUND);
}
var userId=tokenBusiness.validationAccessToken(accessToken); // 있으면 id 가져오기
if(userId!=null){ // id가 있으면
// 한가지 요청에 대해 글로벌 하게 저장할 수 있는 스레드 로컬
var requestContext= Objects.requireNonNull(RequestContextHolder.getRequestAttributes());
requestContext.setAttribute("userId", userId, RequestAttributes.SCOPE_REQUEST); //request단위로 저장
return true;
}
//없으면 예외
throw new ApiException(ErrorCode.BAD_REQEUST, "인증 실패");
}
}
요청 헤더에서 토큰을 가져온다음 토큰이 있는지 null 체크를 한다.
그리고 요청 단위로 유지되는 글로벌한 스레드 로컬에 userId를 저장한다.
그 다음 요청을 처리하는 컨트롤러가 실행된다.
@RestController
@RequiredArgsConstructor
@RequestMapping("/api/user") //Controller -> Business -> Service -> Repository
public class UserApiController { // 로그인된 사용자에 대해 처리하는 controller
private final UserBusiness userBusiness;
@GetMapping("/me")
public Api<UserResponse> me(
@UserSession User user
){ // 로그인 했을 때 나의 정보를 가져가는 코드
//reuqestContext는 request가 들어올 때마다 생성
// reqest가 filter -> interceptor -> controller를 돌고 response로 나갈 때까지 유지되는 스레드 로컬
var response = userBusiness.me(user);
return Api.OK(response);
}
}
컨트롤러 메소드가 실행될 때 Sptring MVC는 @UserSession 어노테이션을 보고 HandlerMethodArgumentResolver 구현체들을 탐색한다.
@Target(ElementType.PARAMETER)
@Retention(RetentionPolicy.RUNTIME)
public @interface UserSession {
}
@Component
@RequiredArgsConstructor
public class UserSessionResolver implements HandlerMethodArgumentResolver { //request요청이 들어오면 실행 aop방식으로
private final UserService userService;
@Override
public boolean supportsParameter(MethodParameter parameter) { // 인터셉터에 이어서 뒤에 있는 컨트롤러가 어노테이션이 있는지 체크 역할
// 지원하는 파라미터 체크, 어노테이션 체크
// 1. 어노테이션이 있는지 체크
var annotation = parameter.hasParameterAnnotation(UserSession.class); // UserSession 어노테이션이 있거나
// 2. 파라미터의 타입 체크
var parameterType=parameter.getParameterType().equals(User.class);
return (annotation && parameterType);
}
@Override
public Object resolveArgument(MethodParameter parameter, ModelAndViewContainer mavContainer, NativeWebRequest webRequest, WebDataBinderFactory binderFactory) throws Exception {
// supportsParameter에서 true 반환시 여기 실행
// requestContext holder에서 찾아오기
var requestContext= RequestContextHolder.getRequestAttributes();
var userId=requestContext.getAttribute("userId", RequestAttributes.SCOPE_REQUEST);
var userEntity=userService.getUserWithThrow(Long.parseLong(userId.toString()));
// 사용자 정보 셋팅
return User.builder()
.id(userEntity.getId())
.name(userEntity.getName())
.email(userEntity.getEmail())
.password(userEntity.getPassword())
.status(userEntity.getStatus())
.address(userEntity.getAddress())
.registeredAt(userEntity.getRegisteredAt())
.unregisteredAt(userEntity.getUnregisteredAt())
.lastLoginAt(userEntity.getLastLoginAt())
.build(); // 컨트롤러로 리턴
}
}
HandlerMethodArgumentResolver 의 구현체 중 UserSessionResolver 가 해당 메소드를 처리할 수 있는지 확인하기 위해
supportsParameter 메소드를 호출하여 확인한다.
이후 조건을 만족하면 resolveArgument 메소드가 실행되어 컨트롤러 메소드 파라미터에 들어갈 값을 리턴한다.
public UserResponse me(
User user
) {
/*var requestContext= Objects.requireNonNull(RequestContextHolder.getRequestAttributes());
var userId=requestContext.getAttribute("userId", RequestAttributes.SCOPE_REQUEST);*/
var userEntity=userService.getUserWithThrow(user.getId());
var response=userConverter.toResponse(userEntity);
return response;
}
UserBusiness의 코드 중 일부이다.
입력 받은 user를 통해 business -> service -> repository 순으로 한번 더 검증을 하는데,
이미 UserSessionResolver 에서 검증을 하기 때문에 user객체를 response로만 변환시켜 리턴하여도 무관하다.
'Springboot' 카테고리의 다른 글
[스프링 부트] Stream.map() 함수 (0) | 2024.07.20 |
---|---|
[스프링 부트] orElseThrow 메소드 구현부 (0) | 2024.07.15 |
[스프링 부트] Interceptor를 통한 인증 (0) | 2024.07.10 |
[스프링 부트] Exception Handler (0) | 2024.07.10 |
[스프링 부트] Api 공통 spec (0) | 2024.07.10 |