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-configuration 、idp.example.com/.well-known/openid-configuration/issuer 或 idp.example.com/.well-known/oauth-authorization-server/issuer 是授權伺服器支援的端點。此端點稱為 Provider Configuration 端點或 Authorization Server Metadata 端點。 |
就這樣!
啟動預期
當使用此屬性和這些依賴項時,資源伺服器將自動設定自身以驗證 JWT 編碼的 Bearer Tokens。
它通過確定的啟動過程實現這一點
-
查詢 Provider Configuration 或 Authorization Server Metadata 端點以取得
jwks_url
屬性 -
查詢
jwks_url
端點以取得支援的演算法 -
設定驗證策略,以查詢
jwks_url
以取得找到的演算法的有效公鑰 -
設定驗證策略,以驗證每個 JWT 的
iss
宣告是否符合idp.example.com
。
此過程的一個結果是,授權伺服器必須啟動並接收請求,資源伺服器才能成功啟動。
如果資源伺服器查詢授權伺服器時,授權伺服器已關閉(在適當的超時時間內),則啟動將失敗。 |
執行階段預期
應用程式啟動後,資源伺服器將嘗試處理任何包含 Authorization: Bearer
標頭的請求
GET / HTTP/1.1
Authorization: Bearer some-token-value # Resource Server will process this
只要指示了此方案,資源伺服器將嘗試根據 Bearer Token 規範處理請求。
給定格式正確的 JWT,資源伺服器將
-
根據從啟動期間從
jwks_url
端點取得並與 JWT 匹配的公鑰驗證其簽名 -
驗證 JWT 的
exp
和nbf
時間戳記以及 JWT 的iss
宣告,以及 -
將每個 scope 對應到具有字首
SCOPE_
的授權。
隨著授權伺服器提供新的金鑰,Spring Security 將自動輪換用於驗證 JWT 的金鑰。 |
預設情況下,產生的 Authentication#getPrincipal
是一個 Spring Security Jwt
物件,而 Authentication#getName
對應到 JWT 的 sub
屬性(如果存在)。
從這裡,考慮跳到
JWT 身份驗證如何運作
接下來,讓我們看看 Spring Security 用於支援基於 servlet 的應用程式(例如我們剛才看到的那個)中的 JWT 身份驗證的架構組件。
JwtAuthenticationProvider
是一個 AuthenticationProvider
實作,它利用 JwtDecoder
和 JwtAuthenticationConverter
來驗證 JWT。
讓我們看看 JwtAuthenticationProvider
在 Spring Security 中如何運作。該圖說明了 AuthenticationManager
在來自 讀取 Bearer Token 的圖中的詳細運作方式。

JwtAuthenticationProvider
用法 來自 讀取 Bearer Token 的身份驗證
Filter
將 BearerTokenAuthenticationToken
傳遞給由 ProviderManager
實作的 AuthenticationManager
。
ProviderManager
設定為使用類型為 JwtAuthenticationProvider
的 AuthenticationProvider。
JwtAuthenticationProvider
使用 JwtDecoder
解碼、驗證和驗證 Jwt
。
然後
JwtAuthenticationProvider
使用 JwtAuthenticationConverter
將 Jwt
轉換為授權的 Collection
。
當身份驗證成功時,傳回的
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
看起來像
-
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 一樣簡單
-
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 建立的第二個 @Bean
是 JwtDecoder
,它將 String
令牌解碼為驗證過的 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
。
篩選器鏈的指定方式如下
-
Xml
<http>
<intercept-uri pattern="/**" access="authenticated"/>
<oauth2-resource-server>
<jwt decoder-ref="jwtDecoder"/>
</oauth2-resource-server>
</http>
JwtDecoder
的指定方式如下
-
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 中提供
-
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
-
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
@Bean
與 decoder()
具有相同的效果。您可以使用 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 Boot、NimbusJwtDecoder 建構器或來自 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
信任單個對稱金鑰
使用單個對稱金鑰也很簡單。您可以簡單地載入 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 通常會具有 scope
或 scp
屬性,指示已授予它的 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
宣告以及 exp
和 nbf
時間戳記宣告。
在需要自訂驗證的情況下,資源伺服器附帶了兩個標準驗證器,並且還接受自訂 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 類型 |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
可以使用 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 的建構函式。 |