SpringBoot3 登录注册基础篇(二) - RSA 非对称加密实战

SpringBoot3 登录注册基础篇(二) - RSA 非对称加密实战

第一章. 为什么选择 RS256

在开始生成密钥之前,我们需要先回答一个核心问题:JWT 的签名算法应该选择 HS256 还是 RS256?

1.1. HS256 的架构困境

HS256 是对称加密签名和验证使用同一个密钥算法。它的工作流程是:

1
2
签名:Token + 密钥 → 签名
验证:Token + 密钥 → 验证签名是否匹配

假设我们的系统有 20 个微服务,每个服务都需要验证 Token。那么这 20 个服务都必须持有同一个密钥。这带来两个风险:

风险一:攻击面扩大

密钥存在于 20 个不同的配置文件、环境变量、容器镜像中。任何一个环节泄漏,整个系统就沦陷了。

风险二:密钥轮换困难

当我们需要更换密钥时(如定期安全审计、员工离职),必须同时更新 20 个服务的配置并重启。在生产环境中,这几乎不可能做到原子性切换。

1.2. RS256 的架构优势

RS256 是非对称加密使用私钥签名,公钥验证算法。它的工作流程是:

1
2
签名:Token + 私钥 → 签名
验证:Token + 公钥 → 验证签名是否匹配

在这种架构下:

  • 认证服务 持有私钥,负责签发 Token。私钥是整个系统中唯一的敏感数据,只需要保护一个点。
  • 网关和业务服务 持有公钥,负责验证 Token。公钥可以公开分发,即使泄漏也不会影响安全性(攻击者无法用公钥伪造签名)。

这种设计的核心价值在于 职责分离:只有认证服务才有 “印钞权”,其他服务只能 “验钞” 而不能 “造钞”。

1.3. 架构对比

对比维度HS256(对称加密)RS256(非对称加密)
密钥数量1 个(签名和验证共用)2 个(私钥签名,公钥验证)
密钥分发所有服务都需要持有密钥只有认证服务持有私钥
安全风险任何一个服务泄漏,全局沦陷只有认证服务泄漏才会沦陷
密钥轮换需要同时更新所有服务只需要更新认证服务
性能快(对称加密)稍慢(非对称加密)

在微服务架构下,我们选择 RS256,用稍微的性能损失换取更高的安全性和更灵活的架构。


第二章. 使用 OpenSSL 生成 RSA 密钥对

2.1. 密钥格式要求

Java 的 KeyFactory 对密钥格式有严格要求:

  • 私钥必须是 PKCS#8 格式
  • 公钥必须是 X.509 格式

如果格式不对,会抛出 InvalidKeySpecException 异常。

2.2. 生成 PKCS#8 格式的私钥

打开终端(Windows 用户可以使用 Git Bash),输入以下命令:

注意,openssl 通常在 git 窗口上才可以执行,请使用 git bash 执行此命令

1
openssl genpkey -algorithm RSA -out private_key.pem -pkeyopt rsa_keygen_bits:2048

