使用 JWT 與 Spring Security OAuth

Spring Security
Remote
1
01:27 AM · Nov 30 ,2025

1. 概述

在本教程中,我們將討論如何使我們的 Spring Security OAuth2 實現利用 JSON Web Token。

我們還將繼續構建 Spring REST API + OAuth2 + Angular 文章中的 OAuth 教程。

2. OAuth2 授權服務器

此前,Spring Security OAuth 棧提供了將授權服務器配置為 Spring Application 的可能性。我們必須將其配置為使用 JwtTokenStore,以便使用 JWT 令牌。

然而,Spring 已經廢棄了 OAuth 棧,現在我們將使用 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/certs

JWT 包含 Token 內的所有信息,因此資源服務器需要驗證 Token 的簽名以確保數據未被篡改。 jwk-set-uri 屬性包含服務器可以用於此目的的公鑰

The issuer-uri 屬性指向 Authorization Server 的基本 URI,也可用於驗證 iss 主張作為額外的安全措施。

此外,如果 jwk-set-uri 屬性未設置,資源服務器將嘗試使用 issuer-uri 以確定該密鑰的位置,從 Authorization Server 元數據端點

重要的是要注意,添加 issuer-uri 屬性需要我們有 Authorization Server 在我們啓動 Resource Server 應用程序之前運行。

現在讓我們看看如何使用 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 配置;我們需要明確指定我們想要它表現為資源服務器,並且我們將使用 JWT 格式的訪問令牌,分別使用 oauth2ResourceServer()jwt() 方法。

上述 JWT 配置是 Spring Boot 實例提供的默認配置。 我們可以根據需要進行自定義,如稍後所見。

4. 自定義聲明在令牌中

現在,讓我們設置一些基礎設施,以便能夠添加幾個 自定義聲明在訪問令牌中。 框架提供的標準聲明都很好,但大多數時候我們需要在令牌中獲得一些額外信息,以便在客户端利用。

以下是一個自定義聲明的示例,organization,它將包含給定用户的組織名稱。

4.1. 授權服務器配置

為此,我們需要向 realm 定義文件 baeldung-realm.json 添加幾個配置:

  • 添加一個屬性 organization 到我們的用户 user [email protected]
    "attributes" : {
      "organization" : "baeldung"
    },
  • 添加一個 protocolMapper 叫做 organization 到 jwtClient 配置中:
    "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 服務器不同。

有了這個新的配置運行起來,我們將會獲得一個額外的屬性,organization = baeldung,在令牌的 payload 中,用於 user [email protected]

{
  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 庫來實現這個功能。

我們將使用 organization 聲明在我們的 AppService 中,並添加一個函數 getOrganization

getOrganization(){
  var token = Cookie.get("access_token");
  var payload = this.jwtHelper.decodeToken(token);
  this.organization = payload.organization; 
  return this.organization;
}

這個函數使用 JwtHelperServiceangular2-jwt 庫中解碼訪問令牌並獲取我們的自定義聲明。 現在,我們只需要在我們的 AppComponent 中顯示它:

@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.JwtAuthenticationPrincipal 中提取它,就像為任何其他屬性在 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 設置為其聲明轉換器:

@Bean
public JwtDecoder jwtDecoder(OAuth2ResourceServerProperties properties) {
    NimbusJwtDecoder jwtDecoder = NimbusJwtDecoder.withJwkSetUri(
      properties.getJwt().getJwkSetUri()).build();
    
    jwtDecoder.setClaimSetConverter(new OrganizationSubClaimAdapter());
    return jwtDecoder;
}

現在,當我們訪問用户 [] 的 /user/info API 時,我們將會得到 organization 的值是 UNKNOWN

請注意,覆蓋 Spring Boot 默認配置的 JwtDecoder bean 應該謹慎進行,以確保包含所有必要的配置。

6. Loading Keys From a Java Keystore

In our previous configuration, we used the Authorization Server’s default public key to verify our token’s integrity.

We can also use a keypair and certificate stored in a Java Keystore file to do the signing process.

6.1. Generate JKS Java KeyStore File

Let’s first generate the keys, and more specifically a .jks file, using the command line tool keytool:

keytool -genkeypair -alias mytest 
                    -keyalg RSA 
                    -keypass mypass 
                    -keystore mytest.jks 
                    -storepass mypass

The command will generate a file called mytest.jks which contains our keys, the Public and Private keys.

Also make sure keypass and storepass are the same.

6.2. Export Public Key

Next we need to export our Public key from generated JKS. We can use the following command to do so:

keytool -list -rfc --keystore mytest.jks | openssl x509 -inform pem -pubkey

A sample response will look like this:

-----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
urIrzezXWBOyOaV6Pyh5OJ8/hWuj9y/Pi/dBP96sH+o9wylpwICRUWPAG0mF7dX
eRC4iBtf4BKswtH2ZjYYX6wbccFl65aVA09Cn739EFZj0ccQi10/rRHtbHlhhKnj
iy+b10S6ps2XAXtUWfZEEJuN/mvUJ+YnEkZw30wHrENwq5QFiSpdpHFlNR8CasPn
WUUmdV+JBFzTMsz3TwWxplOjB3YacsCO0imU+5l+AQ51CnkCAwEAAaMhMB8wHQYD
VR0OBBYEFOGefUBGquEX9Ujak34PyRskHk+WMA0GCSqGSIb3DQEBCwUAA4IBAQB3
1eLfNeq45yO1cXNl0C1IQLknP2WXg89AHEbKkUOA1ZKTOizNYJIHW5MYJU/zScu0
yBobhTDe5hDTsATMa9sN5CPOaLJwzpWV/ZC6WyhAWTfljzZC6d2rL3QYrSIRxmsp
/J1Vq9WkesQdShnEGy7GgRgJn4A8CKecHSzqyzXulQ7Zah6GoEUD+vjb+BheP4aN
hiYYOuXD+HsdKeQqS+7eM5U7WW6dz2Q8mtFJ5qAxjY75T0pPrHwZMlJUhUZ+Q2V
FfweJEaoNB9w9McPe1cAiE+oeejZ0jq0el3/dJsx3rlVqZN+lMhRJJeVHFyeb3XF
lLFCUGhA7hxn2xf3x1JW
-----END CERTIFICATE-----

6.3. Maven Configuration

We don’t want the JKS file to be picked up by the maven filtering process, so we’ll make sure to exclude it in the pom.xml:

<build>
    <resources>
        <resource>
            <directory>src/main/resources</directory>
            <filtering>true</filtering>
            <excludes>
                <exclude>*.jks</exclude>
            </excludes>
        </resource>
    </resources>
</build>

If we’re using Spring Boot, we need to make sure that our JKS file is added to the application classpath via the Spring Boot Maven Plugin 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. Authorization Server

Now we will configure Keycloak to use our Keypair from mytest.jks by adding it to the realm definition JSON file’s KeyProvider section as follows:

{
  "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" ]
  }
},

Here we have set the priority to 101, greater than any other Keypair for our Authorization Server, and set active to true. This is done to ensure that our Resource Server will pick this particular Keypair from the jwk-set-uri property we specified earlier.

Again, this configuration is specific to Keycloak and may differ for other OAuth Server implementations.

7. 結論

在本文中,我們重點介紹瞭如何設置我們的 Spring Security OAuth2 項目以使用 JSON Web Tokens。

user avatar
0 位用戶收藏了這個故事!
收藏

發佈 評論

Some HTML is okay.