引言
最近在项目上遇到了前后端通信时需要对数据进行加密的需求,网上搜罗了一大堆方案,大多介绍的都不太全,不能直接应用到项目中,所以就借此机会出一个完整的前后端数据通信加解密处理方案。
为什么需要接口加密
在现代Web应用中,数据安全传输面临三大核心挑战:
- 防窃听:防止敏感数据在传输过程中被第三方截获
- 防篡改:确保数据在传输过程中不被恶意修改
- 防重放:避免请求被截获后重复发送
单纯使用HTTPS
并不能解决所有问题,特别是当传输包含用户隐私、支付信息等敏感数据时,这种风险更加不可接受。接口加密能够确保即使数据被截获,攻击者也无法理解其中的内容。
混合加密的技术选型
- 算法特性对比
算法类型 | 代表算法 | 特点 | 适用场景 |
---|---|---|---|
对称加密 | AES | 加密速度快,但密钥分发难 | 大数据量业务数据加密 |
非对称加密 | RSA | 安全性高,但加密速度较慢 | 密钥交换与数据签名 |
- 混合加密的优势
- 性能平衡:AES处理业务数据,RSA保护AES密钥
- 完美前向保密:每次会话生成独立AES密钥
- 密钥管理简化:服务端只需保管RSA私钥
实现原理
- 客户端与服务端先约定并保存好RSA密钥对
- 客户端随机生成AES密钥,使用RSA公钥加密这个密钥
- 使用AES密钥加密请求体数据
- 将原始的请求体数据与时间戳、随机字符串、AES密钥一起生成签名
- 将加密后的AES密钥和加密后的数据以及时间戳、随机字符串等参数一起发送给服务端
- 服务端接收到请求后,先用RSA私钥解密得到AES密钥,再用AES密钥解密数据,然后再对数据做签名校验
- 服务端对应接口拿到解密后的数据,做后续的业务逻辑处理
系统交互流程
sequenceDiagram
participant 前端
participant 过滤器
participant 后端服务
前端->>过滤器: POST /userData/submit-form
Note Over 前端, 过滤器: 请求头:
Note Over 前端, 过滤器: - timestamp: 时间戳
Note Over 前端, 过滤器: - nonce: 随机数
Note Over 前端, 过滤器: - signature: 签名
Note Over 前端, 过滤器: - encrypted-key: RSA加密后的AES密钥
Note Over 前端, 过滤器: 请求体:
Note Over 前端, 过滤器: { data: AES加密的业务数据 }
过滤器->>过滤器: 1. 验证时间戳有效性(±5分钟)
过滤器->>过滤器: 2. 检查nonce是否重复(Redis防重放)
过滤器->>过滤器: 3. RSA解密获取AES密钥
过滤器->>过滤器: 4. AES解密请求体数据
过滤器->>过滤器: 5. 验证签名(SHA256WithRSA)
过滤器->>后端服务: 6. 明文请求体(JSON格式)
后端服务->>过滤器: 7. 明文响应数据
过滤器->>过滤器: 8. AES加密响应
过滤器->>前端: 9. 返回加密数据(HTTP 200)
Note Over 过滤器, 后端服务: 响应头:
Note Over 过滤器, 后端服务: - encrypted-key: RSA加密的AES密钥
关键代码实现
前端加密示例
javascript
// 请求拦截器
service.interceptors.request.use(
config => {
const timestamp = Date.now().toString();
const nonce = crypto.generateNonce();
// 判断是否为form-data请求
const isFormData = config.headers['Content-Type']?.includes('multipart/form-data');
if (!isFormData) {
// 生成AES密钥与随机向量
const aesKey = crypto.generateAESKey();
const iv = crypto.generateIV();
// RSA公钥加密AES密钥
const keyData = JSON.stringify({ key: aesKey, iv: iv });
const encryptedAESKey = crypto.rsaEncrypt(keyData);
// AES密钥加密请求体
const originalData = config.data || {};
const encryptedData = crypto.aesEncrypt(originalData, aesKey, iv);
// 生成签名
const signature = crypto.generateSignature(originalData, timestamp, nonce,
// 添加请求头
config.headers['X-Encrypted-Key'] = encryptedAESKey;
config.headers['X-Signature'] = signature;
config.headers['X-Timestamp'] = timestamp;
config.headers['X-Nonce'] = nonce;
config.data = {
data: encryptedData,
};
}
return config;
},
error => {
console.error('请求错误:', error);
return Promise.reject(error);
}
);
后端解密示例
- 过滤器解密
java
@Component
public class DecryptionFilter implements Filter {
private final Logger log = LoggerFactory.getLogger(DecryptionFilter.class);
private final SecurityService securityService;
private final RSAUtils rsaUtils;
public DecryptionFilter(SecurityService securityService, RSAUtils rsaUtils) {
this.securityService = securityService;
this.rsaUtils = rsaUtils;
}
@Override
public void doFilter(ServletRequest request, ServletResponse response, FilterChain chain) throws IOException, ServletException {
HttpServletRequest httpRequest = (HttpServletRequest) request;
HttpServletResponse httpResponse = (HttpServletResponse) response;
if (!requiresDecryption(httpRequest)) {
chain.doFilter(request, response);
return;
}
String encryptedKey = httpRequest.getHeader(CommonConstants.ENCRYPTED_KEY);
String signature = httpRequest.getHeader(CommonConstants.SIGNATURE);
String timestamp = httpRequest.getHeader(CommonConstants.TIMESTAMP);
String nonce = httpRequest.getHeader(CommonConstants.NONCE);
// 参数完整性校验
if (StringUtils.isAnyBlank(encryptedKey, signature, timestamp, nonce)) {
sendErrorResponse(httpResponse, "请求头参数缺失", HttpStatus.BAD_REQUEST);
return;
}
try {
// 时间戳校验(5分钟内)
if (!securityService.validateTimestamp(Long.parseLong(timestamp))) {
sendErrorResponse(httpResponse, "请求已过期", HttpStatus.BAD_REQUEST);
return;
}
// 重复请求校验
if (!securityService.validateNonce(nonce)) {
sendErrorResponse(httpResponse, "重复的请求", HttpStatus.BAD_REQUEST);
return;
}
HttpRequestWrapper requestWrapper = new HttpRequestWrapper(httpRequest);
Map<String, Object> requestBodyMap = JSON.parseObject(requestWrapper.getBody(), Map.class);
String encryptedData = (String) requestBodyMap.get("data");
if (StringUtils.isBlank(encryptedData)) {
chain.doFilter(request, response);
}
// RSA私钥解密AES密钥
String aesKeyJson = rsaUtils.decryptByBlock(encryptedKey);
AESKey aesKey = JSON.parseObject(aesKeyJson, AESKey.class);
// 解密请求数据
String decryptedData;
try {
log.info("获取到加密数据: \n{}", encryptedData);
decryptedData = AESUtils.decrypt(encryptedData, aesKey);
log.info("解密后的数据: \n{}", decryptedData);
} catch (Exception e) {
log.error("数据解密失败: {}", e.getMessage(), e);
sendErrorResponse(httpResponse, "内部服务器错误", HttpStatus.INTERNAL_SERVER_ERROR);
return;
}
Map<String, Object> params = JSON.parseObject(decryptedData, Map.class);
// 数据签名验证
if (!securityService.verifySignature(params, timestamp, nonce, aesKey.getKey(), signature)) {
sendErrorResponse(httpResponse, "数据签名验证失败", HttpStatus.BAD_REQUEST);
return;
}
log.info("数据签名验证通过!");
requestWrapper.setBody(decryptedData);
chain.doFilter(requestWrapper, response);
} catch (Exception e) {
sendErrorResponse(httpResponse, "数据解密失败", HttpStatus.BAD_REQUEST);
}
}
/**
* 判断请求是否需要解密处理
*/
private boolean requiresDecryption(HttpServletRequest request) {
// 1. 只处理POST/PUT/PATCH请求
String method = request.getMethod().toUpperCase();
if (!"POST".equals(method) && !"PUT".equals(method) && !"PATCH".equals(method)) {
return false;
}
// 2. 检查Content-Type
String contentType = request.getContentType();
if (contentType == null || !contentType.toLowerCase().contains(MediaType.APPLICATION_JSON_VALUE)) {
return false;
}
// 3. 只处理特定路径的请求
String uri = request.getRequestURI();
return uri.startsWith("/userData");
}
private void sendErrorResponse(HttpServletResponse response, String message, HttpStatus status)
throws IOException {
response.setStatus(status.value());
response.setContentType("application/json;charset=UTF-8");
response.setCharacterEncoding("UTF-8");
response.getWriter().write(
String.format("{\"msg\":\"%s\",\"code\":%d}", message, status.value())
);
}
}
- 自定义请求包装器 (
用于重复读取请求体以及替换请求体内容
)
java
public class HttpRequestWrapper extends HttpServletRequestWrapper {
private byte[] body;
public HttpRequestWrapper(HttpServletRequest request) throws IOException {
super(request);
if (request.getContentLength() > 0) {
this.body = readBytes(request.getReader());
} else {
this.body = new byte[0];
}
}
public HttpRequestWrapper(HttpServletRequest request, String body) {
super(request);
this.body = body.getBytes();
}
public String getBody() {
return new String(body);
}
public void setBody(String body) {
this.body = body.getBytes();
}
@Override
public ServletInputStream getInputStream() {
return new ByteArrayServletInputStream(body);
}
@Override
public BufferedReader getReader() {
return new BufferedReader(new InputStreamReader(getInputStream()));
}
private byte[] readBytes(BufferedReader reader) throws IOException {
StringBuilder sb = new StringBuilder();
char[] buffer = new char[1024];
int bytesRead;
while ((bytesRead = reader.read(buffer)) != -1) {
sb.append(buffer, 0, bytesRead);
}
return sb.toString().getBytes();
}
private static class ByteArrayServletInputStream extends ServletInputStream {
private final ByteArrayInputStream buffer;
public ByteArrayServletInputStream(byte[] body) {
this.buffer = new ByteArrayInputStream(body);
}
@Override
public int read() {
return buffer.read();
}
@Override
public boolean isFinished() {
return buffer.available() == 0;
}
@Override
public boolean isReady() {
return true;
}
@Override
public void setReadListener(ReadListener readListener) {
}
}
}
注意: 以上代码示例中部分代码可根据实际需求自行调整
如只处理特定请求
uri.startsWith("/userData");
这里只做示例验证,跟实际业务无关。
本文涉及到的技术组件
- 前端加密库:
crypto-js
- Java安全库:
Bouncy Castle
常见问题
Q1:如何防止重放攻击?
java
// 服务端校验nonce
public boolean validateNonce(String nonce) {
Boolean success = stringRedisTemplate.opsForValue().setIfAbsent(
"nonce:" + nonce,
"1",
timestampTolerance,
TimeUnit.MINUTES
);
return success != null && success;
}
Q2:为什么选择过滤器解密?
- 统一处理:集中所有加密接口的安全逻辑
- 业务无感知:控制器无需关心解密细节
- 提前拦截:在进入Spring MVC前完成验证
- 性能可控:可针对加密操作单独优化
Q2:iOS/Android如何兼容?
-
使用跨平台加密库:React Native用
react-native-crypto-js
-
统一算法参数:
javaCipher.getInstance("RSA/ECB/OAEPWithSHA-256AndMGF1Padding")
Q4:RSA密钥对如何生成
可通过在线生成工具生成,生成后注意保存好密钥对。
扩展阅读
结语
AES+RSA混合加密方案在保证安全性的同时兼顾了系统性能,是当前Web应用加密通信的最佳实践。本文提供的实现方案具有以下特点:
- 开箱即用:完整代码片段可直接集成
- 灵活扩展:支持算法动态切换
- 全栈覆盖:包含前后端完整实现
项目源码:可在GitHub获取完整实现示例
安全建议:定期更换密钥对,长期密钥不超过90天