Using JWT with Spring Security OAuth – 在Spring Security OAuth中使用JWT

最后修改: 2018年 1月 21日

中文/混合/英文(键盘快捷键:t)

1. Overview

1.概述

In this tutorial, we’ll discuss how to get our Spring Security OAuth2 implementation to make use of JSON Web Tokens.

在本教程中,我们将讨论如何让我们的Spring Security OAuth2实现利用JSON Web Tokens。

We’re also continuing to build on the Spring REST API + OAuth2 + Angular article in this OAuth series.

我们还将在本OAuth系列中的Spring REST API + OAuth2 + Angular文章的基础上继续努力。

2. The OAuth2 Authorization Server

2.OAuth2授权服务器

Previously, the Spring Security OAuth stack offered the possibility of setting up an Authorization Server as a Spring Application. We then had to configure it to use JwtTokenStore so that we could use JWT tokens.

以前,Spring Security OAuth栈提供了将授权服务器设置为Spring应用程序的可能性。然后我们必须将其配置为使用JwtTokenStore ,以便我们能够使用JWT令牌。

However, the OAuth stack has been deprecated by Spring and now we’ll be using Keycloak as our Authorization Server.

然而,OAuth协议栈已被Spring弃用,现在我们将使用Keycloak作为我们的授权服务器。

So this time, we’ll set up our Authorization Server as an embedded Keycloak server in a Spring Boot app. It issues JWT tokens by default, so there is no need for any other configuration in this regard.

所以这一次,我们将把我们的授权服务器设置为Spring Boot应用中的一个嵌入式Keycloak服务器。它默认发行JWT令牌,所以在这方面不需要任何其他配置。

3. Resource Server

3.资源服务器

Now let’s take a look at how to configure our Resource Server to use JWT.

现在让我们来看看如何配置我们的资源服务器以使用JWT。

We’ll do this in an application.yml file:

我们将在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

JWTs include all the information within the Token, so the Resource Server needs to verify the Token’s signature to make sure the data has not been modified. The jwk-set-uri property contains the public key that the server can use for this purpose.

JWT包括Token中的所有信息,所以资源服务器需要验证Token的签名以确保数据未被修改。jwk-set-uri属性包含公钥服务器可以为此目的使用。

The issuer-uri property points to the base Authorization Server URI, which can also be used to verify the iss claim as an added security measure.

issuer-uri属性指向基本的授权服务器URI,它也可以被用来验证iss要求,作为一种额外的安全措施。

Additionally, if the jwk-set-uri property is not set, the Resource Server will attempt to use the issuer-uri to determine the location of this key from the Authorization Server metadata endpoint.

此外,如果没有设置jwk-set-uri属性,资源服务器将尝试使用issuer-uri来确定来自Authorization Server元数据端点的此密钥位置。

It is important to note, adding the issuer-uri property mandates that we should have the Authorization Server running before we can start the Resource Server application.

值得注意的是,添加issuer-uri属性后,我们应该在启动资源服务器应用程序之前运行授权服务器

Now let’s see how we can configure JWT support using Java configuration: 

现在让我们看看h我们如何使用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();
    }
}

Here we are overriding the default Http Security configuration; we need to specify explicitly that we want this to behave as a Resource Server and that we’ll be using JWT formatted Access Tokens using the methods oauth2ResourceServer() and jwt(), respectively.

在这里,我们要覆盖默认的Http安全配置;我们需要明确指定我们希望它表现为一个资源服务器,并且我们将使用oauth2ResourceServer()jwt()方法使用JWT格式的访问令牌,分别是。

The above JWT configuration is what the default Spring Boot instance is providing us with. This can also be customized as we’ll see shortly.

上面的JWT配置是默认的Spring Boot实例为我们提供的。这也是可以定制的,我们很快就会看到。

4. Custom Claims in the Token

4.代币中的自定义要求

Now let’s set up some infrastructure to be able to add a few custom claims in the Access Token returned by the Authorization Server. The standard claims provided by the framework are all well and good, but most of the time we’ll need some extra information in the token to utilize on the Client side.

现在,让我们建立一些基础设施,以便能够在授权服务器返回的访问令牌中添加一些自定义请求。框架提供的标准声明都是很好的,但大多数时候我们需要在令牌中添加一些额外的信息,以便在客户端使用。

Let’s take an example of a custom claim, organization, that will contain the name of a given user’s organization.

让我们举个例子,一个自定义索赔,organization,它将包含一个特定用户的组织名称。

4.1. Authorization Server Configuration

4.1.授权服务器配置

For this we need to add a couple of configurations to our realm definition file, baeldung-realm.json:

为此,我们需要在我们的境界定义文件baeldung-realm.json中添加几个配置。

  • Add an attribute organization to our user john@test.com:
    "attributes" : {
      "organization" : "baeldung"
    },
  • Add a protocolMapper called organization to the jwtClient configuration:
    "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"
      }
    }],

For a standalone Keycloak setup, this can also be done using the Admin console. 

