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의 발급자 주소
단계 동작 의미
1 issuer-uri 기반으로 메타데이터 엔드포인트 호출 인증 서버 정보 조회
2 메타데이터에서 jwks_uri 확인 공개키 목록 위치 찾기
3 jwks_uri로 공개키 조회 JWT 서명 검증용 키 확보
4 JwtDecoder 자동 구성 JWT 검증기 생성
5 JWT의 iss 클레임 검증 설정한 issuer와 토큰 issuer가 같은지 확인
6 exp, nbf, 서명 등 검증 만료·위조 여부 확인

 

설정) application.yml

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

 

3. How JWT Authentication Works

  1. BearerTokenAuthenticationFilter
    • Authorization 헤더를 추출하고 BearerTokenAuthenticationToken을 생성함
    • 이 토큰이 AuthenticationManager에 전달됨
    • 내부적으로 JwtAuthenticationProvider를 사용함
  2. JwtAuthenticationProvider
    • JwtDecoder를 사용하여 JWT 검증을 수행함
    • JwtAuthenticationConverter: 검증이 통과되면 GrantedAuthority 목록을 생성함
    • ➡️ JwtAuthenticationToken 객체가 생성되어 SecurityContextHolder에 저장됨
  3. 인증 성공 후 컨트롤러 등의 보호된 리소스에 접근 가능해짐

 

동작) 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

  • 매핑 방식을 커스터마이징 할 수 있음
  • ✅ex) 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