OAuth 2.0 資源伺服器 JWT

JWT 的最少依賴

大多數資源伺服器支援都收集在 spring-security-oauth2-resource-server 中。然而,解碼和驗證 JWT 的支援在 spring-security-oauth2-jose 中,這意味著兩者都是必需的,以便擁有一個支援 JWT 編碼的 Bearer Token 的工作資源伺服器。

JWT 的最少配置

使用 Spring Boot 時,將應用程式配置為資源伺服器包括兩個基本步驟。首先,包含所需的依賴項,其次,指示授權伺服器的位置。

指定授權伺服器

在 Spring Boot 應用程式中,要指定要使用的授權伺服器,只需執行

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

其中 idp.example.com/issuer 是授權伺服器將頒發的 JWT token 中 iss 宣告的值。資源伺服器將使用此屬性進一步進行自我配置,發現授權伺服器的公鑰,並隨後驗證傳入的 JWT。

要使用 issuer-uri 屬性,還必須滿足以下條件之一:idp.example.com/issuer/.well-known/openid-configurationidp.example.com/.well-known/openid-configuration/issueridp.example.com/.well-known/oauth-authorization-server/issuer 是授權伺服器支援的端點。此端點被稱為 提供程式配置 端點或 授權伺服器元資料 端點。

就是這樣!

啟動預期

當使用此屬性和這些依賴項時,資源伺服器將自動配置自身以驗證 JWT 編碼的 Bearer Tokens。

它透過確定性啟動過程實現這一點

  1. 查詢提供程式配置或授權伺服器元資料端點以獲取 jwks_url 屬性

  2. 查詢 jwks_url 端點以獲取支援的演算法

  3. 配置驗證策略以查詢 jwks_url 以獲取發現的演算法的有效公鑰

  4. 配置驗證策略以根據 idp.example.com 驗證每個 JWT 的 iss 宣告。

此過程的結果是授權伺服器必須啟動並接收請求,以便資源伺服器成功啟動。

如果授權伺服器在資源伺服器查詢它時宕機(在適當的超時時間內),則啟動將失敗。

執行時預期

應用程式啟動後,資源伺服器將嘗試處理任何包含 Authorization: Bearer 頭的請求

GET / HTTP/1.1
Authorization: Bearer some-token-value # Resource Server will process this

只要指示了此方案,資源伺服器將嘗試根據 Bearer Token 規範處理請求。

給定格式良好的 JWT,資源伺服器將

  1. 根據啟動期間從 jwks_url 端點獲取並與 JWT 匹配的公鑰驗證其簽名

  2. 驗證 JWT 的 expnbf 時間戳以及 JWT 的 iss 宣告,以及

  3. 將每個作用域對映到帶有字首 SCOPE_ 的許可權。

隨著授權伺服器提供新金鑰,Spring Security 將自動輪換用於驗證 JWT 的金鑰。

預設情況下,結果 Authentication#getPrincipal 是一個 Spring Security Jwt 物件,如果存在,Authentication#getName 對映到 JWT 的 sub 屬性。

從這裡,考慮跳轉到

JWT 認證如何工作

接下來,讓我們看看 Spring Security 用於支援 servlet 應用程式中的 JWT 認證的架構元件,就像我們剛剛看到的那樣。

讓我們看看 JwtAuthenticationProvider 如何在 Spring Security 中工作。該圖解釋了 讀取 Bearer Token 中的圖中的 AuthenticationManager 如何工作的詳細資訊。

jwtauthenticationprovider
圖 1. JwtAuthenticationProvider 用法

數字 1 讀取 Bearer Token 中的認證 FilterBearerTokenAuthenticationToken 傳遞給由 ProviderManager 實現的 AuthenticationManager

數字 2 ProviderManager 配置為使用型別為 JwtAuthenticationProviderAuthenticationProvider

數字 3 JwtAuthenticationProvider 使用 JwtDecoder 解碼、驗證和校驗 Jwt

數字 4 JwtAuthenticationProvider 然後使用 JwtAuthenticationConverterJwt 轉換為授予許可權的 Collection

數字 5 認證成功時,返回的 Authentication 型別為 JwtAuthenticationToken,其 principal 是由配置的 JwtDecoder 返回的 Jwt,並且包含至少 FACTOR_BEARER 的一組許可權。最終,返回的 JwtAuthenticationToken 將由認證 Filter 設定在 SecurityContextHolder 上。

直接指定授權伺服器 JWK Set Uri

如果授權伺服器不支援任何配置端點,或者如果資源伺服器必須能夠獨立於授權伺服器啟動,那麼也可以提供 jwk-set-uri

spring:
  security:
    oauth2:
      resourceserver:
        jwt:
          issuer-uri: https://idp.example.com
          jwk-set-uri: https://idp.example.com/.well-known/jwks.json
JWK Set uri 沒有標準化,但通常可以在授權伺服器的文件中找到

因此,資源伺服器在啟動時不會 ping 授權伺服器。我們仍然指定 issuer-uri,以便資源伺服器仍然驗證傳入 JWT 上的 iss 宣告。

此屬性也可以直接在 DSL 上提供。

提供受眾

如前所述,issuer-uri 屬性驗證 iss 宣告;這是傳送 JWT 的人。

