{
"alg": "HS256",
"typ": "JWT"
}
{
"sub": "1234567890",
"name": "John Doe",
"iat": 1516239022
}
// HMAC SHA256 알고리즘 사용 시
HMACSHA256(
base64UrlEncode(header) +
"." +
base64UrlEncode(payload),
your-256-bit-secret
)
dependencies {
implementation 'io.jsonwebtoken:jjwt-api:0.11.5'
runtimeOnly 'io.jsonwebtoken:jjwt-impl:0.11.5'
runtimeOnly 'io.jsonwebtoken:jjwt-jackson:0.11.5'
}
import io.jsonwebtoken.Jwts;
import io.jsonwebtoken.io.Decoders;
import io.jsonwebtoken.io.Encoders;
import io.jsonwebtoken.security.Keys;
import java.nio.charset.StandardCharsets;
import java.security.Key;
import java.util.Calendar;
import java.util.Date;
import java.util.Map;
public class JwtTokenizer {
// (1) Secret Key의 byte[]를 Base64 형식의 문자열로 인코딩
public String encodeBase64SecretKey(String secretKey) {
return Encoders.BASE64.encode(secretKey.getBytes(StandardCharsets.UTF_8));
}
// (2) 인증된 사용자에게 JWT를 최초로 발급
public String generateAccessToken(Map<String, Object> claims,
String subject,
Date expiration,
String base64EncodedSecretKey) {
// (2-1) Base64 형식 Secret Key 문자열을 이용해 Key 객체를 얻음
Key key = getKeyFromBase64EncodedKey(base64EncodedSecretKey);
return Jwts.builder()
.setClaims(claims) // (2-2) Custom Claims에는 주로 인증된 사용자와 관련된 정보를 추가
.setSubject(subject) // (2-3) JWT에 대한 제목을 추가
.setIssuedAt(Calendar.getInstance().getTime()) // (2-4) JWT 발행 일자를 설정 (java.util.Date)
.setExpiration(expiration) // (2-5) JWT의 만료일시를 지정 (java.util.Date)
.signWith(key) // (2-6) 서명을 위한 Key 객체를 설정
.compact(); // (2-7) JWT를 생성하고 직렬화
}
// (3) Access Token이 만료되었을 경우, Access Token을 새로 생성할 수 있게 해주는 Refresh Token을 생성
public String generateRefreshToken(String subject, Date expiration, String base64EncodedSecretKey) {
Key key = getKeyFromBase64EncodedKey(base64EncodedSecretKey);
return Jwts.builder()
.setSubject(subject)
.setIssuedAt(Calendar.getInstance().getTime())
.setExpiration(expiration)
.signWith(key)
.compact();
}
// (4) JWT의 서명에 사용할 Secret Key를 생성
private Key getKeyFromBase64EncodedKey(String base64EncodedSecretKey) {
// (4-1) Base64 형식으로 인코딩 된 Secret Key를 디코딩한 후, byte array를 반환
byte[] keyBytes = Decoders.BASE64.decode(base64EncodedSecretKey);
// (4-2) key byte array를 기반으로 적절한 HMAC 알고리즘을 적용한 Key 객체를 생성
Key key = Keys.hmacShaKeyFor(keyBytes);
return key;
}
// (5) JWT 검증 Signature를 검증해 위/변조 확인
public void verifySignature(String jws, String base64EncodedSecretKey) {
Key key = getKeyFromBase64EncodedKey(base64EncodedSecretKey);
Jwts.parserBuilder()
.setSigningKey(key) // (5-1) 메서드로 서명에 사용된 Secret Key를 설정
.build()
.parseClaimsJws(jws); // (5-2) JWT를 파싱해서 Claims를 얻음 / jws 는 signature가 포함된 JWT
}
}
인코딩(Encoding) :
어떠한 정보/데이터의 형식을 데이터 표준화, 보안 등을 위해 다른 형태, 형식으로 변환하는 것
base64 :
문자를 8비트로 끊어진 2진법에서 6비트씩 끊어 새로운 문자를 형성, 남는 비트엔 0, 부족한 비트엔 = 으로 패딩
import io.jsonwebtoken.ExpiredJwtException;
import io.jsonwebtoken.io.Decoders;
import org.junit.jupiter.api.BeforeAll;
import org.junit.jupiter.api.DisplayName;
import org.junit.jupiter.api.Test;
import org.junit.jupiter.api.TestInstance;
import java.util.*;
import java.util.concurrent.TimeUnit;
import static org.assertj.core.api.Assertions.*;
import static org.junit.jupiter.api.Assertions.*;
@TestInstance(TestInstance.Lifecycle.PER_CLASS)
public class JwtTokenizerTest {
private static JwtTokenizer jwtTokenizer;
private String secretKey;
private String base64EncodedSecretKey;
// (1)
@BeforeAll
public void init() {
jwtTokenizer = new JwtTokenizer();
secretKey = "jungseo99881234998812349988123499881234";
base64EncodedSecretKey = jwtTokenizer.encodeBase64SecretKey(secretKey);
}
// (2)
@Test
public void encodeBase64SecretKeyTest() {
System.out.println(base64EncodedSecretKey);
assertThat(secretKey).isEqualTo(new String(Decoders.BASE64.decode(base64EncodedSecretKey)));
}
// (3)
@Test
public void generateAccessTokenTest() {
String accessToken = getAccessToken(Calendar.MINUTE, 10);
assertThat(accessToken).isNotNull();
}
// (4)
public void generateRefreshTokenTest() {
String subject = "test refresh token";
Calendar calendar = Calendar.getInstance();
calendar.add(Calendar.HOUR, 24);
Date expiration = calendar.getTime();
String refreshToken = jwtTokenizer.generateRefreshToken(subject, expiration, base64EncodedSecretKey);
System.out.println(refreshToken);
assertThat(refreshToken).isNotNull();
}
// (5)
@DisplayName("does not throw any Exception when jws verify")
@Test
public void verifySignatureTest() {
String accessToken = getAccessToken(Calendar.MINUTE, 10);
assertDoesNotThrow(() -> jwtTokenizer.verifySignature(accessToken, base64EncodedSecretKey));
}
// (6)
@DisplayName("throw ExpiredJwtException when jws verify")
@Test
public void verifyExpirationTest() throws InterruptedException{
String accessToken = getAccessToken(Calendar.SECOND, 1);
assertDoesNotThrow(() -> jwtTokenizer.verifySignature(accessToken, base64EncodedSecretKey));
TimeUnit.MILLISECONDS.sleep(1500);
assertThrows(ExpiredJwtException.class, () -> jwtTokenizer.verifySignature(accessToken, base64EncodedSecretKey));
}
private String getAccessToken(int timeUnit, int timeAmount) {
Map<String, Object> claims = new HashMap<>();
claims.put("memberId", 1);
claims.put("roles", List.of("USER"));
String subject = "test access token";
Calendar calendar = Calendar.getInstance();
calendar.add(timeUnit, timeAmount);
Date expiration = calendar.getTime();
String accessToken = jwtTokenizer.generateAccessToken(claims, subject, expiration, base64EncodedSecretKey);
return accessToken;
}
}