Spring/Spring Security

[Spring Security] 8-2. OAuth 2.0 Resource Server: JWT

noahkim_ 2025. 7. 30. 12:58

1. Minimal Dependencies for JWT

의존성 모듈 역할
spring-security-oauth2-resource-server 리소스 서버 기본 기능
spring-security-oauth2-jose JWT 디코딩 및 서명 검증 기능

 

2. Minimal Configuration for JWTs

issuer-uri

  • Authorization Server의 메타데이터 엔드포인트를 자동으로 탐색 (.well-known/oauth-authorization-server/issuer)
  • public key 및 검증 설정을 구성함 (jwks_uri)
동작 내용 설명
issuer-uri로 metadata endpoint 요청
issuer-uri 값에 따라 호출
- .well-known/oauth-authorization-server or .well-known/openid-configuration 
메타데이터에서 jwks_uri 추출 메타데이터(JSON)에서 jwks_uri 항목 확인
공개키 및 서명 알고리즘 확인
jwks_uri로 요청하여 키와 알고리즘 파악
JWT 서명 검증 전략 구성
키 정보와 알고리즘을 바탕으로 JwtDecoder 자동 생성
iss 클레임 검증 설정
JWT의 iss 클레임 값이 issuer-uri와 일치해야 인증 성공

 

설정) application.yml

spring:
  security:
    oauth2:
      resourceserver:
        jwt:
          issuer-uri: https://idp.example.com/issuer

 

 

3. How JWT Authentication Works

단계 설명
1️⃣
BearerTokenAuthenticationFilter가 요청에서 Authorization 헤더 추출 후 BearerTokenAuthenticationToken 생성
2️⃣
이 토큰이 AuthenticationManager(실제로는 ProviderManager)에 전달됨
3️⃣
ProviderManager는 내부적으로 JwtAuthenticationProvider를 사용함
4️⃣
JwtAuthenticationProvider가 JwtDecoder를 사용하여 JWT를 디코딩, 서명 검증, 클레임 검증 수행
5️⃣
검증이 통과되면, JwtAuthenticationConverter를 통해 GrantedAuthority 목록을 생성
6️⃣
최종적으로 JwtAuthenticationToken 객체가 생성되어 SecurityContextHolder에 저장됨
7️⃣
인증 성공 후 컨트롤러 등의 보호된 리소스에 접근 가능해짐

 

동작) JwtAuthenticationProvider

더보기
동작 내용 설명
JWT 서명 검증
JWT의 서명을 jwks_uri에서 받은 공개키로 검증
클레임 검증
exp(만료), nbf(Not Before), iss(발급자) 값 검증
scope 매핑
scope 값 → SCOPE_ 접두어 붙여 권한(GrantedAuthority)으로 변환
Authentication.getPrincipal() → Jwt principal 객체는 JWT 자체
Authentication.getName() → sub sub 클레임 값이 사용자명처럼 사용됨

 

4. Specifying the Authorization Server JWK Set Uri Directly

jwk-set-uri

  • 공개키를 직접 가져와 JWT 서명 검증을 수행

 

5. Supplying Audiences

aud

  • 토큰이 의도된 대상 (Resource Server)

 

6. Overriding or Replacing Boot Auto Configuration

  • Spring Boot에 의해 자동설정됨
Bean 설명 코드
SecurityFilterChain
모든 요청을 JWT로 인증하도록 구성
.oauth2ResourceServer().jwt()
JwtDecoder
issuer-uri를 기반으로 JWT 검증용 디코더 생성
 

 

설정) 자동 설정

더보기
@Bean
public SecurityFilterChain filterChain(HttpSecurity http) throws Exception {
    http
        .authorizeHttpRequests(auth -> auth.anyRequest().authenticated())
        .oauth2ResourceServer(oauth2 -> oauth2.jwt(Customizer.withDefaults()));
    return http.build();
}

 

커스터마이징) 설정

더보기
@Bean
public SecurityFilterChain filterChain(HttpSecurity http) throws Exception {
    http
        .authorizeHttpRequests(auth -> auth
            .requestMatchers("/messages/**").access(hasScope("message:read"))
            .anyRequest().authenticated()
        )
        .oauth2ResourceServer(oauth2 -> oauth2
            .jwt(jwt -> jwt
	            .jwkSetUri("https://idp.example.com/jwks.json")
            	.jwtAuthenticationConverter(myConverter())
                .jwt.decoder(myCustomDecoder())				
            )
        );
        
    return http.build();
}

 

커스터마이징) jwtDecoder

더보기
@Bean
public JwtDecoder jwtDecoder() {
    return NimbusJwtDecoder.withJwkSetUri("https://idp.example.com/jwks.json").build();
}

 

Configuring Trusted Algorithms

Via Spring Boot

Using a Builder

From JWK Set response

Trusting a Single Asymmetric Key

Via Spring Boot

Using a Builder

Trusting a Single Symmetric Key

Configuring Authorization

  • JWT의 scope 클레임을 GrantedAuthority로 매핑
  • 모든 scope 값은 SCOPE_ 접두어가 붙은 GrantedAuthority로 변환됨
  • 권한 적용 시, hasScope()를 활용함 (OAuth2AuthorizationManagers)

 