Boot 還有 audiences 屬性用於驗證 aud 宣告;這是 JWT 傳送給誰。

資源伺服器的受眾可以這樣表示

spring:
  security:
    oauth2:
      resourceserver:
        jwt:
          issuer-uri: https://idp.example.com
          audiences: https://my-resource-server.example.com
如果需要,您還可以以程式設計方式新增 aud 驗證

結果將是,如果 JWT 的 iss 宣告不是 idp.example.com,並且其 aud 宣告不包含 my-resource-server.example.com 在其列表中,則驗證將失敗。

覆蓋或替換 Boot 自動配置

Spring Boot 會為資源伺服器生成兩個 @Bean

第一個是 SecurityFilterChain,它將應用程式配置為資源伺服器。當包含 spring-security-oauth2-jose 時,此 SecurityFilterChain 如下所示

預設 JWT 配置
  • Java

  • Kotlin

@Bean
public SecurityFilterChain filterChain(HttpSecurity http) throws Exception {
    http
        .authorizeHttpRequests((authorize) -> authorize
            .anyRequest().authenticated()
        )
        .oauth2ResourceServer((oauth2) -> oauth2.jwt(Customizer.withDefaults()));
    return http.build();
}
@Bean
open fun filterChain(http: HttpSecurity): SecurityFilterChain {
    http {
        authorizeHttpRequests {
            authorize(anyRequest, authenticated)
        }
        oauth2ResourceServer {
            jwt { }
        }
    }
    return http.build()
}

如果應用程式沒有公開 SecurityFilterChain bean,那麼 Spring Boot 將公開上述預設 bean。

替換它就像在應用程式中公開 bean 一樣簡單

自定義 JWT 配置
  • Java

  • Kotlin

import static org.springframework.security.oauth2.core.authorization.OAuth2AuthorizationManagers.hasScope;

@Configuration
@EnableWebSecurity
public class MyCustomSecurityConfiguration {
    @Bean
    public SecurityFilterChain filterChain(HttpSecurity http) throws Exception {
        http
            .authorizeHttpRequests((authorize) -> authorize
                .requestMatchers("/messages/**").access(hasScope("message:read"))
                .anyRequest().authenticated()
            )
            .oauth2ResourceServer((oauth2) -> oauth2
                .jwt((jwt) -> jwt
                    .jwtAuthenticationConverter(myConverter())
                )
            );
        return http.build();
    }
}
import org.springframework.security.oauth2.core.authorization.OAuth2AuthorizationManagers.hasScope

@Configuration
@EnableWebSecurity
class MyCustomSecurityConfiguration {
    @Bean
    open fun filterChain(http: HttpSecurity): SecurityFilterChain {
        http {
            authorizeHttpRequests {
                authorize("/messages/**", hasScope("message:read"))
                authorize(anyRequest, authenticated)
            }
            oauth2ResourceServer {
                jwt {
                    jwtAuthenticationConverter = myConverter()
                }
            }
        }
        return http.build()
    }
}

上述要求以 /messages/ 開頭的任何 URL 都具有 message:read 範圍。

oauth2ResourceServer DSL 上的方法也將覆蓋或替換自動配置。

例如,Spring Boot 建立的第二個 @BeanJwtDecoder,它String token 解碼為 Jwt 的已驗證例項

JWT 解碼器
  • Java

  • Kotlin

@Bean
public JwtDecoder jwtDecoder() {
    return JwtDecoders.fromIssuerLocation(issuerUri);
}
@Bean
fun jwtDecoder(): JwtDecoder {
    return JwtDecoders.fromIssuerLocation(issuerUri)
}
呼叫 JwtDecoders#fromIssuerLocation 是為了呼叫 Provider Configuration 或 Authorization Server Metadata 端點,以獲取 JWK Set Uri。

如果應用程式沒有公開 JwtDecoder bean,那麼 Spring Boot 將公開上述預設 bean。

並且可以使用 jwkSetUri() 覆蓋其配置,或使用 decoder() 替換。

或者,如果您根本不使用 Spring Boot,那麼這兩個元件——過濾器鏈和 JwtDecoder 可以在 XML 中指定。

過濾器鏈的指定方式如下

預設 JWT 配置
  • Xml

<http>
    <intercept-uri pattern="/**" access="authenticated"/>
    <oauth2-resource-server>
        <jwt decoder-ref="jwtDecoder"/>
    </oauth2-resource-server>
</http>

JwtDecoder 的指定方式如下

JWT 解碼器
  • Xml

<bean id="jwtDecoder"
        class="org.springframework.security.oauth2.jwt.JwtDecoders"
        factory-method="fromIssuerLocation">
    <constructor-arg value="${spring.security.oauth2.resourceserver.jwt.jwk-set-uri}"/>
</bean>

使用 jwkSetUri()

授權伺服器的 JWK Set Uri 可以作為配置屬性配置,也可以在 DSL 中提供

JWK Set Uri 配置
  • Java

  • Kotlin

  • Xml