命令解释

  • openssl genpkey:使用 OpenSSL 生成密钥(新版命令,自动生成 PKCS#8 格式)
  • -algorithm RSA:指定算法为 RSA
  • -out private_key.pem:输出文件名
  • -pkeyopt rsa_keygen_bits:2048:密钥长度为 2048 位

执行后,你会看到类似这样的输出:

1
2
.....+++
.....+++

并且在当前目录下会生成一个 private_key.pem 文件。

用文本编辑器打开 private_key.pem,你应该看到:

1
2
3
-----BEGIN PRIVATE KEY-----
MIIEvQIBADANBgkqhkiG9w0BAQEFAASCBKcwggSjAgEAAoIBAQC...
-----END PRIVATE KEY-----

关键标识:-----BEGIN PRIVATE KEY-----(没有 “RSA” 字样),这就是 PKCS#8 格式。

2.3. 从私钥中提取公钥

继续在终端中输入:

1
openssl rsa -pubout -in private_key.pem -out public_key.pem

命令解释

  • openssl rsa:使用 OpenSSL 处理 RSA 密钥
  • -pubout:输出公钥
  • -in private_key.pem:指定输入文件(刚才生成的私钥)
  • -out public_key.pem:输出文件名

执行后,你会看到:

1
writing RSA key

用文本编辑器打开 public_key.pem,你应该看到:

1
2
3
-----BEGIN PUBLIC KEY-----
MIIBIjANBgkqhkiG9w0BAQEFAAOCAQ8AMIIBCgKCAQEAtkR...
-----END PUBLIC KEY-----

2.4. 将密钥放入项目

auth-web 模块中,找到 src/main/resources 目录,创建 certs 文件夹:

1
2
3
4
auth-web/src/main/resources/
└── certs/
├── private_key.pem
└── public_key.pem

将刚才生成的两个文件复制到这个目录下。

安全提示:必须在 .gitignore 中添加 **/certs/private_key.pem,防止私钥被提交到 Git 仓库。


第三章. 封装 RsaKeyManager 工具类

3.1. 为什么需要转换

Java 的 PrivateKeyPublicKey 对象需要的是二进制格式。我们需要一个工具类来完成以下工作:

  1. 读取 resources 目录下的密钥文件
  2. 去除 PEM 格式的头尾标记(-----BEGIN...-----
  3. 将 Base64 编码的字符串解码为字节数组
  4. 使用 Java 的 KeyFactory 将字节数组转换为密钥对象

3.2. RsaKeyManager 核心代码

📄 文件路径auth-core/src/main/java/com/example/auth/core/util/crypto/RsaKeyManager.java

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
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
package com.example.auth.core.util.crypto;


import org.springframework.core.io.ClassPathResource;
import org.springframework.stereotype.Component;
import org.springframework.util.FileCopyUtils;

import java.io.InputStreamReader;
import java.nio.charset.StandardCharsets;
import java.security.KeyFactory;
import java.security.PrivateKey;
import java.security.PublicKey;
import java.security.spec.PKCS8EncodedKeySpec;
import java.security.spec.X509EncodedKeySpec;
import java.util.Base64;

@Component
public class RsaKeyManager {

// 从 resources 目录读取文件
public String readResourceFile(String resourcePath) throws Exception {
ClassPathResource resource = new ClassPathResource(resourcePath);
try (InputStreamReader reader = new InputStreamReader(
resource.getInputStream(), StandardCharsets.UTF_8)) {
return FileCopyUtils.copyToString(reader);
}
}

// 加载私钥
public PrivateKey loadPrivateKey(String privateKeyPem) throws Exception {
// 清理 PEM 格式的头尾标记
String content = privateKeyPem
.replace("-----BEGIN PRIVATE KEY-----", "")
.replace("-----END PRIVATE KEY-----", "")
.replace("-----BEGIN RSA PRIVATE KEY-----", "")
.replace("-----END RSA PRIVATE KEY-----", "")
.replaceAll("\\s", "");

// Base64 解码
byte[] keyBytes = Base64.getDecoder().decode(content);

// 使用 PKCS8 规范解析私钥
PKCS8EncodedKeySpec keySpec = new PKCS8EncodedKeySpec(keyBytes);
KeyFactory keyFactory = KeyFactory.getInstance("RSA");
return keyFactory.generatePrivate(keySpec);
}

// 加载公钥
public PublicKey loadPublicKey(String publicKeyPem) throws Exception {
String content = publicKeyPem
.replace("-----BEGIN PUBLIC KEY-----", "")
.replace("-----END PUBLIC KEY-----", "")
.replaceAll("\\s", "");

byte[] keyBytes = Base64.getDecoder().decode(content);

// 使用 X509 规范解析公钥
X509EncodedKeySpec keySpec = new X509EncodedKeySpec(keyBytes);
KeyFactory keyFactory = KeyFactory.getInstance("RSA");
return keyFactory.generatePublic(keySpec);
}
}

3.3. 关键知识点

为什么要去除 PEM 格式的头尾标记?

PEM 格式是一种文本格式,用于存储密钥和证书。它的结构是:

1
2
3
-----BEGIN [类型]-----
[Base64编码的密钥数据]
-----END [类型]-----

Java 的 KeyFactory 只能识别纯粹的 Base64 编码数据,不能识别这些标记,所以必须先去除。

PKCS8 和 X509 是什么?

  • PKCS8:私钥的存储格式标准(Public Key Cryptography Standards #8)
  • X509:公钥证书的标准格式

这两个标准定义了密钥在二进制层面的存储结构。

为什么用 ClassPathResource 而不是 File?

当项目打包成 jar 包后,resources 目录下的文件会被打包到 jar 包内部。此时:

  • new File("certs/public_key.pem") 会失败
  • new ClassPathResource("certs/public_key.pem") 可以正确读取

第四章. 本章小结

我们完成了 RSA 密钥对的生成与加载。

核心成果

步骤操作产出
1使用 OpenSSL 生成私钥private_key.pem(PKCS#8 格式)
2从私钥中提取公钥public_key.pem(X.509 格式)
3将密钥放入项目auth-web/src/main/resources/certs/
4封装工具类RsaKeyManager

方法速查

类名方法名作用
RsaKeyManagerreadResourceFile(String path)读取 resources 目录下的文件
RsaKeyManagerloadPrivateKey(String pem)加载私钥
RsaKeyManagerloadPublicKey(String pem)加载公钥

架构对比

对比维度HS256RS256
密钥数量1 个2 个(私钥+公钥)
密钥分发所有服务都需要只有认证服务持有私钥
安全风险任何服务泄漏,全局沦陷只有认证服务泄漏才沦陷