woodisco 2023. 9. 11. 16:35

 

JWT 토큰 인증 처리

 

JWT(JavaScript Web Token)은 웹 애플리케이션과 서버 간에 정보를 안전하게 전달하는 데 사용되는 표준 방식이다. JWT는 특정한 형태의 문자열로 이루어져 있으며, 세 부분으로 구성된다: 헤더(Header), 페이로드(Payload), 서명(Signature).

◇ 헤더(Header): JWT의 유형 및 해싱 알고리즘 등의 메타데이터가 포함된다.
◇ 페이로드(Payload): 실제로 전달하고자 하는 클레임(claim) 정보가 포함된다. 클레임은 사용자에 대한 정보를 나타내며, 토큰의 유효성을 검증하는 데 사용된다.
◇ 서명(Signature): 토큰의 유효성을 검증하는 데 사용되는 서명이다. 서명은 헤더와 페이로드를 해싱한 후, 비밀 키를 사용하여 생성된다.

JWT의 작동 방식은 다음과 같습니다:
1. 클라이언트가 서버에 로그인을 요청합니다.
2. 서버는 사용자 정보를 확인한 후, 해당 정보를 기반으로 JWT를 생성합니다.
3. 서버는 생성된 JWT를 클라이언트에게 전달합니다.
4. 클라이언트는 이후 요청 시, HTTP 요청 헤더나 요청 파라미터 등에 JWT를 포함하여 서버에 전송합니다.
5. 서버는 수신된 JWT를 검증하고, 클라이언트의 요청에 응답합니다.

JWT를 사용하는 가장 큰 이점 중 하나는 토큰에 정보를 포함시킬 수 있다는 것입니다. 이로써 클라이언트는 토큰을 해독하여 사용자의 인증 및 권한 정보를 확인할 수 있습니다.

그러나 주의할 점은 JWT에는 서명만 있을 뿐 암호화가 되어 있지 않다는 것입니다. 따라서 중요한 정보는 담지 않는 것이 좋습니다. 또한, 서명을 통해 유효성을 검증하지만 토큰 자체의 안전한 보관이 중요합니다.

 


 

Service 작성

 

@Service
public class Test_ServiceImpl implements Test_Service {
    
    private static final Logger LOG = LoggerFactory.getLogger(Test_ServiceImpl.class);
    
    @Autowired
    Config_properties config_properties;

    // 공개키 디코딩 : Base64로 인코딩되어 있던 공개키가 바이트 배열로 변환 (검증)
    private static final byte[] test_検証 = java.util.Base64.getDecoder().decode("공개키");

    // 공개키 디코딩 : Base64로 인코딩되어 있던 공개키가 바이트 배열로 변환 (혼방)
    private static final byte[] test_本番 = java.util.Base64.getDecoder().decode("공개키");

    /**
     * A⇒B JWT 토큰체크
     *
     * @param _token 토큰
     * @return 체크 결과 (true : OK false : NG)
     */
    public boolean Check(String _token) {
        
        boolean checkResult = false;
        Map<String, Object> res = TokenCheck(_token);

        if (res.get("result").toString().equals("OK")) {
            checkResult = true;
            return checkResult;
        }

        return checkResult;
    }

    /**
     * 토큰검증
     *
     * @return OK、NG
     */
    private Map<String, Object> TokenCheck(String _token) {

        Map<String, Object> result = new HashMap<>();
        
        try {
            if (!config_properties.getTest_jwt().equals("false")) {
                result = jwt_ok(_token);
                LOG.info("JWT OK");
            } else {
                result = jwt_ng(_token);
                LOG.info("JWT OK");
            }
        } catch (Exception e) {
            LOG.error("JWT NG");
            LOG.error(e.getMessage(), e);
            result.put("result", "NG");
            return result;
        }

        return result;
    }

    /**
     * 헤더의 서명검증을 실시해 jwt 해석
     *
     * @return OK、NG
     */
    private Map<String, Object> jwt_ok(String _token) throws Exception {
        
        Map<String, Object> result = new HashMap<>();
        RSAPublicKey publicKey;
        
        if (config_properties.getTest_connection().equals("stg")) {
            publicKey = (RSAPublicKey) KeyFactory.getInstance("RSA").generatePublic(new X509EncodedKeySpec(test_検証));
        } else {
            publicKey = (RSAPublicKey) KeyFactory.getInstance("RSA").generatePublic(new X509EncodedKeySpec(test_本番));
        }

        Algorithm algorithm = Algorithm.RSA512(publicKey, null);
        JWTVerifier verifier = JWT.require(algorithm).build();
        DecodedJWT jwt = verifier.verify(_token);
        
        // 解析結果
        result.put("result", "OK");
        result.put("test1", CommonUtil.nulvalue(jwt.getClaim("test1").asString()));
        result.put("test2", CommonUtil.nulvalue(jwt.getClaim("test2").asString()));
       
        return result;
    }

