🔍 JWT란? (JSON Web Token)
- JWT(JSON Web Token)는 사용자 인증 및 정보 전달을 위한 토큰 기반 인증 방식!
- 서버와 클라이언트 간 상태를 저장하지 않고(stateless) 인증할 수 있어서,
REST API 등에서 많이 사용된다.
✅ 1. JWT 구조
JWT는 .(점)으로 구분된 3개의 부분으로 구성된다.
📖 예시
eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJzdWIiOiJ1c2VyMTIiLCJyb2xlIjoiQURNSU4iLCJpYXQiOjE2OTQ2MjQ4MDB9.f9XY4qkAeUpmEYAf_CtwJfpFq93j98gVwVFXD2DdeuA
🔹 1. Header (헤더)
- 토큰의 타입(JWT)과 서명에 사용할 알고리즘(예: HS256, RS256)을 포함
{
"alg": "HS256",
"typ": "JWT"
}
🔹 2. Payload (페이로드)
- 사용자 정보(클레임, Claim)**가 담긴 부분
- 토큰의 크기가 커지면 네트워크 트래픽을 많이 잡아먹기 때문에 필요한 정보만 담도록 해야한다!
{
"sub": "user12", // 사용자 ID
"role": "ADMIN", // 역할(Role)
"iat": 1694624800 // 발급 시간 (Unix Timestamp)
}
🔹 3. Signature (서명)
- JWT가 변조되지 않았음을 보장하는 역할
- HMAC, RSA, ECDSA 등의 알고리즘을 사용해 생성
HMACSHA256(
base64UrlEncode(header) + "." + base64UrlEncode(payload),
secretKey
)
✅ 2. JWT 특징
✔ 무상태(stateless)
- 서버에서 세션을 유지할 필요 없음 → 서버 부하 감소
✔ 자체 인증(Self-contained) - 사용자 정보가 토큰에 포함됨 → DB 조회 없이 인증 가능
✔ 안전한 서명(Signature) 검증 - 비밀 키(HS256) 또는 공개 키(RS256)로 서명 → 변조 방지
⭐ MSA에서 사용 이유
- 각 서비스 자체 인증: JWT는 토큰 자체에 인증 정보를 담고 있기 때문에, 인증 서버가 없이도 각 서비스가 JWT를 통해 인증을 쉽게 검증할 수 있다.
- 확장성 및 유연성: 서버 확장(스케일링)이나 다른 서비스 연동시에도 인증 및 권한 정보를 공유할 수 있어서 확장성과 유연성이 뛰어남.
✅ 3. JWT 인증 흐름
1️⃣ 사용자가 로그인 요청 (ID/PW)
2️⃣ 서버가 JWT를 생성 후 반환, JWT는 클라이언트 측에 저장.
3️⃣ 클라이언트가 JWT를 포함해 요청 (Authorization: Bearer <token>)
4️⃣ 서버에서 JWT 검증 후 응답 반환
⭐ 세션 기반 인증과 달리 서버가 별도로 상태(Session)를 저장하지 않아도 됨!
✅ 4. JWT 생성
키 생성 방식에 따라 제공, 검증 방식이 다르다!!
- 강력한 해시 알고리즘(HMAC-SHA256, RS256 등) 사용
- 취약한 알고리즘(예: none 알고리즘) 사용 ❌
👉 키 저장 위치
- 파일 시스템 (private_key.pem, public_key.pem)
- 데이터베이스 (MySQL, PostgreSQL, Redis 등)
- 환경 변수 또는 보안 서비스 (AWS Secrets Manager, HashiCorp Vault)
- HSM (Hardware Security Module) 같은 하드웨어 기반 키 관리 시스템
✅ 4-0. 키 생성 라이브러리 추가
일반적으로 jsonwebtoken 라이브러리를 이용한 JwtTokenProvider을 작성하여 사용.
Private key가 필요하다.
📖 gradle.build
implementation("io.jsonwebtoken:jjwt-api:0.11.5")
runtimeOnly("io.jsonwebtoken:jjwt-impl:0.11.5")
runtimeOnly("io.jsonwebtoken:jjwt-jackson:0.11.5")
✅ 4-1. RSA 방식 (비대칭키 기반)
✔ 인증 서버(ex.auth-service)는 개인키(Private Key)로 JWT 서명
✔ 검증 서버(ex.gateway)는 공개키(Public Key)로 JWT 검증
📖 JWT 발급 (RSA 키 사용)
@Component
public class JwtTokenProvider {
private final KeyPair keyPair = {keyPair}; // RSA 키 쌍 (공개 키 + 개인 키)
public String createToken(String username, List<String> roles) {
return Jwts.builder()
.setSubject(username)
.claim("roles", roles)
.setIssuedAt(new Date())
.setExpiration(new Date(System.currentTimeMillis() + 3600000)) // 1시간 유효
.signWith(keyPair.getPrivate(), SignatureAlgorithm.RS256) // 개인 키로 서명
.compact();
}
public PublicKey getPublicKey() {
return keyPair.getPublic(); // 공개 키 검증 서버에게 제공
}
}
📖 JWK Set 공개 (Auth 서버에서 제공)
@RestController
@RequestMapping("/.well-known")
public class JwkSetController {
private final JwtTokenProvider jwtTokenProvider;
public JwkSetController(JwtTokenProvider jwtTokenProvider) {
this.jwtTokenProvider = jwtTokenProvider;
}
@GetMapping("/jwks.json")
public Map<String, Object> getJwks() throws Exception {
PublicKey publicKey = jwtTokenProvider.getPublicKey();
RSAKey rsaKey = new RSAKey.Builder((RSAPublicKey) publicKey)
.keyID("my-key-id") // 키 ID (임의 값)
.algorithm(JWSAlgorithm.RS256)
.build();
return new JWKSet(rsaKey).toJSONObject();
}
}
✅ 4-2. HS256 방식 (대칭키 기반)
✔ 인증 서버와 검증 서버에 동일한 Secret key 사용.
📖 JWT 발급 (HS256 키 사용)
@Component
public class JwtTokenProvider {
private final String SECRET_KEY = {secret-key}; // 안전한 저장 필요 (환경 변수 사용 권장)
private final long EXPIRATION_TIME = 1000 * 60 * 60; // 1시간
// JWT 생성 (HS256 사용)
public String createToken(String username, List<String> roles) {
Claims claims = Jwts.claims().setSubject(username);
claims.put("roles", roles);
return Jwts.builder()
.setClaims(claims)
.setIssuedAt(new Date())
.setExpiration(new Date(System.currentTimeMillis() + EXPIRATION_TIME))
.signWith(SignatureAlgorithm.HS256, SECRET_KEY.getBytes())
.compact();
}
@SuppressWarnings("deprecation")
public boolean validateToken(String token) {
try {
Jwts.parserBuilder()
.setSigningKey(SECRET_KEY.getBytes())
.build()
.parseClaimsJws(token); // JWT의 형식(Header.Payload.Signature 형태인지) & 서명 & 만료 검증
return true;
// } catch (ExpiredJwtException e) {
// System.out.println("❌ 만료된 토큰입니다!");
// } catch (MalformedJwtException e) {
// System.out.println("❌ 잘못된 형식의 토큰입니다!");
// } catch (SignatureException e) {
// System.out.println("❌ 서명이 올바르지 않은 토큰입니다!");
} catch (Exception e) {
return false;
}
}
}
✅ 5. JWT 검증
Filter을 별도로 작성하여 @Configuration에서 적용하고,
위 JwtTokenProvider.validateToken함수를 호출하여 jwt 토큰을 검증하여 요청을 통과시키거나 차단한다.
🔍 filter란?
JWT 인증에서 필터(Filter)는 HTTP 요청과 응답을 가로채서 특정 처리를 수행하는 컴포넌트.
키 생성 방식에 따라 제공, 검증 방식이 다르다!! 검증 방식은 아래 코드 참고
🔗 Spring Security ( 인증 구현 방식 참고!)
⭐ 토큰 검증 위치
보통 Spring Security에서 검증 하는데 Gateway도 사용하고 있다면 둘 중 하나에서만 Filter로 JWT 검증을 하면 된다.
- Gateway: JwtGatewayFilterFactory 작성.Gateway에서 요청을 처리할 때(요청이 백엔드 서비스로 넘어가기 전) JWT 토큰을 검증, 요청이 백엔드 서비스로 넘어갈 수 있도록 합니다.
- Spring Security: JwtAuthenticationFilter 작성. 요청이 백엔드 서비스에 도달한 후, 해당 요청이 인증된 사용자에 의해 이루어졌는지 확인하는 역할을 합니다.
✅ 6. Key와 JWT 관리
토큰 생성에 사용하는 “Key”와 결과물인 JWT(AccessToken, RefreshToken)의 안전한 관리가 필요하다!!
🌟 아래의 관리 방법을 모두 적용해야 하는 것은 아니다!! 본인의 상황에 맞게 적용할 것.
필수라고 생각 되는 것에는 번호뒤에 ⭐ 붙임!!
✅ 6-1. Key 관리
1️⃣ ⭐Secret Key를 안전하게 보관하기
- 환경 변수(.env) 또는 Key Management System(KMS) 사용
- 소스 코드에 직접 노출 ❌
2️⃣ Secret Key 주기적으로 변경 (Key Rotation)
- 정기적으로 새로운 키를 생성하고, 기존 키를 폐기하는 Key Rotation 적용
- 예전 키로 서명된 JWT는 일정 기간 동안만 허용(그레이스 기간)
3️⃣ 다중 Secret Key 관리 (키 버전 적용)
- 예: "kid"(Key ID) 필드를 JWT에 추가하여,
여러 개의 키를 운영하면서 점진적으로 교체 가능
4️⃣ MSA 환경에서는 서비스별 Secret Key 분리
- 하나의 키를 모든 서비스에서 사용 ❌
- 각 서비스별로 다른 Secret Key 사용 → 특정 서비스만 위험해짐
5️⃣ JWT 대체 방법 고려 (Opaque Token)
- 서버에서 서명 검증 없이 DB에서 직접 토큰을 확인하는 방식 고려
- JWT는 클라이언트에서 검증 가능하지만, Secret Key 손상의 위험이 크기 때문
6️⃣ Key 유출 감지를 위한 모니터링(로그 분석)
- 비정상적으로 많은 JWT 발급 요청, 유효하지 않은 JWT 시도 감지 등
✅ 6-2. JWT 관리
1️⃣⭐Access Token을 안전하게 보관하기
- Access Token: localStorage
- Refresh Token: HttpOnly, secure 쿠키
- 🔗 저장소 비교
2️⃣ ⭐Refresh Token의 이용
- Access Token을 짧게 설정하고 Refresh Token을 활용
- Refresh Token을 DB에서 관리하고, 탈취 시 전체 무효화 가능하도록 설계
- Refresh Token을 사용할 때 DB/redis에 저장된 것과 같은지 한번 더 검증
3️⃣ 토큰 블랙리스트 이용
- Redis 같은 DB에 저장, 이를 만료될 때까지 가지고 있음으로써 해당 토큰을 재사용할 수 없도록 한다.
- 로그아웃 엔드포인트 또는 탈취 의심 시 적용.
❌ 단점: 매번 요청마다 DB를 조회해야 함 (성능 부담 😢)
✔ 대안: 2️⃣ Refresh Token의 이용
4️⃣ Token에 사용자의 IP 담아서 검증
- 기존에 발급된 IP주소와 다른 IP주소가 탐지되었다면 해당 토큰을 블랙리스트에 등록하고 사용자에게 다시 로그인을 유도 한다.
- 탈취된 토큰에 대한 대응.
❌단점: IP는 변할 수 있음 (프록시, VPN 사용, 유동 IP, 모바일 네트워크 문제 등) -> IP 변하면 정상 사용자도 로그아웃 됨.
✔ 대안:
- IP와 User-Agent 정보를 서버에서 세션 형태로 따로 관리
- 요청 시 IP가 다르면 추가 인증 (ex. OTP) 요구
- Refresh Token 재발급 시 IP 검증
5️⃣ 사용자 정보 업데이트 (권한 변경 등)
- JWT는 발급 후 수정 할 수 없다.
✔ 대안:
- Access Token 대신 Opaque Token(서버 저장형 토큰) 활용
- Refresh Token 재발급 시점에 정보 반영
- Token 버전 필드 추가 후, Refresh 시 검증
'study > 보안' 카테고리의 다른 글
| SSO란? (0) | 2025.11.29 |
|---|---|
| SSL이란? (Secure Sockets Layer) (0) | 2025.03.24 |
| 저장소 비교 (0) | 2024.12.01 |
| 방화벽/IDS/IPS (0) | 2024.03.12 |
| CNAPP (CWPP/SCPM/CIEM) (0) | 2024.03.12 |