@Configuration
@EnableWebSecurity
public class DirectlyConfiguredJwkSetUri {
    @Bean
    public SecurityFilterChain filterChain(HttpSecurity http) throws Exception {
        http
            .authorizeHttpRequests((authorize) -> authorize
                .anyRequest().authenticated()
            )
            .oauth2ResourceServer((oauth2) -> oauth2
                .jwt((jwt) -> jwt
                    .jwkSetUri("https://idp.example.com/.well-known/jwks.json")
                )
            );
        return http.build();
    }
}
@Configuration
@EnableWebSecurity
class DirectlyConfiguredJwkSetUri {
    @Bean
    open fun filterChain(http: HttpSecurity): SecurityFilterChain {
        http {
            authorizeHttpRequests {
                authorize(anyRequest, authenticated)
            }
            oauth2ResourceServer {
                jwt {
                    jwkSetUri = "https://idp.example.com/.well-known/jwks.json"
                }
            }
        }
        return http.build()
    }
}
<http>
    <intercept-uri pattern="/**" access="authenticated"/>
    <oauth2-resource-server>
        <jwt jwk-set-uri="https://idp.example.com/.well-known/jwks.json"/>
    </oauth2-resource-server>
</http>

使用 jwkSetUri() 優先於任何配置屬性。

使用 decoder()

jwkSetUri() 更強大的是 decoder(),它將完全替換任何 Boot 自動配置的 JwtDecoder

JWT 解碼器配置
  • Java

  • Kotlin

  • Xml

@Configuration
@EnableWebSecurity
public class DirectlyConfiguredJwtDecoder {
    @Bean
    public SecurityFilterChain filterChain(HttpSecurity http) throws Exception {
        http
            .authorizeHttpRequests((authorize) -> authorize
                .anyRequest().authenticated()
            )
            .oauth2ResourceServer((oauth2) -> oauth2
                .jwt((jwt) -> jwt
                    .decoder(myCustomDecoder())
                )
            );
        return http.build();
    }
}
@Configuration
@EnableWebSecurity
class DirectlyConfiguredJwtDecoder {
    @Bean
    open fun filterChain(http: HttpSecurity): SecurityFilterChain {
        http {
            authorizeHttpRequests {
                authorize(anyRequest, authenticated)
            }
            oauth2ResourceServer {
                jwt {
                    jwtDecoder = myCustomDecoder()
                }
            }
        }
        return http.build()
    }
}
<http>
    <intercept-uri pattern="/**" access="authenticated"/>
    <oauth2-resource-server>
        <jwt decoder-ref="myCustomDecoder"/>
    </oauth2-resource-server>
</http>

當需要更深層次的配置,例如驗證對映請求超時時,這非常方便。

公開 JwtDecoder @Bean

或者,公開一個 JwtDecoder @Bean 具有與 decoder() 相同的效果。您可以使用 jwkSetUri 構造一個,如下所示

  • Java

  • Kotlin

@Bean
public JwtDecoder jwtDecoder() {
    return NimbusJwtDecoder.withJwkSetUri(jwkSetUri).build();
}
@Bean
fun jwtDecoder(): JwtDecoder {
    return NimbusJwtDecoder.withJwkSetUri(jwkSetUri).build()
}

或者您可以使用發行者,並讓 NimbusJwtDecoder 在呼叫 build() 時查詢 jwkSetUri,如下所示

  • Java

  • Kotlin

@Bean
public JwtDecoder jwtDecoder() {
    return NimbusJwtDecoder.withIssuerLocation(issuer).build();
}
@Bean
fun jwtDecoder(): JwtDecoder {
    return NimbusJwtDecoder.withIssuerLocation(issuer).build()
}

或者,如果預設值適合您,您也可以使用 JwtDecoders,它除了配置解碼器的驗證器外,還執行上述操作

  • Java

  • Kotlin

@Bean
public JwtDecoders jwtDecoder() {
    return JwtDecoders.fromIssuerLocation(issuer);
}
@Bean
fun jwtDecoder(): JwtDecoders {
    return JwtDecoders.fromIssuerLocation(issuer)
}

配置受信任的演算法

預設情況下,NimbusJwtDecoder,因此資源伺服器,將僅信任和使用 RS256 驗證令牌。

您可以透過 Spring BootNimbusJwtDecoder 構建器或從 JWK Set 響應自定義此設定。

透過 Spring Boot

設定演算法最簡單的方法是將其作為屬性

spring:
  security:
    oauth2:
      resourceserver:
        jwt:
          jws-algorithms: RS512
          jwk-set-uri: https://idp.example.org/.well-known/jwks.json

使用構建器

然而,為了獲得更大的能力,我們可以使用 NimbusJwtDecoder 附帶的構建器

  • Java

  • Kotlin

@Bean
JwtDecoder jwtDecoder() {
    return NimbusJwtDecoder.withIssuerLocation(this.issuer)
            .jwsAlgorithm(RS512).build();
}
@Bean
fun jwtDecoder(): JwtDecoder {
    return NimbusJwtDecoder.withIssuerLocation(this.issuer)
            .jwsAlgorithm(RS512).build()
}

多次呼叫 jwsAlgorithm 將配置 NimbusJwtDecoder 信任多種演算法,如下所示

  • Java

  • Kotlin

@Bean
JwtDecoder jwtDecoder() {
    return NimbusJwtDecoder.withIssuerLocation(this.issuer)
            .jwsAlgorithm(RS512).jwsAlgorithm(ES512).build();
}
@Bean
fun jwtDecoder(): JwtDecoder {
    return NimbusJwtDecoder.withIssuerLocation(this.issuer)
            .jwsAlgorithm(RS512).jwsAlgorithm(ES512).build()
}

