后台接口加解密实践:什么时候该用 `@ApiEncrypt` 和 `@ApiDecrypt`

关键词:接口安全 / 传输加密 / SM4 国密 / 动态密钥协商 / 请求体加解密


一、问题场景:你的接口正在"裸奔"

打开浏览器 DevTools 的 Network 面板,找到你后台的登录接口,看一眼 Request Payload:

perl 复制代码
{
  "username": "admin",
  "password": "MyPass@2025"
}

明文。谁能看到?公司 WiFi 网的任何抓包工具、中间代理、甚至浏览器插件的恶意脚本。

有人说"我上了 HTTPS,传输层加密就够了"。但 HTTPS 只保护传输过程,到了客户端(浏览器内存)和收到之后的反向代理(Nginx 日志、网关日志)依然是明文。如果你的网关会把请求体打到日志里------运维同学转头就能看到所有用户的密码。

更关键的是:前端加密不是防黑客,是防"中间人" ------包括日志系统、监控系统、CDN 缓存、甚至是合法的内部运维人员。

Forge Admin 的 forge-starter-crypto 模块就是为此而生。它提供了一套从注解声明到底层密钥协商的全链路接口加解密方案,开发者只需要加两个注解,剩下的全部自动完成。


二、解决方案:三个注解解决 90% 的加密需求

2.1 响应加密:@ApiEncrypt

在 Controller 类或方法上加 @ApiEncrypt,该接口的返回值会自动加密:

less 复制代码
@RestController
@RequestMapping("/api/user")
public class UserController {

    @GetMapping("/profile")
    @ApiEncrypt  // 返回的 UserDTO 自动加密
    public RespInfo<UserDTO> getProfile() {
        return RespInfo.success(userService.getCurrentUser());
    }
}

前端收到的响应不再是一个 JSON 对象,而是一个加密信封:

json 复制代码
{
  "data": "SM4加密后的密文Base64字符串...",
  "algorithm": "SM4"
}

前端的 crypto-interceptor.js 拦截器会自动解密,业务代码完全无感。

2.2 请求解密:@ApiDecrypt

对称的,在需要接收加密请求体的接口上加 @ApiDecrypt

less 复制代码
@PostMapping("/update-password")
@ApiDecrypt  // 请求体自动解密后再进 Controller
public RespInfo<Void> updatePassword(@RequestBody UpdatePasswordRequest request) {
    userService.updatePassword(request);
    return RespInfo.success();
}

前端的 encrypt-request.js 拦截器会在发起请求前将 config.data 加密成 { data: "...", algorithm: "SM4" },后端 DecryptRequestBodyAdvice@RequestBody 反序列化之前先解密。

2.3 字段级加密:@CryptoField

有些场景不需要全接口加密,只需要对个别敏感字段(身份证、银行卡号)加密:

typescript 复制代码
@Data
public class UserProfileVO {
    private String username;

    @CryptoField(algorithm = "SM4")
    private String idCard;  // 序列化时自动加密,反序列化时自动解密

    @Desensitize(type = DesensitizeType.PHONE)
    private String phone;   // 仅脱敏,不加密
}

@CryptoField 通过 Jackson 的 ContextualSerializer/ContextualDeserializer 在 JSON 转换时自动介入。更妙的是,它可以和 @Desensitize 配合使用------脱敏只改展示(日志里看到的还是明文),加密才是真正的数据保护。

@Desensitize 内置了 8 种脱敏策略:

类型 策略 示例
PHONE 保留前3后4 138****1234
ID_CARD 保留前6后4 110101********1234
EMAIL @前保留首字符 t**@example.com
BANK_CARD 保留后4位 ****1234
NAME 保留姓氏 张*
PASSWORD 全部替换 ******
CUSTOM prefixKeep/suffixKeep 自定义 灵活配置

三、数据结构:加密信封协议

整个加解密方案的核心是一个简单的信封协议,前后端统一遵循:

请求信封(前端→后端)

json 复制代码
{
  "data": "加密后的请求体 Base64 字符串",
  "algorithm": "SM4"
}

响应信封(后端→前端)

json 复制代码
{
  "data": "加密后的响应体 Base64 字符串",
  "algorithm": "SM4"
}

注意 algorithm 字段的存在------它允许前端在单个请求中覆盖算法,在后端配置默认 SM4 的情况下,某个接口可以单独指定使用 AES。这个字段是整个协议"协商性"的体现。

对应的后端数据结构:

arduino 复制代码
// 请求信封
public class EncryptedRequest {
    private String data;        // 密文
    private String algorithm;   // 加密算法标识
}

