抓包还能看到密码?Vue3 + Spring Boot 3 接口 AES/RSA 加解密(@ApiEncrypt)全链路实战
一句话 :HTTPS 管「路上」,
@ApiEncrypt管「包里」------敏感接口的 Body 在到达 Controller 之前就该是密文,前端 axios 一眼看不见明文 JSON。

开源仓库:GitCode · RuoyiOffice|AtomGit · RuoyiOffice
为什么「已经上了 HTTPS」还不够?
| 场景 | HTTPS 帮得上吗 | 密文 Body 帮得上吗 |
|---|---|---|
| 公网链路被窃听 | ✅ | ✅(双重) |
| 员工本机 Fiddler / Charles 装根证书 | ❌ 明文尽收眼底 | ✅ Body 仍是乱码 |
| 前端误把密码打进 console / 埋点 | 部分 | 减少传输侧泄露面 |
| 数据库被拖库 | ❌ | ❌(要靠落库字段加密) |
| 接口被重放 | ❌ | ❌(要靠签名 + nonce + 时效) |
很多团队把「传输安全」和「存储安全」「防重放」揉成一锅粥。RuoyiOffice 里其实是三件独立兵器:
| 能力 | 解决什么 | 关键词 |
|---|---|---|
| API 传输加解密 | 抓包看不到业务 JSON | @ApiEncrypt / Filter / X-Api-Encrypt |
| 字段落库加密 | DBA/备份文件看不到手机号 | TypeHandler / 列级密文 |
| API 签名 | 防篡改、防重放 | appId + timestamp + sign |
本文只打透第一件:请求解密 + 响应加密 的前后端全链路。
登录页本身是敏感入口的典型------密码绝不能以「可复制的明文 JSON」躺在抓包工具里。

全链路长什么样?
text
Vue3 RequestClient
│ headers.isEncrypt = true
│ data = AES/RSA.encrypt(JSON)
│ Header: X-Api-Encrypt: true
▼
HTTP POST /admin-api/... (Body = Base64/密文串)
▼
ApiEncryptFilter(早于 Controller、早于多数业务 Filter 可读点)
│ 有加密头 → DecryptRequestWrapper 还原明文 Body
│ 方法标了 @ApiEncrypt(request=true) 却无加密头 → 直接拒
▼
Controller / Service (以为自己在收普通 VO)
▼
若 response=true → EncryptResponseWrapper 写出密文 + 响应头
▼
前端响应拦截器:见加密头 → decrypt → 再走统一 Result 解析
设计上有个关键取舍:不用 RequestBodyAdvice / ResponseBodyAdvice ,而用 Servlet Filter。原因很务实------访问日志、异常日志、签名校验往往要在「业务反序列化之前」看到原始字节流;Filter 更靠前、更可控。
后端:一个注解,两种开关
java
@Target({ElementType.TYPE, ElementType.METHOD})
@Retention(RetentionPolicy.RUNTIME)
public @interface ApiEncrypt {
/** 是否解密请求体,默认 true */
boolean request() default true;
/** 是否加密响应体,默认 true */
boolean response() default true;
}
用法示例(示意):
java
@PostMapping("/update-password")
@ApiEncrypt // 请求解密 + 响应加密
public CommonResult<Boolean> updatePassword(@RequestBody @Valid PwdReq req) {
// req 里已是明文,业务无感知加解密
return success(userService.updatePassword(req));
}
@GetMapping("/profile")
@ApiEncrypt(request = false, response = true) // 只加密出参
public CommonResult<ProfileVO> getProfile() { ... }
Filter 核心分支(概念伪代码)
java
ApiEncrypt ann = resolveHandlerAnnotation(request);
boolean needReq = ann != null && ann.request();
boolean needResp = ann != null && ann.response();
String encryptHeader = request.getHeader("X-Api-Encrypt");
if (!needReq && !needResp && isBlank(encryptHeader)) {
chain.doFilter(request, response); // 普通接口放行
return;
}
// POST/PUT/DELETE:有加密头则包装解密;标注了 request 却没头 → 参数错误
if (isWriteMethod && isNotBlank(encryptHeader)) {
request = new DecryptRequestWrapper(request, decryptor);
} else if (needReq) {
throw invalidParam("请求未包含加密标头");
}
if (needResp) {
response = new EncryptResponseWrapper(response); // 先包着,链尾再真正加密
}
chain.doFilter(request, response);
// 链结束后:若 needResp,把缓冲内容加密写回,并打上加密响应头
要点:
- 注解驱动 + 请求头双保险:前端必须带加密头,后端方法必须声明需要解密,避免「误加密普通上传」或「漏加密敏感口」。
- 只处理写方法 Body:GET 通常无 Body;加密主要打在 JSON POST。
- 算法可配置:AES(对称,性能好)或 RSA(非对称,密钥角色对调要小心)。
配置:算法、密钥、请求头
配置项语义(勿把密钥提交进 Git):
| 项 | 含义 |
|---|---|
| enable | 总开关;关闭则整条 Filter 可短路 |
| header | 默认 X-Api-Encrypt,请求/响应共用语义 |
| algorithm | AES 或 RSA(国密 SM2/SM4 可二次扩展) |
| requestKey | 后端用来解密请求的钥 |
| responseKey | 后端用来加密响应的钥 |
AES vs RSA:密钥角色别搞反
| 算法 | 后端 requestKey | 前端请求加密用 | 后端 responseKey | 前端响应解密用 |
|---|---|---|---|---|
| AES | 共享密钥 | 同一共享密钥 | 共享密钥(可与请求同或不同) | 对应共享密钥 |
| RSA | 私钥 | 公钥 | 公钥 | 私钥 |
AES 密钥长度要对齐实现(常见 16/32 字节);前端 CryptoJS ECB + PKCS7 必须与后端 Hutool AES 模式一致,否则「能加密不能解密」。
前端:Vue3 RequestClient 怎么接
RuoyiOffice web-antd 在 axios 封装里挂了加解密工具:
ts
const apiEncrypt = createApiEncrypt(import.meta.env);
// 请求拦截:按调用方标记加密
if ((config.headers || {}).isEncrypt) {
config.data = apiEncrypt.encryptRequest(config.data);
config.headers[apiEncrypt.getEncryptHeader()] = 'true';
}
// 响应拦截:后端打了加密头则先解密再交给业务
const encryptHeader = apiEncrypt.getEncryptHeader();
if (
(response.headers[encryptHeader] === 'true' ||
response.headers[encryptHeader.toLowerCase()] === 'true') &&
typeof response.data === 'string'
) {
response.data = apiEncrypt.decryptResponse(response.data);
}
业务侧敏感调用:
ts
await requestClient.post('/system/user/profile/update-password', data, {
headers: { isEncrypt: true },
});
环境变量里配置与后端一致的算法、header 名、请求加密钥、响应解密钥。生产密钥走私密配置/CI Secret,禁止写进公开仓库的 .env.example 真值。
和「字段加密」「API 签名」怎么分工?
text
浏览器 ──(1 传输加密)──▶ API ──(2 签名校验)──▶ Service ──(3 字段加密)──▶ DB
@ApiEncrypt appId+sign TypeHandler
| 问法 | 该用谁 |
|---|---|
| Charles 里密码是明文? | 传输加密 |
| 别人改了金额字段重放? | 签名 + 时效 + nonce |
| 备份 SQL 里手机号裸奔? | 字段加密 / 脱敏展示 |
| 双击提交两单? | 幂等 @Idempotent,不是加密 |
加密解决「看不懂」,签名解决「改不了、重放难」,幂等解决「做两次等于做一次」------别指望一个注解包打天下。
对比落库加密架构(另一条线):

