JWT 발급절차
JWT생성
。로그인한계정을 기반으로 사용자 자격증명 (Credential) , 사용자 데이터 (payload) ,RSA 키쌍을 인코딩하여JWT생성.
JWT를HTTP Request의Authentication Header의 일부로서 전송.
。Authorization:Bearer JWT코드
서버에서 전송된JWT를 Decoding
。JWT는 암호화가 적용되어 있으므로,Encoding된JWT를Decoding시RSA 키쌍을 필요로 한다.
nimbus 의존성추가
spring-boot-starter-oauth2-resource-server
。Spring Boot Application을Spring Security와 통합하여OAuth 2.0 Resource Server로 설정하는 Library.
。해당라이브러리에 대한의존성추가 시Nimbus JOSE+JWT라이브러리도 추가
implementation 'org.springframework.boot:spring-boot-starter-oauth2-resource-server'
@Configuration Class에JWT Encoder및 필요한Spring Bean정의하기
。KeyPair,RSAKey,JWKSource,JwtDecoder,JwtEncoder를 각각Spring Bean으로 등록@Configuration @RequiredArgsConstructor public class KeyConfiguration { private final PojoJwtProperties pojoJwtProperties; @Bean public KeyPair generateKeyPair() throws NoSuchAlgorithmException { // RSA 알고리즘을 사용하는 KeyPairGenerator 설정 및 Key객체 생성하여 SpringBean으로 등록 // Key 크기 : 2048bit 설정 KeyPairGenerator keyPairGenerator = KeyPairGenerator.getInstance("RSA"); keyPairGenerator.initialize(2048); return keyPairGenerator.generateKeyPair(); } @Bean // Spring Bean에 등록된 KeyPair을 매개변수로 의존성주입 public RSAKey generateRSAKey(KeyPair keyPair){ String keyId = pojoJwtProperties.getJwtProperties().keyId(); return new RSAKey // Public Key객체를 RSAPublicKey로 casting하여 설정. .Builder((RSAPublicKey)keyPair.getPublic()) // Private Key 객체를 RSAPrivateKey로 casting하여 설정. .privateKey((RSAPrivateKey)keyPair.getPrivate()) // application.yml에 설정한 Key ID를 가져오기 .keyID(keyId) .build(); } @Bean // Spring Bean에 등록된 RSAKey를 매개변수로 의존성주입 public JWKSource generateJWKSource(RSAKey rsaKey){ JWKSet jwkSet = new JWKSet(rsaKey); return (((jwkSelector, securityContext) -> jwkSelector.select(jwkSet))); } @Bean // Spring Bean에 등록된 RSAKey를 매개변수로 의존성주입 public JwtDecoder jwtDecoder(RSAKey rsaKey) throws JOSEException { return NimbusJwtDecoder.withPublicKey(rsaKey.toRSAPublicKey()).build(); } @Bean // Spring Bean에 등록된 JWKSource<SecurityContext>를 매개변수로 의존성주입 public JwtEncoder jwtEncoder(JWKSource<SecurityContext> jwkSource) throws JOSEException { return new NimbusJwtEncoder(jwkSource); } }
RSA 암호화 알고리즘을 설정한KeyPairGenerator를 통해Key Pair 객체를 생성하여Spring Bean으로 등록
。@Configuration Class에@Bean Method정의
。RSA암호화 알고리즘을 통해Public Key,Private Key로 구성된Key Pair객체를 생성하여Spring Bean으로 등록
▶KeyPair을 필요로하는Method의 매개변수로 자동으로의존성 주입@Bean public KeyPair generateKeyPair() throws NoSuchAlgorithmException { // RSA 알고리즘을 사용하는 KeyPairGenerator객체 생성 및 Key 크기 : 2048bit 설정 KeyPairGenerator keyPairGenerator = KeyPairGenerator.getInstance("RSA"); keyPairGenerator.initialize(2048); return keyPairGenerator.generateKeyPair(); }▶
RSA 알고리즘으로 생성된2048 bit크기의Public Key,Private Key를 포함한KeyPair 객체가 생성되어Spring Bean으로서 등록
KeyPair:import java.security.KeyPair;
。KeyPairGenerator객체.generateKeyPair()를 통해 생성된Public Key,Private Key로 구성된Key Pair객체.
KeyPair객체.getPublic():
。Key Pair객체의Public Key를 호출하는 Method.
KeyPair객체.getPrivate():
。Key Pair객체의Private Key를 호출하는 Method.
KeyPairGenerator:import java.security.KeyPairGenerator;
。Java에서KeyPair객체= 비대칭키 (Public Key,Private Key)를 생성 시 활용하는 Class.
▶RSA,EC등의 암호화 알고리즘에 적합한Key Pair을 생성 가능.
KeyPairGenerator.getInstance("RSA"):
。특정암호화 알고리즘을 기반으로하는KeyPairGenerator 객체를 생성.
("RSA", "EC" , "DSA"등 )
ex)KeyPairGenerator keyPairGenerator = KeyPairGenerator.getInstance("RSA");
。해당 암호화 알고리즘이 없을 경우의Exception을 반드시 정의
▶try ~ catch문 또는throws NoSuchAlgorithmException정의
KeyPairGenerator객체.initialize(키크기):
。KeyPairGenerator객체의 생성할Key 크기를 설정.
▶2048설정 시2048 bit의Key가 생성됨.
KeyPairGenerator객체.generateKeyPair():
。비대칭키(Public Key+Private Key쌍 )를 포함한KeyPair객체로서 생성.
생성될
Public Key의KeyID를application.yml에 정의 및 가져오기
。application.yml에개인정보를정적으로 저장하면 안되므로placeholder를 통해사용자에 의해동적으로환경변수를 설정하여 입력받도록 설정
▶ 해당key id로KeyPairGenerator에 의해 생성된Public Key는 향후JWT 토큰 검증시 식별되어 사용됨
。 추가적으로AccessToken과RefreshToken의만료시간을yml파일에 지정하여개발자가소스코드를 수정하지않아도 단순하게yml파일을 수정하여만료시간을 수정하도록 설정jwt: keyId: ${ketId:TopOfTheHeadKey} accessTokenExpiration: ${accssTokenMinute:5} refreshTokenExpiration: ${refreshTokenMinute:720}。
@ConfigurationProperties를 통해 해당 값을 가져오기
@ConfigurationProperties@ConfigurationProperties(prefix = "jwt") public record JwtProperties( String keyId, Long accessTokenExpiration, Long refreshTokenExpiration ) { }@Getter @Component @RequiredArgsConstructor @EnableConfigurationProperties(JwtProperties.class) public class PojoJwtProperties { private final JwtProperties jwtProperties; }
KeyPair 객체를의존성 주입받아서RSAKey 객체생성 후Spring Bean으로 등록 :import com.nimbusds.jose.jwk.RSAKey;
。Nimbus JOSE+JWT라이브러리를 활용하여RSAKey 객체생성.
。@Configuration Class내@Bean Method를 정의하여Spring Bean으로 주입된KeyPair객체를 기반으로Private Key,Public Key,Key ID를 각각 설정하여RSAKey 객체를 생성 후Spring Bean으로 등록@Configuration @RequiredArgsConstructor public class KeyConfiguration { private final PojoJwtProperties pojoJwtProperties; @Bean // Spring Bean에 등록된 KeyPair을 매개변수로 의존성주입 public RSAKey generateRSAKey(KeyPair keyPair){ String keyId = pojoJwtProperties.getJwtProperties().keyId(); return new RSAKey // Public Key객체를 RSAPublicKey로 casting하여 설정. .Builder((RSAPublicKey)keyPair.getPublic()) // Private Key 객체를 RSAPrivateKey로 casting하여 설정. .privateKey((RSAPrivateKey)keyPair.getPrivate()) // application.yml에 설정한 Key ID를 가져오기 .keyID(keyId) .build(); } }
RSAKey:import com.nimbusds.jose.jwk.RSAKey;:
。Nimbus JOSE+JWT라이브러리에서RSA기반JWK( JSON Web Key )를 다루는클래스
。RSA암호화 알고리즘으로 생성한KeyPairinstance의Public Key와Private Key를JWK Format으로 변환하는 역할을 수행.
▶ 생성된JWK는JWT검증시 사용.
RSAKey객체.Builder( RSAPublicKey객체 )
。RSAPublicKey 객체를 기반으로RSAKey 객체생성.
▶(RSAPublicKey)keyPair.getPublic()
。RSAKey객체.privateKey(),RSAKey객체.keyID()등의 추가 정의 없이RSAKey객체.Builder( RSAPublicKey객체 )만 사용 시Public Key만 포함된JWK 객체생성.
RSAKey객체.privateKey( RSAPrivateKey객체 )
。RSAKey 객체에RSAPrivateKey 객체를Private Key으로서 포함하는메서드
▶JWT Signature의 검증수행 시Private Key가 필요하므로, 해당메서드를 통해JWK에Private Key를 추가.
▶(RSAPrivateKey)keyPair.getPrivate()
。RSAKey객체.Builder()에 추가 정의 후.build()를 추가 선언하여Private Key가 추가정의된RSAKey 객체생성.
RSAKey객체.toRSAPublicKey():
。RSAKey 객체를RSAPublicKeytype의 객체로 변환
RSAKey객체.keyID(ID문자열)
。JWK의 고유식별자 (kid)를 지정 시 사용.
▶Authetication Server에서JWKSet을 통해 여러개의RSAPublicKey를 관리하므로kid를 지정.
▶kid를 정의할경우, 차후JWT Signature의 검증 시 특정Key의 선택 시식별에 유용함.
RSAKey객체.build()
。추가 설정된RSAKey 객체를 생성.
RSAKey 객체를의존성 주입받아서JWKSource( JSON Web Key Source )을 생성 후Spring Bean으로 등록
。Spring Bean으로 주입된RSAKey 객체를 통해JWKSet 객체를 생성 후JWKSource 객체를 생성하는@Bean Method정의
。JWKSet 객체로JWKSource 객체를 생성
▶JWKSource Interface의 instance를 생성 및Abstract Method get()를 상속하여JWKSelector.select(JWKSet객체)를 return
▶Abstract Method get()은JWKSet으로부터JWKSelector를 통해 특정조건에 부합하는JWK를 선택하여List<JWK>로 반환.@Bean // Spring Bean에 등록된 RSAKey를 매개변수로 의존성주입 public JWKSource generateJWKSource(RSAKey rsaKey){ JWKSet jwkSet = new JWKSet(rsaKey); JWKSource jwkSource = new JWKSource(){ @Override public List<JWK> get(JWKSelector jwkSelector, SecurityContext securityContext){ // // JWKSet 객체로부터 JWKSelector를 통해 특정조건에 부합하는 JWK를 선택하여 List<JWK>로 반환. return jwkSelector.select(jwkSet); } }; return jwkSource; }。
람다식을 통해 간단하게 표현 가능.@Bean // Spring Bean에 등록된 RSAKey를 매개변수로 의존성주입 public JWKSource generateJWKSource(RSAKey rsaKey){ JWKSet jwkSet = new JWKSet(rsaKey); return (((jwkSelector, securityContext) -> jwkSelector.select(jwkSet))); }
JWK( JSON Web Key )
。Public Key정보를JSON Format으로 표현한 데이터구조.
▶JWT Signature의 검증을 수행하기 위해서Public Key가 필요하므로 주로RSA방식의JWT의Signature를 검증 시 활용.
JWKSet:
。여러 개의JWK를 하나로 묶어 관리하는 객체
JWKSet.toJSONObject():
。JWKSet객체를Object객체로JSON Conversion
JWKSet.getKeyByKeyId(kid):
。JWKSet이 포함한 특정kid를 가진Public Key를 포함하는JWK를 검색하여 return.
▶OAuth2.0 Authentication Server에 특정Public Key의JWK를 제공 시 사용.
JWKSource:
。JWT의발급및검증을 수행 시 활용하는인터페이스.
▶JWT를 처리하는Business logic를 추상화.
。JWT의 검증 수행 시JWKSource에 의해JWT 검증에 필요한Public Key를 제공.public interface JWKSource<C extends SecurityContext> { List<JWK> get(JWKSelector var1, C var2) throws KeySourceException; }
get(JWKSelector var1, C var2)
。JWT의 검증을 위해 적절한Public Key를 포함하는JWK를 찾는 역할을 수행하는JWTSource Interface의Abstract Method.
▶ 선택한JWK를List<JWK>type으로 반환
。JWKSelector 객체를 이용하여 특정 조건을 만족하는JWK를 선택하도록Filtering을 수행.
JWKSelector
。복수 이상의JWK중 특정 조건을 만족하는JWK를 선택하는 역할을 수행.
▶ 복수 이상의JWK가 존재하는 경우JWK의kid를 기반으로 특정JWK를 선택.
JWKSelector.select(JWKSet객체)
。JWKSet에서 특정 조건에 부합하는JWK를 선택하여 Return.
JWT의디코딩및검증을 수행하는JwtDecoder객체를Spring Bean으로 등록
。JwtDecoder객체의 경우구현체인NumbusJwtDecoder의NumbusJwtDecoder.withPublicKey(RSAPublicKey객체)메서드를 통해 생성하여Spring Bean으로 등록
NimbusJwtDecoder.withPublicKey(RSAPublicKey객체):
。사용자의RSAPublicKey객체를 인자로 전달하여Base64로인코딩된JWT를디코딩후Signature의유효성검증을 수행하는NimbusJwtDecoder 객체를 생성하는static method// Spring Bean에 등록된 RSAKey를 매개변수로 의존성주입 @Bean public JwtDecoder jwtDecoder(RSAKey rsaKey) throws JOSEException { return NimbusJwtDecoder.withPublicKey(rsaKey.toRSAPublicKey()).build(); }
JwtDecoder객체.decode(JWT토큰)
。Bearer 접두사를 제거한JWT 토큰을 인자로 전달 시JWKSource에 사전에 등록되어JWT 검증에 필요한Public Key를 제공받아서JWT토큰의Signature,exp,iat등을 모두 검증하는메서드
▶ 해당JWT의유효성 검증을 수행하여만료여부도 판단.
▶ 해당JwtDecoder는JWKSource를 통해검증에 필요한Public Key를 참조하여 해당 서버의Public Key로 발급한JWT토큰인지검증을 수행
。Oauth 2.0 Server에서 활용하는 경우Spring Security는OAuth2.0 Resource Server로 동작 시JWT Token을 자동으로 검증하지만, 기본 설정으로JWT를 검증하는JWK Set을 알 수 없으므로,auth server에서 제공하는 공개키인JWK Set를 통해 비대칭키인JWT의 서명을 검증.@Bean public JwtDecoder jwtDecoder() { return NimbusJwtDecoder.withJwkSetUri("https://auth-server-url/.well-known/jwks.json").build(); }▶
OAuth2.0의Public Key를 사용하는 경우 다음처럼 설정
JWT Encoder를Spring Bean으로 등록
。RSA 암호화 알고리즘을 통한Encrypt하여JWT를 생성하는JWT Encoder구현.
。JWKSource 객체를주입받아JwtEncoder 인터페이스의구현체인NimbusJwtEncoder 객체를 생성하여Spring Bean으로 등록하는@Bean Method생성// Spring Bean에 등록된 JWKSource<SecurityContext>를 매개변수로 의존성주입 @Bean public JwtEncoder jwtEncoder(JWKSource<SecurityContext> jwkSource){ return new NimbusJwtEncoder(jwkSource); }
JwtEncoder
。JWT를 생성(Encrypt)하는 역할을 수행하는인터페이스
▶ 기본구현체로서NimbusJwtEncoder를 제공.
。JwtDecoder와 함께 사용 시JWT기반의Authentication System을 구현가능.
JwtEncoder.encode(JwtEncoderParameters객체):
。JwtEncoderParameters 객체를 매개변수로 전달받아JWT를 생성하여 반환하는메서드
▶JWT의 내용 (Claims) , 서명 (Signature)를 포함하여JWT를 생성.
NimbusJwtEncoder:
。Spring Security에서 제공하는JwtEncoder의구현 Class
▶RSA등의 암호화 알고리즘을 활용한JWT의 생성이 가능.
。생성자에JWKSource 객체를 전달하여NimbusJwtEncoder 객체를 생성 가능
JWT발급및검증용도의JWT Class생성 및Spring Bean으로 등록
。로그인시검증이 끝난 경우 해당클래스를 통해클라이언트에게계정의ID를식별자로 하는JWT 토큰을 발급하여 반환
。Request의Authorization Header에 포함된JWT 토큰을 받아서 사전에Spring Bean으로 등록한JwtDecoder객체.decode(Bearer가 제거된 토큰)을 통해검증을 수행
▶ 해당JwtDecoder는JWKSource를 통해검증에 필요한Public Key를 참조하여 해당 서버의Public Key로 발급한JWT토큰인지검증을 수행
▶JWT의검증 목적의Filter 구현체에서 해당검증기능을 사용
JWT를 검증하는 필터@Component @RequiredArgsConstructor public class JwtService { private final JwtEncoder jwtEncoder; private final JwtDecoder jwtDecoder; private static final String ACCOUNTID_CLAIM_KEY = "accountId"; private static final String ROLE_CLAIM_KEY = "role"; private static final String EMAIL_CLAIM_KEY = "email"; private final AccountRepository accountRepository; // JWT 토큰 발급 public String issue(UUID accountId, Long expirationMinute){ Account foundedAccount = accountRepository.findByIdOrThrow(accountId); var claims = JwtClaimsSet .builder() .subject(accountId.toString()) .claim(ACCOUNTID_CLAIM_KEY,accountId.toString()) .claim(ROLE_CLAIM_KEY, foundedAccount.getRole().toString()) .claim(EMAIL_CLAIM_KEY, foundedAccount.getEmail()) .issuer("all4runner") .issuedAt(Instant.now()) .expiresAt(Instant.now().plusSeconds(60 * expirationMinute)) .build(); return jwtEncoder // 수집된 Claim 정보로 정적팩토리메서드로 JwtEncoderParameters 객체 생성 후 // 해당 객체를 기반으로 JWT 토큰 생성하여 반환 .encode(JwtEncoderParameters.from(claims)) .getTokenValue(); } // JWT 토큰 검증 public boolean validate(String token){ try { // 여기서 Signature, exp, iat 등을 모두 검증 jwtDecoder.decode(token); return true; } catch (JwtException e) { // 서명 불일치, 만료, 포맷 오류 등 모든 예외 return false; } } // Jwt Token Id 가져오기 public Long parseId(String token){ return Long.valueOf((String)jwtDecoder .decode(token) .getClaims() .get(ACCOUNTID_CLAIM_KEY)); } // 로그인 시 구현된 JWT 필터에서 검증이 다 끝난 경우 해당 Token의 Claims 정보를 기반으로 // SpringContext에 등록할 UserDetail 구현체 public DefaultCurrentUser getUserDetailFromToken(String token){ Map<String,Object> claims = jwtDecoder .decode(token) .getClaims(); UUID accountId = UUID.fromString( claims.get(ACCOUNTID_CLAIM_KEY).toString() ); String email = claims.get(EMAIL_CLAIM_KEY).toString(); AccountRole role = AccountRole.valueOf( claims.get(ROLE_CLAIM_KEY).toString() ); return new DefaultCurrentUser( accountId, email, role ); } }。
getUserDetailFromToken(String token)는 JWT 필터에서 검증이 완료된Jwt 토큰의Claims를 기반으로UserDetail 구현체를 생성하여 반환
▶ 이후 해당UserDetail 구현체를 기반으로AuthenticationToken 클래스 객체를 생성하여SecurityContext에 등록
Jwt:
org.springframework.security.oauth2.jwt.Jwt
。Spring Security에서JWT를 다룰때 사용하는 Class
▶JWT Token의Header,Claims,Signature를 포함하는Jwtinstance를 생성하여 활용.public final class Jwt { private final String tokenValue; // 서명된 JWT 문자열 private final Instant issuedAt; // 발급 시간 private final Instant expiresAt; // 만료 시간 private final Map<String, Object> headers; // JWT 헤더 (alg, typ 등) private final Map<String, Object> claims; // JWT 페이로드 (sub, exp, roles 등) public String getTokenValue(); // 서명된 JWT 문자열 가져오기 public Instant getIssuedAt(); // 발급 시간 가져오기 public Instant getExpiresAt(); // 만료 시간 가져오기 public Map<String, Object> getHeaders(); // JWT 헤더 가져오기 public Map<String, Object> getClaims(); // JWT 페이로드(Claims) 가져오기 }
JwtEncoderParameters:
。Spring Security에서JWT를 생성 시 필요한 Parameter(Claim,Header등 )를 포함하는 용도의 Class.
▶ 주로JwtEncoder객체.encode(JwtEncoderParameters객체)method의 매개변수로 전달되어JWT를 생성 시 활용됨.
。JwtClaimSet 객체를 추가하여 사용자정보 (이름,권한,발급시간),expire date등의Claims를 설정.
。JoseHeader 객체를 추가하여JWT Header를 설정 가능.
JwtEncoderParameters.from(JoseHeader객체,JwtClaimsSet객체)
。JwtEncoder를 통해JWT를encrypt하여 생성 시 필요한 parameter(JoseHeader,JwtClaimsSet)를 설정하는 역할을 수행하는JwtEncoderParameters의staticmethod
▶ 해당 parameter를 포함한JwtEncoderParametersinstance를 생성하여 반환.
。설정된JwtEncoderParameters객체는JwtEncoder객체.encode(JwtEncoderParameters객체)를 통해JWT로 생성
JwtClaimSet:
。JWT의Claims정보를 포함하는 용도로 사용되는 Class.
▶JWT에사용자정보 ( 이름, 권한, 발급시간 ),expire date을 포함할 수 있다.
。JWT이 전달하는Claim을 지시하는JSON Object로서 활용됨.
JWT payload의Claim종류
。iss: ( issuer ) : 발급자
。sub: ( subject ): 사용자 ID
。aud: ( audiance ) : 대상자
。exp: ( expiration ) : 토큰만료시간
。iat( Issued at ) : 토큰발급시간JwtClaimsSet.builder() .issuer("my-app") // 발급자 설정 .subject("user123") // 사용자 ID .issuedAt(Instant.now()) // 발급 시간 .expiresAt(Instant.now().plusSeconds(60*60)) // 만료 시간 (1시간) .claim("roles", List.of("USER", "ADMIN")) // 사용자 역할 추가 .claim("email", "user@example.com") // 추가 클레임 .build();
JwtClaimsSet.builder():
JwtClaimsSet 객체생성
JwtClaimsSet객체.issuer("발급자(iss)명칭"):
JwtClaimsSet 객체에 발급자 추가.
JwtClaimsSet객체.subject("사용자ID(subject)"):
JwtClaimsSet 객체에 사용자ID 추가.
JwtClaimsSet객체.issuedAt(Instant.now()):
JwtClaimsSet 객체에 발급시간(iat) 추가
JwtClaimsSet객체.expiresAt(Instant.now().plusSeconds(60 * 60)):
JwtClaimsSet 객체에 만료시간(exp) 추가
JwtClaimsSet객체.claim("설정할 Claim명", "설정될 Claim"):
JwtClaimsSet객체에Claim추가
▶Authentication 객체의authorities->authoritykey의 value를 할당하여Jwt의 권한을 정의.
ex)JwtClaimsSet객체.claim("roles",List.of("USER", "ADMIN")):
。사용자역할 역할의 커스텀Claim정의.
JwtClaimsSet객체.build():
Configuration이 정의된JwtClaimsSet객체생성.