[Java/자바] JWT 토큰 발급 및 JWT/JWK 검증 처리 방법 정리

 

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;
}

private.key 위치

 

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