    /**
     * 헤더의 서명검증을 무시해 jwt 해석
     *
     * @return OK、NG
     */
    private Map<String, Object> jwt_ng(String _token) throws Exception {
        
        Map<String, Object> result = new HashMap<>();
        // 토큰을 3개로 분할
        String[] parts = _token.split("\\.");
        
        ObjectMapper mapper = new ObjectMapper();
        
        // payload를 Base64으로 디코딩
        byte[] payloadBytes;
        try {
            payloadBytes = Base64.getDecoder().decode(parts[1]);
        } catch (Exception e) {
            payloadBytes = Base64.getUrlDecoder().decode(parts[1]);
        }
        
        // payload를 JSON로 변환
        Map<String, Object> payloadMap = mapper.readValue(payloadBytes, new TypeReference<Map<String, Object>>() {});
        result.put("result", "OK");
        result.put("test1", CommonUtil.nulvalue(payloadMap.get("test1")));
        result.put("test2", CommonUtil.nulvalue(payloadMap.get("test2")));
        
        return result;
    }
}

 
◇ 디코딩 : 정보나 데이터를 특정한 형식에서 원래의 형태로 변환하는 과정을 말한다. 이 과정은 데이터를 이해하기 쉽고 사용하기 쉽도록 변환하는 데 사용된다.
◇ java.util.Base64.getDecoder().decode("공개키");

byte[] decodedBytes = java.util.Base64.getDecoder().decode(encodedString);
"공개키"라는 문자열을 Base64로 디코딩하여 해당하는 바이트 배열을 반환하는 것입니다. 이는 주로 공개 키를 암호화나 디지털 서명 등의 작업에 사용할 때 사용됩니다.

◇ (RSAPublicKey) KeyFactory.getInstance("RSA").generatePublic(new X509EncodedKeySpec(test_検証));

・ KeyFactory 인스턴스 생성: KeyFactory.getInstance("RSA")는 RSA 알고리즘을 사용하여 키를 생성하기 위한 KeyFactory 인스턴스를 가져온다. 이 KeyFactory 인스턴스는 공개 키를 생성하는 데 사용된다.
・ X509EncodedKeySpec 생성: new X509EncodedKeySpec(test_検証)는 X.509 표준에 따라 인코딩 된 공개 키를 해석하기 위한 X509EncodedKeySpec 객체를 생성한다. 이 객체는 공개 키를 바이트 배열 형태로 가지고 있어야 합니다. test_検証는 해당 공개 키의 바이트 배열을 나타낸다.
・ generatePublic 호출: generatePublic() 메서드는 위에서 생성한 X509EncodedKeySpec 객체를 사용하여 공개 키를 생성한다. 이 메서드는 해당 공개 키를 RSAPublicKey 인스턴스로 반환한다.
・ 형변환: 마지막으로 (RSAPublicKey)는 해당 반환 값을 RSAPublicKey로 형변환한다. 이렇게 하면 이후에 반환된 키를 RSAPublicKey로 사용할 수 있다.

결과적으로 이 코드는 주어진 바이트 배열로부터 RSA 공개 키를 생성하고, 그것을 RSAPublicKey 형태로 반환한다. 이러한 공개 키는 주로 암호화나 디지털 서명과 같은 공개 키 암호화 기술에서 사용된다.

◇ Algorithm algorithm = Algorithm.RSA512(publicKey, null);
    JWTVerifier verifier = JWT.require(algorithm).build();
    DecodedJWT jwt = verifier.verify(_token);

Algorithm algorithm = Algorithm.RSA512(publicKey, null); :
이 부분에서는 RSA512 알고리즘을 사용하여 JWT의 서명을 확인하기 위한 알고리즘을 생성합니다. 이때 publicKey는 이전 단계에서 생성된 RSA 공개 키입니다. 알고리즘 생성 시에는 이 공개 키가 사용됩니다.
JWTVerifier verifier = JWT.require(algorithm).build(); :
JWTVerifier를 생성합니다. 이때 생성된 알고리즘을 사용하여 JWT 토큰을 검증할 수 있는 JWTVerifier를 만듭니다. DecodedJWT jwt = verifier.verify(_token); :
이 부분에서는 실제로 JWTVerifier를 사용하여 JWT 토큰의 유효성을 검증합니다. _token은 검증할 JWT 토큰을 나타냅니다. 이때 JWTVerifier는 앞서 설정한 알고리즘을 사용하여 토큰의 서명을 확인하고, 토큰의 내용을 검증합니다. 

◇ mapper.readValue(payloadBytes, new TypeReference<Map<String, Object>>() {});

해당 코드는 Jackson 라이브러리를 사용하여 JSON 형식의 데이터를 Java Map으로 변환하는 과정을 나타냅니다. 여기서 mapper는 ObjectMapper 객체를 가리키며, JSON 데이터를 Java 객체로 변환하거나 Java 객체를 JSON으로 직렬화하는 데 사용됩니다.
readValue() 메서드는 JSON 데이터를 읽어와서 지정된 Java 객체로 변환하는 역할을 합니다. 첫 번째 인자로는 변환할 JSON 데이터를 가진 바이트 배열(payloadBytes)이 주어지고, 두 번째 인자로는 변환될 Java 객체의 타입을 지정합니다.

이 코드에서는 JSON 데이터가 Map 형식으로 구성되어 있으므로, 변환될 Java 객체의 타입을 Map<String, Object>로 지정합니다. 이를 위해 TypeReference를 사용하여 제네릭 타입을 명시합니다.