implementation("io.jsonwebtoken:jjwt-api:0.12.5")
runtimeOnly("io.jsonwebtoken:jjwt-impl:0.12.5")
runtimeOnly("io.jsonwebtoken:jjwt-jackson:0.12.5")
Feb 01, 2024 이후로 업데이트가 없다.
JWT는 다음 작업으로 만들 수 있다.
Jwts.builder() 메소드를 사용해서 JwtBuilder 인스턴스를 생성한다.signWith나 encryptWith 메소드를 호출해 JWT를 암호화하거나 디지털 서명을 한다.compact() 메소드로 압축된 JWT string을 만든다.https://github.com/jwtk/jjwt
https://jwt.io/introduction
를 참고한다.
https://github.com/jwtk/jjwt?tab=readme-ov-file#jwt-header
JJWT는 필요한 헤더를 알아서 생성하기 때문에 신경 쓸 필요가 없다.
Automatic Headers: You do not need to set the alg, enc or zip headers - JJWT will always set them automatically as needed.
추가가 필요하다면 headers().add("key", value)와 같은 식으로 추가하면 된다.
https://github.com/jwtk/jjwt?tab=readme-ov-file#jwt-payload
payload를 추가하는데 content와 claims의 두 가지 방식이 있다. content는 bytearray, claims은 JSON 형식으로 정보를 저장하는 방식이다.
기본적인 정보는 다음 메소드로 빌드가 가능하다.
issuer: sets the iss (Issuer) Claim
subject: sets the sub (Subject) Claim
audience: sets the aud (Audience) Claim
expiration: sets the exp (Expiration Time) Claim
notBefore: sets the nbf (Not Before) Claim
issuedAt: sets the iat (Issued At) Claim
id: sets the jti (JWT ID) Claim
예시:
String jws = Jwts.builder()
.issuer("me")
.subject("Bob")
.audience().add("you").and()
.expiration(expiration) //a java.util.Date
.notBefore(notBefore) //a java.util.Date
.issuedAt(new Date()) // for example, now
.id(UUID.randomUUID().toString()) //just an example id
/// ... etc ...
https://github.com/jwtk/jjwt?tab=readme-ov-file#signed-jwts
서명에 필요한 secret key를 생성하는 데에는 두 가지 방법이 있다. (public, private 의 비대칭 키로 서명할 수도 있다.)
JJWT는 SecretKey 라는 객체로 이를 관리한다.
SecretKey key = Jwts.SIG.HS256.key().build(); //or HS384.key() or HS512.key()
HMAC-SHA 알고리즘을 사용하여 서명을 할 때 사용될 secret key 를 생성한다.
이때 HMAC-SHA 알고리즘은 서명 후 생성되는 digest의 크기에 따라 256, 384, 512(bits)로 나뉜다.
256을 사용한다면 키는 최소 32bytes 가 넘어야 한다. (256/4 = 32)
키 문자열을 따로 지정하거나, 보안성을 위해 openssh 를 사용하여 랜덤 문자열을 얻는다.
openssh rand -base64 256를 통해 서명을 할 키 문자열을 생성할 수 있다.
파일이나 환경변수에서 키 문자열을 읽어오거나 byte array를 읽어서 secretString 따위의 변수에 저장하고,
다음 코드로 SecretKey를 생성한다.
val secretKey: SecretKey = Keys.hmacShaKeyFor(Decoders.BASE64.decode(secretString))
header, payload, signature 모두를 포함하여 JWT 토큰을 생성하는 과정은 다음과 같다.
Jwts.builder()
// header를 추가
.header()
.add("key1", "value1")
.add("key2", "value2")
// 혹은
.add(headerMap)
.and() // 다시 builder 로 돌아온다.
// payload 추가
.content(bytearray, "Content-Type") // 컨텐트 유형 명시
// 혹은
.claim("key1", value1)
.claim("Key2", value2)
// 혹은
.claims(contentMap)
// 부가정보 추가
.issuer("me")
.subject("Bob")
.audience().add("you").and()
.expiration(expiration) //a java.util.Date
.notBefore(notBefore) //a java.util.Date
.issuedAt(new Date()) // for example, now
.id(UUID.randomUUID().toString()) //just an example id
// 서명
.signWith(secretKey)
.compact()
expiration과 notBefore가 Date 인스턴스임에 주의한다.
try {
val jwt = Jwts.parser()
.verifyWith(secretKey) // private key로 서명했다면 여기선 public key 로 인증한다.
.build()
.parseSignedClaims(token)
val header = jwt.header
val content = jwt.payload
}
catch (e: JwtExeption) {
logger.error("Error!")
}
내가 구현한 방법은 이러하다.
properties에 Jwt에 관한 변수를 적고 이를 Spring에 임포트 한다.
유효 기간에 관한 변수는 Enum으로 관리한다.
enum class TimeUnit(private val type: String, private val duration: Long) {
YEAR("year", 365),
DAY("day", 365),
HOUR("hour", 24),
MINUTE("minute", 60),
SECOND("second", 60),
MILLISECOND("millisecond", 1000);
fun getLifetimeInMilli(duration: Long): Long {
var lifetimeInMilli = duration
for (i in ordinal + 1 ..< entries.size) {
lifetimeInMilli *= TimeUnit.entries[i].duration
}
return lifetimeInMilli
}
}
위와 같이 이넘 클래스를 정의하였다.
val lifetimeType = TimeUnit.HOUR
lifetimeType.getLifetimeByMilli(12)
위 작업으로 쉽게 12시간에 대한 Millisecond를 구할 수 있다.
문자열로 정의된 환경변수에 따라, 유효시간은 서버가 열릴 때 (JWT Util 클래스가 로드될 때) 한번 정의되고 만료시간은 토큰이 생성될 때마다 유효시간을 사용해 계산돼야 하므로 Calendar 를 이용하거나 when 문을 사용하기 보다 millisecond를 구하는 것이 편하다고 생각했다.
password.yaml 에 아래 사항을 적고 application.yaml에 임포트 한다.
application.yaml
spring:
config.import: classpath:password.yaml
password.yaml
jwt:
secret: "서명 알고리즘의 최소 길이를 만족하는 secret key"
lifetime:
type: HOUR
duration: 12
gitignore를 사용해서 password.yaml이 어딘가에 업로드 되지 않도록 주의한다.
다음으로, 해당 환경변수를 임포트 해준다.
@Value는 Kotlin의 Spring Boot에선 먹히지 않는 것 같다. 현재 버전에서 아무리 해도 인식되지 않는다.
https://docs.spring.io/spring-framework/reference/languages/kotlin/spring-projects-in.html#injecting-configuration-properties
공식 문서에서도 @ConfigurationProperties를 사용할 것을 권하고 있다.
@Configuration
@ConfigurationProperties(prefix = "jwt")
data class JwtProperties(
var secret: String = "",
var lifetime: Lifetime = Lifetime()
) {
data class Lifetime (
var type: String = "",
var duration: Long = 0
)
}
이후 main 클래스에 @ConfigurationPropertiesScan 어노테이션을 달면 된다.
@EnableConfigurationProperties(프로퍼티클래스이름::class)를 해도 된다. Scan을 쓰는 이유는 나중에 프로퍼티 클래스가 많아질 때를 대비해서이다. 성능 차이는 모르겠다.
이후 JWT Util을 구현하여 생성자로 넣어준다. Main에서 어노테이션을 달아주면서 Bean이 등록되었을 것이다.
@Component
class JwtUtil(jwtProperties: JwtProperties) {
private final val secretString: String = jwtProperties.secret
private final val secretKey: SecretKey = Keys.hmacShaKeyFor(Decoders.BASE64.decode(secretString))
private final var lifetime = 0L
init {
val lifetimeType = TimeUnit.valueOf(jwtProperties.lifetime.type)
lifetime = lifetimeType.getLifetimeByMilli(jwtProperties.lifetime.duration)
}
fun createToken() {
// ...
val now = Date()
val currentTime = now.time
val expiration = Date(currentTime + lifetime)
// ...
val token = Jwts.builder()
.issuedAt(now)
.expiration(expiration)
.
}
}