티스토리 뷰

JWT를 사용하는것 자체가 암호화된 json 문자열로 전송을 하는건데 무슨 암호화 및 복호화를 또 하느냐!?

암호화가 된다 하더라도 jwt.io 에서 알고리즘만 알고 있다면 다시 복호화된 값으로 볼수 있다. 그래서 보통 JWT로 통신을 할때는 payload에 중요한 정보를 넣지 않는것이 권장이 된다. userId, expireTime 등등의 정보만 보통은 가지고 있다. 하지만 중요한 정보를 payload에 넣어야 될수도 있고 userId도 노출이 되면 안되는 경우도 있다. 이럴 경우에는 payload 부분을 암호화를 해줘야 한다. 서로 통신을 하는 것이므로 단방향 암호화는 사용할 수 없고 양방향 암호화를 사용해야 한다. 양방향 암호화의 대표적인 알고리즘은 AES(Advanced Encryption Standard), DES(Data Encryption Standard) 가 있는데 이중 AES256 알고리즘을 사용해서 JWT payload 부분의 암호화 및 복호화를 진행하겠다. 


암호화 모듈 구성 (Aes256Util.java)

import javax.crypto.BadPaddingException;
import javax.crypto.Cipher;
import javax.crypto.IllegalBlockSizeException;
import javax.crypto.NoSuchPaddingException;
import javax.crypto.SecretKey;
import javax.crypto.spec.IvParameterSpec;
import javax.crypto.spec.SecretKeySpec;
import java.security.InvalidKeyException;
import java.security.NoSuchAlgorithmException;
import java.security.InvalidAlgorithmParameterException;

import org.apache.commons.codec.binary.Base64;

public class Aes256Util {
   private static volatile Aes256Util INSTANCE;

   final static String secretKey = "oingisprettyintheworld1234567890"; // 32byte
   static String IV = ""; // 16byte

   public static Aes256Util getInstance() {
      if (INSTANCE == null) {
         synchronized (Aes256Util.class) {
            if (INSTANCE == null)
               INSTANCE = new Aes256Util();
         }
      }
      return INSTANCE;
   }

   private Aes256Util() {
      IV = secretKey.substring(0, 16);
   }

   // 암호화
   public static String AES_Encode(String str)
         throws java.io.UnsupportedEncodingException, NoSuchAlgorithmException, NoSuchPaddingException,
         InvalidKeyException, InvalidAlgorithmParameterException, IllegalBlockSizeException, BadPaddingException {
      byte[] keyData = secretKey.getBytes();

      SecretKey secureKey = new SecretKeySpec(keyData, "AES");

      Cipher c = Cipher.getInstance("AES/CBC/PKCS5Padding");
      c.init(Cipher.ENCRYPT_MODE, secureKey, new IvParameterSpec(IV.getBytes()));

      byte[] encrypted = c.doFinal(str.getBytes("UTF-8"));
      String enStr = new String(Base64.encodeBase64(encrypted));

      return enStr;
   }

   // 복호화
   public static String AES_Decode(String str)
         throws java.io.UnsupportedEncodingException, NoSuchAlgorithmException, NoSuchPaddingException,
         InvalidKeyException, InvalidAlgorithmParameterException, IllegalBlockSizeException, BadPaddingException {
      byte[] keyData = secretKey.getBytes();
      SecretKey secureKey = new SecretKeySpec(keyData, "AES");
      Cipher c = Cipher.getInstance("AES/CBC/PKCS5Padding");
      c.init(Cipher.DECRYPT_MODE, secureKey, new IvParameterSpec(IV.getBytes("UTF-8")));

      byte[] byteStr = Base64.decodeBase64(str.getBytes());

      return new String(c.doFinal(byteStr), "UTF-8");
   }
}

참고 : shplab.tistory.com/entry/%EC%9E%90%EB%B0%94Java-AES256-%EC%97%90-%EB%8C%80%ED%95%B4%EC%84%9C%EC%95%94%ED%98%B8%ED%99%94-%EB%B3%B5%ED%98%B8%ED%99%94

 

AES256 암호화 복호화를 하는데 가장 잘 구현한 코드를 가지고 왔다. secretKey 정도만 변경해서 사용하면 된다. 


JWT payload 암호화

일단 암호화 모듈을 적용하기 앞서 JWT의 기본 동작 원리에 대해 학습을 할 필요가 있다. 학습은 필자가 예전에 샘플 프로젝트를 설명한 포스팅이 있으니 그것을 참조하면 된다.

 

jwt.io 에서 본 payload

JwtUtil 이라는 놈을 통해 JWT를 생성해냈고 jwt.io 에서 생성한 문자열을 넣어보면 우측에 위와 같이 PAYLOAD 가 표시가 된다. 바로 이렇게 누구나 볼수 있게 표시가 되는것을 암호화 한다는것이다. 어디서 어떻게 암호화를 하는가? 

보통은 JWT를 만들어 낼때 JwtUtil을 통해서 다음과 같은 방법으로 만들어낸다. 

 

JwtUtil.java AS-IS

