Spring Security ‐ OAuth 2.0 Resource Server API - thought-corner/backend-roadmap GitHub Wiki

JWT API

spring:
  security:
    oauth2:
      resourceserver:
        jwt:
          issuer-uri: http://localhost:8080/realms/oauth2                                 # 서비스 공급자 위치
          jwk-set-uri: http://localhost:8080/realms/oauth2/protocol/openid-connect/certs  # OAuth 2.0 JwkSetUri 엔드포인트
          jws-algorithms: RS256                                                           # OAuth 2.0 JWS 서명 알고리즘
          audiences: http://localhost:8081                                                # 수신자 위치
          public-key-location: classpath:certs/publicKey.txt                              # JWS 검증을 위한 PublicKey 파일 위치
  • spring.security.oauth2.resourceserver.jwt.issuer-uri : 프로퍼티를 설정하면 JWT로 인코딩한 Bearer 토큰을 검증하는 리소스 서버가 자동으로 설정된다.
  • Open ID Connect Provider 설정 엔드포인트 또는 인가 서버 메타데이터 엔드포인트를 검색해서 jwk-set-url 엔드포인트를 찾아 검증을 진행한다.
  • 2가지 검증 전략을 설정한다.
    • 리소스 서버는 인가서버의 jwk-set-uri 엔드포인트로 유효한 공개키를 질의하기 위한 검증 전략을 설정한다.
    • issuer-uri에 대한 각 JWT 클레임을 검증할 전략을 설정한다.

JwtDecoder 세부 흐름

  • JwtDecoder는 문자열로 된 JWT(Json Web Token)를 컴팩트 클레임 표현 형식에서 Jwt 인스턴스로 디코딩하는 역할을 한다.
  • JwtDecoder 는 JWT 가 JSON 웹 서명(JWS) 구조로 생성된 경우 JWS 서명에 대한 검증의 책임이 있다.
  • 기본 구현체로 NimbusJwtDecoder가 있다.
  • JwkSetUriJwtDecoderBuilder : 원격 JWK Set URL에서 공개키를 받아 검증하는 디코더를 만들며, 넷 중 유일하게 키 회전을 지원해 jwk-set-uriissuer-uri 설정이 모두 여기로 합류한다.
  • PublicKeyJwtDecoderBuilder : 로컬 RSAPublicKey 하나로 검증하는 디코더를 만들며 네트워크가 없는 대신 키 회전이 불가능하다.
  • SecretKeyJwtDecoderBuilder : 발급자와 공유한 대칭키로 HMAC(HS256·HS384·HS512) 서명을 검증하는 디코더를 만든다.
  • JwkSourceJwtDecoderBuilder : Nimbus JWKSource를 통째로 주입받는 탈출구로 다중 인가서버나 커스텀 캐시처럼 위 셋으로 표현되지 않는 키 조달 방식을 직접 구현할 때 쓴다.
  • JwkSetUriJwtDecoderBuilder$SpringJWKSource : JwkSetUriJwtDecoderBuilder 내부에서 실제로 JWK Set을 가져오고 캐싱하는 JWKSource 구현체다.
  • RestTemplateWithNimbusDefaultTimeouts : 조회에 쓰이는 기본 RestTemplate으로, 인가서버 지연이 리소스 서버 전체로 번지지 않도록 연결·읽기 타임아웃을 각각 500ms로 못 박아둔다.

JwtDecoder 생성

// JwtDecoderConfiguration이 @ConditionalOnMissingBean(JwtDecoder.class)로 Decoder를 자동 구성한다.
@Bean
SecurityFilterChain filterChain(HttpSecurity http) throws Exception {
    http
        .authorizeHttpRequests(auth -> auth.anyRequest().authenticated())
        .oauth2ResourceServer(rs -> rs.jwt(Customizer.withDefaults()));
    return http.build();
}
// 검증기를 추가하려면 @Bean으로 직접 선언한다.
private static final String ISSUER = "http://localhost:8080/realms/oauth2";

@Bean
JwtDecoder jwtDecoder() {
    NimbusJwtDecoder decoder = NimbusJwtDecoder
            .withIssuerLocation(ISSUER)        // 디스커버리로 jwk-set-uri 획득
            .jwsAlgorithm(SignatureAlgorithm.RS256)
            .validateType(true)                // typ 헤더 검사, ID 토큰 오용 차단
            .build();

    decoder.setJwtValidator(JwtValidators.createDefaultWithValidators(
            new JwtIssuerValidator(ISSUER),                 // iss
            new JwtAudienceValidator("oauth2-client-app")   // aud
    ));
    return decoder;
}
// 권한을 매핑하길 원한다면?
@Bean
JwtAuthenticationConverter jwtAuthenticationConverter() {
    JwtGrantedAuthoritiesConverter authorities = new JwtGrantedAuthoritiesConverter();
    authorities.setAuthorityPrefix("ROLE_");            // 기본 SCOPE_
    authorities.setAuthoritiesClaimName("roles");       // 기본 scope 또는 scp
    authorities.setAuthoritiesClaimDelimiter(" ");

    JwtAuthenticationConverter converter = new JwtAuthenticationConverter();
    converter.setJwtGrantedAuthoritiesConverter(authorities);
    converter.setPrincipalClaimName("preferred_username");  // 기본 sub
    return converter;
}