OAuth 2.0 資源伺服器 JWT
JWT 的最少依賴
大多數資源伺服器的支援都收集在 spring-security-oauth2-resource-server
中。然而,解碼和驗證 JWT 的支援在 spring-security-oauth2-jose
中,這意味著兩者都是擁有一個支援 JWT 編碼的 Bearer 令牌的正常工作的資源伺服器所必需的。
JWT 的最少配置
使用 Spring Boot 時,將應用程式配置為資源伺服器包含兩個基本步驟。首先,包含所需的依賴項,其次,指明授權伺服器的位置。
指定授權伺服器
在 Spring Boot 應用程式中,要指定使用哪個授權伺服器,只需執行以下操作:
spring:
security:
oauth2:
resourceserver:
jwt:
issuer-uri: https://idp.example.com/issuer
其中 idp.example.com/issuer
是授權伺服器將頒發的 JWT 令牌中 iss
宣告的值。資源伺服器將使用此屬性進一步自行配置、發現授權伺服器的公鑰,並隨後驗證傳入的 JWT。
就這樣!
啟動期望
使用此屬性和這些依賴項時,資源伺服器將自動配置自身以驗證 JWT 編碼的 Bearer 令牌。
它透過確定性的啟動過程實現此目的:
-
查詢提供商配置或授權伺服器元資料端點以獲取
jwks_url
屬性 -
查詢
jwks_url
端點以獲取支援的演算法 -
配置驗證策略以查詢
jwks_url
以獲取找到的演算法的有效公鑰 -
配置驗證策略以針對
idp.example.com
驗證每個 JWT 的iss
宣告。
此過程的一個結果是,授權伺服器必須啟動並接收請求,資源伺服器才能成功啟動。
如果在資源伺服器查詢授權伺服器時(給定適當的超時時間)授權伺服器宕機,則啟動將失敗。 |
執行時期望
應用程式啟動後,資源伺服器將嘗試處理包含 Authorization: Bearer
頭的任何請求
GET / HTTP/1.1
Authorization: Bearer some-token-value # Resource Server will process this
只要指明瞭此方案,資源伺服器將嘗試根據 Bearer 令牌規範處理請求。
給定一個格式正確的 JWT,資源伺服器將:
-
根據啟動期間從
jwks_url
端點獲取並與 JWT 匹配的公鑰驗證其簽名 -
驗證 JWT 的
exp
和nbf
時間戳以及 JWT 的iss
宣告,並且 -
將每個範圍對映到帶有字首
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 中如何工作。下圖解釋了從 讀取 Bearer 令牌 圖中的 AuthenticationManager
的工作細節。

JwtAuthenticationProvider
用法 來自 讀取 Bearer 令牌 的認證
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 上提供。 |
提供 Audience
如前所述,issuer-uri
屬性驗證 iss
宣告;這是指誰傳送了 JWT。
Boot 還具有 audiences
屬性,用於驗證 aud
宣告;這是指 JWT 被髮送給了誰。
資源伺服器的 audience 可以如下所示指定:
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
範圍。
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 會呼叫提供商配置或授權伺服器元資料端點,以便派生 JWK Set Uri。 |
如果應用程式未暴露 JwtDecoder
bean,則 Spring Boot 將暴露上述預設 bean。
其配置可以使用 jwkSetUri()
進行覆蓋,或使用 decoder()
進行替換。
或者,如果您根本不使用 Spring Boot,那麼這兩個元件——過濾鏈和 JwtDecoder
——都可以在 XML 中指定。
過濾鏈如下指定:
-
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()
,它將完全替換 JwtDecoder
的任何 Boot 自動配置
-
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
,它除了配置解碼器的 validator 外,還執行上述操作
-
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" : "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 {
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
時間戳宣告。
在需要自定義驗證的情況下,資源伺服器提供了兩個標準 validator,並且也接受自定義的 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 秒的時鐘偏差。 |
配置自定義 Validator
使用 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 set 5 分鐘,您可能需要調整此設定。此外,它沒有考慮到更復雜的快取模式,如逐出或使用共享快取。
要調整資源伺服器快取 JWK set 的方式,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 的建構函式。 |