public String createJwt(String user_id) {
      
    JwtBuilder jwtBuilder = Jwts.builder();
    Map<String, Object> paramMap = new HashMap();
      
    // header
    jwtBuilder.setHeaderParam("typ", "ACCESS_JWT");
    jwtBuilder.setHeaderParam("alg", algorithm);
      
    // payload
    paramMap.put("user_id", user_id);
    jwtBuilder.setClaims(paramMap);
    jwtBuilder.setExpiration(new Date(new Date().getTime() + expirTime1)); // 만료일
      
    // signature
    jwtBuilder.signWith(SignatureAlgorithm.forName(algorithm), secretKey.getBytes());
      
    return jwtBuilder.compact();
}

즉 Jwts.builder에 header와 payload, signature를 붙여서 만드는 것이다. 이중 우리가 암호화를 하려고 하는 부분은 payload이고 이 부분을 보면 된다. payload에는 Claims라는걸 넣어줘야 하고 이 Claims는 값을 담고 있는 map이다. map에 값을 담기 전에 암호화를 해주고 담으면 된다

 

JwtUtil.java TO-BE

public String createJwt(String user_id) {
      
    JwtBuilder jwtBuilder = Jwts.builder();
    Map<String, Object> paramMap = new HashMap();
      
    // header
    jwtBuilder.setHeaderParam("typ", "ACCESS_JWT");
    jwtBuilder.setHeaderParam("alg", algorithm);
      
    // payload
    Aes256Util a256 = Aes256Util.getInstance();
    String encryptClaims = a256.AES_Encode(user_id);
    
    paramMap.put("user_id", encryptClaims);
    jwtBuilder.setClaims(paramMap);
    jwtBuilder.setExpiration(new Date(new Date().getTime() + expirTime1)); // 만료일
      
    // signature
    jwtBuilder.signWith(SignatureAlgorithm.forName(algorithm), secretKey.getBytes());
      
    return jwtBuilder.compact();
}

일반적으로 암호화를 하는 방식과 동일하다. 

 

jwt.io 에서 본 암호화된 payload

다시 jwt.io에서 JWT 문자열을 넣어서 보면 위와 같이 payload에 암호화를 했던 부분이 이렇게 표현됨을 볼 수 있다. 


JWT payload 복호화

암호화를 했으니 복호화를 해야한다. 보통 암호화는 JwtUtil에서 수행을 하고 복호화는 JwtFilter에서 수행을 한다. 

JwtFilter는 대략 이렇게 생겼다. 

JwtFilter.java AS-IS

protected void doFilterInternal(HttpServletRequest httpServletRequest, HttpServletResponse httpServletResponse, FilterChain filterChain) throws ServletException, IOException {

    String authorizationHeader = httpServletRequest.getHeader("Authorization");

    String token = null;
    String userName = null;

    if (authorizationHeader != null && authorizationHeader.startsWith("Bearer ")) {
        token = authorizationHeader.substring(7);
        userName = jwtUtil.extractUsername(token);
    }
    
    ... userName으로 인증절차 수행...
}

즉 JWT로 들어온 요청인지 판단하여 payload부에서 값을 추출하여 인증을 하는 방식이다. 하지만 지금은 암호화된 payload의 값이 들어오므로 여기에서 복호화를 해줘야한다. 복호화도 간단하다. 위의 과정에서 추출된 userName이라는것이 현재는 암호화된 값으로 들어오니 이 값을 복호화해주면 된다. 

 

JwtFilter.java TO-BE

@Override
protected void doFilterInternal(HttpServletRequest httpServletRequest, HttpServletResponse httpServletResponse, FilterChain filterChain) throws ServletException, IOException {

    String authorizationHeader = httpServletRequest.getHeader("Authorization");

    String token = null;
    String userName = null;

    if (authorizationHeader != null && authorizationHeader.startsWith("Bearer ")) {
        token = authorizationHeader.substring(7);
        userName = jwtUtil.extractUsername(token);
		
	    Aes256Util a256 = null;
        try {
            a256 = Aes256Util.getInstance();
            String decryptUserName = a256.AES_Decode(userName);
        } catch (InvalidKeyException | NoSuchAlgorithmException | NoSuchPaddingException
            | InvalidAlgorithmParameterException | IllegalBlockSizeException | BadPaddingException e) {
            // TODO Auto-generated catch block
            e.printStackTrace();
        }
    }
	
	... 생략 ...
}

암호화할때랑 거의 유사한 방법으로 Aes256Util의 싱글톤객체의 AES_Decode 메소드를 사용해 복호화를 한다. 결과값이 다시 복호화가 되어 원래의 문자열로 돌아오게 된다. 

 

복잡해 보이지만 엄청 간단하다. Aes256Util 하나 만들고 

String을 암호화할때는 AES_Encode 메소드를 사용하고 JWT 생성하기 전에 해주고 

String을 복호화할때는 AES_Decode 메소드를 사용해서 JWT Filter에서 사용되기 전에 해주면 된다. 

 

끝!

댓글
최근에 올라온 글
최근에 달린 댓글
«   2024/12   »
1 2 3 4 5 6 7
8 9 10 11 12 13 14
15 16 17 18 19 20 21
22 23 24 25 26 27 28
29 30 31