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 令牌。
它透過確定性的啟動程序來達成此目的
-
命中提供者組態或授權伺服器 Metadata 端點,處理回應以取得
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 對應到具有前綴
SCOPE_
的授權。
當授權伺服器提供新的金鑰時,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(如先前的清單所示)。
若要取代它,請在應用程式中公開 @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 { }
}
}
}
先前的組態需要 message:read
scope 用於任何以 /messages/
開頭的 URL。
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)
}
呼叫 |
可以使用 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()
更強大,因為它完全取代了 JwtDecoder
的任何 Spring Boot 自動組態
-
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 Builder 自訂此行為。
使用 Spring Boot 自訂信任演算法
設定演算法最簡單的方法是作為屬性
spring:
security:
oauth2:
resourceserver:
jwt:
jws-algorithms: RS512
jwk-set-uri: https://idp.example.org/.well-known/jwks.json
使用 Builder 自訂信任演算法
為了獲得更大的能力,我們可以使用 NimbusReactiveJwtDecoder
隨附的 builder
-
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 或透過 使用 Builder 提供公開金鑰。
透過 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
使用 Builder
若要直接連線 RSAPublicKey
,請使用適當的 NimbusReactiveJwtDecoder
builder
-
Java
-
Kotlin
@Bean
public ReactiveJwtDecoder jwtDecoder() {
return NimbusReactiveJwtDecoder.withPublicKey(this.key).build();
}
@Bean
fun jwtDecoder(): ReactiveJwtDecoder {
return NimbusReactiveJwtDecoder.withPublicKey(key).build()
}
信任單一對稱金鑰
您也可以使用單一對稱金鑰。您可以載入 SecretKey
並使用適當的 NimbusReactiveJwtDecoder
builder
-
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(或授權) — 例如
{ ..., "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
}