现在,让我们启动一个新项目来实现一个利用利用对称密钥署名的 JWT 的系统。
对付这个实现,我将实现一个项目为授权做事器,再实现一个项目用于资源做事器。
我们首先简要回顾一下在《Spring Security 动手实践:职责分离》系列文章中详细先容过的 JWT。
然后,我们在一个示例中实现这些。

1.1 利用 JWT

在本节中,我们简要回顾一下 JWT。
我们在《Spring Security 动手实践:职责分离》详细谈论了 JWT,但是我认为最好先复习一下 JWT 是如何事情的。
然后,我们连续实现授权做事器和资源做事器。
我们在本文中谈论的所有内容都依赖于 JWT,因此,这便是为什么我认为在进一步谈论第一个示例之前,必须先从这个复习开始。

JWT 是一个令牌实现。
令牌由三个部分组成:头部、正文和署名
头部和正文中的详细信息用 JSON 表示,它们被 Base64 编码
第三部分是利用利用头部和正文作为输入的加密算法天生的署名( 图 1 )。
加密算法也意味着须要一个密钥。
这个密钥就像一个密码。
拥有精确密钥的人可以对令牌进行署名或验证署名是否真实。
如果令牌上的署名是真实的,则担保在署名后没有人变动该令牌。

php微信设置acesstokenOAuth 2  应用 JWT 和加密签名 Vue.js

图 1

图 1 一个 JWT 由三部分组成:头部、正文和署名。
头部和正文包含 JSON 表示的详细信息。
这些部件采取 Base64 编码,然后进行署名。
令牌是由由点分隔的这三个部分组成的字符串。

当 JWT 被署名时,我们也称它为 JWS ( JSON Web Token Signed )
常日,运用加密算法对令牌进行署名就足够了,但有时您可以选择对其进行加密。
如果一个令牌被署名,您可以看到它的内容,而不须要任何密钥或密码。
但是,纵然黑客看到了令牌中的内容,他们也不能变动令牌的内容,由于如果他们这样做,署名就会失落效 ( 图 2 )。
为了有效,署名必须

利用精确的密钥天生匹配已署名的内容

图 2

图 2 黑客截取了一个令牌并变动了其内容。
资源做事器谢绝调用,由于令牌的署名不再与内容匹配。

如果一个令牌被加密了,我们也称它为 JWE ( JSON Web Token Encrypted )
如果没有有效的密钥,您将无法看到加密令牌的内容。

1.2 实现授权做事器来发行 JWT

在本节中,我们将实现一个授权做事器,该做事器将 JWT 发送给客户端进行授权。
您在《OAuth 2 :实现资源做事器》 学习了管理令牌的组件是 TokenStore。
在本节中,我们要做的是利用 Spring Security 供应的 TokenStore的不同实现。
我们利用的实现的名称是 JwtTokenStore,由它管理 JWT 。
我们还将在本节中测试授权做事器。
稍后,在 1.3 节中,我们将实现一个资源做事器,并拥有一个利用 JWT 的完全部系。
您可以通过两种办法利用 JWT 实现令牌验证:

如果我们利用相同的密钥对令牌进行署名和验证署名,我们就说该密钥是对称的。
如果我们利用一个密钥对令牌进行署名,而利用另一个密钥来验证署名,则我们称利用了非对称密钥对。

在本例中,我们利用对称密钥实现署名。
这种方法意味着授权做事器和资源做事器都知道并利用相同的密钥。
授权做事器利用密钥对令牌进行署名,资源做事器利用相同的密钥验证署名 ( 图 3 )。

图 3

图 3 利用对称密钥。
授权做事器和资源做事器都共享同一个密钥。
授权做事器利用该密钥为令牌署名,而资源做事器利用该密钥来验证该署名。

让我们创建项目并添加所需的依赖项。
下一个代码片段展示了我们须要添加的依赖关系,它们与我们在《OAuth 2 :实现授权做事器》和《OAuth 2 :实现资源做事器》中用于授权做事器的依赖关系相同。

<dependency> <groupId>org.springframework.boot</groupId> <artifactId>spring-boot-starter-security</artifactId></dependency><dependency> <groupId>org.springframework.boot</groupId> <artifactId>spring-boot-starter-web</artifactId></dependency> <dependency> <groupId>org.springframework.cloud</groupId> <artifactId>spring-cloud-starter-oauth2</artifactId></dependency>

我们配置 JwtTokenStore 的方法与《OAuth 2 :实现资源做事器》示例配置 JdbcTokenStore 的方法相同。
此外,我们须要定义一个 JwtAccessTokenConverter类型的工具。
利用 JwtAccessTokenConverter,我们配置授权做事器如何验证令牌;在我们的例子中,利用对称密钥。
下面的清单展示了如何在配置类中配置 JwtTokenStore 。

清单 1 配置 JwtTokenStore