// 响应信封
public class EncryptedResponse {
    private String data;
    private String algorithm;
}

四、实现链路:从注解到密文的完整旅程

4.1 请求解密链路

scss 复制代码
前端 encryptRequest()
  → HTTP POST { data: "密文", algorithm: "SM4" }
    → DecryptRequestBodyAdvice.supports() 判断是否需要解密
      → 检查 X-Inner-Call 头(内部调用跳过)
      → IApiConfigManager 查 API 配置(数据库配置优先)
      → 降级到 @ApiDecrypt 注解
    → beforeBodyRead() 读取原始 JSON → 解析为 EncryptedRequest
      → EncryptorFactory.getEncryptor("SM4") 获取加密器
      → SessionKeyStore 优先拿会话密钥,降级用默认密钥
      → 解密 data 字段 → 得到明文 JSON 字符串
    → Controller 方法接收到已反序列化的 Java 对象

关键设计 :解密发生在 @RequestBody 反序列化之前 ,也就是说 Controller 收到的 @RequestBody 对象已经是明文,业务代码完全不感知加密层的存在。

4.2 响应加密链路

scss 复制代码
Controller 返回 RespInfo<UserDTO>
  → EncryptResponseBodyAdvice.supports() 判断是否需要加密
    → 自动跳过 ResponseEntity<byte[]>(文件下载不加密)
    → 同上:API 配置 > @ApiEncrypt 注解
  → beforeBodyWrite() 将响应体序列化为 JSON 字符串
    → 获取加密器 → 加密 → 包装为 EncryptedResponse
    → 写入输出流
  → 前端 decryptResponse() 拦截器自动解密

4.3 密钥管理:默认密钥 + 动态协商双层机制

这是整个方案安全的基石。如果所有请求用同一个固定密钥,那和不用加密区别不大------密钥一旦从配置文件泄露,历史数据全部可解密。

默认密钥(降级方案):

vbnet 复制代码
forge:
  crypto:
    secret-key: "Base64编码的16字节密钥"

仅当动态密钥不可用时使用,例如:

  • 前端还没完成密钥协商就发起了加密请求
  • Redis 中会话密钥已过期
  • 内部服务间调用

动态密钥协商(核心机制):

vbnet 复制代码
┌─────────┐                            ┌─────────┐
│  前端   │                            │  后端   │
└────┬────┘                            └────┬────┘
     │  GET /crypto/public-key              │
     │─────────────────────────────────────→│ 返回 RSA 公钥
     │                                      │
     │  生成 16 字节随机会话密钥              │
     │  用 RSA 公钥加密                     │
     │                                      │
     │  POST /crypto/exchange               │
     │  { encryptedKey: "..." }             │
     │─────────────────────────────────────→│ RSA 私钥解密
     │                                      │ 存入 Redis
     │                                      │ crypto:session:{sessionId}
     │  后续所有请求用会话密钥加密             │
     │─────────────────────────────────────→│ 从 Redis 取出会话密钥解密

密钥的生命周期:

  • 会话密钥过期时间默认 7200 秒(2 小时),超时自动失效
  • 前端解密失败(padding error)时自动触发重新协商
  • sessionId 优先从 Authorization: Bearer {token} 提取,降级到 X-Session-Id

4.4 内部调用绕过

你不是每时每刻都需要加密。微服务间的内部调用如果也要加密解密,纯属浪费 CPU。模块通过请求头 X-Inner-Call: true 自动跳过加解密流程,零配置。


五、设计取舍:我们做了哪些决定,为什么

5.1 默认算法:为什么是 SM4 而不是 AES?

AES 是全球标准,库支持好,硬件加速成熟。但我们选择了 SM4 作为默认算法,原因:

  1. 合规要求:国密算法是国内政务、金融系统的硬性要求,SM4 是其中之一
  2. 安全性等价:SM4 是 128 位分组密码,安全强度与 AES-128 对等,已通过国家密码管理局认证
  3. 可切换 :我们保留了 AES 作为可选项,@ApiEncrypt(algorithm = "AES") 一行切换

这个决策的代价是引入了 BouncyCastle 依赖(约 4MB),对于不需要国密的项目是额外负担。但考虑到 Forge Admin 的目标用户群体(国内政企),这个取舍是值得的。

5.2 为什么不用 HTTPS 就完事?

这是个经典问题。HTTPS 保护的是传输信道 ,而我们的方案保护的是数据本身

