ArgumentResolver
🟧 01. ArgumentResolver와 HandlerMethodArgumentResolver
ArgumentResolver
는 Spring MVC에서 Controller
에 들어온 HTTP Request
를 가공하여 메서드의 파라미터 타입으로 만드는 역할이다.
이러한 ArgumentResolver
는 HandlerMethodArgumentResolver
를 줄여서 부른다.
🟧 02. HandlerMethodArgumentResolver 이 뭔데 ?
HandlerMethodArgumentResolver
는 스프링 프레임워크가 제공하는 인터페이스이다.
@RequestParam
, @ModelAttribute
, @PathVariable
, @RequestParam
, ...와 같은 어노테이션이나 파라미터 타입을 보고 메서드 인자 값을 처리한다.
인터페이스에는 두 개의 메서드(supportsParameter
, resolveArgument
)가 있다.
아래 코드를 보자.
/**
* Strategy interface for resolving method parameters into argument values in
* the context of a given request.
*/
public interface HandlerMethodArgumentResolver {
// true를 반환할 때
boolean supportsParameter(MethodParameter parameter);
// resolveArgument 메서드를 통해서 변환된 객체를 반환한다.
@Nullable
Object resolveArgument(
MethodParameter parameter,
@Nullable ModelAndViewContainer mavContainer,
NativeWebRequest webRequest,
@Nullable WebDataBinderFactory binderFactory
) throws Exception;
}
🟧 03. supportsParameter는 Resolver 적용이 가능한지 검사한다.
boolean supportsParameter(MethodParameter parameter);
supportsParameter
메서드는 파라미터 타입이 맞는지 확인한다.
- 파라미터 타입이 맞는 경우
resolveArgument
메서드를 실행한다.
- 파라미터 타입이 틀린 경우
resolveArgument
메서드를 실행하지 않는다.
🟧 04. resolveArgument
메서드는 실제 Parameter와 Binding하여 객체를 생성한다.
Object resolveArgument(
MethodParameter parameter,
@Nullable ModelAndViewContainer mavContainer,
NativeWebRequest webRequest,
@Nullable WebDataBinderFactory binderFactory
) throws Exception;
supportsParameter
메서드를 통해서 파라미터 타입 확인이 성공되면 로직을 수행하는 메서드이다.
구현된 로직은 Parameter와 Binding하여 객체를 생성하여 반환한다.
MethodParameter
- 요청 핸들러 메서드가 반환하는 값을 저장하는 컨테이너
- 파라미터 타입, 어노테이션 등의 정보를 가지고 있다.
ModelAndViewContainer
- 요청 핸들러 메서드가 반환하는 값을 저장하는 컨테이너
NativeWebRequest
- 현재 HTTP 요청에 대한 정보를 제공하는 인터페이스
- 객체를 사용하여 HTTP 요청의 Header, QueryParameter 등의 정보를 얻을 수 있다.
WebDataBinderFactory
- 요청 핸들러 메서드의 Parameter를 Binding할 데이터 바인더를 생성하는 팩토리이다.
🟧 05. 예시) ArgumentResolver
커스텀, JWT 가공해서 원하는 데이터 받기
ArgumentResolver는 Spring MVC에서 Controller의 메서드 파라미터를 변환하는데 사용된다.
HTTP Request Header에 Authorization 값을 받아 유효할 때 User를 반환 받으려고 한다.
@Slf4j
@RestController
public class LoginController {
@PostMapping("/test-login")
public String token(User user) {
log.info("User : {}", user);
return "로그인 성공";
}
}
@Getter
@Builder
@NoArgsConstructor
@AllArgsConstructor
@ToString
public class User {
private Long id;
private String username;
}
하지만 이 때 들어온 Authorization 값을 바로 User로 변환할 수 없다.
이유는 토큰 값으로 들어오기 때문이다.
// Authroization Value (토큰값)
eyJhbGciOiJIUzI1NiJ9.eyJpZCI6MSwidXNlcm5hbWUiOiLtl6TrgpgiLCJpYXQiOjE2ODE2MTM1MjQsImV4cCI6MTY4MTY5OTkyNH0.G3vQzNnqgec_h2Ooi9G1q0EMBkSnUKoIK8l5Qlgw4v8
토큰 값은 Encoded하면 원하는 User 데이터가 나오는 것을 확인할 수 있다.
이러한 토큰값 변환을 우리가 직접 해준다면 원하는 User를 받아올 수 있다.
만약 Controller 내부에서 토큰값 변환이 이루어진다면 로직이 더러워질 수 있다.
그래서 ArgumentResolver를 이용해서 해결해보려 한다.
먼저 ArgumentResolver를 커스텀해서 이용하려면 아래 작업이 필요하다.
- @JwtAuthorization 어노테이션 구현
- 토큰 생성 및 파싱을 위한 JwtProvider 구현
- @JwtAuthorization 어노테이션이 있을 경우 동작할 JwtAuthorizationArgumentResolver 구현
- JwtAuthorizationArgumentResolver를 MVC Config에 등록하여 스프링 컨테이너가 알 수 있게 한다.
그럼 진행해보자 !
05-01. @JwtAuthorization 어노테이션 만들기
현재 사용할 ArgumentResolver는 @JwtAuthorization 어노테이션을 보고 Resolver 적용이 가능한지 확인한다.
Controller의 Parameter에 달아줄 것이며 Runtime에 실행될 예정이다.
@JwtAuthorization 어노테이션이 있는 매개변수는 추가 정보를 받을 수 있다.
@Target({ElementType.PARAMETER, ElementType.ANNOTATION_TYPE})
@Retention(RetentionPolicy.RUNTIME)
@Documented
public @interface JwtAuthorization {
}
05-02. 토큰 생성 및 파싱을 위한 JwtProvider 구현
JwtProvier
- createToken() : Token을 발급한다.
- getClaim() : User를 반환한다.
- validateToken() : Token을 검증한다.
@Component
public class JwtProvider {
private SecretKey secretKey
= Keys.hmacShaKeyFor("hyenahyenahyenahyenahyenahyenahyena".getBytes(UTF_8));
// Token을 User객체로 변환한다.
public User getClaim(final String token) {
final Claims claimsBody = Jwts.parserBuilder()
.setSigningKey(secretKey)
.build()
.parseClaimsJws(removeBearer(token))
.getBody();
return User.builder()
.id(Long.valueOf((Integer) claimsBody.getOrDefault("id", 0L)))
.username(claimsBody.getOrDefault("username", "").toString())
.build();
}
private String removeBearer(final String token) {
return token.replace("Bearer", "").trim();
}
// JwtAuthorizationArgumentResolver에서 사용하는 토큰 검증 메서드
public boolean validateToken(final String token) {
try {
return !Jwts.parserBuilder()
.setSigningKey(secretKey)
.build()
.parseClaimsJws(removeBearer(token))
.getBody()
.getExpiration().before(new Date());
} catch (Exception e) {
e.printStackTrace();
throw new TokenException("[ERROR] Token이 없습니다.");
}
}
}
05-03. JwtAuthorizationArgumentResolver 구현
HandlerMethodArgumentResolver 구현체이다.
위에서 설명한 것과 같이 supportsParameter() 메서드, resolveArgument() 메서드가 있다.
supportsParameter()
- supportsParameter() 메서드를 호출하면 @JwtAuthorization 어노테이션이 붙어 있는 Parameter 인지 확인한다.
- @JwtAuthorization 어노테이션이 붙어 있는 Parameter일 경우 resolveArgument() 메서드가 호출된다.
resolveArgument()
- HttpRequestServlet에서 Header의 Key가 Authorization인 Value를 받아온다.
- JwtProvider를 통해서 Token이 유효한지 검증한다.
- Token이 유효할 경우 getClaim() 메서드를 호출하여 반환값 User를 받아온다.
@Slf4j
@RequiredArgsConstructor
@Component
public class JwtAuthorizationArgumentResolver implements HandlerMethodArgumentResolver {
private final JwtProvider jwtProvider;
@Override
public boolean supportsParameter(final MethodParameter parameter) {
return parameter.hasParameterAnnotation(JwtAuthorization.class);
}
@Override
public Object resolveArgument(final MethodParameter parameter,
final ModelAndViewContainer mavContainer,
final NativeWebRequest webRequest,
final WebDataBinderFactory binderFactory
) {
log.info("{} 실행", getClass().getSimpleName());
// NativeWebRequest로 부터 HttpServletRequest를 받아온다.
final HttpServletRequest request = webRequest.getNativeRequest(HttpServletRequest.class);
// HttpServletRequest가 존재하면 Header에서 Key가 Authorization인 Value를 받아온다. (토큰값)
if (Objects.nonNull(request)) {
final String token = request.getHeader("Authorization");
// 토큰값이 null이 아닐 경우
// 토큰값이 비어있는 값이 아닐 경우
// 토큰값이 JwtProvider에 의해 검증이 통과될 경우
if (Objects.nonNull(token) && !token.trim().equals("") && jwtProvider.validateToken(token)) {
// 토큰값으로부터 User를 생성하여 반환한다.
return jwtProvider.getClaim(token);
}
}
final JwtAuthorization annotation = parameter.getParameterAnnotation(JwtAuthorization.class);
// @JwtAuthorization 어노테이션은 있지만 위에서 통과되지 않았을 경우
if (Objects.nonNull(annotation)) {
// 비어있는 User를 반환한다.
return new User();
}
throw new TokenException("[ERROR] 권한이 없습니다.");
}
}
05-04. JwtAuthorizationArgumentResolver, MVC Config 등록
addArgumentResolvers() 메서드를 오버라이딩하여 JwtAuthorizationArgumentResolver를 등록한다.
이제 스프링 컨테이너는 @JwtAuthorization 어노테이션이 들어올 경우 어떤 처리를 해줄지 알고 있다.
@RequiredArgsConstructor
@Configuration
public class WebConfig implements WebMvcConfigurer {
private final JwtAuthorizationArgumentResolver jwtAuthorizationArgumentResolver;
@Override
public void addArgumentResolvers(final List<HandlerMethodArgumentResolver> resolvers) {
// JwtAuthorizationArgumentResolver를 추가한다.
resolvers.add(jwtAuthorizationArgumentResolver);
}
}
05-05. 토큰값으로 User 객체 변환 확인
1. @JwtAuthorization 어노테이션 파라미터에 추가한다.
@Slf4j
@RestController
public class LoginController {
// @JwtAuthorization 어노테이션을 파라미터에 추가한다.
@PostMapping("/test-login")
public String testLogin(@JwtAuthorization User user) {
log.info("User : {}", user);
return user.toString();
}
}
2. Postman을 이용해서 HTTP Request Header에 토큰 값을 넣어 /test-login에 전송한다.
3. 토큰값이 User로 변환된 것을 확인할 수 있다.
👊 정리하자면
ArgumentResolver는 Spring MVC에서 Controller의 메서드에 어노테이션이나 파라미터 타입을 보고 Resolver 적용이 가능한지 확인한다.
ArgumentResolver의 supportsParameter 메서드가 true를 반환하면 Resolver 적용이 가능한 상태이다.
때문에 resolveArgument 메서드를 호출하여 해당 타입에 맞게 변환해서 반환해준다.
(실제 타입 변환되는 장소는 다를 수 있다 !)
사용한 레포지터리
'🍃 스프링' 카테고리의 다른 글
[Spring] 필터와 인터셉터의 차이 (Filter, Interceptor) (0) | 2023.05.07 |
---|---|
[Spring] 필터(Filter)란 뭘까? (0) | 2023.05.07 |
[Spring MVC] 디스패처 서블릿(DispatcherServlet) 둘러보기 (0) | 2023.04.24 |
[Spring MVC - Exception] @ExceptionHandler와 ExceptionHandlerExceptionResolver로 예외 처리하기 (1/2) (0) | 2023.04.17 |