OAuth 2.0 資源伺服器 JWT
JWT 的最小依賴項
大多數資源伺服器支援都集中在 spring-security-oauth2-resource-server 中。然而,解碼和驗證 JWT 的支援在 spring-security-oauth2-jose 中,這意味著兩者都是必需的,才能擁有一個支援 JWT 編碼的 Bearer Token 的工作資源伺服器。
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 Token。
它透過確定性啟動過程實現這一點
-
訪問提供程式配置或授權伺服器元資料端點,處理響應中的
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 Token 規範處理請求。
給定一個格式良好的 JWT,資源伺服器
-
根據在啟動時從
jwks_url端點獲取並與 JWT 頭匹配的公鑰驗證其簽名。 -
驗證 JWT 的
exp和nbf時間戳以及 JWT 的iss宣告。 -
將每個範圍對映到一個以
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((authorize) -> authorize
.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((authorize) -> authorize
.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 會呼叫提供程式配置或授權伺服器元資料端點以派生 JWK Set URI。如果應用程式沒有暴露 |
其配置可以透過使用 jwkSetUri() 來覆蓋,或者透過使用 decoder() 來替換。
使用 jwkSetUri()
你可以將授權伺服器的 JWK Set URI 配置為配置屬性或在 DSL 中提供它
-
Java
-
Kotlin
@Bean
SecurityWebFilterChain springSecurityFilterChain(ServerHttpSecurity http) {
http
.authorizeExchange((authorize) -> authorize
.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((authorize) -> authorize
.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_ 字首。
這意味著,要使用從 JWT 派生的範圍保護端點或方法,相應的表示式應包含此字首
-
Java
-
Kotlin
import static org.springframework.security.oauth2.core.authorization.OAuth2ReactiveAuthorizationManagers.hasScope;
@Bean
SecurityWebFilterChain springSecurityFilterChain(ServerHttpSecurity http) {
http
.authorizeExchange((authorize) -> authorize
.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((authorize) -> authorize
.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 配置,指示授權伺服器的頒發者 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 秒。 |
配置 RFC 9068 驗證
如果您需要要求令牌滿足 RFC 9068,您可以按以下方式配置驗證
-
Java
-
Kotlin
@Bean
JwtDecoder jwtDecoder() {
NimbusReactiveJwtDecoder jwtDecoder = NimbusReactiveJwtDecoder.withIssuerLocation(issuerUri)
.validateTypes(false).build();
jwtDecoder.setJwtValidator(JwtValidators.createAtJwtValidator()
.audience("https://audience.example.org")
.clientId("client-identifier")
.issuer("https://issuer.example.org").build());
return jwtDecoder;
}
@Bean
fun jwtDecoder(): JwtDecoder {
val jwtDecoder = NimbusReactiveJwtDecoder.withIssuerLocation(issuerUri)
.validateTypes(false).build()
jwtDecoder.setJwtValidator(JwtValidators.createAtJwtValidator()
.audience("https://audience.example.org")
.clientId("client-identifier")
.issuer("https://issuer.example.org").build())
return jwtDecoder
}
配置自定義驗證器
您可以使用 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
}