본문 바로가기
팀 프로젝트/최종 프로젝트

JWT Payload 암호화하기(AES)

by pon9 2025. 2. 23.

JWT Payload를 암호화해야하는 이유

보통 jwt는 사용자의 고유id, email, 이름같은 중요한 정보를 담고있다.

그런데 이 기본jwt는 이 정보를 Base64로만 인코딩하기때문에 누구나 jwt.io에서 쉽게 디코딩해서 볼 수 있다.

보통 이 jwt토큰은 개발자 도구에서 쉽게 접근가능하기 때문에 중요한 정보가 이런식으로 노출된다면 골치아파진다.

그래서 저 PAYLOAD를 암호화한다는 거다.

 

 

대칭 vs 비대칭

암호화 방법에는 AES, RSA, AES+RSA 등 다양한 방법이 존재하는데

이 중 AES암호화가 대중적인 이유는 보통 RSA보다 100배 이상 빠르기 때문이다.

jwt토큰은 거의 모든 요청마다 검증되어야 하기에 서버 부하를 크게 줄일 수 있다.

 

이처럼 단순하게 말하면 "빨라서 좋다"지만 정확히 말하자면 AES는 대칭 키, RSA는 비대칭 키로 구분된다.

 

대칭 키 암호화인 AES는 암호화와 복호화에 동일한 키를 사용하기 때문에 연산 속도가 빠르다.

그렇기에 CPU부하가 적고 응답 시간이 짧아져서 성능 면에서 이점이 있다.

 

반면 비대칭 키 암호화인 RSA는 서로 다른 키 쌍(공개 키와 개인 키)을 사용해서 암호화와 복호화를 수행한다.

비대칭 키의 장점은 서명을 통해 누가 데이터를 보냈는지 확인할 수 있다는 점이다.

공개 키와 개인 키를 각각 따로 사용하기 때문에,

1. 발신자는 본인의 개인 키로 데이터를 서명하고, 수신자는 발신자의 공개 키로 서명을 검증함으로써 발신자의 신원을 확인할 수 있다

2. 데이터가 전송 중에 변경되지 않음을 보장한다. 만약 데이터 변경 시도 시 서명 검증에 실패한다.

이러한 구조 덕분에 RSA는 인증 시스템, 디지털 서명 등에서 사용자 인증에 널리 사용된다. 

 

하지만 우리 서비스에서는 "사용자가 누군지" 굳이 RSA로 검증할 필요가 없다.

jwt 자체가 인증된 사용자 정보를 포함한다. 토큰이 발급될 때 이미 인증된 사용자 정보가 들어있기 때문이다

서비스 구조 상 우리 서버 인스턴스 상에서만 통신이 이루어지고, 우리 서버 간에 공유된 대칭 키 하나를 안전하게 관리하는 것으로 충분하다

 

그러므로 대칭 키 암호화 AES로 가보자

 

 

java에서의 AES 암호화/복호화

secret key와 암호화 대상을 함께 Cipher로 암호화하면 끝이다. java코드로도 쉽게 구현가능하다

public String encrypt(String plainText) {
    try {
        String aesSecretKey = jwtSecurityProperties.getSecret().getAesKey();
        SecretKeySpec secretKey = new SecretKeySpec(aesSecretKey.getBytes(), "AES");
        Cipher cipher = Cipher.getInstance("AES/ECB/PKCS5Padding");
        cipher.init(Cipher.ENCRYPT_MODE, secretKey);
        byte[] encryptedBytes = cipher.doFinal(plainText.getBytes());
        return Base64.getEncoder().encodeToString(encryptedBytes);
    } catch (Exception e) {
        throw new EncryptException(ServerErrorCode.TOKEN_ENCRYPTION_FAILED);
   }
}

public String decrypt(String encryptedText) {
    try {
        String aesSecretKey = jwtSecurityProperties.getSecret().getAesKey();
        SecretKeySpec secretKey = new SecretKeySpec(aesSecretKey.getBytes(), "AES");
        Cipher cipher = Cipher.getInstance("AES/ECB/PKCS5Padding");
        cipher.init(Cipher.DECRYPT_MODE, secretKey);
        byte[] decodedBytes = Base64.getDecoder().decode(encryptedText);
        byte[] decryptedBytes = cipher.doFinal(decodedBytes);
        return new String(decryptedBytes);
    } catch (Exception e) {
        throw new UnAuthorizedException(AuthErrorCode.TOKEN_DECRYPTION_FAILED);
    }
}

자바에서 Cipher기능을 제공해주기때문에 환경변수 등에서 secretKey를 가져와서 정해진 틀에 따라 암호화-복호화 가능하다.

 

이걸 spring에서 사용할 때는 암호화와 복호화가 필요한 각각 시점을 생각하면 적용하기 쉽다.

일반적으로,

1. Jwtfilter: 요청을 받는 곳은 보통 jwtfilter이고, 이곳에서 복호화된 토큰으로 필터링을 거쳐야 한다.

2. JwtUtil: token을 생성하거나 jwt를 파싱을 담당하므로 메인 encrypt와 decrypt는 이곳에서 진행된다.

jwtUtil의 토큰 생성을 담당하는 메서드다.

원래는 userId와 role을 각각 jwtBuilder에 넣었는데 이젠 aes암호화 과정을 거친 후 같이 저장된다.

이렇게 encryptedPayload 통째로 넣으면 된다

암호화 된 토큰을 복호화 할 때는 똑같이 aes256Util.decrypt 과정을 거치고

암호화 할 때 userId와 role사이에 : 를 넣어줬으므로 이걸 split로 배열로 만들고 편히 사용하면 된다

 

 

결과

평소처럼 auth/login을 하면 외형은 평소와 같지만

이제 Payload가 암호화 된 것을 볼 수 있다

 

여담으로 속도차이가 궁금해서 이전 기록을 찾아봤는데 티도 안 난다

aes 적용 이전이나 이후나 300-400ms정도 걸리는 듯