OAuth 2.0 資源伺服器 JWT

JWT 的最小依賴項

大多數資源伺服器支援都收集在 spring-security-oauth2-resource-server 中。然而,解碼和驗證 JWT 的支援在 spring-security-oauth2-jose 中,這表示為了擁有一個支援 JWT 編碼的 Bearer Tokens 的可運作資源伺服器,兩者都是必要的。

JWT 的最小設定

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

指定授權伺服器

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

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

其中 idp.example.com/issuer 是授權伺服器將發行的 JWT 令牌的 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 是授權伺服器支援的端點。此端點稱為 Provider Configuration 端點或 Authorization Server Metadata 端點。

就這樣!

啟動預期

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

它通過確定的啟動過程實現這一點

  1. 查詢 Provider Configuration 或 Authorization Server Metadata 端點以取得 jwks_url 屬性

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

  3. 設定驗證策略,以查詢 jwks_url 以取得找到的演算法的有效公鑰

  4. 設定驗證策略,以驗證每個 JWT 的 iss 宣告是否符合 idp.example.com

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

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

執行階段預期

應用程式啟動後,資源伺服器將嘗試處理任何包含 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 對應到具有字首 SCOPE_ 的授權。

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

預設情況下,產生的 Authentication#getPrincipal 是一個 Spring Security Jwt 物件,而 Authentication#getName 對應到 JWT 的 sub 屬性(如果存在)。

從這裡,考慮跳到

JWT 身份驗證如何運作

接下來,讓我們看看 Spring Security 用於支援基於 servlet 的應用程式(例如我們剛才看到的那個)中的 JWT 身份驗證的架構組件。

讓我們看看 JwtAuthenticationProvider 在 Spring Security 中如何運作。該圖說明了 AuthenticationManager 在來自 讀取 Bearer Token 的圖中的詳細運作方式。

jwtauthenticationprovider
圖 1. JwtAuthenticationProvider 用法

number 1 來自 讀取 Bearer Token 的身份驗證 FilterBearerTokenAuthenticationToken 傳遞給由 ProviderManager 實作的 AuthenticationManager

number 2 ProviderManager 設定為使用類型為 JwtAuthenticationProviderAuthenticationProvider

number 3 JwtAuthenticationProvider 使用 JwtDecoder 解碼、驗證和驗證 Jwt

number 4 然後 JwtAuthenticationProvider 使用 JwtAuthenticationConverterJwt 轉換為授權的 Collection

number 5 當身份驗證成功時,傳回的 Authentication 的類型為 JwtAuthenticationToken,並且具有一個 principal,即由設定的 JwtDecoder 傳回的 Jwt。最終,傳回的 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 {
        authorizeRequests {
            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 {
            authorizeRequests {
                authorize("/messages/**", hasScope("message:read"))
                authorize(anyRequest, authenticated)
            }
            oauth2ResourceServer {
                jwt {
                    jwtAuthenticationConverter = myConverter()
                }
            }
        }
        return http.build()
    }
}

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

oauth2ResourceServer DSL 上的方法也將覆寫或取代自動設定。

例如,Spring Boot 建立的第二個 @BeanJwtDecoder,它String 令牌解碼為驗證過的 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,則可以在 XML 中指定這兩個組件 - 篩選器鏈和 JwtDecoder

篩選器鏈的指定方式如下

預設 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 {
            authorizeRequests {
                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 {
            authorizeRequests {
                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 @Beandecoder() 具有相同的效果。您可以使用 jwkSetUri 建構一個,如下所示

  • Java

  • Kotlin

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

或者您可以使用 issuer,並讓 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(或授權),例如

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

在這種情況下,資源伺服器將嘗試強制將這些 scope 轉換為授權列表,並在每個 scope 前面加上字串 "SCOPE_"。

這表示要使用從 JWT 衍生的 scope 保護端點或方法,相應的表達式應包含此前綴

授權設定
  • 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(OAuth2ResourceServerConfigurer::jwt);
        return http.build();
    }
}
import org.springframework.security.oauth2.core.authorization.OAuth2AuthorizationManagers.hasScope;

@Configuration
@EnableWebSecurity
class DirectlyConfiguredJwkSetUri {
    @Bean
    open fun filterChain(http: HttpSecurity): SecurityFilterChain {
        http {
            authorizeRequests {
                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 {
            authorizeRequests {
                authorize(anyRequest, authenticated)
            }
           oauth2ResourceServer {
               jwt {
                   jwtAuthenticationConverter = CustomAuthenticationConverter()
               }
           }
        }
        return http.build()
    }
}

設定驗證

使用 最小 Spring Boot 設定,指示授權伺服器的 issuer 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 秒的時鐘偏差。

設定自訂驗證器

使用 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

String

jti

String

nbf

Instant

sub

String

可以使用 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 的建構函式。