유저 로그인 인증 및 인가를 JWT로 하기로 한다.
다음의 요구사항들이 있다.
argument resolver를 이용해 구현해보자.
implementation 'io.jsonwebtoken:jjwt-api:0.11.5'
runtimeOnly 'io.jsonwebtoken:jjwt-impl:0.11.5'
runtimeOnly 'io.jsonwebtoken:jjwt-jackson:0.11.5'
JwtProvider
@Slf4j
@Component
public class JwtProvider {
@Value("${jwt.secret-key}")
private String secretKey;
private static final long TOKEN_VALID_TIME = 24 * 60 * 60 * 1000L;
private static final long REFRESH_TOKEN_VALID_TIME = 30 * 24 * 60 * 60 * 1000L;
public String createToken(Member member) {
Claims claims = Jwts.claims();
claims.put("id", member.getId());
claims.put("username", member.getUsername());
Date now = new Date();
return Jwts.builder()
.setHeaderParam("typ", "JWT")
.setClaims(claims)
.setIssuedAt(now)
.setExpiration(new Date(now.getTime() + TOKEN_VALID_TIME))
.signWith(
Keys.hmacShaKeyFor(secretKey.getBytes(StandardCharsets.UTF_8)),
SignatureAlgorithm.HS256
)
.compact();
}
}
JwtProvider
@Slf4j
@Component
public class JwtProvider {
...
public boolean validateToken(String token) {
try {
return !Jwts.parserBuilder()
.setSigningKey(Keys.hmacShaKeyFor(secretKey.getBytes(StandardCharsets.UTF_8)))
.build()
.parseClaimsJws(removeBearer(token))
.getBody()
.getExpiration().before(new Date());
} catch (Exception e) {
e.printStackTrace();
throw new RuntimeException("토큰 검증 실패");
}
}
}
@JwtAuthorization
@Target({ ElementType.PARAMETER, ElementType.ANNOTATION_TYPE })
@Retention(RetentionPolicy.RUNTIME)
@Documented
public @interface JwtAuthorization {
boolean required() default true;
}
MemberInfo
@Getter
@Builder
@NoArgsConstructor
@AllArgsConstructor
@ToString
public class MemberInfo {
private Long id;
private String username;
}
JwtProvider
MemberInfo
를 리턴한다@Slf4j
@Component
public class JwtProvider {
...
public MemberInfo getClaim(String token) {
Claims claimsBody = Jwts.parserBuilder()
.setSigningKey(Keys.hmacShaKeyFor(secretKey.getBytes(StandardCharsets.UTF_8)))
.build()
.parseClaimsJws(removeBearer(token))
.getBody();
return MemberInfo.builder()
.id(Long.valueOf((Integer) claimsBody.getOrDefault("id", 0L)))
.username(claimsBody.getOrDefault("username", "").toString())
.build();
}
private String removeBearer(String token) {
return token.replace("Bearer", "").trim();
}
}
@JwtAuthorization
어노테이션이 있을 경우에 동작MemberInfo
리턴JwtAuthorizationArgumentResolver
@Slf4j
@Component
@RequiredArgsConstructor
public class JwtAuthorizationArgumentResolver implements HandlerMethodArgumentResolver {
private final JwtProvider jwtProvider;
// @JwtAuthorization 어노테이션이 있을 경우 동작
@Override
public boolean supportsParameter(MethodParameter parameter) {
return parameter.hasParameterAnnotation(JwtAuthorization.class);
}
@Override
public Object resolveArgument(MethodParameter parameter, ModelAndViewContainer mavContainer, NativeWebRequest webRequest, WebDataBinderFactory binderFactory) throws Exception {
log.info("JwtAuthorizationArgumentResolver 동작!!");
HttpServletRequest httpServletRequest = webRequest.getNativeRequest(HttpServletRequest.class);
// 헤더 값 체크
if (httpServletRequest != null) {
String token = httpServletRequest.getHeader("Authorization");
if (token != null && !token.trim().equals("")) {
// 토큰 있을 경우 검증
if (jwtProvider.validateToken(token)) {
// 검증 후 MemberInfo 리턴
return jwtProvider.getClaim(token);
}
}
// 토큰은 없지만 필수가 아닌 경우 체크
JwtAuthorization annotation = parameter.getParameterAnnotation(JwtAuthorization.class);
if (annotation != null && !annotation.required()) {
// 필수가 아닌 경우 기본 객체 리턴
return new MemberInfo();
}
}
// 토큰 값이 없으면 에러
throw new RuntimeException("권한 없음.");
}
}
WebConfig
@Configuration
@RequiredArgsConstructor
public class WebConfig implements WebMvcConfigurer {
private final JwtAuthorizationArgumentResolver jwtAuthorizationArgumentResolver;
@Override
public void addArgumentResolvers(List<HandlerMethodArgumentResolver> resolvers) {
resolvers.add(jwtAuthorizationArgumentResolver);
}
}
@Slf4j
@RestController
@RequestMapping("/api")
public class TestController {
@GetMapping("/token/none")
public String none() {
log.info("token none");
return "success";
}
}
GET http://localhost:8080/api/token/none
@Slf4j
@RestController
@RequestMapping("/api")
public class TestController {
...
@GetMapping("/token/required")
public String required(
@JwtAuthorization MemberInfo memberInfo
) {
log.info("token payload : {}", memberInfo);
return "success";
}
}
GET http://localhost:8080/api/token/required
MemberInfo
객체에 토큰 데이터 바인딩@JwtAuthorization(required = false)
@Slf4j
@RestController
@RequestMapping("/api")
public class TestController {
...
@GetMapping("/token/optional")
public String optional(
@JwtAuthorization(required = false) MemberInfo memberInfo
) {
log.info("token payload : {}", memberInfo);
return "success";
}
}
GET http://localhost:8080/api/token/optional
MemberInfo
객체에 토큰 데이터 바인딩