설정) 요청 경로에 따라 권한 적용

더보기
@Bean
public SecurityFilterChain filterChain(HttpSecurity http) throws Exception {
    http
        .authorizeHttpRequests(auth -> auth
            .requestMatchers("/contacts/**").access(hasScope("contacts"))
            .requestMatchers("/messages/**").access(hasScope("messages"))
            .anyRequest().authenticated()
        )
        .oauth2ResourceServer(oauth2 -> oauth2.jwt(Customizer.withDefaults()));
    return http.build();
}
  • hasScope("contacts")는 내부적으로 hasAuthority("SCOPE_contacts")와 같음

 

 

설정) 메서드 레벨에서 인가 설정

더보기
@PreAuthorize("hasAuthority('SCOPE_messages')")
public List<Message> getMessages() {
    ...
}
  • 메서드 호출 시 JWT에 "scope": "messages" 가 포함되어 있어야 함
  • Spring Security Method Security 활성화 필요 (@EnableMethodSecurity)

 

 

Extracting Authorities Manually

  • 매핑 방식을 커스터마이징 할 수 있음
  • scope 클레임 이름 변경, 접두어 변경 등

 

커스터마이징) JwtAuthenticationConverter

더보기
@Bean
public JwtAuthenticationConverter jwtAuthenticationConverter() {
    JwtGrantedAuthoritiesConverter converter = new JwtGrantedAuthoritiesConverter();
    converter.setAuthoritiesClaimName("authorities"); // <-- 클레임 이름 변경
    converter.setAuthorityPrefix("ROLE_");

    JwtAuthenticationConverter jwtAuthConverter = new JwtAuthenticationConverter();
    jwtAuthConverter.setJwtGrantedAuthoritiesConverter(converter);
    return jwtAuthConverter;
}

 

설정) JwtAuthenticationConverter

더보기
http
    .oauth2ResourceServer(oauth2 -> oauth2
        .jwt(jwt -> jwt.jwtAuthenticationConverter(jwtAuthenticationConverter()))
    );

 

Configuring Validation

Customizing Timestamp Validation

서버 간 시간이 약간 다를 수 있어서 JWT의 nbf와 exp 검증에서 오차 허용 필요

 

커스터마이징) JwtDecoder (JwtTimestampValidator)

더보기
@Bean
JwtDecoder jwtDecoder() {
    NimbusJwtDecoder jwtDecoder = (NimbusJwtDecoder)
        JwtDecoders.fromIssuerLocation(issuerUri);

    OAuth2TokenValidator<Jwt> withClockSkew = new DelegatingOAuth2TokenValidator<>(
        new JwtTimestampValidator(Duration.ofSeconds(60)),  // 60초 clock skew 허용
        new JwtIssuerValidator(issuerUri)
    );

    jwtDecoder.setJwtValidator(withClockSkew);
    return jwtDecoder;
}

 

Configuring RFC 9068 Validation

RFC 9068에 맞춘 JWT 유효성 검증

 

커스터마이징) JwtDecoder (claim: aud, clientId, issuer 등)

더보기
@Bean
JwtDecoder jwtDecoder() {
    NimbusJwtDecoder jwtDecoder = NimbusJwtDecoder.withIssuerLocation(issuerUri)
        .validateTypes(false)  // typ 헤더 검증 비활성화
        .build();

    jwtDecoder.setJwtValidator(
        JwtValidators.createAtJwtValidator()
            .audience("https://audience.example.org")
            .clientId("client-identifier")
            .issuer("https://issuer.example.org")
            .build()
    );
    return jwtDecoder;
}

 

Configuring a Custom Validator

직접 검증 로직을 작성할 수 있음

 

커스터마이징) OAuth2TokenValidator

더보기
static class AudienceValidator implements OAuth2TokenValidator<Jwt> {
    OAuth2Error error = new OAuth2Error("custom_code", "Custom error message", null);

    @Override
    public OAuth2TokenValidatorResult validate(Jwt jwt) {
        if (jwt.getAudience().contains("messaging")) {
            return OAuth2TokenValidatorResult.success();
        } else {
            return OAuth2TokenValidatorResult.failure(error);
        }
    }
}

 

 

커스터마이징) JwtDecoder (validator)

더보기
@Bean
JwtDecoder jwtDecoder() {
    NimbusJwtDecoder jwtDecoder = (NimbusJwtDecoder)
        JwtDecoders.fromIssuerLocation(issuerUri);

    OAuth2TokenValidator<Jwt> audienceValidator = audienceValidator();  // 위에서 만든 커스텀 validator
    OAuth2TokenValidator<Jwt> withIssuer = JwtValidators.createDefaultWithIssuer(issuerUri);

    OAuth2TokenValidator<Jwt> combined = new DelegatingOAuth2TokenValidator<>(withIssuer, audienceValidator);

    jwtDecoder.setJwtValidator(combined);
    return jwtDecoder;
}

 

Configuring Claim Set Mapping

Customizing the Conversion of a Single Claim

Adding a Claim

Removing a Claim

Renaming a Claim

Configuring Timeouts