elevne's Study Note
Spring Security : JWT 적용해보기 (3) 본문
Jwt 각종 처리를 담당하는 JwtService 코드를 정리해본다. 우선 토큰을 생성하는 메소드부터 살펴보았다.
public String generateToken(
Map<String, Object> extraClaims,
UserDetails userDetails
) {
return Jwts
.builder()
.setClaims(extraClaims)
.setSubject(userDetails.getUsername())
.setIssuedAt(new Date(System.currentTimeMillis()))
.setExpiration(new Date(System.currentTimeMillis() + 1000 * 60))
.signWith(getSignInKey(), SignatureAlgorithm.HS256)
.compact();
}
public String generateToken(UserDetails userDetails) {
return generateToken(new HashMap<>(), userDetails);
}
generateToken 메소드는 빈 HashMap 객체와 UserDetails 객체를 받는다. Jwts.builder() ..... compact() 로 작성된다. setClaims 는 JWT 페이로드를 지정된 이름/값 쌍으로 채워진 JSON 클레임 인스턴스로 설정한다. JWT 본문을 JSON으로 지정하지 않고 일반 텍스트 문자열로 지정하려면 setPayload(String) 메서드를 대신 사용할 수 있다. setSubject(userDetails.getUsername()) 으로 해당 JWT 발급의 목적을 설정, setIssuedAt 과 setExpiration 으로 발급시간과 만기시간을 지정한다 (위에서 만료시간은 1 분 뒤로 해두었다). 그 다음으로는 signWith 에 사용될 키와 알고리즘을 명시한다.
private Key getSignInKey() {
byte[] keyBytes = Decoders.BASE64.decode(SECRET_KEY);
return Keys.hmacShaKeyFor(keyBytes);
}
Keys.hmacShaKeyFor 메소드는 새로운 키를 발급해준다. 마지막으로 compact() 메소드로 실제로 JWT를 빌드하고 JWT Compact Serialization 규칙에 따라 URL-safe 한 소형 문자열로 직렬화한다.
그럼 이러한 토큰을 유저로부터 받았을 때 어떻게 유효한지 파악할 수 있을까? 아래와 같은 메소드를 사용한다.
public boolean isTokenValid(String token, UserDetails userDetails) {
final String username = extractUsername(token);
return (username.equals(userDetails.getUsername())) && !isTokenExpired(token);
}
토큰에 들어있는 유저의 이름이 DB 에서 조회한 유저와 동일하고, 토큰이 아직 만료되지 않았는지 확인한다. extractUsername, isTokenExpired 코드는 아래와 같이 구성된다.
protected boolean isTokenExpired(String token) {
return extractExpiration(token).before(new Date());
}
private Date extractExpiration(String token) {
return extractClaim(token, Claims::getExpiration);
}
public String extractUsername(String token) {
return extractClaim(token, Claims::getSubject);
}
public <T> T extractClaim(String token, Function<Claims, T> claimsResolver) {
final Claims claims = extractAllClaims(token);
return claimsResolver.apply(claims);
}
private Claims extractAllClaims(String token) {
return Jwts
.parserBuilder()
.setSigningKey(getSignInKey())
.build()
.parseClaimsJws(token)
.getBody();
}
Jwt 토큰에서 모든 Claim 을 받아온 후, 그 안에서 getExpiration, getSubject 를 통해 정보를 가져오는 것이다. 모든 Cliam 들을 읽을 때는 JwtParser 이 사용된다. Jwts. ... build() 로 JwtParser 객체를 빌드하고, 분석하려는 토큰을 parseClaimJws 메소드로 넣어준다. 마지막으로 getBody 메소드로 Claims 객체를 받아온다.
Jwt 가 만료되었을 때 새로 발급받을 수 있게끔 해주는 RefreshToken 라는 것이 있다. 이는 기본 토큰 (Access Token) 과 다르게 만료 시간을 길게 설정한다. 아래와 같이 생성해줄 수 있다.
@Transactional
public String generateRefreshToken(String userId) {
User user = userRepository.findByUserId(userId).orElseThrow();
refreshTokenRepository.deleteByUserId(user.getId());
String token = UUID.randomUUID().toString();
RefreshToken refreshToken = RefreshToken.builder()
.user(user)
.token(token)
.expiryDate(Instant.now().plusMillis(1000 * 60 * 60 * 24 * 7))
.build();
refreshTokenRepository.save(refreshToken);
return token;
}
@Entity
@Data
@Builder
@AllArgsConstructor
@NoArgsConstructor
public class RefreshToken {
@Id
@GeneratedValue(strategy = GenerationType.IDENTITY)
private int id;
private String token;
private Instant expiryDate;
@ManyToOne
@JoinColumn(name = "userId")
private User user;
}
RefreshToken 은 단순히 랜덤 UUID 값을 사용한다. 프론트 코드에서 Access Token 과 함께 요청을 보냈을 때 시간만료로 요청을 성공하지 못했을 때, RefreshToken 을 보내 새로운 토큰을 발급받을 수 있게끔 한다. 프론트에서는 새로운 토큰을 발급받아 다시 요청을 보낼 수 있다. 우선은 정말 간단하게 아래와 같이 작성해두었다.
public JwtResponse authenticate(UserLoginDTO userLoginDTO) {
authenticationManager.authenticate(
new UsernamePasswordAuthenticationToken(
userLoginDTO.getUserId(),
userLoginDTO.getPassword()
)
);
User user = userRepository.findByUserId(userLoginDTO.getUserId())
.orElseThrow();
String accessToken = jwtService.generateToken(user);
String refreshToken = jwtService.generateRefreshToken(userLoginDTO.getUserId());
return JwtResponse
.builder()
.accessToken(accessToken)
.refreshToken(refreshToken)
.build();
}
public JwtResponse refreshToken(RefreshTokenRequest request) {
RefreshToken refreshToken = jwtService.findByToken(request.getToken())
.orElseThrow(() -> new RuntimeException("REFRESH TOKEN IS NOT IN DB"));
refreshToken = jwtService.verifyRefreshExpiration(refreshToken);
String accessToken = jwtService.generateToken(refreshToken.getUser());
return JwtResponse.builder()
.accessToken(accessToken)
.refreshToken(request.getToken())
.build();
}
Reference:
'Backend > Spring' 카테고리의 다른 글
Spring WebSocket (STOMP - Redis) (0) | 2023.07.01 |
---|---|
Spring WebSocket (STOMP) (0) | 2023.06.30 |
Spring WebSocket (0) | 2023.06.28 |
Spring Security : JWT 적용해보기 (2) (0) | 2023.06.11 |
Spring Security : JWT 적용해보기 (1) (0) | 2023.06.10 |