或者,您可以呼叫 jwsAlgorithms

  • Java

  • Kotlin

@Bean
JwtDecoder jwtDecoder() {
    return NimbusJwtDecoder.withIssuerLocation(this.issuer)
            .jwsAlgorithms(algorithms -> {
                    algorithms.add(RS512);
                    algorithms.add(ES512);
            }).build();
}
@Bean
fun jwtDecoder(): JwtDecoder {
    return NimbusJwtDecoder.withIssuerLocation(this.issuer)
            .jwsAlgorithms {
                it.add(RS512)
                it.add(ES512)
            }.build()
}

從 JWK Set 響應

由於 Spring Security 的 JWT 支援是基於 Nimbus 的,因此您也可以使用它的所有強大功能。

例如,Nimbus 有一個 JWSKeySelector 實現,它將根據 JWK Set URI 響應選擇演算法集。您可以這樣使用它來生成 NimbusJwtDecoder

  • Java

  • Kotlin

@Bean
public JwtDecoder jwtDecoder() {
    // makes a request to the JWK Set endpoint
    JWSKeySelector<SecurityContext> jwsKeySelector =
            JWSAlgorithmFamilyJWSKeySelector.fromJWKSetURL(this.jwkSetUrl);

    DefaultJWTProcessor<SecurityContext> jwtProcessor =
            new DefaultJWTProcessor<>();
    jwtProcessor.setJWSKeySelector(jwsKeySelector);

    return new NimbusJwtDecoder(jwtProcessor);
}
@Bean
fun jwtDecoder(): JwtDecoder {
    // makes a request to the JWK Set endpoint
    val jwsKeySelector: JWSKeySelector<SecurityContext> = JWSAlgorithmFamilyJWSKeySelector.fromJWKSetURL<SecurityContext>(this.jwkSetUrl)
    val jwtProcessor: DefaultJWTProcessor<SecurityContext> = DefaultJWTProcessor()
    jwtProcessor.jwsKeySelector = jwsKeySelector
    return NimbusJwtDecoder(jwtProcessor)
}

信任單個非對稱金鑰

比使用 JWK Set 端點支援資源伺服器更簡單的是硬編碼 RSA 公鑰。公鑰可以透過 Spring Boot 或透過使用構建器提供。

透過 Spring Boot

透過 Spring Boot 指定金鑰非常簡單。金鑰的位置可以這樣指定

spring:
  security:
    oauth2:
      resourceserver:
        jwt:
          public-key-location: classpath:my-key.pub

或者,為了實現更復雜的查詢,您可以對 RsaKeyConversionServicePostProcessor 進行後處理

  • Java

  • Kotlin

@Bean
BeanFactoryPostProcessor conversionServiceCustomizer() {
    return beanFactory ->
        beanFactory.getBean(RsaKeyConversionServicePostProcessor.class)
                .setResourceLoader(new CustomResourceLoader());
}
@Bean
fun conversionServiceCustomizer(): BeanFactoryPostProcessor {
    return BeanFactoryPostProcessor { beanFactory ->
        beanFactory.getBean<RsaKeyConversionServicePostProcessor>()
                .setResourceLoader(CustomResourceLoader())
    }
}

指定金鑰的位置

key.location: hfds://my-key.pub

然後自動裝配值

  • Java

  • Kotlin

@Value("${key.location}")
RSAPublicKey key;
@Value("\${key.location}")
val key: RSAPublicKey? = null

使用構建器

要直接連線 RSAPublicKey,您只需使用相應的 NimbusJwtDecoder 構建器,如下所示

  • Java

  • Kotlin

@Bean
public JwtDecoder jwtDecoder() {
    return NimbusJwtDecoder.withPublicKey(this.key).build();
}
@Bean
fun jwtDecoder(): JwtDecoder {
    return NimbusJwtDecoder.withPublicKey(this.key).build()
}

信任單個對稱金鑰

使用單個對稱金鑰也很簡單。您只需載入 SecretKey 並使用適當的 NimbusJwtDecoder 構建器,如下所示

  • Java

  • Kotlin

@Bean
public JwtDecoder jwtDecoder() {
    return NimbusJwtDecoder.withSecretKey(this.key).build();
}
@Bean
fun jwtDecoder(): JwtDecoder {
    return NimbusJwtDecoder.withSecretKey(key).build()
}

配置授權

從 OAuth 2.0 授權伺服器頒發的 JWT 通常會有一個 scopescp 屬性,指示其已授予的範圍(或許可權),例如

{ ...​, "scope" : "messages contacts"}

在這種情況下,資源伺服器將嘗試將這些作用域強制轉換為一個授予許可權列表,每個作用域都帶有字首 "SCOPE_"。

這意味著要使用從 JWT 派生的作用域保護端點或方法,相應的表示式應包含此字首

授權配置
  • Java

  • Kotlin

  • Xml

import static org.springframework.security.oauth2.core.authorization.OAuth2AuthorizationManagers.hasScope;

