이번 포스팅에서는 KeyPair와 KeyFactory에 대해 설명하고, 실제 프로젝트에서 어떻게 활용했는지 공유해 보려고 합니다 ! 특히, JWT 인증을 구현하는 과정에서 이 두 가지 클래스가 어떤 역할을 했고, 이를 통해 인증 흐름을 어떻게 설계했는지 제가 구현한것을 기반으로 구체적으로 다룰 예정입니다 !
RSA는 대표적인 공개키 암호화 알고리즘으로, 비대칭 키를 사용하는 암호화 기법입니다. 여기서 비대칭 키란, 개인키(Private Key)와 공개키(Public Key)를 한 쌍으로 생성하여 사용하는 방식을 의미하는데 RSA의 가장 큰 특징은 개인키와 공개키를 서로 다르게 사용한다는 점입니다. 개인키로 암호화한 데이터를 공개키로 복호화할 수 있고, 반대로 공개키로 암호화한 데이터는 개인키로만 복호화할 수 있다. 이를 활용해 암호화, 디지털 서명, 인증 등의 다양한 기능을 구현할 수 있다 !
여기서 왜 개인키와 공개키가 필요한지 의문이 들수있다 ! 이 의문은 제가 이전에 포스팅한 PKI에 대해서 읽어보시면 조금이나마 도움이 될 것 같습니다 !!
https://velog.io/@1im_chaereong/posts?tag=PKI
본격적인 구현에 앞서, KeyPair와 KeyFactory에 대해 알아보겠습니다.
KeyPair는 공개키와 개인키를 쌍으로 생성하는 클래스다. 이를 통해 RSA 기반의 비대칭 키를 쉽게 생성할 수 있다. KeyPairGenerator 클래스를 사용하여 키 쌍을 생성하고, 여기서 나온 KeyPair 객체를 통해 공개키와 개인키를 얻을 수 있다. 즉, KeyPair는 RSA 알고리즘을 활용해 우리에게 필요한 암호화 키들을 제공하는 역할을 한다.
KeyPairGenerator keyPairGenerator = KeyPairGenerator.getInstance("RSA");
keyPairGenerator.initialize(2048); // 키 길이 2048비트 설정
KeyPair keyPair = keyPairGenerator.generateKeyPair();
PrivateKey privateKey = keyPair.getPrivate();
PublicKey publicKey = keyPair.getPublic();
이렇게 생성된 개인키와 공개키는 각각 데이터 서명 및 검증에 사용된다.
KeyFactory는 키의 형식을 변환해주는 클래스다. 일반적으로 키를 저장하거나 전달할 때는 문자열 형태로 인코딩하여 사용하게 된다. KeyFactory는 인코딩된 키를 다시 키 객체(Key Object)로 변환하는 데 사용된다. 즉, 외부에서 제공된 키를 실제 암호화 작업에 사용할 수 있도록 변환하는 것이 KeyFactory의 주된 역할이다.
아래와 같이 Base64로 인코딩된 키 문자열을 KeyFactory를 통해 PublicKey나 PrivateKey 객체로 변환할 수 있다.
byte[] keyBytes = Base64.getDecoder().decode(publicKeyString);
X509EncodedKeySpec keySpec = new X509EncodedKeySpec(keyBytes);
KeyFactory keyFactory = KeyFactory.getInstance("RSA");
PublicKey publicKey = keyFactory.generatePublic(keySpec);
이 과정에서 X509EncodedKeySpec은 공개키의 인코딩 형식을 나타내며, 이를 통해 문자열 형태의 공개키를 실제 PublicKey 객체로 변환하게 된다.
이번에는 JWT 인증을 사용해 SSO(Single Sign-On) 구조를 구현하였다. IDP(Identity Provider) 서버와 SP(Service Provider) 서버를 구성하고, IDP 서버에서 JWT를 발급하고 SP 서버에서 이를 검증하는 방식으로 인증을 구현했다. 이 과정에서 KeyPair와 KeyFactory를 활용하여 공개키 기반 서명 검증을 구현하였다.
먼저 IDP 서버에서 KeyPair를 사용해 공개키와 개인키를 생성하였다. 생성된 키들은 각각 다음과 같은 역할을 한다:
IDP 서버의 KeyPairProvider 클래스는 다음과 같이 공개키와 개인키를 생성하고 관리한다.
@Component
public class KeyPairProvider {
private final PrivateKey privateKey;
private final PublicKey publicKey;
public KeyPairProvider() {
KeyPair keyPair = generateKeyPair();
this.privateKey = keyPair.getPrivate();
this.publicKey = keyPair.getPublic();
}
private KeyPair generateKeyPair() {
try {
KeyPairGenerator keyPairGenerator = KeyPairGenerator.getInstance("RSA");
keyPairGenerator.initialize(2048);
return keyPairGenerator.generateKeyPair();
} catch (Exception e) {
throw new RuntimeException("Error generating KeyPair", e);
}
}
public String getEncodedPublicKey() {
return Base64.getEncoder().encodeToString(publicKey.getEncoded());
}
}
이렇게 생성된 공개키는 엔드포인트(/api/publicKey)를 통해 SP 서버에 제공된다.
SP 서버는 IDP 서버에서 제공한 JWT의 서명을 검증하여 유효성을 판단한다. 이때, KeyFactory를 사용해 공개키 문자열을 PublicKey 객체로 변환한 후 서명 검증에 활용한다.
아래는 SP 서버의 필터에서 JWT의 서명을 검증하는 과정이다.
private boolean verifySignature(String data, String signature) {
try {
byte[] keyBytes = Base64.getDecoder().decode(publicKeyValue);
X509EncodedKeySpec keySpec = new X509EncodedKeySpec(keyBytes);
KeyFactory keyFactory = KeyFactory.getInstance("RSA");
PublicKey publicKey = keyFactory.generatePublic(keySpec);
Signature sig = Signature.getInstance("SHA256withRSA");
sig.initVerify(publicKey);
sig.update(data.getBytes(StandardCharsets.UTF_8));
return sig.verify(Base64.getUrlDecoder().decode(signature));
} catch (Exception e) {
e.printStackTrace();
return false;
}
}
이번 프로젝트에서는 RSA 기반의 공개키 암호화와 디지털 서명을 활용해 JWT 인증을 구현하였다. KeyPair를 통해 공개키와 개인키를 생성하고, KeyFactory를 사용해 외부에서 제공된 공개키를 검증에 사용할 수 있는 PublicKey 객체로 변환하였다. 이 과정을 통해 IDP 서버가 발급한 JWT의 무결성을 SP 서버에서 확인할 수 있었다.
이를 통해 안전한 인증 시스템을 구현할 수 있었고, 각 키의 역할과 그 활용법을 명확히 이해하게 되었다. 공개키와 개인키의 올바른 사용은 보안에서 매우 중요하며, 이를 적절히 활용함으로써 신뢰할 수 있는 인증 흐름을 구축할 수 있었다.