Files
eapim-online/docs/JWT_SIGNATURE_VERIFICATION.md
T
2026-04-03 17:22:56 +09:00

253 lines
8.2 KiB
Markdown

# JWT 서명 검증 및 인증서 관리
## 1. 전체 구조
```
┌─────────────────────────────────────────────────────────────────┐
│ Authorization Server (elink-oauth) │
│ elink-oauth-dev.jks │
│ ├── 개인키 (RSA Private Key) ← JWT 서명 생성용 │
│ └── 공개키 인증서 (Certificate) │
└─────────────────────────────────────────────────────────────────┘
│ 공개키만 추출
┌─────────────────────────────────────────────────────────────────┐
│ API Gateway (ApiAuthFilter) │
│ elink-oauth-dev.pub │
│ └── 공개키만 (RSA Public Key) ← JWT 서명 검증용만 │
└─────────────────────────────────────────────────────────────────┘
```
---
## 2. JKS vs PUB 파일 차이점
| 항목 | `.jks` (Java KeyStore) | `.pub` (Public Key) |
|------|----------------------|---------------------|
| 형식 | 바이너리 (Java 전용) | PEM 텍스트 (`-----BEGIN PUBLIC KEY-----`) |
| 포함 내용 | 개인키 + 공개키 인증서 | 공개키만 |
| 보호 | KeyStore 비밀번호 + Key 비밀번호 | 없음 (공개 가능) |
| 사용처 | AuthorizationServerConfig - **토큰 서명** | ApiAuthFilter - **토큰 검증** |
| 코드 로딩 | `KeyStoreKeyFactory``KeyPair` | `X509EncodedKeySpec``RSAPublicKey` |
| 보안 등급 | **극비** (노출 시 토큰 위조 가능) | 공개 배포 가능 |
### 코드에서의 사용 위치
```java
// AuthorizationServerConfig.java - JKS로 JWT 서명
KeyStoreKeyFactory keyStoreKeyFactory = new KeyStoreKeyFactory(keystore, keystorePassword.toCharArray());
KeyPair keyPair = keyStoreKeyFactory.getKeyPair(keyAlias, keyPassword.toCharArray());
converter.setKeyPair(keyPair); // 개인키로 JWT 서명
// ApiAuthFilter.java - PUB로 JWT 검증
X509EncodedKeySpec keySpec = new X509EncodedKeySpec(Base64.getDecoder().decode(publicKey));
KeyFactory keyFactory = KeyFactory.getInstance("RSA");
jwsVerifier = new RSASSAVerifier((RSAPublicKey) keyFactory.generatePublic(keySpec)); // 공개키로 검증만
```
---
## 3. JWT 서명 검증 상세 동작 (`signedJWT.verify(jwsVerifier)`)
### JWT 구조
```
eyJhbGciOiJSUzI1NiJ9 . eyJzdWIiOiJ1c2VyMSIsImV4cCI6...} . MEUCIQDx...
Header Payload Signature
(Base64url) (Base64url) (Base64url)
```
### 서명 생성 과정 (Authorization 서버 측)
```
[Header].[Payload]
SHA-256 해싱
digest (32 bytes)
RSA 개인키로 암호화 (PKCS#1 v1.5 패딩)
Signature (Base64url 인코딩)
```
### 서명 검증 과정 (ApiAuthFilter 측)
```
JWT 수신
① Header + Payload 분리
"eyJhbGciOiJSUzI1NiJ9.eyJzdWIiOiJ1c2VyMSIs..."
② Signature 파트를 Base64url 디코딩
→ 바이너리 서명값 확보
③ RSA 공개키로 서명값 복호화 ← RSASSAVerifier (Nimbus JOSE)
→ digest_from_signature 추출
④ ①의 "Header.Payload" 문자열을 SHA-256 해싱
→ digest_from_message 계산
⑤ digest_from_signature == digest_from_message ?
→ true : 서명 유효 (개인키 소유자가 서명한 것 확인)
→ false : 서명 불일치 → Token invalid 예외
```
### Nimbus JOSE 라이브러리 내부 흐름
```java
// RSASSAVerifier.verify() 내부
public boolean verify(JWSHeader header, byte[] signingInput, Base64URL signature) {
String jcaAlg = "SHA256withRSA"; // RS256 알고리즘
Signature verifier = Signature.getInstance(jcaAlg);
verifier.initVerify(publicKey); // RSA 공개키 초기화
verifier.update(signingInput); // [Header].[Payload] 바이트 입력
return verifier.verify(signature.decode()); // Signature 바이트와 대조 검증
}
```
### 보안 원리 (RSA 비대칭 키)
| 시도 | 결과 |
|------|------|
| Payload 변조 후 전송 | digest 불일치 → 검증 실패 |
| 개인키 없이 서명 위조 | RSA 수학적으로 불가능 |
| 공개키로 새 서명 생성 | 공개키로는 암호화 불가 |
| 만료된 토큰 재사용 | exp 체크에서 차단 |
### ApiAuthFilter 검증 흐름
```
클라이언트 요청
Bearer 토큰 추출 (Authorization 헤더 or access_token 파라미터)
SignedJWT.parse(token) ← JWT 파싱
만료시간(exp) 확인
signedJWT.verify(jwsVerifier) ← RSA 공개키로 서명 검증
Scope 검증 (oob/public이면 패스, 아니면 API별 허용 scope 확인)
client_id 확인 (헤더 vs 토큰 payload 비교)
등록된 APP 및 API 접근 권한 확인
```
---
## 4. Self-Signed 인증서 생성 방법
### 사전 요구사항
- **JDK** (keytool 포함)
- **OpenSSL**
### 1단계: JKS 파일 생성
```bash
keytool -genkeypair \
-alias elink-oauth \
-keyalg RSA \
-keysize 2048 \
-sigalg SHA256withRSA \
-validity 3650 \
-keystore elink-oauth-dev.jks \
-storepass elink1234 \
-keypass elink1234 \
-dname "CN=elink-oauth, OU=eActive, O=eActive, L=Seoul, ST=Seoul, C=KR"
```
> 옵션 설명
> - `-alias` : 키 별칭 (코드의 `certification.keyAlias` 설정값과 일치해야 함)
> - `-keysize 2048` : RSA 키 길이 (최소 2048 권장)
> - `-validity 3650` : 유효기간 (일 단위, 3650 = 10년)
> - `-storepass` : KeyStore 전체 비밀번호 (`certification.keystorePassword`)
> - `-keypass` : 개별 키 비밀번호 (`certification.keyPassword`)
### 2단계: JKS에서 공개키(.pub) 추출
**방법 A: keytool + openssl 조합**
```bash
# JKS → 인증서(CER) 추출
keytool -exportcert \
-alias elink-oauth \
-keystore elink-oauth-dev.jks \
-storepass elink1234 \
-file elink-oauth-dev.cer
# CER에서 공개키 추출 → PEM 형식(.pub)
openssl x509 \
-inform DER \
-in elink-oauth-dev.cer \
-pubkey -noout \
-out elink-oauth-dev.pub
```
**방법 B: JKS → PKCS12 변환 후 openssl**
```bash
# JKS → PKCS12 변환
keytool -importkeystore \
-srckeystore elink-oauth-dev.jks \
-destkeystore elink-oauth-dev.p12 \
-deststoretype PKCS12 \
-srcalias elink-oauth \
-srcstorepass elink1234 \
-deststorepass elink1234
# PKCS12에서 공개키 추출
openssl pkcs12 \
-in elink-oauth-dev.p12 \
-passin pass:elink1234 \
-nokeys \
-out temp-cert.pem
openssl x509 \
-in temp-cert.pem \
-pubkey -noout \
-out elink-oauth-dev.pub
```
### 3단계: 생성 결과 확인
```bash
# JKS 내용 확인
keytool -list -v \
-keystore elink-oauth-dev.jks \
-storepass elink1234
# 공개키 내용 확인
cat elink-oauth-dev.pub
# 결과:
# -----BEGIN PUBLIC KEY-----
# MIIBIjANBgkqhkiG9w0BAQEFAAOCAQ8AMIIBCgKCAQEA...
# -----END PUBLIC KEY-----
```
---
## 5. 파일 배치
```
Authorization Server (elink-oauth)
└── src/main/resources/certificate/
└── elink-oauth-dev.jks ← 개인키 포함, 절대 외부 노출 금지
API Gateway (elink-online-common)
└── src/main/resources/certificate/
└── elink-oauth-dev.pub ← 공개키만, 배포 가능
```
## 6. 설정 파일 (`OAuthServer` PropGroup)
| 프로퍼티 키 | 설명 | 사용 위치 |
|------------|------|----------|
| `certification.keystorePath` | JKS 파일 경로 | AuthorizationServerConfig |
| `certification.keystorePassword` | KeyStore 비밀번호 | AuthorizationServerConfig |
| `certification.keyAlias` | 키 별칭 | AuthorizationServerConfig |
| `certification.keyPassword` | 키 비밀번호 | AuthorizationServerConfig |
| `certification.publicKeyPath` | PUB 파일 경로 | ApiAuthFilter |