@Configuration
@EnableWebSecurity
public class DirectlyConfiguredJwkSetUri {
    @Bean
    public SecurityFilterChain filterChain(HttpSecurity http) throws Exception {
        http
            .authorizeHttpRequests((authorize) -> authorize
                .requestMatchers("/contacts/**").access(hasScope("contacts"))
                .requestMatchers("/messages/**").access(hasScope("messages"))
                .anyRequest().authenticated()
            )
            .oauth2ResourceServer((oauth2) -> oauth2
                .jwt(Customizer.withDefaults())
            );
        return http.build();
    }
}
import org.springframework.security.oauth2.core.authorization.OAuth2AuthorizationManagers.hasScope;

@Configuration
@EnableWebSecurity
class DirectlyConfiguredJwkSetUri {
    @Bean
    open fun filterChain(http: HttpSecurity): SecurityFilterChain {
        http {
            authorizeHttpRequests {
                authorize("/contacts/**", hasScope("contacts"))
                authorize("/messages/**", hasScope("messages"))
                authorize(anyRequest, authenticated)
            }
            oauth2ResourceServer {
                jwt { }
            }
        }
        return http.build()
    }
}
<http>
    <intercept-uri pattern="/contacts/**" access="hasAuthority('SCOPE_contacts')"/>
    <intercept-uri pattern="/messages/**" access="hasAuthority('SCOPE_messages')"/>
    <oauth2-resource-server>
        <jwt jwk-set-uri="https://idp.example.org/.well-known/jwks.json"/>
    </oauth2-resource-server>
</http>

或者與方法安全類似

  • Java

  • Kotlin

@PreAuthorize("hasAuthority('SCOPE_messages')")
public List<Message> getMessages(...) {}
@PreAuthorize("hasAuthority('SCOPE_messages')")
fun getMessages(): List<Message> { }

手動提取許可權

然而,在許多情況下,此預設值是不夠的。例如,某些授權伺服器不使用 scope 屬性,而是使用自己的自定義屬性。或者,有時,資源伺服器可能需要將屬性或屬性組合轉換為內部許可權。

為此,Spring Security 附帶 JwtAuthenticationConverter,它負責 Jwt 轉換為 Authentication。預設情況下,Spring Security 將使用 JwtAuthenticationConverter 的預設例項連線 JwtAuthenticationProvider

作為配置 JwtAuthenticationConverter 的一部分,您可以提供一個輔助轉換器,用於將 Jwt 轉換為授予許可權的 Collection

假設您的授權伺服器在名為 authorities 的自定義宣告中通訊許可權。在這種情況下,您可以配置 JwtAuthenticationConverter 應該檢查的宣告,如下所示

許可權宣告配置
  • Java

  • Kotlin

  • Xml

@Bean
public JwtAuthenticationConverter jwtAuthenticationConverter() {
    JwtGrantedAuthoritiesConverter grantedAuthoritiesConverter = new JwtGrantedAuthoritiesConverter();
    grantedAuthoritiesConverter.setAuthoritiesClaimName("authorities");

    JwtAuthenticationConverter jwtAuthenticationConverter = new JwtAuthenticationConverter();
    jwtAuthenticationConverter.setJwtGrantedAuthoritiesConverter(grantedAuthoritiesConverter);
    return jwtAuthenticationConverter;
}
@Bean
fun jwtAuthenticationConverter(): JwtAuthenticationConverter {
    val grantedAuthoritiesConverter = JwtGrantedAuthoritiesConverter()
    grantedAuthoritiesConverter.setAuthoritiesClaimName("authorities")

    val jwtAuthenticationConverter = JwtAuthenticationConverter()
    jwtAuthenticationConverter.setJwtGrantedAuthoritiesConverter(grantedAuthoritiesConverter)
    return jwtAuthenticationConverter
}
<http>
    <intercept-uri pattern="/contacts/**" access="hasAuthority('SCOPE_contacts')"/>
    <intercept-uri pattern="/messages/**" access="hasAuthority('SCOPE_messages')"/>
    <oauth2-resource-server>
        <jwt jwk-set-uri="https://idp.example.org/.well-known/jwks.json"
                jwt-authentication-converter-ref="jwtAuthenticationConverter"/>
    </oauth2-resource-server>
</http>

<bean id="jwtAuthenticationConverter"
        class="org.springframework.security.oauth2.server.resource.authentication.JwtAuthenticationConverter">
    <property name="jwtGrantedAuthoritiesConverter" ref="jwtGrantedAuthoritiesConverter"/>
</bean>

<bean id="jwtGrantedAuthoritiesConverter"
        class="org.springframework.security.oauth2.server.resource.authentication.JwtGrantedAuthoritiesConverter">
    <property name="authoritiesClaimName" value="authorities"/>
</bean>

您還可以將許可權字首配置為不同。不是為每個許可權都加上 SCOPE_ 字首,您可以將其更改為 ROLE_,如下所示

許可權字首配置
  • Java

  • Kotlin

  • Xml