保护点 HTTPS forge-starter-crypto
网络抓包
代理/网关日志
浏览器内存 ✅(加密后传输)
CDN 缓存
数据库存储 ✅(@CryptoField + TypeHandler)

二者是互补关系,不是替代关系。

5.3 防重放:默认关闭的设计哲学

防重放攻击(Replay Attack)的 ReplayAttackFilter 我们默认是关闭 的,需要手动 enable-replay-protection: true 开启。

原因很简单:防重放需要前端配合发送 X-Timestamp + X-Nonce 头,且 Redis 要存储大量 nonce。对于大部分内部管理系统来说,这是过度设计。只有面向公网暴露的接口(如支付回调、开放 API)才需要开启。

5.4 已知的可优化点

  • EncryptTypeHandler 中的密钥硬编码(forge_client_secret_key_16b)仅适用于客户端数据保护场景,服务端生产环境应改为从配置读取
  • 动态密钥协商的 RSA 密钥对默认自动生成,建议生产环境通过配置固定

六、二开指南:怎么接入你自己的项目

6.1 引入依赖

xml 复制代码
<dependency>
    <groupId>com.mdframe.forge</groupId>
    <artifactId>forge-starter-crypto</artifactId>
</dependency>

6.2 基础配置

yaml 复制代码
forge:
  crypto:
    enabled: true
    secret-key: "your-base64-encoded-16-byte-key"
    algorithm: SM4          # 默认算法
    enable-replay-protection: false

6.3 三步接入

第一步:在需要的 Controller 上加注解:

less 复制代码
@ApiEncrypt  // 类级别,所有接口响应加密
@RestController
public class SensitiveController {
    @ApiDecrypt  // 方法级别,单个接口请求解密
    @PostMapping("/submit")
    public RespInfo<Void> submit(@RequestBody SensitiveData data) {
        // 业务代码完全不用改
    }
}

第二步 :前端配置拦截器(forge-admin-ui 已内置,新项目需引入 crypto-interceptor.js):

javascript 复制代码
import { setupCrypto } from '@/utils/crypto/crypto-interceptor'

setupCrypto(axiosInstance)

第三步:扩展自定义算法(如果需要):

less 复制代码
@Component
public class SM2Encryptor implements Encryptor {
    @Override
    public String encrypt(String plainText, byte[] key) { /* SM2 实现 */ }
    @Override
    public String decrypt(String cipherText, byte[] key) { /* SM2 实现 */ }
    @Override
    public CryptoAlgorithm getAlgorithm() { return CryptoAlgorithm.SM2; }
}

七、体验预告

forge-starter-crypto 的设计目标是"加两个注解就能工作"。从实际接入效果来看,一个标准 CRUD 接口的接入成本是 3 行代码(1 行注解 + 2 行前端拦截器配置),无需改动任何业务逻辑。

下一步我们将推出基于数据库配置的动态 API 加密策略------在管理后台界面勾选哪些接口需要加密、使用什么算法,无需重启服务即可生效。

下一篇预告:B05《文件访问不能只返回 URL:Forge Admin 鉴权图片和文件存储设计》------私有文件怎么在后台安全展示,为什么直接返回 OSS URL 是安全漏洞

体验 Forge Admin

相关推荐
武子康3 分钟前
Java-07 深入浅出 MyBatis数据库一对多关系模型实战:表结构设计与查询实现
java·后端
花椒技术1 小时前
企业内部 Agent 落地复盘:Gateway、Skill 和二次确认如何串起受控业务执行
后端·agent·ai编程
Agent手记2 小时前
制造业生产流程自动化,Agent需要具备哪些能力?深度拆解2026工业级智能体落地范式与核心架构
大数据·人工智能·ai·架构·自动化
REDcker2 小时前
Linux OverlayFS详解
java·linux·运维
Royzst2 小时前
xml知识点
java·服务器·前端
Yunzenn3 小时前
深度分析字节最新研究cola-DLM 第 07 章:推理流水线逐行拆解 —— 从 prompt 到生成文本
人工智能·驱动开发·深度学习·chatgpt·架构·prompt·github
我是一颗柠檬3 小时前
【MySQL全面教学】MySQL事务与ACID Day9(2026年)
数据库·后端·mysql
枕星而眠3 小时前
数据结构八大排序详解(一):四大简单排序
c语言·数据结构·c++·后端
IT_陈寒3 小时前
React useEffect闭包陷阱差点把我整失业了
前端·人工智能·后端
鱼鳞_3 小时前
苍穹外卖-Day08(缓存套餐)
java·redis·缓存