在Spring Boot项目里处理用户提交的敏感数据时,如果接口直接接收明文身份证号、银行卡号或手机号,等于把这些值暴露在浏览器开发者工具、代理抓包、网关日志和数据库记录中。RSA非对称加密提供了一种不共享密钥就能实现端到端保护的思路:客户端持有公钥对字段加密,服务端持有私钥解密,即使传输内容被截获,没有私钥也无法还原。本文以一个常见的用户信息提交接口为例,逐步说明如何生成密钥对、封装加解密工具、在Spring Boot中完成敏感字段的自动解密,以及实际使用时需要注意的长度限制和安全边界。

为什么敏感信息要用非对称加密而不是对称加密
对称加密算法如AES加解密速度快,适合处理大量数据,但它有一个天然的短板:加密和解密使用同一个密钥。如果前端使用AES加密敏感字段,那么前端代码里就必须存放这个密钥,而前端代码对用户来说几乎等于透明,即使做了混淆,密钥也有可能被逆向出来。密钥一旦泄露,整个加密机制就失效了。
RSA属于非对称加密,它有一对密钥:公钥和私钥。公钥可以公开,随便放在前端配置、接口响应或者移动端包里;私钥只保留在服务端,不参与网络传输。前端用公钥加密,服务端用私钥解密,这种设计从根本上避免了密钥分发问题。即便攻击者截获了公钥和密文,在没有私钥的情况下也无法还原明文。所以,对于身份证号、银行卡号、手机号这类长度不大但对保密性要求极高的字段,RSA是非常合适的保护手段。
不过也要清楚RSA的性能比AES差很多,而且单次加密的明文长度受密钥长度限制。2048位密钥配合PKCS1填充时,单块最多只能加密245字节。如果要对很长的文本做非对称加密,通常采用混合加密方案,即用RSA加密一个随机生成的AES密钥,再用AES加密真实数据。本文聚焦小字段场景,直接使用RSA加密敏感字段本身。
生成并配置RSA密钥对
RSA密钥对可以通过JDK自带的keytool命令生成,也可以通过Java代码生成。对于Spring Boot项目来说,用Java代码生成密钥对并导出Base64字符串比较直观,也方便在部署时动态生成。下面的代码使用KeyPairGenerator生成2048位密钥对,并将公钥和私钥以Base64形式打印出来。
import java.security.KeyPair;
import java.security.KeyPairGenerator;
import java.security.PrivateKey;
import java.security.PublicKey;
import java.util.Base64;
public class RsaKeyGenerator {
public static void main(String[] args) throws Exception {
KeyPairGenerator generator = KeyPairGenerator.getInstance("RSA");
generator.initialize(2048);
KeyPair keyPair = generator.generateKeyPair();
PublicKey publicKey = keyPair.getPublic();
PrivateKey privateKey = keyPair.getPrivate();
String publicKeyBase64 = Base64.getEncoder().encodeToString(publicKey.getEncoded());
String privateKeyBase64 = Base64.getEncoder().encodeToString(privateKey.getEncoded());
System.out.println("Public Key: " + publicKeyBase64);
System.out.println("Private Key: " + privateKeyBase64);
}
}
生成后可以把公钥下发给前端使用,私钥则需要安全保存。生产环境中不要把私钥硬编码在代码里,也不要直接提交到Git仓库。建议放到环境变量、密钥管理服务或者配置中心中。Spring Boot可以通过@Value注解读取配置,例如在application.yml里配置公钥和私钥的Base64字符串,然后在需要使用的地方注入。私钥属于高敏感配置,如果项目有配置加密能力,应该对配置本身再做一层加密。
需要注意,输出密钥时不要打印到生产日志,密钥生成工具只用于初始化阶段,完成之后可以将生成逻辑移除或加上严格权限控制。2048位密钥目前仍是比较安全的选择,如果合规要求更高,可以使用4096位密钥,但加解密耗时也会明显增加。
封装RSA加解密工具类并处理长度限制
RSA的加密操作需要使用Cipher类,算法指定为RSA/ECB/PKCS1Padding。由于2048位密钥在PKCS1填充下最多加密245字节,如果敏感字段超过这个长度,就需要分段加密。分段加密的思路是把明文字节数组按固定块大小切开,每一块单独调用doFinal,再把每块密文Base64编码后用分隔符拼接起来。解密时先按分隔符拆分,再对每个块分别解密并拼接明文。
import javax.crypto.Cipher;
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;
public class RsaUtils {
private static final String ALGORITHM = "RSA";
private static final int MAX_ENCRYPT_BLOCK = 245;
private static final int MAX_DECRYPT_BLOCK = 256;
public static String encrypt(String data, String publicKeyBase64) throws Exception {
PublicKey publicKey = getPublicKey(publicKeyBase64);
Cipher cipher = Cipher.getInstance("RSA/ECB/PKCS1Padding");
cipher.init(Cipher.ENCRYPT_MODE, publicKey);
byte[] dataBytes = data.getBytes(StandardCharsets.UTF_8);
int inputLen = dataBytes.length;
StringBuilder result = new StringBuilder();
for (int i = 0; i < inputLen; i += MAX_ENCRYPT_BLOCK) {
int length = Math.min(inputLen - i, MAX_ENCRYPT_BLOCK);
byte[] block = cipher.doFinal(dataBytes, i, length);
result.append(Base64.getEncoder().encodeToString(block));
if (i + length < inputLen) {
result.append("|");
}
}
return result.toString();
}
public static String decrypt(String encryptedData, String privateKeyBase64) throws Exception {
PrivateKey privateKey = getPrivateKey(privateKeyBase64);
Cipher cipher = Cipher.getInstance("RSA/ECB/PKCS1Padding");
cipher.init(Cipher.DECRYPT_MODE, privateKey);
String[] blocks = encryptedData.split("\\|");
StringBuilder result = new StringBuilder();
for (String block : blocks) {
byte[] decoded = Base64.getDecoder().decode(block);
byte[] decryptedBlock = cipher.doFinal(decoded);
result.append(new String(decryptedBlock, StandardCharsets.UTF_8));
}
return result.toString();
}
private static PublicKey getPublicKey(String base64) throws Exception {
byte[] keyBytes = Base64.getDecoder().decode(base64);
X509EncodedKeySpec spec = new X509EncodedKeySpec(keyBytes);
KeyFactory keyFactory = KeyFactory.getInstance(ALGORITHM);
return keyFactory.generatePublic(spec);
}
private static PrivateKey getPrivateKey(String base64) throws Exception {
byte[] keyBytes = Base64.getDecoder().decode(base64);
PKCS8EncodedKeySpec spec = new PKCS8EncodedKeySpec(keyBytes);
KeyFactory keyFactory = KeyFactory.getInstance(ALGORITHM);
return keyFactory.generatePrivate(spec);
}
}
这段代码里有一个容易踩坑的细节:解密时使用split方法拆分密文,分隔符是竖线,而竖线在Java正则表达式里是特殊字符,所以必须写成split("\\|")。如果直接写split("|"),结果会按每个字符拆分,导致解密失败。另外,如果使用1024位密钥,最大加密块大小会变成117字节,解密块为128字节,需要根据实际密钥长度调整常量。
工具类中公钥和私钥的还原分别使用X509EncodedKeySpec和PKCS8EncodedKeySpec,这是Java中标准的密钥编码格式。Base64字符串可以直接作为配置传入,不依赖证书文件。如果项目里已经使用证书体系,也可以从证书中读取公钥,私钥则通过KeyStore加载,本质思路一致。
在Spring Boot接口中接入RSA加解密
假设有一个用户信息保存接口,前端提交的JSON包含加密后的身份证号和银行卡号字段。Controller接收到请求后,从配置中读取私钥,调用RsaUtils.decrypt将密文还原为明文,再进行业务校验和入库。解密的异常需要单独捕获,避免把底层异常堆栈直接返回给前端。
import org.springframework.beans.factory.annotation.Value;
import org.springframework.http.ResponseEntity;
import org.springframework.web.bind.annotation.PostMapping;
import org.springframework.web.bind.annotation.RequestBody;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.RestController;
@RestController
@RequestMapping("/api/user")
public class UserController {
@Value("${rsa.private-key}")
private String privateKey;
@PostMapping("/save")
public ResponseEntity<String> saveUser(@RequestBody UserRequest request) {
try {
String idCard = RsaUtils.decrypt(request.getIdCard(), privateKey);
String bankCard = RsaUtils.decrypt(request.getBankCard(), privateKey);
// 后续校验与入库逻辑
return ResponseEntity.ok("success");
} catch (Exception e) {
return ResponseEntity.badRequest().body("解密失败");
}
}
}
前端侧通常会使用类似JSEncrypt的库,在提交请求前用公钥对敏感字段逐个加密,并把密文Base64字符串放进请求体。需要注意的是,Base64字符串中可能包含加号、斜杠和等号,如果通过URL查询参数传输,需要先做URL编码,或者改用Base64 URL安全编码。更好的做法是把加密字段放在POST请求体中,避免URL长度和编码问题。
接口联调时,建议先写一个简单的测试用例验证工具类本身是否正确。可以先用公钥加密一段已知文本,再用私钥解密,确认结果一致。然后再联调前端加密、后端解密。如果解密失败,常见原因包括密钥不匹配、公钥私钥用反、Base64字符串在传输过程中被截断或被错误编码、分隔符处理不当等。定位问题时可以在服务端打印密文长度和解密异常类型,但不要打印完整密文和私钥。
实际使用中的几个关键注意事项
首先是密钥长度和加密块大小的关系。不同密钥长度对应不同的最大明文块大小,1024位密钥是117字节,2048位是245字节,4096位是501字节。如果业务字段可能超过这个长度,要么改为混合加密,要么在前端限制输入长度并做提示。强行对过长明文加密会抛出异常,用户体验很差。
其次是私钥管理。私钥一旦泄露,整个加密机制就失去了意义。生产环境要避免把私钥写在application.yml这类可能被查看的配置文件中,更不能提交到代码仓库。可以使用环境变量注入、KMS密钥管理服务或者专门的配置中心。如果私钥需要轮换,要设计好公钥下发的版本机制,让前端能够获取到与当前私钥匹配的公钥。
最后是防重放和数据完整性。RSA加密本身不能防止攻击者截获密文后重复提交同一个请求,所以对于关键操作还需要配合时间戳、随机nonce和签名机制。另外,敏感数据解密后在使用和存储时也要做好脱敏,例如数据库中只存脱敏后的数据,或者使用专门的加密存储方案,不要认为传输层加密之后就万事大吉。
Spring BootRSA加密非对称加密修改时间:2026-10-04 15:48:00