@Bean
public JwtAuthenticationConverter jwtAuthenticationConverter() {
    JwtGrantedAuthoritiesConverter grantedAuthoritiesConverter = new JwtGrantedAuthoritiesConverter();
    grantedAuthoritiesConverter.setAuthorityPrefix("ROLE_");

    JwtAuthenticationConverter jwtAuthenticationConverter = new JwtAuthenticationConverter();
    jwtAuthenticationConverter.setJwtGrantedAuthoritiesConverter(grantedAuthoritiesConverter);
    return jwtAuthenticationConverter;
}
@Bean
fun jwtAuthenticationConverter(): JwtAuthenticationConverter {
    val grantedAuthoritiesConverter = JwtGrantedAuthoritiesConverter()
    grantedAuthoritiesConverter.setAuthorityPrefix("ROLE_")

    val jwtAuthenticationConverter = JwtAuthenticationConverter()
    jwtAuthenticationConverter.setJwtGrantedAuthoritiesConverter(grantedAuthoritiesConverter)
    return jwtAuthenticationConverter
}
<http>
    <intercept-uri pattern="/contacts/**" access="hasAuthority('SCOPE_contacts')"/>
    <intercept-uri pattern="/messages/**" access="hasAuthority('SCOPE_messages')"/>
    <oauth2-resource-server>
        <jwt jwk-set-uri="https://idp.example.org/.well-known/jwks.json"
                jwt-authentication-converter-ref="jwtAuthenticationConverter"/>
    </oauth2-resource-server>
</http>

<bean id="jwtAuthenticationConverter"
        class="org.springframework.security.oauth2.server.resource.authentication.JwtAuthenticationConverter">
    <property name="jwtGrantedAuthoritiesConverter" ref="jwtGrantedAuthoritiesConverter"/>
</bean>

<bean id="jwtGrantedAuthoritiesConverter"
        class="org.springframework.security.oauth2.server.resource.authentication.JwtGrantedAuthoritiesConverter">
    <property name="authorityPrefix" value="ROLE_"/>
</bean>

或者,您可以透過呼叫 JwtGrantedAuthoritiesConverter#setAuthorityPrefix("") 完全移除字首。

為了更大的靈活性,DSL 支援完全用實現 Converter<Jwt, AbstractAuthenticationToken> 的任何類替換轉換器

  • Java

  • Kotlin

static class CustomAuthenticationConverter implements Converter<Jwt, AbstractAuthenticationToken> {
    public AbstractAuthenticationToken convert(Jwt jwt) {
        return new CustomAuthenticationToken(jwt);
    }
}

// ...

@Configuration
@EnableWebSecurity
public class CustomAuthenticationConverterConfig {
    @Bean
    public SecurityFilterChain filterChain(HttpSecurity http) throws Exception {
        http
            .authorizeHttpRequests((authorize) -> authorize
                .anyRequest().authenticated()
            )
            .oauth2ResourceServer((oauth2) -> oauth2
                .jwt((jwt) -> jwt
                    .jwtAuthenticationConverter(new CustomAuthenticationConverter())
                )
            );
        return http.build();
    }
}
internal class CustomAuthenticationConverter : Converter<Jwt, AbstractAuthenticationToken> {
    override fun convert(jwt: Jwt): AbstractAuthenticationToken {
        return CustomAuthenticationToken(jwt)
    }
}

// ...

@Configuration
@EnableWebSecurity
class CustomAuthenticationConverterConfig {
    @Bean
    open fun filterChain(http: HttpSecurity): SecurityFilterChain {
       http {
            authorizeHttpRequests {
                authorize(anyRequest, authenticated)
            }
           oauth2ResourceServer {
               jwt {
                   jwtAuthenticationConverter = CustomAuthenticationConverter()
               }
           }
        }
        return http.build()
    }
}

配置驗證

使用最少的 Spring Boot 配置,指示授權伺服器的發行者 URI,資源伺服器將預設驗證 iss 宣告以及 expnbf 時間戳宣告。

在需要自定義驗證的情況下,資源伺服器附帶兩個標準驗證器,也接受自定義的 OAuth2TokenValidator 例項。

自定義時間戳驗證

JWT 通常有一個有效期視窗,視窗的開始由 nbf 宣告指示,結束由 exp 宣告指示。

然而,每個伺服器都可能出現時鐘漂移,這可能導致令牌在一個伺服器上看起來已過期,但在另一個伺服器上卻沒有。在分散式系統中,隨著協作伺服器數量的增加,這可能會導致一些實現上的麻煩。

資源伺服器使用 JwtTimestampValidator 來驗證令牌的有效期視窗,並且可以配置 clockSkew 以緩解上述問題

  • Java

  • Kotlin

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

     OAuth2TokenValidator<Jwt> withClockSkew = new DelegatingOAuth2TokenValidator<>(
            new JwtTimestampValidator(Duration.ofSeconds(60)),
            new JwtIssuerValidator(issuerUri));

     jwtDecoder.setJwtValidator(withClockSkew);

     return jwtDecoder;
}
@Bean
fun jwtDecoder(): JwtDecoder {
    val jwtDecoder: NimbusJwtDecoder = JwtDecoders.fromIssuerLocation(issuerUri) as NimbusJwtDecoder

    val withClockSkew: OAuth2TokenValidator<Jwt> = DelegatingOAuth2TokenValidator(
            JwtTimestampValidator(Duration.ofSeconds(60)),
            JwtIssuerValidator(issuerUri))

    jwtDecoder.setJwtValidator(withClockSkew)

    return jwtDecoder
}
預設情況下,資源伺服器配置的時鐘偏差為 60 秒。

配置 RFC 9068 驗證

如果您需要要求滿足 RFC 9068 的令牌,您可以透過以下方式配置驗證

  • Java

  • Kotlin

