JWT 검증하기 전에 parsing 해보자
문제
웹 서비스의 단일 키값으로 서명하는 방식은 하나의 키 유출로 모든 인증 프로세스가 유효하지 않게 된다.
이 문제를 개선시키기 위해, 전달할 토큰을 생성하면서 유저에게 고유한 키값을 할당한다.
구현
일반적으로 JWS를 검증하기 전에는 Jwts.parser() 메서드를 호출할 수 없으므로 저장한 key id 값을 추출할 수 없다.
jjwt에서는 다음과 같은 상황에서 key locator를 권장하고 있다.
If you need to support dynamic key lookup when encountering JWTs, you'll need to implement the Locator<Key> interface and specify an instance on the JwtParserBuilder via the keyLocator method.
먼저 LocatorAdapter를 확장하는 CustomKeyLocator를 생성한다.
@RequiredArgsConstructor
public class CustomKeyLocator extends LocatorAdapter<Key> {
private final JwtService jwtService;
@Override
public Key locate(ProtectedHeader header) {
String keyId = header.getKeyId();
return lookupKey(keyId);
}
private SecretKey lookupKey(String keyId) {
Jwt jwt = jwtService.loadJwtById(Long.parseLong(keyId));
return getJwtKeyFromSecret(jwt.getJwtSecret());
}
private SecretKey getJwtKeyFromSecret(String jwtSecret) {
return Keys.hmacShaKeyFor(Decoders.BASE64.decode(jwtSecret));
}
}
토큰을 생성할 때, 검증하기 전에 찾고 싶은 값을
다음처럼 .header().keyId({keyId}).and() 에 저장한다.
public String generateToken(Long userId, String role, Duration expiredAt) {
Date now = new Date();
Jwt jwt = jwtService.createJwt(userId);
String jwtSecret = jwt.getJwtSecret();
return Jwts.builder()
.issuer(ISSUER)
.header().keyId(String.valueOf(jwt.getId())).and()
.subject(String.valueOf(userId))
.claim("role", role)
.issuedAt(now)
.expiration(new Date(now.getTime() + expiredAt.toMillis()))
.signWith(getJwtKeyFromSecret(jwtSecret))
.compact();
}
정상적으로 동작하는 것을 확인하기 위해 테스트 코드를 작성한다.
@Transactional
@SpringBootTest
class JwtProviderTest {
@Autowired
JwtProvider jwtProvider;
@Autowired
JwtService jwtService;
@Autowired
UserService userService;
@DisplayName("사용자의 고유한 access token 생성한다. 생성된 토큰에는 jwt id, sub(userId), 만료기간을 포함한다.")
@Test
void generateToken() throws Exception {
//given
String email = "test@test.com";
String name = "test";
User user = userService.signUp(email, name);
//when
String token = jwtProvider.generateToken(user.getId(), user.getRole().toString(), ACCESS_TOKEN_DURATION);
//then
Claims claims = jwtProvider.loadPayloadAndValidateToken(token);
Long userIdFromToken = Long.parseLong(claims.getSubject());
assertThat(userIdFromToken).isEqualTo(user.getId());
assertThat(claims.get("role")).isEqualTo(user.getRole().toString());
}
@DisplayName("발급된 토큰으로 사용자가 요청하면 토큰을 검증할 수 있다.")
@Test
void validateToken() throws Exception {
//given
String email = "test@test.com";
String name = "test";
User user = userService.signUp(email, name);
String token = jwtProvider.generateToken(user.getId(), user.getRole().toString(), ACCESS_TOKEN_DURATION);
//when //then
assertThatCode(() -> jwtProvider.loadPayloadAndValidateToken(token)).doesNotThrowAnyException();
}
}
참고 이슈
https://github.com/jwtk/jjwt/issues/67
위 이슈를 요약하면
jwt 검증하기 전에 토큰에 저장된 속성값을 사용하고 싶다.
위 이슈의 답변을 기반으로 구현했으며, deprecated 인터페이스 말고 깃허브 메인 리드미에 권장하는 방법을 사용했다.
'프로젝트' 카테고리의 다른 글
[소마] OAuth2.0 인증 성공 후, AuthenticationSuccessHandler 테스트해보자 (0) | 2024.02.19 |
---|---|
[소마] 스프링에서 리다이렉트했는데 자꾸 로그인 페이지로 넘어갈 때 (0) | 2024.01.27 |
[소마] jwt 인증 프로세스에서 공통 로직을 분리하자 (0) | 2024.01.26 |
[소마] 로그인 페이지 서버사이드 mvc 구현에서 endpoint 호출로 수정 (0) | 2024.01.17 |
[소마] application.properties 에 노출되면 안되는 값을 암호화하자 (0) | 2024.01.15 |