@Configuration@EnableAuthorizationServerpublic class AuthServerConfig extends AuthorizationServerConfigurerAdapter { //从 application.properties 文件中获取对称密钥的值 @Value("${jwt.key}") private String jwtKey; @Autowired private AuthenticationManager authenticationManager; @Override public void configure( ClientDetailsServiceConfigurer clients) throws Exception { clients.inMemory() .withClient("client") .secret("secret") .authorizedGrantTypes("password", "refresh_token") .scopes("read"); } @Override public void configure( AuthorizationServerEndpointsConfigurer endpoints) { endpoints .authenticationManager(authenticationManager) .tokenStore(tokenStore()) .accessTokenConverter( jwtAccessTokenConverter()); //配置令牌存储和访问令牌转换器工具 } @Bean public TokenStore tokenStore() { return new JwtTokenStore( // 利用与之关联的访问令牌转换器创建令牌存储 jwtAccessTokenConverter()); } @Bean public JwtAccessTokenConverter jwtAccessTokenConverter() { var converter = new JwtAccessTokenConverter(); // 设置访问令牌转换器工具的对称密钥的值 converter.setSigningKey(jwtKey); return converter; }}

我将此示例的对称密钥的值存储在 application.properties 文件中,如下一个代码段所示。
但是,不要忘却署名密钥是敏感数据,您该当在现实场景中将其存储在密钥存储库中。

jwt.key=MjWP5L7CiD

请记住,在前面《OAuth 2 :实现授权做事器》和《OAuth 2 :实现资源做事器》的授权做事器示例中,对付每个授权做事器,我们还定义了 UserDetailsService 和 PasswordEncoder 。
清单 2 提醒您如何为授权做事器配置这些组件。
为了使阐明简短,本章中我不会对下面的所有示例重复相同的清单。

清单 2 配置授权做事器的用户管理

@Configurationpublic class WebSecurityConfig extends WebSecurityConfigurerAdapter { @Bean public UserDetailsService uds() { var uds = new InMemoryUserDetailsManager(); var u = User.withUsername("john") .password("12345") .authorities("read") .build(); uds.createUser(u); return uds; } @Bean public PasswordEncoder passwordEncoder() { return NoOpPasswordEncoder.getInstance(); } @Bean public AuthenticationManager authenticationManagerBean() throws Exception { return super.authenticationManagerBean(); }}

现在我们可以启动授权做事器并调用 /oauth/token 端点以得到访问令牌。
下面的代码片段展示了 cURL 命令来调用 /oauth/token 端点:

curl -v -XPOST -u client:secret http://localhost:8080/oauth/token?grant_type=password&username=xiaohua&password=12345&scope=read

相应体:

{ "access_token":"eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXV...", "token_type":"bearer", "refresh_token":"eyJhbGciOiJIUzI1NiIsInR5cCI6Ikp...", "expires_in":43199, "scope":"read", "jti":"7774532f-b74b-4e6b-ab16-208c46a19560"}

您可以在相应中不雅观察到,访问令和刷新令牌现在都是 JWT。
在代码片段中,我已经缩短了令牌,以使代码片段更具可读性。
您将在掌握台的相应中看到,令牌要长得多。
不才一个代码段中,您可以找到令牌正文的解码 ( JSON )形式:

{ "user_name": "xiaohua", "scope": [ "read" ], "generatedInZone": "Europe/Bucharest", "exp": 1583874061, "authorities": [ "read" ], "jti": "38d03577-b6c8-47f5-8c06-d2e3a713d986", "client_id": "client"}

在设置了授权做事器后,我们现在就可以实现该资源做事器了。

1.3 实现利用 JWT 的资源做事器

在本节中,我们实现资源做事器,它利用对称密钥来验证由我们在 1.2 节中设置的授权做事器发出的令牌。
在本节的末了,您将理解如何编写一个完全的 OAuth 2 系统,该系统利用利用对称密钥署名的 JWT。
正如下面的代码片段所示,我们创建一个新项目并将所需的依赖项添加到 pom.xml 中。

<dependency> <groupId>org.springframework.boot</groupId> <artifactId>spring-boot-starter-oauth2-resource-server</artifactId></dependency><dependency> <groupId>org.springframework.boot</groupId> <artifactId>spring-boot-starter-web</artifactId></dependency><dependency> <groupId>org.springframework.cloud</groupId> <artifactId>spring-cloud-starter-oauth2</artifactId></dependency>

我没有在《OAuth 2 :实现授权做事器》和《OAuth 2 :实现资源做事器》中利用的根本上添加任何新的依赖项。
由于我们须要一个端点来保护,以是我定义了一个掌握器和一种方法来公开用于测试资源做事器的大略端点。
以下清单定义了掌握器。

清单 3 HelloController 类

@RestControllerpublic class HelloController { @GetMapping("/hello") public String hello() { return "Hello!"; }}

现在我们有了一个要保护的端点,我们可以声明配置 TokenStore 的配置类。
我们将为资源做事器配置 TokenStore,就像为授权做事器配置一样。
最主要的方面是确保为密钥利用相同的值。
资源做事器须要密钥来验证令牌的署名。
下一个清单定义了资源做事器配置类。

清单 4 资源做事器配置类

@Configuration@EnableResourceServerpublic class ResourceServerConfig extends ResourceServerConfigurerAdapter { //从 application.properties 文件注入密钥值 @Value("${jwt.key}") private String jwtKey; @Override public void configure(ResourceServerSecurityConfigurer resources) { //配置 TokenStore resources.tokenStore(tokenStore()); } //声明 TokenStore 并将其添加到 Spring 高下文 @Bean public TokenStore tokenStore() { return new JwtTokenStore( jwtAccessTokenConverter()); } //创建访问令牌转换器并设置用于验证令牌署名的对称密钥 @Bean public JwtAccessTokenConverter jwtAccessTokenConverter() { var converter = new JwtAccessTokenConverter(); converter.setSigningKey(jwtKey); return converter; }}

把稳

不要忘了在 application .properties 文件中设置密钥的值。

用于对称加密或署名的密钥只是字节的随机字符串。
您利用随机算法天生它。
在我们的示例中,您可以利用任何字符串值,例如“ abcde”。
在实际情形下,最好利用随机天生的值,长度最好大于258个字节。
有关更多信息,我建议利用 David Wong 的《 Real-World Cryptography 》(Manning,2020年)。
在 David Wong 的书的第8章中,您将找到有关随机性和秘钥的详细谈论:

https://livebook.manning.comhttps://livebook.manning.com/book/real-world-cryptography/chapter-8/

由于我在同一台打算机受骗地运行授权做事器和资源做事器,以是我须要为这个运用程序配置一个不同的端口。
下一个代码段显示了 application.properties 文件的内容:

server.port=9090jwt.key=MjWP5L7CiD

现在我们可以启动资源做事器,并利用先前从授权做事器得到的有效 JWT 调用 /hello 端点。
在我们的示例中,您必须将令牌添加到前缀为 “Bearer” 的要求的 Authorization HTTP 要求头中。
下面的代码片段展示了如何利用 cURL 调用端点:

curl -H "Authorization:Bearer eyJhbGciOiJIUzI1NiIs..." http://localhost:9090/hello

相应体:

Hello!

您刚刚完成了利用 OAuth 2 和 JWT 作为令牌实现的系统。
如您所见,Spring Security 使此实现变得随意马虎。
在本节中,您学习了如何利用对称密钥来署名和验证令牌。
但是您可能会创造在现实情形下的需求,个中在授权做事器和资源做事器上都不能利用相同的密钥。
在 2 节中,您将学习如何实现一个类似的系统,该系统利用非对称密钥对这些情形进行令牌验证。

利用不带 Spring Security OAuth 项目的对称密钥

正如我们在《OAuth 2 :实现资源做事器》中谈论的那样,您还可以将资源做事器配置为利用带有 oauth2ResourceServer() 的 JWT。
如前所述,这种方法更适宜将来的项目,但您可能会在现有运用程序中找到它。
因此,您须要理解此方法以用于将来的实现,当然,如果要将现有项目迁移到该方法,则当然也要知道。
下一个代码片段向您展示如何在不该用 Spring Security OAuth 项目类的情形下利用对称密钥配置 JWT 身份验证:

@Configurationpublic class ResourceServerConfig extends WebSecurityConfigurerAdapter {@Value("${jwt.key}")private String jwtKey; @Overrideprotected void configure(HttpSecurity http) throws Exception {http.authorizeRequests().anyRequest().authenticated().and().oauth2ResourceServer(c -> c.jwt( j -> j.decoder(jwtDecoder());));}// Omitted code}

如您所见,这一次我利用 Customizer 工具的 jwt() 方法作为参数发送给 oauth2ResourceServer()。
利用 jwt() 方法,我们配置了运用程序所需的详细信息以验证令牌。
在本例中,由于我们谈论的是利用对称密钥进行验证,以是我在同一个类中创建了一个 JwtDecoder 来供应对称密钥的值。
下面的代码片段展示了我如何利用 decoder() 方法设置这个解码器:

@Beanpublic JwtDecoder jwtDecoder() {byte [] key = jwtKey.getBytes();SecretKey originalKey = new SecretKeySpec(key, 0, key.length, "AES");NimbusJwtDecoder jwtDecoder =NimbusJwtDecoder.withSecretKey(originalKey).build();return jwtDecoder;}

我们配置的元素是相同的!如果您选择利用这种方法来设置资源做事器,唯一不同的是语法。

2 在 JWT 中利用非对称密钥署名的令牌

在本节中,我们实现一个 OAuth 2 身份认证的示例,个中授权做事器和资源做事器利用非对称密钥对对令牌进行署名和验证。
有时,授权做事器和资源做事器只共享一个密钥是不可行的,正如我们在第 1 节中实现的那样。
常日,如果授权做事器和资源做事器不是由同一组织开拓的,就会发生这种情形。
在本例中,我们说授权做事器不 “信赖 (trust ) ” 资源做事器,因此您不肯望授权做事器与资源做事器共享密钥。
而且,利用对称密钥,资源做事器就有了很大的能力:不仅可以验证令牌,还可以对其进行署名( 图 4 )。

把稳

我看到过通过邮件或其他不屈安的渠道交流对称密钥的情形。
永久不要这样做!对称密钥是私钥。
有密钥的人可以用它来访问系统。
我的履历法则是,如果您须要在系统外共享密钥,那么它不应该是对称的。

图 4

图 4 如果黑客设法得到对称密钥,他们可以变动令牌并对其署名。
这样,他们就可以访问用户的资源。

当我们不能假定授权做事器和资源做事器之间存在信赖关系时,我们利用非对称密钥对。
因此,您须要知道如何实现这样一个别系。
在本节中,我们将通过一个示例向您展示如何实现此目标所需的所有方面。

什么是非对称密钥对,它是如何事情的? 这个观点很大略。
非对称密钥对有两个密钥:一个称为私钥,另一个称为公钥。
授权做事器利用私钥对令牌进行署名,而其他人只能通过利用私钥对令牌进行署名 ( 图 5)。

图 5

图 5 为了署名令牌,须要利用私钥。
然后任何人都可以利用密钥对的公钥来验证署名者的身份。

公钥连接到私钥,这便是为什么我们称它为一对。
但是公钥只能用于验证署名。
没有人可以利用公钥对令牌进行署名 ( 图 6 )。

图 6

图 6 如果黑客设法得到一个公钥,他们将不能利用它来署名令牌。
公钥只能用于验证署名。

2.1 天生密钥键值对

在本节中,我将教您如何天生非对称密钥对。
我们须要一个密钥对来配置在稍候 2.2 和 2.3 节中实现的授权做事器和资源做事器。
这是一个非对称密钥对(这意味着它具有授权做事器用于署名的私有部分和资源做事器用于验证署名的公共部分)。
为了天生密钥对,我利用 keytool 和 OpenSSL,这是两个易于利用的命令行工具。
您的 JDK 安装了 keytool,因此您可能已经在打算机上安装了它。
对付 OpenSSL,您须要从 https://www.openssl.org/ 下载。
如果您利用 OpenSSL 随附的 Git Bash,则无需单独安装。
我始终喜好利用 Git Bash 进行这些操作,由于它不须要我单独安装这些工具。
有了这些工具后,您须要运行两个命令

天生一个私钥获取前面天生的私钥的公钥

天生一个私钥

要天生私钥,请不才一个代码片段中运行 keytool 命令。
它在一个名为 yyit.jks 的文件中天生一个私钥。
我还利用密码 “yyit123” 来保护私钥,并利用别名 “yyit” 来为密钥指定名称。
在以下命令中,可以看到天生密钥 RSA 的算法。

keytool -genkeypair -alias yyit -keyalg RSA -keypass yyit123 -keystore yyit.jks -storepass yyit123

获取公钥

可以利用 keytool 命令获取之前天生的私钥的公钥。

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

系统提示您输入天生公钥时利用的密码;我的名字是 yyit123。
然后,您该当在输出中找到公钥和证书。
(对付本例,只有键的值是必需的。
)这个键该当类似于下面的代码片段:

-----BEGIN PUBLIC KEY-----MIIBIjANBgkqhkiG9w0BAQEFAAOCAQ8AMIIBCgKCAQEAijLqDcBHwtnsBw+WFSzGVkjtCbO6NwKlYjS2PxE114XWf9H2j0dWmBu7NK+lV/JqpiOi0GzaLYYf4XtCJxTQDD2CeDUKczcd+fpnppripN5jRzhASJpr+ndj8431iAG/rvXrmZt3jLD3v6nwLDxzpJGmVWzcV/OBXQZkd1LHOK5LEG0YCQ0jAU3ON7OZAnFn/DMJyDCky994UtaAYyAJ7mr7IO1uHQxsBg7SiQGpApgDEK3Ty8gaFuafnExsYD+aqua1Ese+pluYnQxuxkk2Ycsp48qtUv1TWp+TH3kooTM6eKcnpSweaYDvHd/ucNg8UDNpIqynM1eS7KpffKQmDwIDAQAB-----END PUBLIC KEY-----

便是这样!我们有一个用于署名 JWT 的私钥和一个用于验证署名的公钥。
现在我们只须要在授权和资源做事器中配置它们。

2.2 实现利用私钥的授权做事器

在本节中,我们将授权做事器配置为利用私钥对 JWT 进行署名。
在 2.1 节中,您学习了如何天生私钥和公钥。
在本节中,我将创建一个 单独项目,但在 pom.xml 文件中将利用与在第 1 节中实现的授权做事器相同的依赖项。

我将私钥文件 yyit.jks 复制到运用程序的 resources 文件夹中。
我将密钥添加到 resources 文件夹中,由于它使我更随意马虎直接从类路径中读取密钥。
但是,并非必须要包含在类路径中。
在 application.properties 文件中,我存储文件名,密钥的别名以及天生密码时用来保护私钥的密码。
我们须要这些详细信息来配置 JwtTokenStore。
下一个代码段向您展示了我的 application.properties 文件的内容:

password=yyit123privateKey=yyit.jksalias=yyit

与我们为授权做事器利用对称密钥所做的配置比较,唯一改变的是 JwtAccessTokenConverter 工具的定义。
我们仍旧利用 JwtTokenStore。
如果您还记得,我们在第 1 节中利用 JwtAccessTokenConverter 来配置对称密钥。
我们利用相同的 JwtAccessTokenConverter 工具来设置私钥。
下面的清单显示了授权做事器的配置类。

清单 5 授权做事器和私钥的配置类

@Configuration@EnableAuthorizationServerpublic class AuthServerConfig extends AuthorizationServerConfigurerAdapter { //1 @Value("${password}") private String password; //2 @Value("${privateKey}") private String privateKey; //3 @Value("${alias}") private String alias; //1,2,3 从 application.properties 文件中注入私钥文件的名称,别名和密码 @Autowired private AuthenticationManager authenticationManager; / Omitted code @Bean public JwtAccessTokenConverter jwtAccessTokenConverter() { var converter = new JwtAccessTokenConverter(); //创建一个 KeyStoreKeyFactory 工具从类路径读取私钥文件 KeyStoreKeyFactory keyStoreKeyFactory = new KeyStoreKeyFactory( new ClassPathResource(privateKey), password.toCharArray() ); //利用KeyStoreKeyFactory 工具检索密钥对,并将密钥对设置为 JwtAccessTokenConverter 工具 converter.setKeyPair( keyStoreKeyFactory.getKeyPair(alias)); return converter; }}

现在可以启动授权做事器并调用 /oauth/token 端点来天生新的访问令牌。
当然,您只看到创建了一个普通的 JWT,但现在的差异在于,要验证它的署名,您须要利用这对中的公钥。
顺便说一下,别忘了令牌只是署名,没有加密。
下面的代码片段展示了如何调用 /oauth/token 端点:

curl -v -XPOST -u client:secret "http://localhost:8080/oauth/token?grant_type=password&username=xiaohua&passwopa=12345&scope=read"

相应体:

{ "access_token":"eyJhbGciOiJSUzI1NiIsInR5...", "token_type":"bearer", "refresh_token":"eyJhbGciOiJSUzI1NiIsInR...", "expires_in":43199, "scope":"read", "jti":"8e74dd92-07e3-438a-881a-da06d6cbbe06"}2.3 实现利用公钥的资源做事器

在本节中,我们实现一个利用公共密钥来验证令牌署名的资源做事器。
当我们完本钱节后,您将拥有一个完全的系统,该系统可以通过 OAuth 2 实现身份认证,并利用公私钥对来保护令牌。
授权做事器利用私钥对令牌署名,而资源做事器利用公钥来验证署名。
请把稳,我们仅利用密钥对令牌进行署名,而不对密钥进行加密。
我们在 pom.xml 中利用与本文前面各节中的示例相同的依赖项。

资源做事器须要利用公钥来验证令牌的署名,因此让我们将该密钥添加到 application.properties 文件中。
在 2.1 节中,您学习了如何天生公 钥。
下一个代码片段显示了我的 application.properites 文件的内容:

server.port=9090publicKey=-----BEGIN PUBLIC KEY-----MIIBIjANBghk...-----END PUBLIC KEY-----

为了更好的可读性,我简化了公钥。
下面的清单向您展示了如何在资源做事器的配置类中配置这个密钥。

清单 6 资源做事器和公钥的配置类

@Configuration@EnableResourceServerpublic class ResourceServerConfig extends ResourceServerConfigurerAdapter { // 从 application.properties 文件中注入密钥 @Value("${publicKey}") private String publicKey; @Override public void configure(ResourceServerSecurityConfigurer resources) { resources.tokenStore(tokenStore()); } //在 Spring 高下文中创建并添加 JwtTokenStore @Bean public TokenStore tokenStore() { return new JwtTokenStore( jwtAccessTokenConverter()); } @Bean public JwtAccessTokenConverter jwtAccessTokenConverter() { var converter = new JwtAccessTokenConverter(); //设置令牌存储用于验证令牌的公钥 converter.setVerifierKey(publicKey); return converter; }}

当然,为了有一个端点,我们还须要添加掌握器。
下一个代码片段定义了掌握器:

@RestControllerpublic class HelloController { @GetMapping("/hello") public String hello() { return "Hello!"; }}

让我们运行并调用端点以测试资源做事器。
这是命令:

curl -H "Authorization:Bearer eyJhbGciOiJSUzI1NiIsInR5cCI6I..." http://localhost:9090/hello

相应体:

Hello!

不该用 Spring Security OAuth 项目的非对称密钥

在本文中,如果运用程序利用非对称密钥进行令牌验证,我们将谈论将利用 Spring Security OAuth 项目的资源做事器迁移到一个大略的 Spring Security 项目时须要做的变动。
实际上,利用非对称密钥与利用对称密钥的项目没有太大差异。
唯一的变动是您须要利用的 JwtDecoder。
在这种情形下,您须要配置密钥对的公共部分,而不是配置用于令牌验证的对称密钥。
下面的代码片段展示了如何做到这一点:

public JwtDecoder jwtDecoder() { try { KeyFactory keyFactory = KeyFactory.getInstance("RSA"); var key = Base64.getDecoder().decode(publicKey); var x509 = new X509EncodedKeySpec(key); var rsaKey = (RSAPublicKey) keyFactory.generatePublic(x509); return NimbusJwtDecoder.withPublicKey(rsaKey).build(); } catch (Exception e) { throw new RuntimeException("Wrong public key"); }}

有了利用公钥验证令牌的 JwtDecoder 后,须要利用 oauth2ResourceServer() 方法设置解码器。
就像对称密钥一样。
下一个代码片段展示了如何做到这一点。

@Configurationpublic class ResourceServerConfig extends WebSecurityConfigurerAdapter { @Value("${publicKey}") private String publicKey; @Override protected void configure(HttpSecurity http) throws Exception { http.oauth2ResourceServer( c -> c.jwt( j -> j.decoder(jwtDecoder()) ) ); http.authorizeRequests() .anyRequest().authenticated(); } // Omitted code}2.4 利用端点公开公钥

在本节中,我们将谈论一种使资源做事器知道公钥的方法——授权做事器公开公钥。
在第 2 节实现的系统中,我们利用公私密钥对对令牌进行署名和验证。
我们在资源做事器端配置了公钥。
资源做事器利用公钥验证 JWT。
但是如果你想改变密匙对呢 ?最好不要永久保持相同的密匙对,这是您要在本节中学习实现的内容。
随着韶光的推移,你该当改换钥匙!这使得您的系统不易受到密钥盗取 ( 图 7 )。

图 7

图 7 如果密钥定期改换,系统的密钥被盗几率会降落。
但是,如果在两个运用程序中都配置了密钥,则很难改换它们。

到目前为止,我们已经在授权做事器端配置了私钥,在资源做事器端配置了公钥 (图 7)。
在两个地方设置使得钥匙更难管理。
但是如果我们只在一边配置它们,您就可以更随意马虎地管理密钥。
办理方案是将全体密钥对移动到授权做事器端,并许可授权做事器利用端点公开公钥 ( 图 8 )。

图 8

图 8 两个密钥都在授权做事器上配置。
为了获取公钥,资源做事器从授权做事器调用端点。
这种方法许可我们更随意马虎地改换密钥,由于我们只须要在一个地方配置它们。

我们利用一个单独的运用程序来证明如何利用 Spring Security 实现这个配置。
与前面示例一样,须要授权做事器和资源做事器。

对付授权做事器,我们将保持与 2.3 节中开拓的项目相同的设置。
我们只须要确保可以访问公开公钥的端点。
是的,Spring Boot 已经配置了这样的端点,但它只是这样。
默认情形下,所有要求都被谢绝。
我们须要覆盖端点的配置,并许可任何具有客户端凭据的人访问它。
在清单 7 中,您将看到须要对授权做事器的配置类进行的变动。
这些配置许可任何具有有效客户端凭据的人调用端点以得到公钥。

清单 7 公开公钥的授权做事器的配置类

@Configuration@EnableAuthorizationServerpublic class AuthServerConfig extends AuthorizationServerConfigurerAdapter { / Omitted code @Override public void configure( ClientDetailsServiceConfigurer clients) throws Exception { clients.inMemory() .withClient("client") .secret("secret") .authorizedGrantTypes("password", "refresh_token") .scopes("read") .and() //添加资源做事器用于调用端点的客户端凭据,端点将公开公钥 .withClient("resourceserver") .secret("resourceserversecret"); } @Override public void configure( AuthorizationServerSecurityConfigurer security) { security.tokenKeyAccess ("isAuthenticated()");//配置授权做事器,为利用有效客户端凭据进行身份验证的任何要求公开公钥的端点 }}

您可以启动授权做事器并调用 /oauth/token_key 端点,以确保精确地实现了配置。
下一个代码片段展示了 cURL 调用:

curl -u resourceserver:resourceserversecret http://localhost:8080/oauth/token_key

相应体:

{ "alg":"SHA256withRSA", "value":"-----BEGIN PUBLIC KEY----- nMIIBIjANBgkq... -----END PUBLIC KEY-----"}

为了使资源做事器利用此端点并获取公钥,您只须要在其属性文件中配置端点和凭据。
下一个代码段定义了资源做事器的 application.properties 文件:

server.port=9090security.oauth2.resource.jwt.key-uri=http://localhost:8080/oauth/token_keysecurity.oauth2.client.client-id=resourceserversecurity.oauth2.client.client-secret=resourceserversecret

由于资源做事器现在从授权做事器的 /oauth/token_key 端点获取公钥,以是您不须要在资源做事器配置类中配置它。
资源做事器的配置类可以保持为空,如下面的代码片段所示:

@Configuration@EnableResourceServerpublic class ResourceServerConfig extends ResourceServerConfigurerAdapter {}

您现在也可以启动资源做事器,并调用它公开的 /hello 端点,以查看全体设置是否如预期的那样事情。
下一个代码片段将向您展示如何利用 cURL 调用 /hello 端点。
在这里,你得到一个令牌,就像我们在 2.3 节中做的那样,并利用它来调用资源做事器的测试端点:

curl -H "Authorization:Bearer eyJhbGciOiJSUzI1NiIsInR5cCI..." http://localhost:9090/hello

相应体:

Hello!3 向 JWT 中添加自定义详细信息

在本节中,我们将谈论如何向 JWT 令牌添加自定义详细信息。
在大多数情形下,您只须要 Spring Security 已经添加到令牌中的内容。
然而,在实际场景中,您有时会创造须要在令牌中添加自定义信息的需求。
在本节中,我们将实现一个示例,在该示例中您将理解如何变动授权做事器以添加 JWT 上的自定义详细信息,以及如何变动资源做事器以读取这些详细信息。
如果您利用我们在前面的示例中天生的一个令牌并对其进行解码,您将看到 Spring Security 添加到令牌的默认值。
下面的清单给出了这些默认值。

清单 8 授权做事器发行的 JWT 正文中的默认详细信息

{ "exp": 1582581543, ##令牌到期的韶光戳 "user_name": "xiaohua", ##通过身份认证以许可客户端访问其资源的用户 "authorities": [ ## 付与该用户的权限 "read" ], "jti": "8e208653-79cf-45dd-a702-f6b694b417e7", ## 令牌的唯一标识符 "client_id": "client", ##要求该令牌的客户端 "scope": [ "read" ## 付与给客户真个权限 ]}

如清单 8 所示,默认情形下,令牌常日存储基本授权所需的所有详细信息。
但是,如果您的现实场景的需求哀求更多的东西呢?

您可以在读者用来阅读书本的运用程序中利用授权做事器。
某些端点仅应供应给特定评论数量以上的用户访问。
仅当用户从特定时区进行身份认证时,才须要许可调用。
您的授权做事器是一个社交网络,您的某些端点仅应由连接数量最少的用户访问。

对付我的第一个示例,您须要将评论数量添加到令牌中。
对付第二个,您添加了客户端连接所在的时区。
对付第三个示例,您须要为用户添加连接数。
无论您是哪种情形,都须要知道如何自定义 JWT。

3.1 配置授权做事器以将自定义详细信息添加到令牌

在本节中,我们将谈论为向令牌添加自定义详细信息而须要对授权做事器进行的变动。
为了使示例大略,我假设需求是添加授权做事器本身的时区。
要向令牌添加额外的细节,您须要创建一个 TokenEnhancer 类型的工具。
下面的清单定义了我为这个示例创建的 TokenEnhancer 工具。

清单 9 自定义令牌增强器

public class CustomTokenEnhancer implements TokenEnhancer { //实现 TokenEnhancer 接口 //重写 enhance() 方法,该方法吸收当前令牌并返回增强令牌 @Override public OAuth2AccessToken enhance( OAuth2AccessToken oAuth2AccessToken, OAuth2Authentication oAuth2Authentication) { //基于吸收到的令牌工具创建新的令牌工具 var token = new DefaultOAuth2AccessToken(oAuth2AccessToken); //将我们想要添加到令牌的详细信息定义为 Map Map<String, Object> info = Map.of("generatedInZone", ZoneId.systemDefault().toString()); //将其他详细信息添加到令牌中 token.setAdditionalInformation(info); //返回包含其他详细信息的令牌 return token; }}

TokenEnhancer 工具的 enhance() 方法吸收我们增强的令牌作为参数,并返回 “增强的” 令牌,个中包含额外的详细信息。
对付本例,我利用了在第 2 节中开拓的相同运用程序,只变动了 configure() 方法以运用令牌增强器。
下面的清单展示了这些变动。

清单 10 配置 TokenEnhancer 工具

@Configuration@EnableAuthorizationServerpublic class AuthServerConfig extends AuthorizationServerConfigurerAdapter {// Omitted code @Override public void configure( AuthorizationServerEndpointsConfigurer endpoints) { //定义了一个 TokenEnhancerChain TokenEnhancerChain tokenEnhancerChain = new TokenEnhancerChain(); //将两个令牌增强器工具添加到列表中 var tokenEnhancers = List.of(new CustomTokenEnhancer(), jwtAccessTokenConverter()); //将令牌增强器列表添加到链中 tokenEnhancerChain .setTokenEnhancers(tokenEnhancers); endpoints .authenticationManager(authenticationManager) .tokenStore(tokenStore()) .tokenEnhancer(tokenEnhancerChain); //配置令牌增强器工具 }}

正如您可以看到的,配置我们的自定义令牌增强器有点繁芜。
我们必须创建一个令牌增强器链,并设置全体链,而不是只设置一个工具,由于访问令牌转换器工具也是一个令牌增强器。
如果我们只配置自定义令牌增强器,就会覆盖访问令牌转换器的行为。
相反,我们将这两个工具都添加到职责链中,然后配置包含这两个工具的链。

让我们启动授权做事器,天生一个新的访问令牌,并检讨它,看看它是什么样子。
下一个代码片段向您展示了如何调用 /oauth/token 端点来获取访问令牌:

curl -v -XPOST -u client:secret "http://localhost:8080/oauth/token?grant_type=password&username=xiaohua&password=12345&scope=read"

相应体:

{ "access_token":"eyJhbGciOiJSUzI...", "token_type":"bearer", "refresh_token":"eyJhbGciOiJSUzI1...", "expires_in":43199, "scope":"read", "generatedInZone":"Europe/Bucharest", "jti":"0c39ace4-4991-40a2-80ad-e9fdeb14f9ec"}

如果对令牌进行解码,则可以看到其正文类似于清单 11 中所示的正文。
您还可以进一步不雅观察到,默认情形下,框架还会在相应中添加自定义详细信息。
但我建议您始终参考令牌中的任何信息。
请记住,通过对令牌进行署名,我们可以确保如果有人变动了令牌的内容,则署名不会得到验证。
这样,我们知道如果署名精确,则没有人会变动令牌的内容。
您对相应本身没有相同的担保。

清单 11 增强的 JWT 的正文

{ "user_name": "xiaohua", "scope": [ "read" ], "generatedInZone": "Europe/Bucharest", "exp": 1582591525, "authorities": [ "read" ], "jti": "0c39ace4-4991-40a2-80ad-e9fdeb14f9ec", "client_id": "client"}3.2 配置资源做事器以读取 JWT 的自定义详细信息

在本节中,我们将谈论须要对资源做事器进行的变动,以读取添加到 JWT 的附加信息。
变动授权做事器以向 JWT 添加自定义细节后,您希望资源做事器能够读取这些信息。
为了访问自定义详细信息,您须要在资源做事器中进行的变动非常大略。

我们在第 1 节中谈论过 AccessTokenConverter 是将令牌转换为身份认证的工具。
这是我们须要变动的工具,以便它也考虑到令牌中的自定义详细信息。
之前,您创建了 JwtAccessTokenConverter 类型的 Bean,如下面的代码片段所示:

@Beanpublic JwtAccessTokenConverter jwtAccessTokenConverter() { var converter = new JwtAccessTokenConverter(); converter.setSigningKey(jwtKey); return converter;}

我们利用这个令牌来设置资源做事器用于令牌验证的密钥。
我们创建了 JwtAccessTokenConverter 的自定义实现,它还考虑了关于令牌的新信息。
最大略的方法是扩展这个类并重写 extractAuthentication() 方法。
此方法转换 Authentication 工具中的令牌。
下一个清单展示了如何实现自定义 AcessTokenConverter。

清单 12 创建自定义 AccessTokenConverter

public class AdditionalClaimsAccessTokenConverter extends JwtAccessTokenConverter { @Override public OAuth2Authentication extractAuthentication(Map<String, ?> map) { //运用由 `JwtAccessTokenConverter` 类实现的逻辑,并得到初始身份认证工具 var authentication = super.extractAuthentication(map); //将自定义详细信息添加到身份认证 authentication.setDetails(map); //返转身份认证工具 return authentication; }}

在资源做事器的配置类中,您现在可以利用自定义访问令牌转换器。
下一个清单定义了配置类中的 AccessTokenConverter Bean。

清单 13 定义新的 AccessTokenConverter Bean

@Configuration@EnableResourceServerpublic class ResourceServerConfig extends ResourceServerConfigurerAdapter { // Omitted code @Bean public JwtAccessTokenConverter jwtAccessTokenConverter() { var converter = new AdditionalClaimsAccessTokenConverter(); converter.setVerifierKey(publicKey); return converter; }}

测试变动的一种大略方法是将它们注入掌握器类,并在 HTTP 相应中返回它们。
清单 14 展示了如何定义掌握器类。

清单 14 掌握器类

@RestControllerpublic class HelloController { @GetMapping("/hello") public String hello(OAuth2Authentication authentication) { //获取添加到 Authentication 工具的额外信息 OAuth2AuthenticationDetails details = (OAuth2AuthenticationDetails) authentication.getDetails(); //返回 HTTP 相应中的详细信息 return "Hello! " + details.getDecodedDetails(); }}

您现在可以启动资源做事器,并利用包含自定义详细信息的 JWT 测试端点。
下一个代码片段将向您展示如何调用 /hello 端点和调用的结果。
getDecodedDetails() 方法返回一个包含令牌详细信息的 Map。
在本例中,为了保持大略,我直接打印了 getDecodedDetails() 返回的全体值。
如果您只须要利用一个特定的值,您可以检讨返回的 Map 并利用其键获取所需的值。

curl -H "Authorization:Bearer eyJhbGciOiJSUzI1NiIsInR5cCI6Ikp... " http://localhost:9090/hello

相应体:

Hello! {user_name=john, scope=[read], generatedInZone=Europe/Bucharest, exp=1582595692, authorities=[read], jti=982b02be-d185-48de-a4d3-9b27337d1a46, client_id=client}

您可以在相应中创造新属性 generatedInZone=Europe/Bucharest。

总结利用加密署名是当前运用程序在 OAuth 2 身份认证架构中验证令牌的常用办法。
当我们将令牌验证与加密署名结合利用时,JSON Web Token ( JWT ) 是利用最广泛的令牌实现。
您可以利用对称密钥对令牌进行署名和验证。
虽然利用对称密钥是一种大略的方法,但是在授权做事器不信赖资源做事器时不能利用它。
如果在您的实现中不能利用对称密钥,那么您可以利用非对称密钥对来实现令牌署名和验证。
建议定期改换密钥,以减少系统密钥被盗的风险。
我们将密钥的周期性变革称为密钥的改换。
可以直接在资源做事器端配置公钥。
虽然这种方法很大略,但它使密钥改换更加困难。
为了简化密钥改换,您可以在授权做事器端配置密钥,并许可资源做事器在特定端点读取它们。
您可以根据实现的需求,通过向其正文添加信息来定制 JWT。
授权做事器将自定义详细信息添加到令牌正文,资源做事器将利用这些详细信息进行授权。