Using JWT with Spring Security OAuth (legacy stack) – 使用JWT与Spring Security OAuth(传统栈)

最后修改: 2020年 3月 29日

中文/混合/英文(键盘快捷键: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 top of the previous article in this OAuth series.

我们还将继续在本OAuth系列中的前一篇文章的基础上进行研究。

 

Before we get started – one important note. Keep in mind that the Spring Security core team is in the process of implementing a new OAuth2 stack – with some aspects already out and some still in progress.

在我们开始之前,有一个重要的说明。请记住,Spring Security核心团队正在实施一个新的OAuth2堆栈–有些方面已经出来了,有些仍在进行中。

For the version of this article using the new Spring Security 5 stack, have a look at our article Using JWT with Spring Security OAuth.

对于使用新的Spring Security 5协议栈的本文版本,请查看我们的文章使用JWT与Spring Security OAuth

Alright, let’s jump right in.

好吧,让我们直接跳进去。

2. Maven Configuration

2.Maven配置

First, we need to add spring-security-jwt dependency to our pom.xml:

首先,我们需要将spring-security-jwt依赖性添加到我们的pom.xml

<dependency>
    <groupId>org.springframework.security</groupId>
    <artifactId>spring-security-jwt</artifactId>
</dependency>

Note that we need to add spring-security-jwt dependency to both the Authorization Server and Resource Server.

请注意,我们需要为授权服务器和资源服务器添加spring-security-jwt依赖。

3. Authorization Server

3.授权服务器

Next, we will configure our Authorization Server to use JwtTokenStore – as follows:

接下来,我们将配置我们的授权服务器以使用JwtTokenStore–如下所示。

@Configuration
@EnableAuthorizationServer
public class OAuth2AuthorizationServerConfig extends AuthorizationServerConfigurerAdapter {
    @Override
    public void configure(AuthorizationServerEndpointsConfigurer endpoints) 
      throws Exception {
        endpoints.tokenStore(tokenStore())
                 .accessTokenConverter(accessTokenConverter())
                 .authenticationManager(authenticationManager);
    }

    @Bean
    public TokenStore tokenStore() {
        return new JwtTokenStore(accessTokenConverter());
    }

    @Bean
    public JwtAccessTokenConverter accessTokenConverter() {
        JwtAccessTokenConverter converter = new JwtAccessTokenConverter();
        converter.setSigningKey("123");
        return converter;
    }

    @Bean
    @Primary
    public DefaultTokenServices tokenServices() {
        DefaultTokenServices defaultTokenServices = new DefaultTokenServices();
        defaultTokenServices.setTokenStore(tokenStore());
        defaultTokenServices.setSupportRefreshToken(true);
        return defaultTokenServices;
    }
}

Note that we used a symmetric key in our JwtAccessTokenConverter to sign our tokens – which means we will need to use the same exact key for the Resources Server as well.

请注意,我们在JwtAccessTokenConverter中使用了一个对称密钥来签署我们的令牌–这意味着我们也需要为资源服务器使用相同的密钥。

4. Resource Server

4.资源服务器

Now, let’s take a look at our Resource Server configuration – which is very similar to the config of the Authorization Server:

现在,让我们看一下我们的资源服务器配置–它与授权服务器的配置非常相似。

@Configuration
@EnableResourceServer
public class OAuth2ResourceServerConfig extends ResourceServerConfigurerAdapter {
    @Override
    public void configure(ResourceServerSecurityConfigurer config) {
        config.tokenServices(tokenServices());
    }

    @Bean
    public TokenStore tokenStore() {
        return new JwtTokenStore(accessTokenConverter());
    }

    @Bean
    public JwtAccessTokenConverter accessTokenConverter() {
        JwtAccessTokenConverter converter = new JwtAccessTokenConverter();
        converter.setSigningKey("123");
        return converter;
    }

    @Bean
    @Primary
    public DefaultTokenServices tokenServices() {
        DefaultTokenServices defaultTokenServices = new DefaultTokenServices();
        defaultTokenServices.setTokenStore(tokenStore());
        return defaultTokenServices;
    }
}

Keep in mind that we’re defining these two servers as entirely separate and independently deployable. That’s the reason we need to declare some of the same beans again here, in the new configuration.

请记住,我们将这两个服务器定义为完全独立的、可独立部署的。这就是我们需要在新的配置中再次声明一些相同的Bean的原因。

5. Custom Claims in the Token

5.代币中的自定义要求

Let’s now set up some infrastructure to be able to add a few custom claims in the Access Token. 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.

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

We’ll define a TokenEnhancer to customize our Access Token with these additional claims.

我们将定义一个TokenEnhancer,用这些额外的要求定制我们的访问令牌。

In the following example, we will add an extra field “organization” to our Access Token – with this CustomTokenEnhancer:

在下面的例子中,我们将向我们的访问令牌添加一个额外的字段”组织“–用这个CustomTokenEnhancer

public class CustomTokenEnhancer implements TokenEnhancer {
    @Override
    public OAuth2AccessToken enhance(
      OAuth2AccessToken accessToken, 
      OAuth2Authentication authentication) {
        Map<String, Object> additionalInfo = new HashMap<>();
        additionalInfo.put(
          "organization", authentication.getName() + randomAlphabetic(4));
        ((DefaultOAuth2AccessToken) accessToken).setAdditionalInformation(
          additionalInfo);
        return accessToken;
    }
}

Then, we’ll wire that into our Authorization Server configuration – as follows:

然后,我们将把它连接到我们的授权服务器配置中 – 如下所示。

@Override
public void configure(
  AuthorizationServerEndpointsConfigurer endpoints) throws Exception {
    TokenEnhancerChain tokenEnhancerChain = new TokenEnhancerChain();
    tokenEnhancerChain.setTokenEnhancers(
      Arrays.asList(tokenEnhancer(), accessTokenConverter()));

    endpoints.tokenStore(tokenStore())
             .tokenEnhancer(tokenEnhancerChain)
             .authenticationManager(authenticationManager);
}

@Bean
public TokenEnhancer tokenEnhancer() {
    return new CustomTokenEnhancer();
}

With this new configuration up and running – here’s what a token token payload would look like:

随着这个新配置的启动和运行–下面是一个令牌的有效载荷的样子。

{
    "user_name": "john",
    "scope": [
        "foo",
        "read",
        "write"
    ],
    "organization": "johnIiCh",
    "exp": 1458126622,
    "authorities": [
        "ROLE_USER"
    ],
    "jti": "e0ad1ef3-a8a5-4eef-998d-00b26bc2c53f",
    "client_id": "fooClientIdPassword"
}

5.1. Use the Access Token in the JS Client

5.1.在JS客户端中使用访问令牌

Finally, we’ll want to make use of the token information over in our AngualrJS client application. We’ll use the angular-jwt library for that.

最后,我们希望在我们的AngualrJS客户端应用程序中使用令牌信息。我们将使用angular-jwt库来实现。

So what we’re going to do is we’re going to make use of the “organization” claim in our index.html:

所以我们要做的是,在我们的index.html中利用”organization“声明。

<p class="navbar-text navbar-right">{{organization}}</p>

<script type="text/javascript" 
  src="https://cdn.rawgit.com/auth0/angular-jwt/master/dist/angular-jwt.js">
</script>

<script>
var app = 
  angular.module('myApp', ["ngResource","ngRoute", "ngCookies", "angular-jwt"]);

app.controller('mainCtrl', function($scope, $cookies, jwtHelper,...) {
    $scope.organiztion = "";

    function getOrganization(){
    	var token = $cookies.get("access_token");
    	var payload = jwtHelper.decodeToken(token);
    	$scope.organization = payload.organization;
    }
    ...
});

6. Access Extra Claims on Resource Server

6.访问资源服务器上的额外要求

But, how can we access that information over on the resource server side?

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

What we’ll do here is – extract the extra claims from the access token:

我们在这里要做的是–从访问令牌中提取额外的要求。

public Map<String, Object> getExtraInfo(OAuth2Authentication auth) {
    OAuth2AuthenticationDetails details =
      (OAuth2AuthenticationDetails) auth.getDetails();
    OAuth2AccessToken accessToken = tokenStore
      .readAccessToken(details.getTokenValue());
    return accessToken.getAdditionalInformation();
}

In the following section, we’ll discuss how to add that extra information to our Authentication details by using a custom AccessTokenConverter

在下一节中,我们将讨论如何通过使用自定义的AccessTokenConverter将额外的信息添加到我们的Authentication细节中。

6.1. Custom AccessTokenConverter

6.1.自定义AccessTokenConverter

Let’s create CustomAccessTokenConverter and set Authentication details with access token claims:

让我们创建CustomAccessTokenConverter并设置带有访问令牌要求的认证细节。

@Component
public class CustomAccessTokenConverter extends DefaultAccessTokenConverter {

    @Override
    public OAuth2Authentication extractAuthentication(Map<String, ?> claims) {
        OAuth2Authentication authentication =
          super.extractAuthentication(claims);
        authentication.setDetails(claims);
        return authentication;
    }
}

Note: DefaultAccessTokenConverter used to set Authentication details to Null.

注意:DefaultAccessTokenConverter用于将认证细节设置为Null。

6.2. Configure JwtTokenStore

6.2.配置JwtTokenStore

Next, we’ll configure our JwtTokenStore to use our CustomAccessTokenConverter:

接下来,我们将配置我们的JwtTokenStore以使用我们的CustomAccessTokenConverter

@Configuration
@EnableResourceServer
public class OAuth2ResourceServerConfigJwt
 extends ResourceServerConfigurerAdapter {

    @Autowired
    private CustomAccessTokenConverter customAccessTokenConverter;

    @Bean
    public TokenStore tokenStore() {
        return new JwtTokenStore(accessTokenConverter());
    }

    @Bean
    public JwtAccessTokenConverter accessTokenConverter() {
        JwtAccessTokenConverter converter = new JwtAccessTokenConverter();
        converter.setAccessTokenConverter(customAccessTokenConverter);
    }
    // ...
}

6.3. Extra Claims Available in the Authentication Object

6.3.认证对象中可用的额外要求

Now that the Authorization Server added some extra claims in the token, we can now access on the Resource Server side, directly in the Authentication object:

现在,授权服务器在令牌中添加了一些额外的要求,我们现在可以在资源服务器端,直接在认证对象中访问。

public Map<String, Object> getExtraInfo(Authentication auth) {
    OAuth2AuthenticationDetails oauthDetails =
      (OAuth2AuthenticationDetails) auth.getDetails();
    return (Map<String, Object>) oauthDetails
      .getDecodedDetails();
}

6.4. Authentication Details Test

6.4.认证细节测试

Let’s make sure our Authentication object contains that extra information:

让我们确保我们的认证对象包含这些额外的信息。

@RunWith(SpringRunner.class)
@SpringBootTest(
  classes = ResourceServerApplication.class, 
  webEnvironment = WebEnvironment.RANDOM_PORT)
public class AuthenticationClaimsIntegrationTest {

    @Autowired
    private JwtTokenStore tokenStore;

    @Test
    public void whenTokenDoesNotContainIssuer_thenSuccess() {
        String tokenValue = obtainAccessToken("fooClientIdPassword", "john", "123");
        OAuth2Authentication auth = tokenStore.readAuthentication(tokenValue);
        Map<String, Object> details = (Map<String, Object>) auth.getDetails();
 
        assertTrue(details.containsKey("organization"));
    }

    private String obtainAccessToken(
      String clientId, String username, String password) {
 
        Map<String, String> params = new HashMap<>();
        params.put("grant_type", "password");
        params.put("client_id", clientId);
        params.put("username", username);
        params.put("password", password);
        Response response = RestAssured.given()
          .auth().preemptive().basic(clientId, "secret")
          .and().with().params(params).when()
          .post("http://localhost:8081/spring-security-oauth-server/oauth/token");
        return response.jsonPath().getString("access_token");
    }
}

Note: we obtained the access token with extra claims from the Authorization Server, then we read the Authentication object from it which contains extra information “organization” in the details object.

注意:我们从授权服务器获得了带有额外要求的访问令牌,然后我们从其中读取Authentication对象,该对象在细节对象中包含额外信息 “组织”。

7. Asymmetric KeyPair

7.非对称密钥对

In our previous configuration we used symmetric keys to sign our token:

在我们之前的配置中,我们使用对称密钥来签署我们的令牌。

@Bean
public JwtAccessTokenConverter accessTokenConverter() {
    JwtAccessTokenConverter converter = new JwtAccessTokenConverter();
    converter.setSigningKey("123");
    return converter;
}

We can also use asymmetric keys (Public and Private keys) to do the signing process.

我们也可以使用非对称密钥(公钥和私钥)来完成签名过程。

7.1. Generate JKS Java KeyStore File

7.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相同。

7.2. Export Public Key

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

We take only our Public key and copy it to our resource server src/main/resources/public.txt:

我们只拿我们的公钥,并把它复制到我们的资源服务器 src/main/resources/public.txt

-----BEGIN PUBLIC KEY-----
MIIBIjANBgkqhkiG9w0BAQEFAAOCAQ8AMIIBCgKCAQEAgIK2Wt4x2EtDl41C7vfp
OsMquZMyOyteO2RsVeMLF/hXIeYvicKr0SQzVkodHEBCMiGXQDz5prijTq3RHPy2
/5WJBCYq7yHgTLvspMy6sivXN7NdYE7I5pXo/KHk4nz+Fa6P3L8+L90E/3qwf6j3
DKWnAgJFRY8AbSYXt1d5ELiIG1/gEqzC0fZmNhhfrBtxwWXrlpUDT0Kfvf0QVmPR
xxCLXT+tEe1seWGEqeOLL5vXRLqmzZcBe1RZ9kQQm43+a9Qn5icSRnDfTAesQ3Cr
lAWJKl2kcWU1HwJqw+dZRSZ1X4kEXNMyzPdPBbGmU6MHdhpywI7SKZT7mX4BDnUK
eQIDAQAB
-----END PUBLIC KEY-----

Alternatively, we can export only the public key by adding the -noout argument:

另外,我们可以通过添加-noout参数,只导出公钥。

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

7.3. Maven Configuration

7.3.Maven配置

Next, 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 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>

7.4. Authorization Server

7.4.授权服务器

Now, we will configure JwtAccessTokenConverter to use our KeyPair from mytest.jks – as follows:

现在,我们将配置JwtAccessTokenConverter,以使用我们来自mytest.jks的KeyPair–如下。

@Bean
public JwtAccessTokenConverter accessTokenConverter() {
    JwtAccessTokenConverter converter = new JwtAccessTokenConverter();
    KeyStoreKeyFactory keyStoreKeyFactory = 
      new KeyStoreKeyFactory(new ClassPathResource("mytest.jks"), "mypass".toCharArray());
    converter.setKeyPair(keyStoreKeyFactory.getKeyPair("mytest"));
    return converter;
}

7.5. Resource Server

7.5.资源服务器

Finally, we need to configure our resource server to use Public key – as follows:

最后,我们需要将我们的资源服务器配置为使用公钥 – 如下所示。

@Bean
public JwtAccessTokenConverter accessTokenConverter() {
    JwtAccessTokenConverter converter = new JwtAccessTokenConverter();
    Resource resource = new ClassPathResource("public.txt");
    String publicKey = null;
    try {
        publicKey = IOUtils.toString(resource.getInputStream());
    } catch (final IOException e) {
        throw new RuntimeException(e);
    }
    converter.setVerifierKey(publicKey);
    return converter;
}

8. Conclusion

8.结论

In this quick 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 tutorial can be found in the github project – this is an Eclipse based project, so it should be easy to import and run as it is.

本教程的完整实现可以在github 项目中找到 – 这是一个基于 Eclipse 的项目,因此应该很容易导入并按原样运行。