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 令牌。
它透過一個確定性的啟動過程實現這一點
-
訪問 Provider Configuration 或 Authorization Server Metadata 端點,處理響應以獲取
jwks_url
屬性。 -
配置驗證策略以查詢
jwks_url
獲取有效的公鑰。 -
配置驗證策略以針對
idp.example.com
驗證每個 JWT 的iss
宣告。
此過程的一個結果是,授權伺服器必須接收請求,以便資源伺服器成功啟動。
如果資源伺服器在查詢授權伺服器時(在適當的超時設定下)授權伺服器宕機,則啟動將失敗。 |
執行時期望
應用程式啟動後,資源伺服器會嘗試處理任何包含 Authorization: Bearer
header 的請求
GET / HTTP/1.1
Authorization: Bearer some-token-value # Resource Server will process this
只要指定了這種方案,資源伺服器就會嘗試根據 Bearer 令牌規範處理請求。
給定一個格式良好的 JWT,資源伺服器會
-
根據啟動期間從
jwks_url
端點獲取並與 JWT header 匹配的公鑰驗證其簽名。 -
驗證 JWT 的
exp
和nbf
時間戳以及 JWT 的iss
宣告。 -
將每個 scope 對映到帶有字首
SCOPE_
的 authority。
隨著授權伺服器提供新金鑰,Spring Security 會自動輪換用於驗證 JWT 令牌的金鑰。 |
預設情況下,生成的 Authentication#getPrincipal
是一個 Spring Security Jwt
物件,如果存在,Authentication#getName
會對映到 JWT 的 sub
屬性。
從這裡,你可以考慮跳轉到
直接指定授權伺服器 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 上提供此屬性。 |
覆蓋或替換 Boot 自動配置
Spring Boot 為資源伺服器生成兩個 @Bean
物件。
第一個 bean 是一個 SecurityWebFilterChain
,它將應用程式配置為資源伺服器。當包含 spring-security-oauth2-jose
時,這個 SecurityWebFilterChain
看起來像
-
Java
-
Kotlin
@Bean
SecurityWebFilterChain springSecurityFilterChain(ServerHttpSecurity http) {
http
.authorizeExchange(exchanges -> exchanges
.anyExchange().authenticated()
)
.oauth2ResourceServer(OAuth2ResourceServerSpec::jwt)
return http.build();
}
@Bean
fun springSecurityFilterChain(http: ServerHttpSecurity): SecurityWebFilterChain {
return http {
authorizeExchange {
authorize(anyExchange, authenticated)
}
oauth2ResourceServer {
jwt { }
}
}
}
如果應用程式沒有暴露 SecurityWebFilterChain
bean,Spring Boot 會暴露預設的那個(如前所示)。
要替換它,在應用程式中暴露 @Bean
-
Java
-
Kotlin
import static org.springframework.security.oauth2.core.authorization.OAuth2ReactiveAuthorizationManagers.hasScope;
@Bean
SecurityWebFilterChain springSecurityFilterChain(ServerHttpSecurity http) {
http
.authorizeExchange(exchanges -> exchanges
.pathMatchers("/message/**").access(hasScope("message:read"))
.anyExchange().authenticated()
)
.oauth2ResourceServer(oauth2 -> oauth2
.jwt(withDefaults())
);
return http.build();
}
import org.springframework.security.oauth2.core.authorization.OAuth2ReactiveAuthorizationManagers.hasScope
@Bean
fun springSecurityFilterChain(http: ServerHttpSecurity): SecurityWebFilterChain {
return http {
authorizeExchange {
authorize("/message/**", hasScope("message:read"))
authorize(anyExchange, authenticated)
}
oauth2ResourceServer {
jwt { }
}
}
}
前面的配置要求任何以 /messages/
開頭的 URL 都需要 message:read
範圍。
oauth2ResourceServer
DSL 中的方法也會覆蓋或替換自動配置。
例如,Spring Boot 建立的第二個 @Bean
是一個 ReactiveJwtDecoder
,它將 String
令牌解碼為已驗證的 Jwt
例項
-
Java
-
Kotlin
@Bean
public ReactiveJwtDecoder jwtDecoder() {
return ReactiveJwtDecoders.fromIssuerLocation(issuerUri);
}
@Bean
fun jwtDecoder(): ReactiveJwtDecoder {
return ReactiveJwtDecoders.fromIssuerLocation(issuerUri)
}
呼叫 ReactiveJwtDecoders#fromIssuerLocation 會呼叫 Provider Configuration 或 Authorization Server Metadata 端點來推導 JWK Set URI。如果應用程式沒有暴露 |
它的配置可以透過使用 jwkSetUri()
來覆蓋,或者透過使用 decoder()
來替換。
使用 jwkSetUri()
你可以將授權伺服器的 JWK Set URI 配置為屬性 或在 DSL 中提供
-
Java
-
Kotlin
@Bean
SecurityWebFilterChain springSecurityFilterChain(ServerHttpSecurity http) {
http
.authorizeExchange(exchanges -> exchanges
.anyExchange().authenticated()
)
.oauth2ResourceServer(oauth2 -> oauth2
.jwt(jwt -> jwt
.jwkSetUri("https://idp.example.com/.well-known/jwks.json")
)
);
return http.build();
}
@Bean
fun springSecurityFilterChain(http: ServerHttpSecurity): SecurityWebFilterChain {
return http {
authorizeExchange {
authorize(anyExchange, authenticated)
}
oauth2ResourceServer {
jwt {
jwkSetUri = "https://idp.example.com/.well-known/jwks.json"
}
}
}
}
使用 jwkSetUri()
優先於任何配置屬性。
使用 decoder()
decoder()
比 jwkSetUri()
更強大,因為它完全替換了 Spring Boot 對 JwtDecoder
的任何自動配置
-
Java
-
Kotlin
@Bean
SecurityWebFilterChain springSecurityFilterChain(ServerHttpSecurity http) {
http
.authorizeExchange(exchanges -> exchanges
.anyExchange().authenticated()
)
.oauth2ResourceServer(oauth2 -> oauth2
.jwt(jwt -> jwt
.decoder(myCustomDecoder())
)
);
return http.build();
}
@Bean
fun springSecurityFilterChain(http: ServerHttpSecurity): SecurityWebFilterChain {
return http {
authorizeExchange {
authorize(anyExchange, authenticated)
}
oauth2ResourceServer {
jwt {
jwtDecoder = myCustomDecoder()
}
}
}
}
當你需要更深入的配置(例如驗證)時,這非常有用。
暴露 ReactiveJwtDecoder
的 @Bean
或者,暴露 ReactiveJwtDecoder
的 @Bean
與使用 decoder()
效果相同:你可以像這樣用 jwkSetUri
構建一個
-
Java
-
Kotlin
@Bean
public ReactiveJwtDecoder jwtDecoder() {
return NimbusReactiveJwtDecoder.withJwkSetUri(jwkSetUri).build();
}
@Bean
fun jwtDecoder(): ReactiveJwtDecoder {
return NimbusReactiveJwtDecoder.withJwkSetUri(jwkSetUri).build()
}
或者你可以使用 issuer,讓 NimbusReactiveJwtDecoder
在呼叫 build()
時查詢 jwkSetUri
,如下所示
-
Java
-
Kotlin
@Bean
public ReactiveJwtDecoder jwtDecoder() {
return NimbusReactiveJwtDecoder.withIssuerLocation(issuer).build();
}
@Bean
fun jwtDecoder(): ReactiveJwtDecoder {
return NimbusReactiveJwtDecoder.withIssuerLocation(issuer).build()
}
或者,如果預設設定適合你,你也可以使用 JwtDecoders
,它除了配置解碼器的驗證器外,還執行上述操作
-
Java
-
Kotlin
@Bean
public ReactiveJwtDecoder jwtDecoder() {
return ReactiveJwtDecoders.fromIssuerLocation(issuer);
}
@Bean
fun jwtDecoder(): ReactiveJwtDecoder {
return ReactiveJwtDecoders.fromIssuerLocation(issuer)
}
配置可信演算法
預設情況下,NimbusReactiveJwtDecoder
,因此資源伺服器,只信任和驗證使用 RS256
的令牌。
你可以使用 Spring Boot 或透過使用 NimbusJwtDecoder 構建器來自定義此行為。
使用 Spring Boot 自定義可信演算法
設定演算法最簡單的方法是將其作為屬性
spring:
security:
oauth2:
resourceserver:
jwt:
jws-algorithms: RS512
jwk-set-uri: https://idp.example.org/.well-known/jwks.json
透過使用構建器自定義可信演算法
然而,為了獲得更大的能力,我們可以使用 NimbusReactiveJwtDecoder
自帶的構建器
-
Java
-
Kotlin
@Bean
ReactiveJwtDecoder jwtDecoder() {
return NimbusReactiveJwtDecoder.withIssuerLocation(this.issuer)
.jwsAlgorithm(RS512).build();
}
@Bean
fun jwtDecoder(): ReactiveJwtDecoder {
return NimbusReactiveJwtDecoder.withIssuerLocation(this.issuer)
.jwsAlgorithm(RS512).build()
}
多次呼叫 jwsAlgorithm
會配置 NimbusReactiveJwtDecoder
信任多個演算法
-
Java
-
Kotlin
@Bean
ReactiveJwtDecoder jwtDecoder() {
return NimbusReactiveJwtDecoder.withIssuerLocation(this.issuer)
.jwsAlgorithm(RS512).jwsAlgorithm(ES512).build();
}
@Bean
fun jwtDecoder(): ReactiveJwtDecoder {
return NimbusReactiveJwtDecoder.withIssuerLocation(this.issuer)
.jwsAlgorithm(RS512).jwsAlgorithm(ES512).build()
}
或者,你可以呼叫 jwsAlgorithms
-
Java
-
Kotlin
@Bean
ReactiveJwtDecoder jwtDecoder() {
return NimbusReactiveJwtDecoder.withIssuerLocation(this.jwkSetUri)
.jwsAlgorithms(algorithms -> {
algorithms.add(RS512);
algorithms.add(ES512);
}).build();
}
@Bean
fun jwtDecoder(): ReactiveJwtDecoder {
return NimbusReactiveJwtDecoder.withIssuerLocation(this.jwkSetUri)
.jwsAlgorithms {
it.add(RS512)
it.add(ES512)
}
.build()
}
信任單個非對稱金鑰
比使用 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: ConfigurableListableBeanFactory ->
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
並使用適當的 NimbusReactiveJwtDecoder
構建器
-
Java
-
Kotlin
@Bean
public ReactiveJwtDecoder jwtDecoder() {
return NimbusReactiveJwtDecoder.withSecretKey(this.key).build();
}
@Bean
fun jwtDecoder(): ReactiveJwtDecoder {
return NimbusReactiveJwtDecoder.withSecretKey(this.key).build()
}
配置授權
從 OAuth 2.0 授權伺服器頒發的 JWT 通常具有 scope
或 scp
屬性,指示已授予的範圍(或許可權)—— 例如
{ ..., "scope" : "messages contacts"}
在這種情況下,資源伺服器會嘗試將這些 scope 強制轉換為授予許可權列表,並在每個 scope 前加上字串 SCOPE_
。
這意味著,要使用從 JWT 派生的 scope 保護端點或方法,相應的表示式應包含此字首
-
Java
-
Kotlin
import static org.springframework.security.oauth2.core.authorization.OAuth2ReactiveAuthorizationManagers.hasScope;
@Bean
SecurityWebFilterChain springSecurityFilterChain(ServerHttpSecurity http) {
http
.authorizeExchange(exchanges -> exchanges
.mvcMatchers("/contacts/**").access(hasScope("contacts"))
.mvcMatchers("/messages/**").access(hasScope("messages"))
.anyExchange().authenticated()
)
.oauth2ResourceServer(OAuth2ResourceServerSpec::jwt);
return http.build();
}
import org.springframework.security.oauth2.core.authorization.OAuth2ReactiveAuthorizationManagers.hasScope
@Bean
fun springSecurityFilterChain(http: ServerHttpSecurity): SecurityWebFilterChain {
return http {
authorizeExchange {
authorize("/contacts/**", hasScope("contacts"))
authorize("/messages/**", hasScope("messages"))
authorize(anyExchange, authenticated)
}
oauth2ResourceServer {
jwt { }
}
}
}
你可以使用方法安全執行類似操作
-
Java
-
Kotlin
@PreAuthorize("hasAuthority('SCOPE_messages')")
public Flux<Message> getMessages(...) {}
@PreAuthorize("hasAuthority('SCOPE_messages')")
fun getMessages(): Flux<Message> { }
手動提取許可權
然而,在某些情況下,此預設設定不足。例如,一些授權伺服器不使用 scope
屬性。相反,它們有自己的自定義屬性。在其他時候,資源伺服器可能需要將屬性或屬性組合適配到內部許可權。
為此,DSL 暴露了 jwtAuthenticationConverter()
-
Java
-
Kotlin
@Bean
SecurityWebFilterChain springSecurityFilterChain(ServerHttpSecurity http) {
http
.authorizeExchange(exchanges -> exchanges
.anyExchange().authenticated()
)
.oauth2ResourceServer(oauth2 -> oauth2
.jwt(jwt -> jwt
.jwtAuthenticationConverter(grantedAuthoritiesExtractor())
)
);
return http.build();
}
Converter<Jwt, Mono<AbstractAuthenticationToken>> grantedAuthoritiesExtractor() {
JwtAuthenticationConverter jwtAuthenticationConverter =
new JwtAuthenticationConverter();
jwtAuthenticationConverter.setJwtGrantedAuthoritiesConverter
(new GrantedAuthoritiesExtractor());
return new ReactiveJwtAuthenticationConverterAdapter(jwtAuthenticationConverter);
}
@Bean
fun springSecurityFilterChain(http: ServerHttpSecurity): SecurityWebFilterChain {
return http {
authorizeExchange {
authorize(anyExchange, authenticated)
}
oauth2ResourceServer {
jwt {
jwtAuthenticationConverter = grantedAuthoritiesExtractor()
}
}
}
}
fun grantedAuthoritiesExtractor(): Converter<Jwt, Mono<AbstractAuthenticationToken>> {
val jwtAuthenticationConverter = JwtAuthenticationConverter()
jwtAuthenticationConverter.setJwtGrantedAuthoritiesConverter(GrantedAuthoritiesExtractor())
return ReactiveJwtAuthenticationConverterAdapter(jwtAuthenticationConverter)
}
jwtAuthenticationConverter()
負責將 Jwt
轉換為 Authentication
。作為其配置的一部分,我們可以提供一個輔助轉換器,將 Jwt
轉換為授予許可權的 Collection
。
該最終轉換器可能類似於以下 GrantedAuthoritiesExtractor
-
Java
-
Kotlin
static class GrantedAuthoritiesExtractor
implements Converter<Jwt, Collection<GrantedAuthority>> {
public Collection<GrantedAuthority> convert(Jwt jwt) {
Collection<?> authorities = (Collection<?>)
jwt.getClaims().getOrDefault("mycustomclaim", Collections.emptyList());
return authorities.stream()
.map(Object::toString)
.map(SimpleGrantedAuthority::new)
.collect(Collectors.toList());
}
}
internal class GrantedAuthoritiesExtractor : Converter<Jwt, Collection<GrantedAuthority>> {
override fun convert(jwt: Jwt): Collection<GrantedAuthority> {
val authorities: List<Any> = jwt.claims
.getOrDefault("mycustomclaim", emptyList<Any>()) as List<Any>
return authorities
.map { it.toString() }
.map { SimpleGrantedAuthority(it) }
}
}
為了更大的靈活性,DSL 支援用任何實現 Converter<Jwt, Mono<AbstractAuthenticationToken>>
的類完全替換轉換器
-
Java
-
Kotlin
static class CustomAuthenticationConverter implements Converter<Jwt, Mono<AbstractAuthenticationToken>> {
public AbstractAuthenticationToken convert(Jwt jwt) {
return Mono.just(jwt).map(this::doConversion);
}
}
internal class CustomAuthenticationConverter : Converter<Jwt, Mono<AbstractAuthenticationToken>> {
override fun convert(jwt: Jwt): Mono<AbstractAuthenticationToken> {
return Mono.just(jwt).map(this::doConversion)
}
}
配置驗證
使用最少的 Spring Boot 配置,指示授權伺服器的 issuer URI,資源伺服器預設會驗證 iss
宣告以及 exp
和 nbf
時間戳宣告。
在需要自定義驗證需求的情況下,資源伺服器提供了兩個標準驗證器,並且還接受自定義的 OAuth2TokenValidator
例項。
自定義時間戳驗證
JWT 例項通常有一個有效期視窗,視窗的開始由 nbf
宣告指示,結束由 exp
宣告指示。
然而,每個伺服器都可能出現時鐘漂移,這可能導致令牌在一個伺服器上看起來已過期,但在另一個伺服器上未過期。隨著分散式系統中協作伺服器數量的增加,這可能會導致一些實現上的問題。
資源伺服器使用 JwtTimestampValidator
來驗證令牌的有效期視窗,你可以使用 clockSkew
配置它以緩解時鐘漂移問題
-
Java
-
Kotlin
@Bean
ReactiveJwtDecoder jwtDecoder() {
NimbusReactiveJwtDecoder jwtDecoder = (NimbusReactiveJwtDecoder)
ReactiveJwtDecoders.fromIssuerLocation(issuerUri);
OAuth2TokenValidator<Jwt> withClockSkew = new DelegatingOAuth2TokenValidator<>(
new JwtTimestampValidator(Duration.ofSeconds(60)),
new IssuerValidator(issuerUri));
jwtDecoder.setJwtValidator(withClockSkew);
return jwtDecoder;
}
@Bean
fun jwtDecoder(): ReactiveJwtDecoder {
val jwtDecoder = ReactiveJwtDecoders.fromIssuerLocation(issuerUri) as NimbusReactiveJwtDecoder
val withClockSkew: OAuth2TokenValidator<Jwt> = DelegatingOAuth2TokenValidator(
JwtTimestampValidator(Duration.ofSeconds(60)),
JwtIssuerValidator(issuerUri))
jwtDecoder.setJwtValidator(withClockSkew)
return jwtDecoder
}
預設情況下,資源伺服器配置了 60 秒的時鐘偏移。 |
配置自定義驗證器
你可以使用 OAuth2TokenValidator
API 新增對 aud
宣告的檢查
-
Java
-
Kotlin
public class AudienceValidator implements OAuth2TokenValidator<Jwt> {
OAuth2Error error = new OAuth2Error("invalid_token", "The required audience is missing", null);
public OAuth2TokenValidatorResult validate(Jwt jwt) {
if (jwt.getAudience().contains("messaging")) {
return OAuth2TokenValidatorResult.success();
} else {
return OAuth2TokenValidatorResult.failure(error);
}
}
}
class AudienceValidator : OAuth2TokenValidator<Jwt> {
var error: OAuth2Error = OAuth2Error("invalid_token", "The required audience is missing", null)
override fun validate(jwt: Jwt): OAuth2TokenValidatorResult {
return if (jwt.audience.contains("messaging")) {
OAuth2TokenValidatorResult.success()
} else {
OAuth2TokenValidatorResult.failure(error)
}
}
}
然後,要將其新增到資源伺服器中,你可以指定 ReactiveJwtDecoder
例項
-
Java
-
Kotlin
@Bean
ReactiveJwtDecoder jwtDecoder() {
NimbusReactiveJwtDecoder jwtDecoder = (NimbusReactiveJwtDecoder)
ReactiveJwtDecoders.fromIssuerLocation(issuerUri);
OAuth2TokenValidator<Jwt> audienceValidator = new AudienceValidator();
OAuth2TokenValidator<Jwt> withIssuer = JwtValidators.createDefaultWithIssuer(issuerUri);
OAuth2TokenValidator<Jwt> withAudience = new DelegatingOAuth2TokenValidator<>(withIssuer, audienceValidator);
jwtDecoder.setJwtValidator(withAudience);
return jwtDecoder;
}
@Bean
fun jwtDecoder(): ReactiveJwtDecoder {
val jwtDecoder = ReactiveJwtDecoders.fromIssuerLocation(issuerUri) as NimbusReactiveJwtDecoder
val audienceValidator: OAuth2TokenValidator<Jwt> = AudienceValidator()
val withIssuer: OAuth2TokenValidator<Jwt> = JwtValidators.createDefaultWithIssuer(issuerUri)
val withAudience: OAuth2TokenValidator<Jwt> = DelegatingOAuth2TokenValidator(withIssuer, audienceValidator)
jwtDecoder.setJwtValidator(withAudience)
return jwtDecoder
}