Spring Boot对访问密钥加密解密——RSA
场景
用户无需登录,仅仅根据给定的访问keyId和keySecret就可以访问接口。
- keyId 等可以明文发送(不涉及机密),后端直接从请求头读取。
- keySecret 不可明文,需要加密后放在另一个请求头(或请求体)里,比如 Encrypted-Data。
- timestamp + 6位随机值(nonce) 用来防重放或检验请求时效,需要在明文里也传递给后端,以方便后端做校验。
- 后端拿到 Encrypted-Data 后,使用私钥解密得到真正的 keySecret、然后再和明文传递过来的 timestamp、randomValue 做进一步匹配。
# 请求头中的信息
keyId
timestamp
randomValue
Encrypted-Data="{\"keySecret\":\"%s\",\"timestamp\":\"%s\",\"randomValue\":\"%s\"}"
客户端
前端调用:拼装并加密数据
假设我们有如下信息:
- keyId:“myKeyId”
- keySecret:“myKeySecret”(需加密)
- timestamp:“20251225103449”(格式 yyyyMMddHHmmss)
- randomValue:6 位随机数,如 “538201”
生成随机数 & 时间戳
function generateTimestamp() {
// 生成形如 20251225103449 的日期字符串 (yyyyMMddHHmmss)
const now = new Date();
const yyyy = now.getFullYear();
const MM = String(now.getMonth() + 1).padStart(2, '0');
const dd = String(now.getDate()).padStart(2, '0');
const HH = String(now.getHours()).padStart(2, '0');
const mm = String(now.getMinutes()).padStart(2, '0');
const ss = String(now.getSeconds()).padStart(2, '0');
return `${yyyy}${MM}${dd}${HH}${mm}${ss}`;
}
function generateRandomValue6() {
// 生成 6 位随机数
return String(Math.floor(Math.random() * 1000000)).padStart(6, '0');
}
加密 keySecret
假设使用 jsencrypt 进行 RSA 加密,并把 keySecret + timestamp + randomValue 打包在一个 JSON 对象里加密。
import axios from 'axios';
import JSEncrypt from 'jsencrypt';
// RSA 公钥
const PUBLIC_KEY_PEM = `
-----BEGIN PUBLIC KEY-----
MIIBIjANBgkqh...
-----END PUBLIC KEY-----
`;
// 拼装并加密
function encryptKeyInfo(keySecret, timestamp, randomValue) {
const payload = {
keySecret,
timestamp,
randomValue
};
const jsonStr = JSON.stringify(payload);
const jsEncrypt = new JSEncrypt();
jsEncrypt.setPublicKey(PUBLIC_KEY_PEM);
const encrypted = jsEncrypt.encrypt(jsonStr);
if (!encrypted) {
throw new Error("加密失败,请检查公钥格式或数据大小");
}
return encrypted; // Base64 编码的加密结果
}
// 发送请求示例
async function callSecureApi() {
const keyId = "myKeyId";
const timestamp = generateTimestamp();
const randomValue = generateRandomValue6();
const keySecret = "myKeySecret"; // 需要加密的机密
// 把 keySecret、timestamp、randomValue 一并加密
const encryptedData = encryptKeyInfo(keySecret, timestamp, randomValue);
// 发请求时,除了 "Encrypted-Data" 外,把 keyId/timestamp/randomValue 也都明文放在 header
// 这样后端可做交叉验证
try {
const resp = await axios.post(
'/api/secure',
{}, // body 可为空或随意
{
headers: {
'keyId': keyId,
'timestamp': timestamp,
'randomValue': randomValue,
'Encrypted-Data': encryptedData
}
}
);
console.log('后端返回:', resp.data);
} catch (e) {
console.error(e);
}
}
Java调用
maven依赖
<dependency>
<groupId>org.apache.httpcomponents</groupId>
<artifactId>httpclient</artifactId>
<version>4.5.14</version>
</dependency>
代码调用
package com.demo.test;
import org.apache.http.client.methods.CloseableHttpResponse;
import org.apache.http.client.methods.HttpPost;
import org.apache.http.entity.StringEntity;
import org.apache.http.impl.client.CloseableHttpClient;
import org.apache.http.impl.client.HttpClients;
import org.apache.http.util.EntityUtils;
import javax.crypto.Cipher;
import java.io.IOException;
import java.security.KeyFactory;
import java.security.PublicKey;
import java.security.spec.X509EncodedKeySpec;
import java.time.LocalDateTime;
import java.time.format.DateTimeFormatter;
import java.util.Base64;
import java.util.Random;
public class ThirdPartyCaller {
// 假设你目标服务的地址
private static final String BASE_URL = "http://localhost:13131";
// 例如 "http://localhost:8080" 或生产环境 "https://yourdomain.com"
public static void main(String[] args) {
try {
// 1) 先获取公钥(PEM 格式)
String publicKeyPem = "-----BEGIN PUBLIC KEY-----\n" +
"MIIBIjANBgkqhkiG9w0BAQEFAAOCAQ8AMIIBCgKCAQEAs3ZYer3Ibej9o4IRlzkC\n" +
"2Hjk0roGuR4ckIafh/DZNHv/JfHgM7R10BLfTnkiGyrps7Lk36Gm9RnafduphCMX\n" +
"nG7nZdCYKt16aE4Tqb7FZ/pLz8WOjpxiSLA361w+knG9UCyytmhXjo6wZ8zxNoSd\n" +
"ePhMkGBVxwCbeZS9wldBbwpRpMY1Dsyve7C8COhEiWIFXz0ruMsskukCue2Q6nNh\n" +
"6dLIN17MtEqa7in7Q6xPvyNPsCkfI8PAvQqGO5thdZoTcT7XPHrBJesfuS8sSmtF\n" +
"xMPfI/Nke/KTykeDcf4PHKd0GP5c6+/p0XVkzxHm7sbFEEdII41e1Gd81gJ6bPQc\n" +
"dQIDAQAB\n" +
"-----END PUBLIC KEY-----";
// 2) 解析 PEM 得到 PublicKey 对象
PublicKey pubKey = parsePublicKeyFromPem(publicKeyPem);
// 3) 准备要加密和要发送的字段
String keyId = "qwertyuiopasdfghjklzxcvbnm";
String keySecret = "demo@123!";
String timestamp = generateTimestamp(); // yyyyMMddHHmmss
String randomValue = generateRandomValue6(); // 6位随机数
// 4) 组装 JSON,再用公钥 RSA 加密
// 若与前端示例一致,可把 (keySecret, timestamp, randomValue) 放到 JSON
String payloadJson = String.format(
"{\"keySecret\":\"%s\",\"timestamp\":\"%s\",\"randomValue\":\"%s\"}",
keySecret, timestamp, randomValue
);
String encryptedData = rsaEncrypt(pubKey, payloadJson);
// 5) 调用 /api/secure 接口
String result = callSecureApi(keyId, timestamp, randomValue, encryptedData);
System.out.println("调用 /aliyun_sms/send 响应: " + result);
} catch (Exception e) {
e.printStackTrace();
}
}
/**
* 第一步:解析 PEM 格式的公钥字符串,得到 PublicKey 对象
* 如果服务器只返回纯Base64(无头尾),可根据实际情况调整。
*/
private static PublicKey parsePublicKeyFromPem(String publicKeyPem) throws Exception {
// 移除头尾行和换行,保留中间的Base64部分
String base64 = publicKeyPem
.replaceAll("-----BEGIN PUBLIC KEY-----", "")
.replaceAll("-----END PUBLIC KEY-----", "")
.replaceAll("\\s", "");
byte[] decoded = Base64.getDecoder().decode(base64);
X509EncodedKeySpec keySpec = new X509EncodedKeySpec(decoded);
KeyFactory keyFactory = KeyFactory.getInstance("RSA");
return keyFactory.generatePublic(keySpec);
}
/**
* 第二步:把要加密的JSON用RSA公钥加密
*/
private static String rsaEncrypt(PublicKey publicKey, String data) throws Exception {
Cipher cipher = Cipher.getInstance("RSA/ECB/PKCS1Padding");
cipher.init(Cipher.ENCRYPT_MODE, publicKey);
byte[] encrypted = cipher.doFinal(data.getBytes());
return Base64.getEncoder().encodeToString(encrypted);
}
/**
* 生成时间戳 (yyyyMMddHHmmss)
*/
private static String generateTimestamp() {
DateTimeFormatter fmt = DateTimeFormatter.ofPattern("yyyyMMddHHmmss");
return LocalDateTime.now().format(fmt);
}
/**
* 生成6位随机数 (000000~999999)
*/
private static String generateRandomValue6() {
Random random = new Random();
int num = random.nextInt(1_000_000);
return String.format("%06d", num);
}
/**
* 第三步:调用 /api/secure 接口,并在header中带上 keyId, timestamp, randomValue, Encrypted-Data
*/
private static String callSecureApi(String keyId, String timestamp,
String randomValue, String encryptedData) throws IOException {
String url = BASE_URL + "/aliyun_sms/send";
try (CloseableHttpClient httpClient = HttpClients.createDefault()) {
HttpPost post = new HttpPost(url);
// 设置header
post.setHeader("keyId", keyId);
post.setHeader("timestamp", timestamp);
post.setHeader("randomValue", randomValue);
post.setHeader("Encrypted-Data", encryptedData);
post.setHeader("Content-Type", "application/json;charset=UTF-8");
// 此示例没给 body,若需要可在 body 放更多数据
//post.setEntity(new StringEntity("{\"name\":\"张三\"}", "UTF-8"));
try (CloseableHttpResponse response = httpClient.execute(post)) {
int statusCode = response.getStatusLine().getStatusCode();
String respBody = EntityUtils.toString(response.getEntity(), "UTF-8");
if (statusCode == 200) {
return respBody;
} else {
throw new IOException("调用 /api/secure 失败, HTTP状态=" + statusCode + ", body=" + respBody);
}
}
}
}
}
服务端
RSA私钥解密
package com.demo.lcpd.utils;
import org.springframework.beans.factory.annotation.Value;
import org.springframework.stereotype.Component;
import javax.crypto.Cipher;
import java.nio.charset.StandardCharsets;
import java.security.KeyFactory;
import java.security.PrivateKey;
import java.security.spec.PKCS8EncodedKeySpec;
import java.util.Base64;
@Component
public class RsaDecryptor {
@Value("${security.rsa.privateKeyPem}")
private String privateKeyPem;
public String decrypt(String encryptedBase64) throws Exception {
// 1) 去掉 PEM 格式里的头尾和换行,只保留真正的Base64部分
String base64Key = removePemFormatting(privateKeyPem);
// 2) 把Base64解码还原成私钥对象
byte[] keyBytes = Base64.getDecoder().decode(base64Key);
PKCS8EncodedKeySpec keySpec = new PKCS8EncodedKeySpec(keyBytes);
KeyFactory keyFactory = KeyFactory.getInstance("RSA");
PrivateKey privateKey = keyFactory.generatePrivate(keySpec);
// 3) 解密
byte[] encryptedBytes = Base64.getDecoder().decode(encryptedBase64);
Cipher cipher = Cipher.getInstance("RSA/ECB/PKCS1Padding");
cipher.init(Cipher.DECRYPT_MODE, privateKey);
byte[] decrypted = cipher.doFinal(encryptedBytes);
return new String(decrypted, StandardCharsets.UTF_8);
}
private String removePemFormatting(String pem) {
// 移除行首行尾 (-----BEGIN PRIVATE KEY----- / -----END PRIVATE KEY-----) 以及换行
return pem
.replaceAll("-----BEGIN PRIVATE KEY-----", "")
.replaceAll("-----END PRIVATE KEY-----", "")
.replaceAll("\\s", "");
}
}
自定义拦截器
package com.demo.lcpd.intercept;
import com.fasterxml.jackson.databind.JsonNode;
import com.fasterxml.jackson.databind.ObjectMapper;
import com.demo.lcpd.utils.RsaDecryptor;
import org.springframework.stereotype.Component;
import org.springframework.web.servlet.HandlerInterceptor;
import javax.annotation.Resource;
import javax.servlet.http.HttpServletRequest;
import javax.servlet.http.HttpServletResponse;
import java.time.LocalDateTime;
import java.time.format.DateTimeFormatter;
import java.util.HashMap;
import java.util.Map;
@Component
public class RsaAuthInterceptor implements HandlerInterceptor {
@Resource
private RsaDecryptor rsaDecryptor;
private Map<String, String> keyStore = new HashMap<>();
//添加允许通过的keyId和keySecret
public RsaAuthInterceptor() {
keyStore.put("qwertyuiopasdfghjklzxcvbnm", "demo@123!");
}
@Override
public boolean preHandle(HttpServletRequest request, HttpServletResponse response, Object handler) throws Exception {
String keyId = request.getHeader("keyId");
String timestamp = request.getHeader("timestamp");
String randomValue = request.getHeader("randomValue");
String encryptedData = request.getHeader("Encrypted-Data");
if (keyId == null || timestamp == null || randomValue == null || encryptedData == null) {
response.sendError(HttpServletResponse.SC_UNAUTHORIZED, "Missing auth headers");
return false;
}
try {
// 2) RSA 私钥解密
String decryptedJson = rsaDecryptor.decrypt(encryptedData);
// decryptedJson 形如: {"keySecret":"myKeySecret","timestamp":"20251225103449","randomValue":"538201"}
// 3) 解析 JSON
ObjectMapper objectMapper = new ObjectMapper();
JsonNode node = objectMapper.readTree(decryptedJson);
String decryptedKeySecret = node.get("keySecret").asText();
String decryptedTimestamp = node.get("timestamp").asText();
String decryptedRandomValue = node.get("randomValue").asText();
// 4) 与明文传递的 timestamp、randomValue 对比
if (!timestamp.equals(decryptedTimestamp) || !randomValue.equals(decryptedRandomValue)) {
response.sendError(HttpServletResponse.SC_UNAUTHORIZED, "Timestamp or randomValue mismatch");
return false;
}
// 5) 校验 keyId 是否存在,以及 keySecret 是否匹配
if (!keyStore.containsKey(keyId)) {
response.sendError(HttpServletResponse.SC_UNAUTHORIZED, "Invalid keyId");
return false;
}
String expectedSecret = keyStore.get(keyId);
if (!expectedSecret.equals(decryptedKeySecret)) {
response.sendError(HttpServletResponse.SC_UNAUTHORIZED, "Invalid keySecret");
return false;
}
// 6) 校验 timestamp 是否在合理范围 (例如 ±15分钟)
if (!checkTimestampValid(timestamp)) {
response.sendError(HttpServletResponse.SC_UNAUTHORIZED, "Timestamp expired or invalid");
return false;
}
// 7) 防重放:检查 (keyId + timestamp + randomValue) 是否已使用过
// 这里仅示例,不做具体实现
// boolean notUsed = checkAndMarkUsed(keyId, timestamp, randomValue);
// if (!notUsed) {
// response.sendError(HttpServletResponse.SC_UNAUTHORIZED, "Replay attack detected");
// return;
// }
// 校验成功
return true;
} catch (Exception e) {
response.sendError(HttpServletResponse.SC_UNAUTHORIZED, "Decryption or parse error");
return false;
}
}
private boolean checkTimestampValid(String timestampStr) {
try {
// 解析 yyyyMMddHHmmss
DateTimeFormatter fmt = DateTimeFormatter.ofPattern("yyyyMMddHHmmss");
LocalDateTime reqTime = LocalDateTime.parse(timestampStr, fmt);
LocalDateTime now = LocalDateTime.now();
// 假设 15 分钟有效期
return !reqTime.isBefore(now.minusMinutes(15)) && !reqTime.isAfter(now.plusMinutes(15));
} catch (Exception e) {
return false;
}
}
}
注册拦截器
package com.demo.lcpd.conf;
import com.demo.lcpd.intercept.RsaAuthInterceptor;
import org.springframework.context.annotation.Configuration;
import org.springframework.web.servlet.config.annotation.InterceptorRegistry;
import org.springframework.web.servlet.config.annotation.WebMvcConfigurer;
import javax.annotation.Resource;
@Configuration
public class WebMvcConfig implements WebMvcConfigurer {
@Resource
private RsaAuthInterceptor rsaAuthInterceptor;
@Override
public void addInterceptors(InterceptorRegistry registry) {
registry.addInterceptor(rsaAuthInterceptor)
.addPathPatterns("/**");
}
}
在 Java 中生成 RSA 公私钥
使用 KeyPairGenerator
public class RsaKeyGeneratorPemExample {
public static void main(String[] args) throws Exception {
KeyPairGenerator keyPairGenerator = KeyPairGenerator.getInstance("RSA");
keyPairGenerator.initialize(2048);
KeyPair keyPair = keyPairGenerator.generateKeyPair();
// Base64
String publicKeyBase64 = Base64.getEncoder().encodeToString(keyPair.getPublic().getEncoded());
String privateKeyBase64 = Base64.getEncoder().encodeToString(keyPair.getPrivate().getEncoded());
// 包装成 PEM
String publicKeyPem = convertToPem(publicKeyBase64, true);
String privateKeyPem = convertToPem(privateKeyBase64, false);
System.out.println(publicKeyPem);
System.out.println(privateKeyPem);
}
private static String convertToPem(String base64Key, boolean isPublic) {
int lineLength = 64;
StringBuilder sb = new StringBuilder();
if (isPublic) {
sb.append("-----BEGIN PUBLIC KEY-----\n");
} else {
sb.append("-----BEGIN PRIVATE KEY-----\n");
}
for (int i = 0; i < base64Key.length(); i += lineLength) {
int end = Math.min(i + lineLength, base64Key.length());
sb.append(base64Key, i, end).append("\n");
}
if (isPublic) {
sb.append("-----END PUBLIC KEY-----\n");
} else {
sb.append("-----END PRIVATE KEY-----\n");
}
return sb.toString();
}
}
运行后就能得到标准的 PEM 形式公私钥,类似:
-----BEGIN PUBLIC KEY-----
MIIBIjANBgkqhkiG9w0BAQEFA...
...LQIDAQAB
-----END PUBLIC KEY-----
-----BEGIN PRIVATE KEY-----
MIIEvQIBADANBgkqhkiG9w0BAQ...
...2ww==
-----END PRIVATE KEY-----
这两个就是 PEM 格式的“公钥”和“私钥”。
前端使用的 PUBLIC_KEY_PEM 是什么?
可以是带头尾的 PEM
在前端(例如使用 jsencrypt)时,最常见的做法就是直接使用 PEM 格式:
const PUBLIC_KEY_PEM = -----BEGIN PUBLIC KEY----- MIIBIjANBgkqh... -----END PUBLIC KEY-----
;
然后在加密时,像这样写:
import JSEncrypt from 'jsencrypt';
const jsEncrypt = new JSEncrypt();
// 直接 set PEM 格式的字符串即可
jsEncrypt.setPublicKey(PUBLIC_KEY_PEM);
const encrypted = jsEncrypt.encrypt("hello world");
jsencrypt 这个库支持 PEM 格式字符串(包含头尾)。也就是说,如果你在 Java 端生成了 PEM,直接整段复制到前端就行。
也可以是纯 Base64 字符串
有些前端库也允许你只传Base64 编码的公钥,而不含头尾。那你就可以给它传:
// 纯 Base64,不带 -----BEGIN----
const PUBLIC_KEY_BASE64 = "MIIBIjANBgkq…";
const jsEncrypt = new JSEncrypt();
jsEncrypt.setPublicKey(`-----BEGIN PUBLIC KEY-----
${PUBLIC_KEY_BASE64}
-----END PUBLIC KEY-----`);
要点总结
1.后端生成 RSA 公私钥:
- 公钥用来给前端加密;
- 私钥留在后端解密,不要泄露。
2.前端使用 jsencrypt:
- PEM 格式(带 -----BEGIN PUBLIC KEY-----)的公钥即可;
- 把要加密的字段 (keySecret, timestamp, randomValue) JSON 化 -> 调用 .encrypt(…) -> 得到密文(Base64)。
3.后端使用私钥解密:
- 先去掉 PEM 的头尾,只保留 Base64 -> 解码 -> Cipher.getInstance(“RSA/ECB/PKCS1Padding”) 解密;
- 拿到明文 JSON,解析出 keySecret, timestamp, randomValue;
- 与前端同时传来的明文 timestamp, randomValue 做比对;再检查 keyId 对应的 keySecret 是否匹配。
4.时间戳 + 随机值:
- 可做防重放;
- 时间戳超时或重复使用同一 (keyId + timestamp + randomValue),则拒绝请求。
5.配合 HTTPS:
- RSA 加密可以避免明文 keySecret 出现在网络上,但最佳实践还是要用 HTTPS,防止更多层面的攻击、劫持、篡改。
原文地址:https://blog.csdn.net/print_helloword/article/details/144723743
免责声明:本站文章内容转载自网络资源,如本站内容侵犯了原著者的合法权益,可联系本站删除。更多内容请关注自学内容网(zxcms.com)!