JWT란?
- JWT(JSON Web Token)란 인증에 필요한 정보들을 암호화시킨 JSON 토큰
- JWT 토큰을 HTTP 헤더에 실어 서버가 클라이언트를 식별하는 방식
JWT 구조
- HEADER(헤더), PAYLOAD(내용), SIGNATURE(서명). 을 구분자로 나누어지는 세 가지 문자열의 조합
pom.xml 추가 (JWT, JWK)
<dependencies>
<!-- jwt -->
<dependency>
<groupId>com.auth0</groupId>
<artifactId>java-jwt</artifactId>
<version>4.2.2</version>
</dependency>
<!-- jwk -->
<dependency>
<groupId>com.auth0</groupId>
<artifactId>jwks-rsa</artifactId>
<version>0.21.3</version>
</dependency>
<dependency>
<groupId>com.nimbusds</groupId>
<artifactId>nimbus-jose-jwt</artifactId>
<version>9.30.1</version>
</dependency>
</dependencies>
JWT 생성 및 검증
- main : JWT 토큰생성 -> JWT 검증 -> JWK 검증
public static void main(String[] args) throws Exception {
// JWT 토큰생성
String token = generateJwt();
// JWT 검증
System.out.println("-- verifyJwtByFile --");
verifyJwtByFile(token);
// JWK 검증
System.out.println("-- verifyJwtByJwk --");
verifyJwtByJwk(token);
}
JWT 생성
- generateJwt : 토큰 생성
public static String generateJwt() {
String token = null;
try {
// privatekey 가져오기
RSAPrivateKey rsaPrivateKey = getPrivateKey();
// privatekey RSA256 암호화
Algorithm algorithm = Algorithm.RSA256(rsaPrivateKey);
// JWK에서 사용할 kid를 지정한다.
Map<String, Object> optionalHeader = new HashMap<>();
optionalHeader.put("kid", "SPRING123");
Instant issuedAt = Instant.now();
JWTCreator.Builder jwtBuilder = JWT.create();
jwtBuilder.withHeader(optionalHeader);
jwtBuilder.withIssuedAt(issuedAt);
jwtBuilder.withIssuer("seyu");
jwtBuilder.withAudience("hong123");
jwtBuilder.withExpiresAt(issuedAt.plusSeconds(EXPIRE_PLUS_SECONDS));
token = jwtBuilder.sign(algorithm);
} catch (JWTCreationException e) {
e.printStackTrace();
}
System.out.printf("==token============================================");
System.out.printf(token);
return token;
}
token=yJ0eXAiOiJKV1QiLCJhbGciOiJSUzI1NiJ9.eyJhdWQiOiJob25nMTIzIiwiaXNzI.......생략
- getPrivateKey : 토큰 생성 시 필요한 private.key 읽기
public static RSAPrivateKey getPrivateKey() {
try{
KeyFactory keyFactory = KeyFactory.getInstance("RSA");
InputStream inputStream = RSAUtil.class.getClassLoader().getResourceAsStream("private.key");
ByteArrayOutputStream byteArrayOutputStream = new ByteArrayOutputStream();
byte[] buffer = new byte[1024];
int bytesRead;
while ((bytesRead = inputStream.read(buffer)) != -1) {
byteArrayOutputStream.write(buffer, 0, bytesRead);
}
byte[] privateKeyBytes = byteArrayOutputStream.toByteArray();
EncodedKeySpec privateKeySpec = new PKCS8EncodedKeySpec(privateKeyBytes);
return (RSAPrivateKey) keyFactory.generatePrivate(privateKeySpec);
} catch (NoSuchAlgorithmException e){
e.printStackTrace();
} catch (IOException e){
e.printStackTrace();
} catch (InvalidKeySpecException e){
e.printStackTrace();
}
return null;
}
JWT 검증
- verifyJwt : 토큰 검증
private static void verifyJwt(String token) {
RSAPublicKey rsaPublicKey;
try {
rsaPublicKey = getPublicKey();
DecodedJWT decodedJWT = null;
Algorithm algorithm = Algorithm.RSA256(rsaPublicKey);
JWTVerifier verifier = JWT.require(algorithm)
.withIssuer("seyu")
.withAudience("hong123")
.build();
decodedJWT = verifier.verify(token);
// Claim 정보 추출
String name = decodedJWT.getClaim("name").asString();
System.out.println("name: " + name); //name: 홍길동
} catch (TokenExpiredException e) {
e.printStackTrace();
} catch (JWTVerificationException e){
e.printStackTrace();
}
}
- getPublicKey : 토큰 검증 시 필요한 public.key 읽기
private static RSAPublicKey getPublicKey() {
try{
KeyFactory keyFactory = KeyFactory.getInstance("RSA");
InputStream inputStream = RSAUtil.class.getClassLoader().getResourceAsStream("public.key");
ByteArrayOutputStream byteArrayOutputStream = new ByteArrayOutputStream();
byte[] buffer = new byte[1024];
int bytesRead;
while ((bytesRead = inputStream.read(buffer)) != -1) {
byteArrayOutputStream.write(buffer, 0, bytesRead);
}
byte[] publicKeyBytes = byteArrayOutputStream.toByteArray();
EncodedKeySpec publicKeySpec = new X509EncodedKeySpec(publicKeyBytes);
return (RSAPublicKey) keyFactory.generatePublic(publicKeySpec);
} catch (NoSuchAlgorithmException e){
e.printStackTrace();
} catch (IOException e){
e.printStackTrace();
} catch (InvalidKeySpecException e){
e.printStackTrace();
}
return null;
}
JWK 란?
- JWK는 암호화 키를 표현하기 위한 다양한 정보를 담은 JSON 객체에 관한 표준이다.
- JWT를 서명하는데 사용했던 public key를 제공하기 위해 JWK 형태로 표현된 key를 접근할 수 있는 URL을 제공한다.
- URL에 접근하면 JWK 형태로 key를 다운로드 할 수 있다.
JWK 생성
public static JWKSet generateJwk() {
RSAPublicKey rsaPublicKey = getPublicKey();
JWK jwk = new RSAKey.Builder(rsaPublicKey).keyID("SPRING123").keyUse(KeyUse.SIGNATURE).build();
List<JWK> jwkList = new ArrayList<>();
jwkList.add(jwk);
return new JWKSet(jwkList);
}
JWK 검증
- JwkProvider provider = new UrlJwkProvider("http://localhost:8080/"); 를 통해 jwks.json을 호출한다
private static void verifyJwtByJwk(String token) throws JwkException {
// JWK의 도메인을 지정한다.
JwkProvider provider = new UrlJwkProvider("http://localhost:8080/");
// JWK는 JWT를 여러개 개인키와 공개키를 관리할 수 있어,
// JWT Header에 유니크 키(kid)를 포함시켜 사용한다.
String kid = JWT.decode(token).getKeyId();
String audience = JWT.decode(token).getAudience().get(0);
Jwk jwk = provider.get(kid);
RSAPublicKey rsaPublicKey = (RSAPublicKey) jwk.getPublicKey();
DecodedJWT decodedJWT = null;
try{
Algorithm algorithm = Algorithm.RSA256(rsaPublicKey);
JWTVerifier verifier = JWT.require(algorithm)
// 발급자와 대상자를 확인한다.
.withIssuer("seyu")
.withAudience(audience)
.build();
decodedJWT = verifier.verify(token);
} catch (TokenExpiredException e) {
e.printStackTrace();
} catch (JWTVerificationException e){
e.printStackTrace();
}
// Claim에서 정보추출
System.out.printf("Issuer: %s, Audience: %s\n", decodedJWT.getIssuer(), decodedJWT.getAudience());
}
jwks.json 호출
@RestController
@Slf4j
public class JWKController {
@GetMapping(value = "/.well-known/jwks.json", produces = MediaType.APPLICATION_JSON_VALUE)
public Map<String, Object> jwks() throws NoSuchAlgorithmException, IOException, InvalidKeySpecException {
JWKSet jwkSet = JWTUtil.generateJwk();
return jwkSet.toJSONObject();
}
}
- postman -> http://localhost:8080/.well-known/jwks.json 호출
JWT 생성 및 검증 사이트
- https://jwt.io/
- 토큰을 넣어보면 우측에 토큰 정보들을 확인 할 수 있다
참고
- https://inpa.tistory.com/entry/WEB-%F0%9F%93%9A-JWTjson-web-token-%EB%9E%80-%F0%9F%92%AF-%EC%A0%95%EB%A6%AC
'Programming > Back-End' 카테고리의 다른 글
[Mybatis] delete 여러 테이블 데이터 한번에 삭제하기 (42) | 2023.09.18 |
---|---|
java.net.BindException "Address alredy in use: bind" 해결/특정 포트 죽이는 방법 (37) | 2023.09.13 |
[SpringBoot] Embedded Tomcat의 catalina.jar (0) | 2023.09.01 |
[Java/자바] URLencode와 Base64url 차이점 (0) | 2023.08.30 |
[Java/자바] 한글 문자열 URL 인코딩(encode)/디코딩(decode) 하기 (35) | 2023.08.30 |