@Bean
JwtDecoder jwtDecoder() {
    NimbusJwtDecoder jwtDecoder = NimbusJwtDecoder.withIssuerLocation(issuerUri)
            .validateTypes(false).build();
    jwtDecoder.setJwtValidator(JwtValidators.createAtJwtValidator()
            .audience("https://audience.example.org")
            .clientId("client-identifier")
            .issuer("https://issuer.example.org").build());
     return jwtDecoder;
}
@Bean
fun jwtDecoder(): JwtDecoder {
    val jwtDecoder = NimbusJwtDecoder.withIssuerLocation(issuerUri)
            .validateTypes(false).build()
    jwtDecoder.setJwtValidator(JwtValidators.createAtJwtValidator()
            .audience("https://audience.example.org")
            .clientId("client-identifier")
            .issuer("https://issuer.example.org").build())
    return jwtDecoder
}

配置自定義驗證器

使用 OAuth2TokenValidator API 新增對 aud 宣告的檢查非常簡單

  • Java

  • Kotlin

OAuth2TokenValidator<Jwt> audienceValidator() {
    return new JwtClaimValidator<List<String>>(AUD, aud -> aud.contains("messaging"));
}
fun audienceValidator(): OAuth2TokenValidator<Jwt?> {
    return JwtClaimValidator<List<String>>(AUD) { aud -> aud.contains("messaging") }
}

或者,為了獲得更多控制,您可以實現自己的 OAuth2TokenValidator

  • Java

  • Kotlin

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);
        }
    }
}

// ...

OAuth2TokenValidator<Jwt> audienceValidator() {
    return new AudienceValidator();
}
internal class AudienceValidator : OAuth2TokenValidator<Jwt> {
    var error: OAuth2Error = OAuth2Error("custom_code", "Custom error message", null)

    override fun validate(jwt: Jwt): OAuth2TokenValidatorResult {
        return if (jwt.audience.contains("messaging")) {
            OAuth2TokenValidatorResult.success()
        } else {
            OAuth2TokenValidatorResult.failure(error)
        }
    }
}

// ...

fun audienceValidator(): OAuth2TokenValidator<Jwt> {
    return AudienceValidator()
}

然後,要將其新增到資源伺服器中,只需指定 JwtDecoder 例項

  • Java

  • Kotlin

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

    OAuth2TokenValidator<Jwt> audienceValidator = audienceValidator();
    OAuth2TokenValidator<Jwt> withIssuer = JwtValidators.createDefaultWithIssuer(issuerUri);
    OAuth2TokenValidator<Jwt> withAudience = new DelegatingOAuth2TokenValidator<>(withIssuer, audienceValidator);

    jwtDecoder.setJwtValidator(withAudience);

    return jwtDecoder;
}
@Bean
fun jwtDecoder(): JwtDecoder {
    val jwtDecoder: NimbusJwtDecoder = JwtDecoders.fromIssuerLocation(issuerUri) as NimbusJwtDecoder

    val audienceValidator = audienceValidator()
    val withIssuer: OAuth2TokenValidator<Jwt> = JwtValidators.createDefaultWithIssuer(issuerUri)
    val withAudience: OAuth2TokenValidator<Jwt> = DelegatingOAuth2TokenValidator(withIssuer, audienceValidator)

    jwtDecoder.setJwtValidator(withAudience)

    return jwtDecoder
}
如前所述,您可以轉而在 Boot 中配置 aud 驗證

配置宣告集對映

Spring Security 使用 Nimbus 庫來解析 JWT 並驗證其簽名。因此,Spring Security 受 Nimbus 對每個欄位值的解釋以及如何將每個欄位強制轉換為 Java 型別的影響。

例如,由於 Nimbus 仍然相容 Java 7,它不使用 Instant 來表示時間戳欄位。

而且,完全有可能使用不同的庫進行 JWT 處理,這可能會做出需要調整的自己的強制轉換決策。

或者,很簡單,資源伺服器可能希望出於特定領域的原因新增或刪除 JWT 中的宣告。

出於這些目的,資源伺服器支援使用 MappedJwtClaimSetConverter 對映 JWT 宣告集。

自定義單個宣告的轉換

預設情況下,MappedJwtClaimSetConverter 將嘗試將宣告強制轉換為以下型別

宣告

Java 型別

aud

Collection<String>

exp

Instant

iat

Instant

iss

字串

jti

字串

nbf

Instant

sub

字串

可以使用 MappedJwtClaimSetConverter.withDefaults 配置單個宣告的轉換策略

  • Java

  • Kotlin

@Bean
JwtDecoder jwtDecoder() {
    NimbusJwtDecoder jwtDecoder = NimbusJwtDecoder.withIssuerLocation(issuer).build();

    MappedJwtClaimSetConverter converter = MappedJwtClaimSetConverter
            .withDefaults(Collections.singletonMap("sub", this::lookupUserIdBySub));
    jwtDecoder.setClaimSetConverter(converter);

    return jwtDecoder;
}
@Bean
fun jwtDecoder(): JwtDecoder {
    val jwtDecoder = NimbusJwtDecoder.withIssuerLocation(issuer).build()

    val converter = MappedJwtClaimSetConverter
            .withDefaults(mapOf("sub" to this::lookupUserIdBySub))
    jwtDecoder.setClaimSetConverter(converter)

    return jwtDecoder
}

