JWT(Json Web Token)이란?
- 인증 정보를 Json 형태로 담아 서버-클라이언트 간 안전하게 전달하기 위한 토큰이다.
- 서버에서 발급하고 클라이언트에 저장
- 토큰 자체에 사용자의 모든 정보를 포함하는 것이 가능하다.
- 사용자의 정보가 변경되면 토큰을 재발급 받아야된다.
- JSON 데이터를 Base64 인코딩을 통해 직렬화
JWT 구조
.(dot)을 기준으로 헤더 내용 서명이 JWT토큰 하나를 이룬다.
만약에 토큰이 완성되면 다음과 같은 형식을 따른다.
Base64
ASCII 중 제어 문자와 일부 특수 문자를 제외한 64개의 안전한 문자이다.
Header
typ: 토큰 타입"JWT"
alg(알고리즘): SHA-256, SHA-384, SHA-512
ECDSA(ES), HMAC(HS), RSASSA-PSS(PS), RSASSA-PKCS(RS)
헤더 JSON에는 다음과 같은 데이터가 담긴다고 볼 수 있다.
그럼 이 헤더를 HS256으로 인코딩을 시키면
다음과 같은 값이 나올 수 있다.
Payload(Data)
- 정보 중 한조각 claim(key)이라고 부르면, key-value 쌍으로 구성된다.
- 등록(registered), 공개(public), 비공개(private) claim으로 나누지만
임의의 데이터를 자유롭게 사용하기 위해서 비공개 claim을 많이 사용
- 어떤 이름으로 JWT를 만들었냐에 따라서 꺼낼 때 내용이 달라질 수 있다.
위의 JSON데이터가 인코딩을 거치면 다음과 같이 나온다.
Signature
- 헤더의 base63 인코딩 값과 Payload base64 인코딩 값을 합친 후 서명키로 다시 해싱한 값
- 보통 secret키가 쓰여지는데(43자 이상) 보통 랜덤으로 키를 만들어서 넣어준다.
JWT를 사용하려면 의존 라이브러리 추가를 해줘야한다.
<dependency>
<groupId>nz.net.ultraq.thymeleaf</groupId>
<artifactId>thymeleaf-layout-dialect</artifactId>
</dependency>
<dependency>
<groupId>io.jsonwebtoken</groupId>
<artifactId>jjwt-api</artifactId>
<version>0.11.5</version>
</dependency>
<dependency>
<groupId>io.jsonwebtoken</groupId>
<artifactId>jjwt-impl</artifactId>
<version>0.11.5</version>
<scope>runtime</scope>
</dependency>
다음과 같이 의존성을 줘야된다.
JwtTests.java
@Test
void createJwt() {
SignatureAlgorithm signatureAlgorithm = SignatureAlgorithm.HS256;
byte[] secretKeyBytes = DatatypeConverter.parseBase64Binary(
"abcdefghijklmnopqrstuvwxyzabcdefghijklmnopqr");
Key signingKey = new SecretKeySpec(secretKeyBytes, signatureAlgorithm.getJcaName());
JwtBuilder builder = Jwts.builder()
// Header
.setHeaderParam("typ", "JWT")
// Payload - Registered Claim
.setSubject("제목").setIssuer("ggoreb.com").setAudience("service user")
// Payload - Secret Claim
.claim("username", "ggoreb").claim("password", 1234).claim("hasPermission", "ADMIN")
// Signature
.signWith(signingKey, signatureAlgorithm);
long now = System.currentTimeMillis();
builder.setExpiration(new Date(now + 3600000)); // 1시간 뒤 토큰 유효기간 만료
String token = builder.compact();
log.info("jwt {}", token);
}
해당 코드는 JWT를 생성하는 테스트 코드이다.
서명에 사용할 알고리즘과 비밀 키(Base64인코딩 된 문자열)를 설정한다.
JwtBuilder로 Header, Payload, signature를 설정해준다.
setExpiration메서드로 1시간 뒤 토큰 유효기간을 만료시킨다.
그럼 이 테스트 코드를 실행시켜보면
다음과 같이 JWT토큰이 생성되는 것을 확인 할 수 있다.
@Test
void getDataFromJwt() {
byte[] secretKeyBytes = DatatypeConverter.parseBase64Binary(
"abcdefghijklmnopqrstuvwxyzabcdefghijklmnopqr");
String jwt = "eyJ0eXAiOiJKV1QiLCJhbGciOiJIUzI1NiJ9.eyJzdWIiOiLsoJzrqqkiLCJpc3MiOiJnZ29yZWIuY29tIiwiYXVkIjoic2VydmljZSB1c2VyIiwidXNlcm5hbWUiOiJnZ29yZWIiLCJwYXNzd29yZCI6MTIzNCwiaGFzUGVybWlzc2lvbiI6IkFETUlOIiwiZXhwIjoxNzQ2OTY0MjQ2fQ.-MMoyH_lXkCq2KtoBQuVqs-UijLONsZ8n7pV3Ctpg7c";
JwtParser jwtParser = Jwts.parserBuilder().setSigningKey(secretKeyBytes).build();
JwsHeader<?> header = jwtParser.parseClaimsJws(jwt).getHeader();
String algorithm = header.getAlgorithm();
log.info("Algorithm {}", algorithm);
String type = header.getType();
log.info("Type {}", type);
Claims claims = jwtParser.parseClaimsJws(jwt).getBody();
log.info("Subject {}", claims.getSubject());
log.info("Issuer {}", claims.getIssuer());
log.info("Audience {}", claims.getAudience());
log.info("claim {}", claims.get("username"));
log.info("claim {}", claims.get("password"));
log.info("claim {}", claims.get("hasPermission"));
}
Jwt확인하는 테스트 코드를 작성해보자
그럼 다음과 같이 Jwt의 상세 내용에 대해서 나온다.
그럼 1시간이 아니라 5초쯤으로 토큰 유효기간을 설정해서 확인하는 테스트코드를 작성해보자
@Test
void getDataFromJwt() {
byte[] secretKeyBytes = DatatypeConverter.parseBase64Binary(
"abcdefghijklmnopqrstuvwxyzabcdefghijklmnopqr");
String jwt = "eyJ0eXAiOiJKV1QiLCJhbGciOiJIUzI1NiJ9.eyJzdWIiOiLsoJzrqqkiLCJpc3MiOiJnZ29yZWIuY29tIiwiYXVkIjoic2VydmljZSB1c2VyIiwidXNlcm5hbWUiOiJnZ29yZWIiLCJwYXNzd29yZCI6MTIzNCwiaGFzUGVybWlzc2lvbiI6IkFETUlOIiwiZXhwIjoxNzQ2OTYxMzUxfQ.KixtgVF1kG6-UUEpnCQLaUiwrq6pTucWw-KFDWH8dcs";
JwtParser jwtParser = Jwts.parserBuilder().setSigningKey(secretKeyBytes).build();
JwsHeader<?> header = jwtParser.parseClaimsJws(jwt).getHeader();
String algorithm = header.getAlgorithm();
log.info("Algorithm {}", algorithm);
String type = header.getType();
log.info("Type {}", type);
Claims claims = jwtParser.parseClaimsJws(jwt).getBody();
log.info("Subject {}", claims.getSubject());
log.info("Issuer {}", claims.getIssuer());
log.info("Audience {}", claims.getAudience());
log.info("claim {}", claims.get("username"));
log.info("claim {}", claims.get("password"));
log.info("claim {}", claims.get("hasPermission"));
}
5초로 생성하고 새롭게 발급받은 JWT를 확인하는 테스트를 실행해보자
다음과 같이 ExpriedJwtException이 뜨는것을 볼 수 있다.
ExpriedJwtException은 JWT가 만료되었을 때 발생하는 예외이다.
JWT토큰 유효기간을 5초로 설정했으니 당연한 얘기이다.
따라서 JWT의 유효기간을 적절하게 설정하는 것이 좋다.