JWT
JWT는 JSON Web Token의 줄임말입니다.
JSON Web Token (JWT) is an open standard (RFC 7519) that defines a compact and self-contained way for securely transmitting information between parties as a JSON object. This information can be verified and trusted because it is digitally signed. JWTs can be signed using a secret (with the HMAC algorithm) or a public/private key pair using RSA or ECDSA.
JWT는 웹 표준으로써 점(.)으로 구분된 3개의 Base64URL 문자열로 구성됩니다.
xxxxx.yyyyy.zzzzz
헤더(header).내용(payload).서명(signature)
Header
header는 일반적으로 토큰 유형(JWT)과 사용 중인 서명 알고리즘(예: HMAC SHA256 또는 RSA)을 나타내는 정보를 포함합니다.
{
"alg": "HS256",
"typ": "JWT"
}
예를 들어 위와 같은 JSON 데이터를 Base64Url로 인코딩하여 JWT의 첫 번째 부분을 형성합니다.
Payload
Token의 두 번째 부분은 Claim을 포함하는 Payload입니다. Claim은 entity(일반적으로 사용자) 및 추가 정보에 대한 설명을 말합니다.
Claim은 세 종류로 나뉩니다.
- registered claim : iss(issuer), exp(expiration time), sub(subject), aud(audience), others 와 같은 이미 정의되어 있는 클레임 집합입니다.
- public claim : 충돌 방지 네임 스페이스를 포함하는 URI로 정의할 수 있습니다.
- private claim : 클라이언트와 서버 간에 정보를 공유하기 위해 생성되는 사용자 지정 클레임입니다.
{
"sub": "1234567890",
"name": "John Doe",
"admin": true
}
페이로드는 위와 같은 JSON 데이터를 Base64Url로 인코딩하여 JWT의 두 번째 부분을 형성합니다.
Signature
Signature는 헤더의 인코딩 값과 정보의 인코딩 값을 합쳐서 주어진 비밀키로 해싱하여 생성되는 서명 값입니다. 보통 사용자의 비밀번호를 비밀키로 사용하곤 합니다.
예를 들어 JMAC SHA256 알고리즘을 사용하려는 경우 다음과 같은 방식으로 서명이 생성됩니다.
HMACSHA256(base64UrlEncode(header) + "." + base64UrlEncode(payload), secret)
Header + Payload + Signature
jwt.io 디버거를 사용하여 JWT를 인코딩, 디코딩할 수 있습니다.
SpringBoot + JWT
JWT 이론적인 설명을 했으니, 실제로 구현을 해보겠습니다.
pom.xml에 jsonwebtoken(version 0.11.2) dependency를 추가합니다.
<dependency>
<groupId>io.jsonwebtoken</groupId>
<artifactId>jjwt-api</artifactId>
<version>0.11.2</version>
</dependency>
<dependency>
<groupId>io.jsonwebtoken</groupId>
<artifactId>jjwt-impl</artifactId>
<version>0.11.2</version>
<scope>runtime</scope>
</dependency>
<dependency>
<groupId>io.jsonwebtoken</groupId>
<artifactId>jjwt-jackson</artifactId> <!-- or jjwt-gson if Gson is preferred -->
<version>0.11.2</version>
<scope>runtime</scope>
</dependency>
아래와 같은 로직으로 토큰 생성, 검증을 할 수 있습니다.
import io.jsonwebtoken.*;
@Service
public class jwtTokenService implements TokenService {
@Resource
private ObjectMapper objectMapper;
private static final String SECRET_KEY = "anstlflxltmxhflwpdltmsenpqxhzmstodtjdrjawmd20210118wpdlejqmfdbxl";
/**
* jwt 생성
*/
@Override
public String createToken(String key, String value, Integer expireTokenDuration) {
LocalDateTime now = LocalDateTime.now();
Timestamp expireTimestamp = Timestamp.valueOf(now.plusHours(expireTokenDuration));
byte[] secretKeyBytes = DatatypeConverter.parseBase64Binary(SECRET_KEY);
return Jwts.builder()
.claim(key, value)
// .claim(key2, value2)
// .claim(key3, value3)
.setExpiration(expireTimestamp)
.setIssuer("moonsiri")
.signWith(Keys.hmacShaKeyFor(secretKeyBytes), SignatureAlgorithm.HS256)
.compact();
}
/**
* jwt 검증
* String value = (String) tokenService.getClaims(token).get(key);
*/
@Override
public Claims getClaims(String token) {
try {
return Jwts.parserBuilder()
.setSigningKey(DatatypeConverter.parseBase64Binary(SECRET_KEY))
.build()
.parseClaimsJws(token)
.getBody();
} catch (JwtException e) {
e.printStackTrace();
// The JWT MUST contain exactly two period characters. 등
throw e;
}
}
/**
* jwt payload
* String value = jwtService.getPayloadWithoutSigning(token).get(key);
*/
@Override
public Map<String, Object> getPayloadWithoutSigning(String token) {
try {
String payloadJsonString = new String(Base64.getDecoder().decode(token.split("\\.")[1]), StandardCharsets.UTF_8);
return objectMapper.readValue(payloadJsonString, new TypeReference<>() {});
} catch (JsonProcessingException e) {
e.printStackTrace();
throw new RuntimeException("잘못된 payload 형식입니다.");
}
}
}
jsonwebtoken version 0.9.1 사용
<dependency>
<groupId>io.jsonwebtoken</groupId>
<artifactId>jjwt</artifactId>
<version>0.9.1</version>
</dependency>
<dependency>
<groupId>javax.xml.bind</groupId>
<artifactId>jaxb-api</artifactId>
<version>2.3.1</version>
</dependency>
import io.jsonwebtoken.*;
@Service
public class jwtTokenService implements TokenService {
private static final String SECRET_KEY = "anstlflxltmxhflwpdltmsenpqxhzmstodtjdrjawmd20210118wpdlejqmfdbxl";
/**
* jwt 생성
*/
@Override
public String createToken(String key, String value, Integer expireTokenDuration) {
LocalDateTime now = LocalDateTime.now();
Timestamp expireTimestamp = Timestamp.valueOf(now.plusHours(expireTokenDuration));
return Jwts.builder()
.claim(key, value)
// .claim(key2, value2)
// .claim(key3, value3)
.setExpiration(expireTimestamp)
.setIssuer("moonsiri")
.signWith(SignatureAlgorithm.HS512, SECRET_KEY)
.compact();
}
/**
* jwt parsing
* String value = (String) tokenService.getClaims().get(key);
*/
@Override
public Claims getClaims(String token) {
try {
return Jwts.parser().setSigningKey(SECRET_KEY)
.parseClaimsJws(token)
.getBody();
} catch (JwtException e) {
e.printStackTrace();
// The JWT MUST contain exactly two period characters. 등
throw e;
}
}
}
Test Code
@SpringBootTest
class TokenServiceImplTest {
@Resource
private TokenService tokenService;
@Test
void 토큰_생성_검증() {
String token = tokenService.createToken("key", "value", 1);
Assertions.assertNotNull(token);
// eyJhbGciOiJIUzI1NiJ9.eyJrZXkiOiJ2YWx1ZSIsImV4cCI6MTYxMDk1OTUxMiwiaXNzIjoibW9vbnNpcmkifQ.SaVEHFP3I7c5tRnPC096eWMj2-ulWSOa5xhsddvHfIM
Claims claims = tokenService.getClaims(token);
String value = (String)claims.get("key");
Assertions.assertEquals("value", value);
}
}
만약 JWT parsing 중에 JwtException 오류가 발생한다면 JWT가 아래의 내용을 충족했는지 확인하길 바랍니다.
- The JWT MUST contain exactly two period characters.
- The JWT MUST be split on the two period characters resulting in three strings. The first string is the Encoded JWT Header; the second is the JWT Second Part; the third is the JWT Third Part.
- The Encoded JWT Header MUST be successfully base64url decoded following the restriction given in this specification that no padding characters have been used.
- The JWT Header MUST be completely valid JSON syntax conforming to RFC 4627 [RFC4627].
- The JWT Header MUST be validated to only include parameters and values whose syntax and semantics are both understood and supported.
- Determine whether the JWT is signed, encrypted, or plaintext by examining the alg (algorithm) header value and optionally, the enc (encryption method) header value, if present.
- Depending upon whether the JWT signed, encrypted, or plaintext, there are three cases:
- If the JWT is signed, all steps specified in [JWS] for validating a JWS MUST be followed. Let the Message be the result of base64url decoding the JWS Payload.
- If the JWT is encrypted, all steps specified in [JWE] for validating a JWE MUST be followed. Let the Message be the JWE Plaintext.
- Else, if the JWT is plaintext, let the Message be the result of base64url decoding the JWE Second Part. The Third Part MUST be verified to be the empty string.
- If the JWT Header contains a typ value of either "JWS" or "JWE", then the Message contains a JWT that was the subject of nested signing or encryption operations, respectively. In this case, return to Step 1, using the Message as the JWT.
- Otherwise, let the JWT Claims Set be the Message.
- The JWT Claims Set MUST be completely valid JSON syntax conforming to RFC 4627 [RFC4627].
- When used in a security-related context, the JWT Claims Set MUST be validated to only include claims whose syntax and semantics are both understood and supported.
- Nimbus JOSE + JWT는 Javascript Object Signing and Encryption (JOSE) 사양 모음과 밀접하게 관련된 JWT (JSON Web Token ) 사양을 구현하는 인기 있는 오픈 소스 (Apache 2.0) Java 라이브러리입니다.
- Java JWT (: JSON Web Token for Java and Andriod)는 JWT , JWS , JWE , JWK 및 JWA RFC 사양 및 Apache 2.0 라이선스 조건에 따른 오픈 소스에 기반한 순수 Java 구현입니다.
[Reference]
https://jwt.io/jwt.io/introduction/
openid.net/specs/draft-jones-json-web-token-07.html
'spring' 카테고리의 다른 글
[SpringBoot] Test환경에 H2 적용하기 (0) | 2021.08.04 |
---|---|
[SpringBoot] Maven Multi-Module Project 생성 (0) | 2021.02.18 |
[Spring] JSONArray를 사용하지 않고 Ajax로 배열 list 넘기기 (2) | 2021.01.06 |
[Spring] java/jsp에서 properties value 불러오기 (0) | 2021.01.05 |
[Spring] local cache key, value 조회 및 삭제 (0) | 2020.12.04 |
댓글