這將保留所有預設值,但會覆蓋 sub 的預設宣告轉換器。

新增宣告

MappedJwtClaimSetConverter 也可以用於新增自定義宣告,例如,以適應現有系統

  • Java

  • Kotlin

MappedJwtClaimSetConverter.withDefaults(Collections.singletonMap("custom", custom -> "value"));
MappedJwtClaimSetConverter.withDefaults(mapOf("custom" to Converter<Any, String> { "value" }))

移除宣告

使用相同的 API,刪除宣告也很簡單

  • Java

  • Kotlin

MappedJwtClaimSetConverter.withDefaults(Collections.singletonMap("legacyclaim", legacy -> null));
MappedJwtClaimSetConverter.withDefaults(mapOf("legacyclaim" to Converter<Any, Any> { null }))

重新命名宣告

在更復雜的場景中,例如同時查詢多個宣告或重新命名宣告,資源伺服器接受任何實現 Converter<Map<String, Object>, Map<String,Object>> 的類

  • Java

  • Kotlin

public class UsernameSubClaimAdapter implements Converter<Map<String, Object>, Map<String, Object>> {
    private final MappedJwtClaimSetConverter delegate =
            MappedJwtClaimSetConverter.withDefaults(Collections.emptyMap());

    public Map<String, Object> convert(Map<String, Object> claims) {
        Map<String, Object> convertedClaims = this.delegate.convert(claims);

        String username = (String) convertedClaims.get("user_name");
        convertedClaims.put("sub", username);

        return convertedClaims;
    }
}
class UsernameSubClaimAdapter : Converter<Map<String, Any?>, Map<String, Any?>> {
    private val delegate = MappedJwtClaimSetConverter.withDefaults(Collections.emptyMap())
    override fun convert(claims: Map<String, Any?>): Map<String, Any?> {
        val convertedClaims = delegate.convert(claims)
        val username = convertedClaims["user_name"] as String
        convertedClaims["sub"] = username
        return convertedClaims
    }
}

然後,例項可以像往常一樣提供

  • Java

  • Kotlin

@Bean
JwtDecoder jwtDecoder() {
    NimbusJwtDecoder jwtDecoder = NimbusJwtDecoder.withIssuerLocation(issuer).build();
    jwtDecoder.setClaimSetConverter(new UsernameSubClaimAdapter());
    return jwtDecoder;
}
@Bean
fun jwtDecoder(): JwtDecoder {
    val jwtDecoder: NimbusJwtDecoder = NimbusJwtDecoder.withIssuerLocation(issuer).build()
    jwtDecoder.setClaimSetConverter(UsernameSubClaimAdapter())
    return jwtDecoder
}

配置超時

預設情況下,資源伺服器與授權伺服器協調時,連線和套接字超時各為 30 秒。

在某些情況下,這可能太短了。此外,它沒有考慮更復雜的模式,如退避和發現。

為了調整資源伺服器連線授權伺服器的方式,NimbusJwtDecoder 接受一個 RestOperations 例項

  • Java

  • Kotlin

@Bean
public JwtDecoder jwtDecoder(RestTemplateBuilder builder) {
    RestOperations rest = builder
            .setConnectTimeout(Duration.ofSeconds(60))
            .setReadTimeout(Duration.ofSeconds(60))
            .build();

    NimbusJwtDecoder jwtDecoder = NimbusJwtDecoder.withIssuerLocation(issuer).restOperations(rest).build();
    return jwtDecoder;
}
@Bean
fun jwtDecoder(builder: RestTemplateBuilder): JwtDecoder {
    val rest: RestOperations = builder
            .setConnectTimeout(Duration.ofSeconds(60))
            .setReadTimeout(Duration.ofSeconds(60))
            .build()
    return NimbusJwtDecoder.withIssuerLocation(issuer).restOperations(rest).build()
}

此外,預設情況下,資源伺服器將授權伺服器的 JWK 集在記憶體中快取 5 分鐘,您可能需要調整此時間。此外,它沒有考慮更復雜的快取模式,如逐出或使用共享快取。

為了調整資源伺服器快取 JWK 集的方式,NimbusJwtDecoder 接受一個 Cache 例項

  • Java

  • Kotlin

@Bean
public JwtDecoder jwtDecoder(CacheManager cacheManager) {
    return NimbusJwtDecoder.withIssuerLocation(issuer)
            .cache(cacheManager.getCache("jwks"))
            .build();
}
@Bean
fun jwtDecoder(cacheManager: CacheManager): JwtDecoder {
    return NimbusJwtDecoder.withIssuerLocation(issuer)
            .cache(cacheManager.getCache("jwks"))
            .build()
}

當給定一個 Cache 時,資源伺服器將使用 JWK Set Uri 作為鍵,JWK Set JSON 作為值。

Spring 不是快取提供商,所以您需要確保包含適當的依賴項,如 spring-boot-starter-cache 和您喜歡的快取提供商。
無論是套接字還是快取超時,您可能都希望直接使用 Nimbus。為此,請記住 NimbusJwtDecoder 附帶一個接受 Nimbus 的 JWTProcessor 的建構函式。
© . This site is unofficial and not affiliated with VMware.