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

8.2 KiB

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 - 토큰 검증
코드 로딩 KeyStoreKeyFactoryKeyPair X509EncodedKeySpecRSAPublicKey
보안 등급 극비 (노출 시 토큰 위조 가능) 공개 배포 가능

코드에서의 사용 위치

// 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 라이브러리 내부 흐름

// 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 파일 생성

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 조합

# 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

# 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단계: 생성 결과 확인

# 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