对于独立的Keycloak设置,这也可以使用管理控制台完成。

It’s important to remember that the JSON configuration above is specific to Keycloak, and can differ for other OAuth servers.

重要的是要记住,上面的JSON配置是针对Keycloak的,对于其他OAuth服务器可能有所不同

With this new configuration up and running, we’ll get an extra attribute, organization = baeldung, in the token payload for john@test.com:

随着这个新配置的启动和运行,我们将在john@test.com的令牌有效载荷中得到一个额外的属性,organization = baeldung

{
  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: "john@test.com"
}

4.2. Use the Access Token in the Angular Client

4.2.在Angular客户端中使用访问令牌

Next we’ll want to make use of the Token information in our Angular Client application. We’ll use the angular2-jwt library for that.

接下来,我们要在我们的Angular客户端应用程序中利用令牌信息。我们将使用angular2-jwt库来实现。

We’ll make use of the organization claim in our AppService, and add a function getOrganization:

我们将利用我们的AppService中的organization要求,并添加一个函数getOrganization

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

This function makes use of JwtHelperService from the angular2-jwt library to decode the Access Token and get our custom claim. Now all we need to do is display it in our AppComponent:

这个函数利用JwtHelperService库中的angular2-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. Access Extra Claims in the Resource Server

5.访问资源服务器中的额外要求

But how can we access that information over on the Resource Server side?

但我们怎样才能在资源服务器那边访问这些信息呢?

5.1. Access Authentication Server Claims

5.1.访问认证服务器的要求

That’s really simple, we just need to extract it from the org.springframework.security.oauth2.jwt.Jwt‘s AuthenticationPrincipal, as we would do for any other attribute in UserInfoController:

这其实很简单,我们只需要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. Configuration to Add/Remove/Rename Claims

5.2.添加/删除/重命名索赔的配置

Now what if we want to add more claims on the Resource Server side? Or remove or rename some?

现在,如果我们想在资源服务器端添加更多的索赔,怎么办?或者删除或重命名一些?

Let’s say we want to modify the organization claim coming in from the Authentication Server to get the value in uppercase. However, if the claim is not present on a user, we need to set its value as unknown.

假设我们想修改从认证服务器进来的organization claim,以获得大写字母的值。但是,如果用户身上没有这个要求,我们需要将其值设置为unknown

To achieve this, we’ll have to add a class that implements the Converter interface and uses MappedJwtClaimSetConverter to convert claims:

为了实现这一点,我们必须添加一个实现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;
    }
}

Then, in our SecurityConfig class, we need to add our own JwtDecoder instance to override the one provided by Spring Boot and set our OrganizationSubClaimAdapter as its claims converter:

然后,在我们的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;
}

Now when we hit our /user/info API for the user mike@other.com, we’ll get the organization as UNKNOWN.

现在,当我们为用户mike@other.com点击我们的/user/info API时,我们会得到organizationUNKNOWN

Note that overriding the default JwtDecoder bean configured by Spring Boot should be done carefully to ensure all the necessary configuration is still included.

请注意,覆盖Spring Boot配置的默认JwtDecoder bean时应谨慎行事,以确保仍包含所有必要的配置。

6. Loading Keys From a Java Keystore

6.从Java钥匙库加载钥匙

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.

我们也可以使用存储在Java Keystore文件中的密钥对和证书来完成签名过程。

6.1. Generate JKS Java KeyStore File

6.1.生成JKS Java KeyStore文件

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

让我们首先使用命令行工具keytool生成密钥,更确切地说,是一个.jks文件:

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.

该命令将生成一个名为mytest.jks的文件,其中包含我们的公钥和私钥。

Also make sure keypass and storepass are the same.

还要确保keypassstorepass相同。

6.2. Export Public Key

6.2.出口公钥

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

接下来我们需要从生成的JKS中导出我们的公钥。我们可以使用下面的命令来做。

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
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 Configuration

6.3.Maven配置

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:

我们不希望JKS文件被maven过滤过程选中,所以我们要确保在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:

如果我们使用Spring Boot,我们需要确保通过Spring Boot Maven插件addResources将我们的JKS文件添加到应用程序的classpath。

<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

6.4.授权服务器

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:

现在我们将配置Keycloak以使用我们来自mytest.jks的密钥对,方法是将其添加到境界定义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" ]
  }
},

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.

在这里,我们将priority设置为101,大于我们的授权服务器的任何其他密钥对,并将active设置为true。这样做是为了确保我们的资源服务器将从我们先前指定的jwk-set-uri属性中挑选这个特定的密钥对。

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

同样,这个配置是针对Keycloak的,对于其他OAuth服务器的实现可能有所不同。

7. Conclusion

7.结语

In this brief article we focused on setting up our Spring Security OAuth2 project to use JSON Web Tokens.

在这篇简短的文章中,我们着重介绍了如何设置Spring Security OAuth2项目以使用JSON Web Tokens。

The full implementation of this article can be found over on GitHub.

本文的完整实现可以在GitHub上找到over on GitHub.