기존에 작성한 token service는 java-jwt를 사용하고있지만 jjwt가 더 많은 사용자와 레퍼런스를 보유하고 있고 가장 최근까지 업데이트되고있기에 패키지를 교체하도록 하였다.
jjwt의 경우
return Jwts.builder()
.subject(sub)
.issuedAt(now)
.expiration(new Date(now.getTime() + ACCESS_TOKEN_EXPIRE_PERIOD))
.signWith(SECRET)
.compact();
위와 같이 키 생성을 하고
return Jwts.parser()
.verifyWith(SECRET)
.build()
.parseSignedClaims(token)
.getPayload()
.getSubject();
위와 같이 키를 검증 및 내용을 가져올 수 있다.
jwt는 헤더, 페이로드, 시그니처로 구성되어있고
헤더에는 알고리즘과 토큰의 타입을
페이로드에는 전달하는 데이터를
시그니처에는 헤더와 페이로드를 인코딩 한 값과 비밀키 값을 해싱한 값으로 구성되어있기에 토큰을 위조하면 시그니처 값이 달라져 위변조 여부를 판별할 수 있도록 한다.
또한 페이로드에는 등록된 클레임, 공개 클레임, 비공개 클레임이 존재하고
등록된 클레임은
공개 클레임은 충돌이 방지되도록 URI 형태의 이름을 가진다.
{
"https://www.leporem.art/jwt_claims/is_staff": true
}
비공개 클레임은 클라이언트와 서버 간의 협의 하에 사용되는 클레임으로 공개 클레임과 달리 충돌할 수 있다.
사용자 ID값만을 식별할 것이기에 별도의 클레임 없이 sub와 exp만을 사용하도록 하였다.
package com.palindrome.studit.global.config.security.application;
import com.palindrome.studit.domain.user.exception.InvalidTokenException;
import io.jsonwebtoken.JwtException;
import io.jsonwebtoken.Jwts;
import io.jsonwebtoken.io.Decoders;
import io.jsonwebtoken.security.Keys;
import lombok.RequiredArgsConstructor;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.beans.factory.annotation.Value;
import org.springframework.stereotype.Service;
import javax.crypto.SecretKey;
import java.util.Date;
@RequiredArgsConstructor
@Service
public class TokenService {
private SecretKey SECRET;
@Value("${jwt.access-token.expire-period}")
private int ACCESS_TOKEN_EXPIRE_PERIOD;
@Value("${jwt.refresh-token.expire-period}")
private int REFRESH_TOKEN_EXPIRE_PERIOD;
@Autowired
public TokenService(@Value("${jwt.secret}") String SECRET_KEY) {
byte[] keyBytes = Decoders.BASE64.decode(SECRET_KEY);
this.SECRET = Keys.hmacShaKeyFor(keyBytes);
}
public String createAccessToken(String sub) {
Date now = new Date();
return Jwts.builder()
.subject(sub)
.issuedAt(now)
.expiration(new Date(now.getTime() + ACCESS_TOKEN_EXPIRE_PERIOD))
.signWith(SECRET)
.compact();
}
public String createRefreshToken() {
Date now = new Date();
return Jwts.builder()
.issuedAt(now)
.expiration(new Date(now.getTime() + REFRESH_TOKEN_EXPIRE_PERIOD))
.signWith(SECRET)
.compact();
}
public String parseSubject(String token) throws InvalidTokenException {
try {
return Jwts.parser()
.verifyWith(SECRET)
.build()
.parseSignedClaims(token)
.getPayload()
.getSubject();
} catch (JwtException e) {
throw new InvalidTokenException();
}
}
public Date parseExpiration(String token) throws InvalidTokenException {
try {
return Jwts.parser()
.verifyWith(SECRET)
.build()
.parseSignedClaims(token)
.getPayload()
.getExpiration();
} catch (JwtException e) {
throw new InvalidTokenException();
}
}
}
jwt 정보를 전달할 DTO를 추가한다.
package com.palindrome.studit.domain.user.dto;
import lombok.Builder;
import lombok.Getter;
import java.util.Date;
@Builder
@Getter
public class JwtDTO {
private String sub;
private Date exp;
}
package com.palindrome.studit.domain.user.api;
import com.palindrome.studit.domain.user.dto.JwtDTO;
import com.palindrome.studit.domain.user.exception.InvalidTokenException;
import com.palindrome.studit.global.config.security.application.TokenService;
import lombok.RequiredArgsConstructor;
import org.springframework.http.HttpStatus;
import org.springframework.http.ResponseEntity;
import org.springframework.web.bind.annotation.*;
@RequiredArgsConstructor
@RestController
@RequestMapping("/auth")
public class AuthController {
private final TokenService tokenService;
@GetMapping("/token-info/{accessToken}")
public JwtDTO getJwtInfo(@PathVariable String accessToken) throws InvalidTokenException {
return JwtDTO.builder()
.sub(tokenService.parseSubject(accessToken))
.exp(tokenService.parseExpiration(accessToken))
.build();
}
@ExceptionHandler({ InvalidTokenException.class })
public ResponseEntity<Object> invalidTokenException(final InvalidTokenException e) {
return ResponseEntity.status(HttpStatus.BAD_REQUEST).body(e.getMessage());
}
}
AuthController를 추가하여 토큰을 검증하도록 한다.