1. 前言

在微服务架构中,服务间的安全通信至关重要。为了解决不同服务之间的认证与授权问题,常常使用 JSON Web Token (JWT) 作为令牌传递机制。JWT 是一种轻量级的令牌格式,包含丰富的用户身份信息,并且可以被服务端验证。利用 Spring Security 6JWT,我们可以轻松实现服务间的安全通信,确保只有经过授权的客户端才能发起服务间请求。

在前面的第 9 章节中,笔者详细介绍了 JWT + Spring Security 的整合,本文仅对 JWT 进行简要介绍。更多详细内容感兴趣的小伙伴可回顾 Spring Security 实战教程(九)前后端分离认证实战 - JWT+SpringSecurity 无缝整合

本文将带领大家快速了解如何使用 Spring Security 6 配合 JWT 实现服务间的安全通信。

微服务间 JWT 通信核心流程

微服务间 JWT 通信流程图

2. 配置 Spring Security 6 与 Nimbus-JOSE-JWT

本章节笔者使用 Nimbus-JOSE-JWT 作为 JWT 处理库。

2.1 引入依赖

1
2
3
4
5
6
7
8
9
10
11
<!-- OAuth2 资源服务器支持 -->
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-oauth2-resource-server</artifactId>
</dependency>
<!-- JWT 处理库(Nimbus 实现) -->
<dependency>
<groupId>com.nimbusds</groupId>
<artifactId>nimbus-jose-jwt</artifactId>
<version>9.37</version>
</dependency>
  • spring-boot-starter-oauth2-resource-server 提供了 Spring Security 中资源服务器的支持。
  • nimbus-jose-jwt 提供了生成和验证 JWT 的功能。

关键依赖说明

依赖项 作用 必需场景
spring-boot-starter-security 安全框架核心 所有安全场景必须
spring-boot-starter-oauth2-resource-server OAuth2 资源服务器支持 JWT 验证必需
nimbus-jose-jwt JWT 编码/解码实现 JWT 处理必需

2.2 配置公钥和私钥

为了使用 RSA 进行 JWT 签名和验证,我们需要配置私钥(用于签名)和公钥(用于验证)。可以将密钥存储为 PEM 格式的文件,并在 Spring Boot 配置中加载这些密钥。

生成公私钥对

首先,生成一个 RSA 公私钥对:

1
2
3
4
5
# 生成私钥(private.pem)
openssl genpkey -algorithm RSA -out private.pem -pkeyopt rsa_keygen_bits:2048

# 提取公钥(public.pem)
openssl rsa -pubout -in private.pem -out public.pem

将密钥加载到 Spring Boot

将私钥和公钥存储为文件,并在配置中加载它们:

1
2
3
4
5
6
7
8
# application.yml 配置公钥和私钥路径
spring:
security:
oauth2:
resourceserver:
jwt:
public-key-location: classpath:public.pem
private-key-location: classpath:private.pem

此处我们将密钥文件放在 resources 目录下,并通过配置 public-key-locationprivate-key-location 来加载它们。

也可指定系统上的其他目录

3. JWT 生成和验证

3.1 生成 JWT 令牌

创建一个方法,通过 RSA 私钥签名 JWT 令牌。使用 Nimbus-JOSE-JWT 生成带有签名的 JWT

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
32
33
34
import com.nimbusds.jose.*;
import com.nimbusds.jose.crypto.RSASSASigner;
import com.nimbusds.jwt.JWTClaimsSet;
import com.nimbusds.jwt.SignedJWT;

import java.security.PrivateKey;
import java.util.Date;

public class JwtUtils {

private final PrivateKey privateKey;

public JwtUtils(PrivateKey privateKey) {
this.privateKey = privateKey;
}

public String createJWT(String subject) throws Exception {
JWTClaimsSet claimsSet = new JWTClaimsSet.Builder()
.subject(subject)
.issueTime(new Date())
.expirationTime(new Date(System.currentTimeMillis() + 3600000)) // 1 小时过期
.build();

SignedJWT signedJWT = new SignedJWT(
new JWSHeader(JWSAlgorithm.RS256),
claimsSet
);

RSASSASigner signer = new RSASSASigner(privateKey);
signedJWT.sign(signer);

return signedJWT.serialize();
}
}

该方法根据传入的用户名(subject)生成一个 JWT 令牌,并使用 RSA 私钥进行签名。生成的 JWT 将包含基本声明 (如 subject、issueTime 和 expirationTime)