落地清单与踩坑
上线前核对
- 前后端
algorithm/ header 名一致 - AES 模式与 padding 一致(ECB/CBC 混用必炸)
- RSA 公私钥角色按上表对齐
- 仅敏感接口开
isEncrypt,大文件上传、multipart 不要整包 AES - 网关/Nginx 勿二次改写 Body 编码
- 访问日志若打印 Body,注意打的是解密前还是解密后(合规)
- 密钥轮换预案:双钥窗口期或强制客户端升级
常见故障
| 现象 | 排查 |
|---|---|
| 后端报「未包含加密标头」 | 前端没带 isEncrypt 或 header 名不一致 |
| 解密结果为空 | 密钥错、算法错、或前端加密了已经是字符串的二次 JSON |
| 响应业务码解析失败 | 响应拦截器没先解密,把密文当 JSON parse |
| 本地 Postman 调不通 | 要用脚本先加密 Body,或临时关该接口注解(勿关生产总开关) |
| 文件上传 400 | multipart 被当成 JSON 解密------排除加密 |
FAQ
Q1:全站所有接口都加密会不会很慢?
A:AES 对 JSON 级别开销通常可接受;全站加密会让排障、开放 API、第三方回调变地狱。建议:登录改密、支付相关、证件号提交 等点状开启。
Q2:为什么 Filter 要在「有注解却无加密头」时直接失败?
A:防止攻击者去掉加密头、企图走明文旁路;也防止前端漏配却「看起来成功」造成假安全感。
Q3:和国密 SM4/SM2?
A:扩展点在算法工厂;加依赖与加解密器分支即可,注解与前端 isEncrypt 协议可复用。
Q4:微服务 / 网关要解密几次?
A:推荐在业务服务入口解密一次;网关只做 TLS 与鉴权,避免双端持钥扩散。若网关必须看到明文做路由,再评估「网关终止加密 + mTLS 到后端」。
Q5:响应也加密后,前端错误提示怎么办?
A:全局异常仍返回统一 Result;只要 Filter 在写出前加密、前端先解密,错误码体验与明文接口一致。
和 RuoyiOffice 的对应关系
| 能力 | 位置(概念) |
|---|---|
@ApiEncrypt + Filter |
Web Starter · encrypt 包 |
| 加解密配置 | api-encrypt 配置项(enable/header/algorithm/keys) |
| 前端工具 | @vben/utils · createApiEncrypt |
| 请求封装 | apps/web-antd · api/request.ts |
| 对照:字段加密 | MyBatis TypeHandler |
| 对照:API 签名 | 签名设计专题 |
总结
- HTTPS ≠ 抓包安全;敏感 Body 需要应用层加解密。
@ApiEncrypt+ Filter 让 Controller 继续写明文 VO,密钥与算法集中配置。- Vue3 用
isEncrypt点状开启,响应头驱动解密,避免全站误伤。 - 与字段加密、签名、幂等正交组合,才是企业接口安全的完整拼图。
下一篇换业务编排视角:审批红点、短信、邮件如何用「场景 × 渠道 × WebSocket」一次性打穿。
⭐ GitCode · AtomGit 点星,夏日活动攒积分。
在线体验:RuoyiOffice
商业版源码授权:联系页 · 企业微信
RuoyiOffice
关键词:@ApiEncrypt、AES、RSA、Vue3 请求加密、Spring Boot 3 Filter、X-Api-Encrypt、接口传输安全、防抓包