JWT 整合 Spring Security OAuth

http://www.baeldung.com/spring-security-oauth-jwt
作者:Eugen Paraschiv
译者:oopsguy.com

1、概述

在本教程中,我们将讨论将 Spring Security OAuth2 与 JSON Web Token 整合。

我们将在上一篇 OAuth 系列文章的基础上开展。

2、Maven 配置

首先,我们需要在 pom.xml 中添加 spring-security-jwt 依赖:

1
2
3
4
<dependency>
<groupId>org.springframework.security</groupId>
<artifactId>spring-security-jwt</artifactId>
</dependency>

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

3、授权服务器

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

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
@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;
}
}

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

4、资源服务器

现在,让我们来看看资源服务器配置,它与授权服务器的配置非常相似:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
@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;
}
}

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

5、自定义 Token 中的 Claims

现在我们来设置一些基础设施,以便能够在 Access Token 中添加一些自定义的 claims。框架提供的标准 claims 很好,但在大多数情况下,我们需要向令牌中添加一些额外的信息才能在客户端使用。

我们定义一个 TokenEnhancer 来定制 Access Token 与这些额外的 claims。

在以下示例中,我们将使用此 CustomTokenEnhancer 向 Access Toekn 添加一个额外的字段 organization

1
2
3
4
5
6
7
8
9
10
11
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;
}
}

然后,我们将将其装配到授权服务器配置中,如下所示:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
@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();
}

使用这个新配置来启动和运行,令牌的 payload 可能类似于:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
{
"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、在 JS 客户端中使用 Access Token

最后,我们将在 AngualrJS 客户端应用中使用令牌信息。我们将使用 angular-jwt 库。

因此我们要做的是在 index.html 中使用 organization claim:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
<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、非对称密钥对

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

1
2
3
4
5
6
@Bean
public JwtAccessTokenConverter accessTokenConverter() {
JwtAccessTokenConverter converter = new JwtAccessTokenConverter();
converter.setSigningKey("123");
return converter;
}

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

6.1、生成 JKS Java KeyStore 文件

让我们首先使用命令行工具 keytool 生成密钥 — 尤其是 .jks 文件。

1
2
3
4
5
keytool -genkeypair -alias mytest 
-keyalg RSA
-keypass mypass
-keystore mytest.jks
-storepass mypass

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

此外,还要确保 keypassstorepass 是一致的。

6.2、导出公钥

接下来,我们需要从生成的 JKS 导出公钥,我们可以使用以下命令:

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

可能的输出如下所示:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
-----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-----

我们仅使用公钥,将其复制到资源服务器的 src/main/resources/public.txt 文件中:

1
2
3
4
5
6
7
8
9
-----BEGIN PUBLIC KEY-----
MIIBIjANBgkqhkiG9w0BAQEFAAOCAQ8AMIIBCgKCAQEAgIK2Wt4x2EtDl41C7vfp
OsMquZMyOyteO2RsVeMLF/hXIeYvicKr0SQzVkodHEBCMiGXQDz5prijTq3RHPy2
/5WJBCYq7yHgTLvspMy6sivXN7NdYE7I5pXo/KHk4nz+Fa6P3L8+L90E/3qwf6j3
DKWnAgJFRY8AbSYXt1d5ELiIG1/gEqzC0fZmNhhfrBtxwWXrlpUDT0Kfvf0QVmPR
xxCLXT+tEe1seWGEqeOLL5vXRLqmzZcBe1RZ9kQQm43+a9Qn5icSRnDfTAesQ3Cr
lAWJKl2kcWU1HwJqw+dZRSZ1X4kEXNMyzPdPBbGmU6MHdhpywI7SKZT7mX4BDnUK
eQIDAQAB
-----END PUBLIC KEY-----

6.3、Maven 配置

接下来,我们不希望 JKS 文件被 maven 过滤处理影响到,因此需要确保在 pom.xml 中排除它:

1
2
3
4
5
6
7
8
9
10
11
<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 添加到应用程序 classpath 中:

1
2
3
4
5
6
7
8
9
10
11
<build>
<plugins>
<plugin>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-maven-plugin</artifactId>
<configuration>
<addResources>true</addResources>
</configuration>
</plugin>
</plugins>
</build>

6.4、授权服务器

现在,我们将配置 JwtAccessTokenConverter 以使用 mytest.jks 中的密钥对,如下所示:

1
2
3
4
5
6
7
8
@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;
}

6.5、资源服务器

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

1
2
3
4
5
6
7
8
9
10
11
12
13
@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;
}

7、结论

在本文中,我们主要配置了 Spring Security OAuth2 项目以整合 JSON Web Token。

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

原文示例代码

https://github.com/eugenp/spring-security-oauth