3.2 验证 JWT 令牌

验证 JWT 令牌需要使用 RSA 公钥来验证其签名。以下是使用 Nimbus-JOSE-JWT 库验证 JWT 的代码:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
import com.nimbusds.jose.*;
import com.nimbusds.jose.crypto.RSASSAVerifier;
import com.nimbusds.jwt.SignedJWT;

import java.security.PublicKey;

public class JwtUtils {

private final PublicKey publicKey;

public JwtUtils(PublicKey publicKey) {
this.publicKey = publicKey;
}

public boolean validateJWT(String token) throws Exception {
SignedJWT signedJWT = SignedJWT.parse(token);

RSASSAVerifier verifier = new RSASSAVerifier(publicKey);

return signedJWT.verify(verifier);
}
}

该方法解析并验证传入的 JWT 令牌,使用 RSA 公钥验证签名的合法性。签名有效时返回 true,否则返回 false。

4. 配置 Spring Security 资源服务器

使用 Spring Security 配置资源服务器,确保每个请求都携带有效的 JWT 令牌,并使用 RSA 公钥验证令牌。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
@Configuration
public class SecurityConfig {

@Bean
public SecurityFilterChain securityFilterChain(HttpSecurity http) throws Exception {
http
.authorizeRequests(auth -> auth
.requestMatchers("/public/**").permitAll()
.anyRequest().authenticated()
)
.oauth2ResourceServer(oauth2 -> oauth2.jwt(jwt -> jwt.decoder(jwtDecoder())));

return http.build();
}

@Bean
public JwtDecoder jwtDecoder() throws Exception {
RSAPublicKey publicKey = (RSAPublicKey) KeyFactory.getInstance("RSA")
.generatePublic(new X509EncodedKeySpec(Files.readAllBytes(Paths.get("classpath:public.pem"))));
return NimbusJwtDecoder.withPublicKey(publicKey).build();
}
}

在上述配置中,我们指定了 JwtDecoder,使用 NimbusJwtDecoder 加载 RSA 公钥,并为每个请求验证 JWT 令牌。通过 oauth2ResourceServer(oauth2 -> oauth2.jwt()) 配置,Spring Security 会自动处理 JWT 校验。

5. 服务间的 JWT 令牌传递与校验

5.1 服务 A 生成 JWT

服务 A 在用户登录后生成 JWT,并将其返回给客户端:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
@RestController
public class AuthController {

private final JwtUtils jwtUtils;

public AuthController(JwtUtils jwtUtils) {
this.jwtUtils = jwtUtils;
}

@PostMapping("/login")
public String login(@RequestBody LoginRequest loginRequest) throws Exception {
if ("user".equals(loginRequest.getUsername()) && "password".equals(loginRequest.getPassword())) {
return jwtUtils.createJWT(loginRequest.getUsername());
}
throw new UnauthorizedException("Invalid credentials");
}
}

5.2 客户端发送 JWT

客户端将 JWT 令牌放入请求的 Authorization 头部:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
public class RestClient {

public static String getProtectedResource(String token) {
HttpHeaders headers = new HttpHeaders();
headers.set("Authorization", "Bearer " + token);

HttpEntity<String> entity = new HttpEntity<>(headers);

RestTemplate restTemplate = new RestTemplate();
ResponseEntity<String> response = restTemplate.exchange(
"http://localhost:8081/api/protected",
HttpMethod.GET,
entity,
String.class
);
return response.getBody();
}
}

5.3 服务 B 校验 JWT

服务 B 验证 JWT 令牌,确保请求来自合法的客户端:

1
2
3
4
5
6
7
8
9
@RestController
@RequestMapping("/api")
public class ProtectedResourceController {

@GetMapping("/protected")
public ResponseEntity<String> getProtectedData() {
return ResponseEntity.ok("This is a protected resource!");
}
}

6. 结语

通过本章节的介绍,相信小伙伴们已经掌握了如何使用 JWTRSA 非对称加密在 Spring Security 6 中实现 JWT 令牌传递与校验机制。非对称加密的优势在于公钥可以公开分发,而私钥仅由服务端持有,从而增强了系统的安全性。

当然,本章节未引入 OpenFeign 作为服务间调用工具,仅使用了最原始的 RestTemplate 进行其他服务的测试请求。小伙伴们可根据实际需求进行调整和完善,进一步提升系统的安全性。