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 令牌。

它透過確定性的啟動過程實現此目的:

  1. 查詢提供商配置或授權伺服器元資料端點以獲取 jwks_url 屬性

  2. 查詢 jwks_url 端點以獲取支援的演算法

  3. 配置驗證策略以查詢 jwks_url 以獲取找到的演算法的有效公鑰

  4. 配置驗證策略以針對 idp.example.com 驗證每個 JWT 的 iss 宣告。

此過程的一個結果是,授權伺服器必須啟動並接收請求,資源伺服器才能成功啟動。

如果在資源伺服器查詢授權伺服器時(給定適當的超時時間)授權伺服器宕機,則啟動將失敗。

執行時期望

應用程式啟動後,資源伺服器將嘗試處理包含 Authorization: Bearer 頭的任何請求

GET / HTTP/1.1
Authorization: Bearer some-token-value # Resource Server will process this

只要指明瞭此方案,資源伺服器將嘗試根據 Bearer 令牌規範處理請求。

給定一個格式正確的 JWT,資源伺服器將:

  1. 根據啟動期間從 jwks_url 端點獲取並與 JWT 匹配的公鑰驗證其簽名

  2. 驗證 JWT 的 expnbf 時間戳以及 JWT 的 iss 宣告,並且

  3. 將每個範圍對映到帶有字首 SCOPE_ 的許可權。

隨著授權伺服器提供新金鑰,Spring Security 將自動輪換用於驗證 JWT 的金鑰。

預設情況下,生成的 Authentication#getPrincipal 是一個 Spring Security Jwt 物件,如果存在,Authentication#getName 將對映到 JWT 的 sub 屬性。

從這裡,可以考慮跳轉到:

JWT 認證如何工作

接下來,讓我們看看 Spring Security 用於支援 servlet 應用程式(例如我們剛剛看到的應用程式)中 JWT 認證的架構元件。

讓我們看看 JwtAuthenticationProvider 在 Spring Security 中如何工作。下圖解釋了從 讀取 Bearer 令牌 圖中的 AuthenticationManager 的工作細節。

jwtauthenticationprovider
圖 1. JwtAuthenticationProvider 用法

number 1 來自 讀取 Bearer 令牌 的認證 FilterBearerTokenAuthenticationToken 傳遞給由 ProviderManager 實現的 AuthenticationManager

number 2 ProviderManager 配置為使用型別為 JwtAuthenticationProviderAuthenticationProvider

number 3 JwtAuthenticationProvider 使用 JwtDecoder 解碼、驗證和校驗 Jwt

number 4 然後 JwtAuthenticationProvider 使用 JwtAuthenticationConverterJwt 轉換為授予許可權的 Collection

number 5 認證成功時,返回的 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 如下所示:

預設 JWT 配置
  • 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 一樣簡單:

自定義 JWT 配置
  • 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 建立的第二個 @BeanJwtDecoder,它將 String 令牌解碼為經過驗證的 Jwt 例項

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 中指定。

過濾鏈如下指定:

預設 JWT 配置
  • Xml

<http>
    <intercept-uri pattern="/**" access="authenticated"/>
    <oauth2-resource-server>
        <jwt decoder-ref="jwtDecoder"/>
    </oauth2-resource-server>
</http>

JwtDecoder 如下指定:

JWT 解碼器
  • 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 中提供

JWK Set Uri 配置
  • 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 自動配置

JWT 解碼器配置
  • 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 @Beandecoder() 具有相同的效果。您可以像這樣構造一個帶有 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 BootNimbusJwtDecoder 構建器 或從 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

使用構建器

要直接注入一個 RSAPublicKey,您只需使用相應的 NimbusJwtDecoder 構建器,如下所示:

  • Java

  • Kotlin

@Bean
public JwtDecoder jwtDecoder() {
    return NimbusJwtDecoder.withPublicKey(this.key).build();
}
@Bean
fun jwtDecoder(): JwtDecoder {
    return NimbusJwtDecoder.withPublicKey(this.key).build()
}

信任單個對稱金鑰

使用單個對稱金鑰也很簡單。您只需載入您的 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 通常會包含 scopescp 屬性,指示已授予的範圍(或許可權),例如:

{ …​, "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 宣告以及 expnbf 時間戳宣告。

在需要自定義驗證的情況下,資源伺服器提供了兩個標準 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 型別

aud

Collection<String>

exp

Instant

iat

Instant

iss

String

jti

String

nbf

Instant

sub

String

可以使用 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 的建構函式。