elevne's Study Note

Spring Security : JWT 적용해보기 (3) 본문

Backend/Spring

Spring Security : JWT 적용해보기 (3)

elevne 2023. 6. 29. 17:44

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() 로 작성된다. setClaimsJWT 페이로드를 지정된 이름/값 쌍으로 채워진 JSON 클레임 인스턴스로 설정한다. JWT 본문을 JSON으로 지정하지 않고 일반 텍스트 문자열로 지정하려면 setPayload(String) 메서드를 대신 사용할 수 있다. setSubject(userDetails.getUsername()) 으로 해당 JWT 발급의 목적을 설정, setIssuedAtsetExpiration 으로 발급시간과 만기시간을 지정한다 (위에서 만료시간은 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: 

https://www.youtube.com/watch?v=KxqlJblhzfI&list=LL&index=1 

'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