1. 概述
在本教程中,我們將討論如何使我們的 Spring Security OAuth2 實現利用 JSON Web Token。
我們還將繼續構建 Spring REST API + OAuth2 + Angular 文章中的 OAuth 教程。
2. OAuth2 授權服務器
此前,Spring Security OAuth 棧提供了將授權服務器配置為 Spring Application 的可能性。 我們需要將其配置為使用 JwtTokenStore 以便使用 JWT 令牌。
然而,Spring Security OAuth 棧已被 Spring 棄用,現在我們將使用 Keycloak 作為我們的授權服務器。
因此,我們這次將授權服務器設置為 Spring Boot 應用中的嵌入式 Keycloak 服務器。 它默認發行 JWT 令牌,因此在這一方面不需要任何其他配置。
3. 資源服務器
現在,讓我們看看如何配置我們的資源服務器以使用 JWT。
我們將這在 application.yml 文件中完成:
server:
port: 8081
servlet:
context-path: /resource-server
spring:
jpa:
defer-datasource-initialization: true
security:
oauth2:
resourceserver:
jwt:
issuer-uri: http://localhost:8083/auth/realms/baeldung
jwk-set-uri: http://localhost:8083/auth/realms/baeldung/protocol/openid-connect/certsJWT 包含所有在令牌內的信息,因此資源服務器需要驗證令牌的簽名,以確保數據未被篡改。 jwk-set-uri 屬性包含服務器可用於此目的的公鑰。
issuer-uri 屬性指向基礎授權服務器 URI,也可用於驗證 iss 聲明作為額外的安全措施。
此外,如果未設置 jwk-set-uri 屬性,資源服務器將嘗試使用 issuer-uri 來確定該密鑰的存儲位置,從 授權服務器元數據端點。
請注意,添加 issuer-uri 屬性要求我們在啓動資源服務器應用程序之前必須運行授權服務器。
現在讓我們看看如何使用 Java 配置來配置 JWT 支持:
@Configuration
public class SecurityConfig extends WebSecurityConfigurerAdapter {
@Override
protected void configure(HttpSecurity http) throws Exception {
http.cors()
.and()
.authorizeRequests()
.antMatchers(HttpMethod.GET, "/user/info", "/api/foos/**")
.hasAuthority("SCOPE_read")
.antMatchers(HttpMethod.POST, "/api/foos")
.hasAuthority("SCOPE_write")
.anyRequest()
.authenticated()
.and()
.oauth2ResourceServer()
.jwt();
}
}在這裏,我們正在覆蓋默認的 Http Security 配置;我們需要明確指定希望它表現為 Resource Server,並且將使用 JWT 格式的 Access Tokens,分別使用 oauth2ResourceServer() 和 jwt() 方法。
上述 JWT 配置是 Spring Boot 默認實例提供的。正如稍後所見,它也可以進行自定義。
4. 訪問令牌中的自定義聲明
現在,我們將設置一些基礎設施,以便能夠向返回的授權服務器的訪問令牌中添加一些 訪問令牌中的自定義聲明。 框架提供的標準聲明都很好,但大多數情況下,我們需要在令牌中包含額外的信息,以便在客户端進行利用。
讓我們以一個自定義聲明為例,即 organization,它將包含給定用户組織的名稱。
4.1. 授權服務器配置
為此,我們需要向 realm 定義文件 baeldung-realm.json 添加一些配置:
- 添加屬性 organization 到用户 [email protected]:
"attributes" : {
"organization" : "baeldung"
},"protocolMappers": [{
"id": "06e5fc8f-3553-4c75-aef4-5a4d7bb6c0d1",
"name": "organization",
"protocol": "openid-connect",
"protocolMapper": "oidc-usermodel-attribute-mapper",
"consentRequired": false,
"config": {
"userinfo.token.claim": "true",
"user.attribute": "organization",
"id.token.claim": "true",
"access.token.claim": "true",
"claim.name": "organization",
"jsonType.label": "String"
}
}],對於獨立的 Keycloak 設置,也可以使用管理控制枱完成。
請務必記住,上述 JSON 配置是特定於 Keycloak 的,可能與其它 OAuth 服務器有所不同。
在新的配置運行成功後,我們將會獲得一個額外的屬性,<em organization = baeldung</em>,在令牌有效負載中用於 <em [email protected]</em>。
{
jti: "989ce5b7-50b9-4cc6-bc71-8f04a639461e"
exp: 1585242462
nbf: 0
iat: 1585242162
iss: "http://localhost:8083/auth/realms/baeldung"
sub: "a5461470-33eb-4b2d-82d4-b0484e96ad7f"
typ: "Bearer"
azp: "jwtClient"
auth_time: 1585242162
session_state: "384ca5cc-8342-429a-879c-c15329820006"
acr: "1"
scope: "profile write read"
organization: "baeldung"
preferred_username: "[email protected]"
}4.2. 在 Angular 客户端中使用訪問令牌
接下來,我們將利用訪問令牌信息在我們的 Angular 客户端應用程序中使用。我們將使用 angular2-jwt 庫來實現這一點。
我們將利用 組織機構聲明在我們的 AppService 中,並添加一個函數 getOrganization:
getOrganization(){
var token = Cookie.get("access_token");
var payload = this.jwtHelper.decodeToken(token);
this.organization = payload.organization;
return this.organization;
}<p>此函數利用 <em >JwtHelperService</em> 從 <em >angular2-jwt</em> 庫中解碼 Access Token 並獲取我們的自定義聲明。現在我們只需要在 <em >AppComponent</em> 中顯示它:</p>
@Component({
selector: 'app-root',
template: `<nav class="navbar navbar-default">
<div class="container-fluid">
<div class="navbar-header">
<a class="navbar-brand" href="/">Spring Security Oauth - Authorization Code</a>
</div>
</div>
<div class="navbar-brand">
<p>{{organization}}</p>
</div>
</nav>
<router-outlet></router-outlet>`
})
export class AppComponent implements OnInit {
public organization = "";
constructor(private service: AppService) { }
ngOnInit() {
this.organization = this.service.getOrganization();
}
}5. 訪問資源服務器上的額外聲明
但我們如何在資源服務器端訪問這些信息呢?
5.1. 訪問認證服務器聲明
這非常簡單,我們只需要從 org.springframework.security.oauth2.jwt.Jwt 的 AuthenticationPrincipal 中提取它,就像我們在 UserInfoController 中提取任何其他屬性一樣:
@GetMapping("/user/info")
public Map<String, Object> getUserInfo(@AuthenticationPrincipal Jwt principal) {
Map<String, String> map = new Hashtable<String, String>();
map.put("user_name", principal.getClaimAsString("preferred_username"));
map.put("organization", principal.getClaimAsString("organization"));
return Collections.unmodifiableMap(map);
}
5.2. 添加/刪除/重命名聲明的配置
現在,如果我們想在資源服務器端添加更多聲明,或者刪除或重命名一些聲明呢?
假設我們想要修改來自身份驗證服務器的 organization 聲明,將其值轉換為大寫。但是,如果聲明在用户中不存在,則需要將其值設置為 unknown。
要實現這一點,我們需要 添加一個實現 Converter 接口的類,並使用 MappedJwtClaimSetConverter 將聲明轉換為大寫:
public class OrganizationSubClaimAdapter 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 organization = convertedClaims.get("organization") != null ?
(String) convertedClaims.get("organization") : "unknown";
convertedClaims.put("organization", organization.toUpperCase());
return convertedClaims;
}
}然後,在我們的 SecurityConfig 類中,我們需要 添加我們自己的 JwtDecoder 實例 以覆蓋 Spring Boot 提供的實例,並設置其 OrganizationSubClaimAdapter 作為其 claims 轉換器:
@Bean
public JwtDecoder jwtDecoder(OAuth2ResourceServerProperties properties) {
NimbusJwtDecoder jwtDecoder = NimbusJwtDecoder.withJwkSetUri(
properties.getJwt().getJwkSetUri()).build();
jwtDecoder.setClaimSetConverter(new OrganizationSubClaimAdapter());
return jwtDecoder;
}
現在當我們調用 /user/info API 獲取用户 [email protected] 時,我們會得到 organization 的值為 UNKNOWN。
請注意,為了覆蓋 Spring Boot 默認配置的 JwtDecoder Bean,應謹慎操作,以確保所有必要的配置都包含在內。
6. 從 Java Keystore 加載密鑰
在之前的配置中,我們使用授權服務器的默認公鑰來驗證令牌的完整性。
我們還可以使用存儲在 Java Keystore 文件中的密鑰對和證書來執行簽名過程。
6.1. 生成 JKS Java 密鑰存儲文件
首先生成密鑰,更具體地説,使用命令行工具 keytool 生成 .jks 文件。
keytool -genkeypair -alias mytest
-keyalg RSA
-keypass mypass
-keystore mytest.jks
-storepass mypass該命令將生成一個名為 mytest.jks 的文件,其中包含我們的公鑰和私鑰。
請確保 keypass 和 storepass 相同。
6.2. 導出公鑰
接下來,我們需要從生成的 JKS 中導出公鑰。可以使用以下命令執行此操作:
keytool -list -rfc --keystore mytest.jks | openssl x509 -inform pem -pubkey以下是一個示例響應:
-----BEGIN PUBLIC KEY-----
MIIBIjANBgkqhkiG9w0BAQEFAAOCAQ8AMIIBCgKCAQEAgIK2Wt4x2EtDl41C7vfp
OsMquZMyOyteO2RsVeMLF/hXIeYvicKr0SQzVkodHEBCMiGXQDz5prijTq3RHPy2
/5WJBCYq7yHgTLvspMy6sivXN7NdYE7I5pXo/KHk4nz+Fa6P3L8+L90E/3qwf6j3
DKWnAgJFRY8AbSYXt1d5ELiIG1/gEqzC0fZmNhhfrBtxwWXrlpUDT0Kfvf0QVmPR
xxCLXT+tEe1seWGEqeOLL5vXRLqmzZcBe1RZ9kQQm43+a9Qn5icSRnDfTAesQ3Cr
lAWJKl2kcWU1HwJqw+dZRSZ1X4kEXNMyzPdPBbGmU6MHdhpywI7SKZT7mX4BDnUK
eQIDAQAB
-----END PUBLIC KEY-----
-----BEGIN CERTIFICATE-----
MIIDCzCCAfOgAwIBAgIEGtZIUzANBgkqhkiG9w0BAQsFADA2MQswCQYDVQQGEwJ1
czELMAkGA1UECBMCY2ExCzAJBgNVBAcTAmxhMQ0wCwYDVQQDEwR0ZXN0MB4XDTE2
MDMxNTA4MTAzMFoXDTE2MDYxMzA4MTAzMFowNjELMAkGA1UEBhMCdXMxCzAJBgNV
BAgTAmNhMQswCQYDVQQHEwJsYTENMAsGA1UEAxMEdGVzdDCCASIwDQYJKoZIhvcN
AQEBBQADggEPADCCAQoCggEBAICCtlreMdhLQ5eNQu736TrDKrmTMjsrXjtkbFXj
Cxf4VyHmL4nCq9EkM1ZKHRxAQjIhl0A8+aa4o06t0Rz8tv+ViQQmKu8h4Ey77KTM
urIr1zezXWBOyOaV6Pyh5OJ8/hWuj9y/Pi/dBP96sH+o9wylpwICRUWPAG0mF7dX
eRC4iBtf4BKswtH2ZjYYX6wbccFl65aVA09Cn739EFZj0ccQi10/rRHtbHlhhKnj
iy+b10S6ps2XAXtUWfZEEJuN/mvUJ+YnEkZw30wHrENwq5QFiSpdpHFlNR8CasPn
WUUmdV+JBFzTMsz3TwWxplOjB3YacsCO0imU+5l+AQ51CnkCAwEAAaMhMB8wHQYD
VR0OBBYEFOGefUBGquEX9Ujak34PyRskHk+WMA0GCSqGSIb3DQEBCwUAA4IBAQB3
1eLfNeq45yO1cXNl0C1IQLknP2WXg89AHEbKkUOA1ZKTOizNYJIHW5MYJU/zScu0
yBobhTDe5hDTsATMa9sN5CPOaLJwzpWV/ZC6WyhAWTfljzZC6d2rL3QYrSIRxmsp
/J1Vq9WkesQdShnEGy7GgRgJn4A8CKecHSzqyzXulQ7Zah6GoEUD+vjb+BheP4aN
hiYY1OuXD+HsdKeQqS+7eM5U7WW6dz2Q8mtFJ5qAxjY75T0pPrHwZMlJUhUZ+Q2V
FfweJEaoNB9w9McPe1cAiE+oeejZ0jq0el3/dJsx3rlVqZN+lMhRJJeVHFyeb3XF
lLFCUGhA7hxn2xf3x1JW
-----END CERTIFICATE-----6.3. Maven 配置
為了避免 JKS 文件被 Maven 過濾過程拾取,我們將確保在 <em pom.xml</em> 中排除它:
<build>
<resources>
<resource>
<directory>src/main/resources</directory>
<filtering>true</filtering>
<excludes>
<exclude>*.jks</exclude>
</excludes>
</resource>
</resources>
</build>如果使用 Spring Boot,我們需要確保將我們的 JKS 文件通過 Spring Boot Maven 插件的 addResources 添加到應用程序類路徑中:
<build>
<plugins>
<plugin>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-maven-plugin</artifactId>
<configuration>
<addResources>true</addResources>
</configuration>
</plugin>
</plugins>
</build>6.4. 授權服務器
現在我們將配置 Keycloak 使用來自 mytest.jks 的密鑰對,通過在 realm 定義 JSON 文件中 KeyProvider 部分中進行如下操作:
{
"id": "59412b8d-aad8-4ab8-84ec-e546900fc124",
"name": "java-keystore",
"providerId": "java-keystore",
"subComponents": {},
"config": {
"keystorePassword": [ "mypass" ],
"keyAlias": [ "mytest" ],
"keyPassword": [ "mypass" ],
"active": [ "true" ],
"keystore": [
"src/main/resources/mytest.jks"
],
"priority": [ "101" ],
"enabled": [ "true" ],
"algorithm": [ "RS256" ]
}
},我們已將 優先級 設置為 101,高於 Authorization Server 中的任何其他 Keypair,並將 啓用 設置為 true。 這樣做是為了確保我們的 Resource Server 將從 jwk-set-uri 屬性中選擇這個特定的 Keypair,該屬性我們在前面指定了。
請注意,這種配置僅適用於 Keycloak,對於其他 OAuth Server 實現可能有所不同。
7. 結論
在本文中,我們重點介紹瞭如何設置我們的 Spring Security OAuth2 項目以使用 JSON Web Tokens。