Skip to content

HTTP 接口加解密 ​

这篇解决什么 ​

接口报文可能被中间人读走,开放给第三方的接口还可能被改参、重放。读完能打开加解密开关、对齐前后端密钥、在方法上挂 @ApiEncrypt / @ApiSignature,以及把调用方 appSecret 写进 Redis。

默认端口 48080,管理端前缀 /admin-api。加解密只处理请求体和响应体,不改库、不替代登录 Token。签名验的是「谁在调、有没有被改」。

示意图:请求先过框架过滤器与切面,再进 Controller。

名称说明仓库路径
加解密 StarterApiEncryptFilter 解密请求、加密响应ruoyi-office/yudao-framework/yudao-spring-boot-starter-web/
签名 Starter@ApiSignature + ApiSignatureAspectruoyi-office/yudao-framework/yudao-spring-boot-starter-protection/
单体入口已引入 web、protectionruoyi-office/yudao-server/pom.xml
管理端拦截isEncrypt 为真才加密 bodyruoyi-office-vben/apps/web-antd/src/api/request.ts
移动端拦截同样看 isEncryptruoyi-office-uniapp/src/http/interceptor.ts

为什么用 Filter,不用 RequestBodyAdvice

访问日志、异常日志、签名切面都要读明文 body。过滤器挂在 REQUEST_BODY_CACHE_FILTER 之后,比 Controller 更早解密,后面的组件读到的是明文。

请求与响应加解密 ​

yudao.api-encrypt.enable 为 true 时,才注册 ApiEncryptFilter。过滤器只拦 /admin-api、/app-api。

示意图:有加密头就解密;注解要求加密却没带头,直接拒绝。响应加密只看注解。

过滤器与包装 ​

名称说明仓库路径
YudaoApiEncryptAutoConfigurationenable=true 才装配过滤器.../encrypt/config/YudaoApiEncryptAutoConfiguration.java
ApiEncryptFilter解析注解、解密、包装响应.../encrypt/core/filter/ApiEncryptFilter.java
ApiDecryptRequestWrapper读密文 body,AES 或 RSA 私钥解密同目录 ApiDecryptRequestWrapper.java
ApiEncryptResponseWrapper缓存输出,最后 Base64 加密同目录 ApiEncryptResponseWrapper.java
顺序API_ENCRYPT_FILTER = REQUEST_BODY_CACHE_FILTER + 1yudao-framework/yudao-common/.../enums/WebFilterOrderEnum.java
作用范围继承 ApiRequestFilter,只过管理端 / App 前缀.../web/core/filter/ApiRequestFilter.java

构造函数里只认 AES、RSA。其它算法名会抛 IllegalArgumentException。ApiEncryptProperties 注释里的 SM2、SM4 要自己在构造函数补实例和 Maven 依赖。

GET 不走解密。解密失败交给 GlobalExceptionHandler,按 CommonResult 写出。

响应加密后会加 X-Api-Encrypt: true,并 Access-Control-Expose-Headers 暴露这个头,否则浏览器里前端读不到、无法解密。

注解 ​

@ApiEncrypt 可标在类或方法。方法优先,没有再看类。

名称说明仓库路径
@ApiEncrypt声明「必须」加解密.../encrypt/core/annotation/ApiEncrypt.java
request默认 true。POST / PUT / DELETE 必须带加密头同上
response默认 true。响应体加密后再写出同上

没有注解时,只要请求头 X-Api-Encrypt 非空,过滤器仍会解密。注解的含义是:不允许明文进来。

当前仓库里还没有业务 Controller 挂这个注解。登录接口也是明文。

java
@PostMapping("/create")
@ApiEncrypt
public CommonResult<Long> createXxx(@Valid @RequestBody XxxSaveReqVO reqVO) {
    return success(xxxService.createXxx(reqVO));
}

只要请求加密、不要响应加密:

java
@ApiEncrypt(request = true, response = false)

先改前端再挂必须加密

方法上加了 @ApiEncrypt 且 request = true,却没带加密头,过滤器抛「请求未包含加密标头」。登录现在是 isEncrypt: false,不要先给 AuthController#login 挂注解。

后端配置 ​

单体 application.yaml 默认打开,并写了 AES 示例密钥。生产文件只覆写开关,密钥仍继承这份。

名称说明仓库路径
enabletrue 才创建过滤器ApiEncryptProperties
header默认 X-Api-Encrypt。yaml 里可改同上
algorithmAES 或 RSA同上
request-key后端解请求:AES 密钥,或 RSA 私钥同上
response-key后端加密响应:AES 密钥,或 RSA 公钥同上
单体默认enable: true,AES,32 位数字密钥ruoyi-office/yudao-server/src/main/resources/application.yaml
生产开关enable: true,注释要求按合同关闭时同步前端.../application-prod.yaml
微服务 systemenable: falseyudao-module-system/yudao-module-system-server/src/main/resources/application.yaml
yaml
yudao:
  api-encrypt:
    enable: true
    algorithm: AES
    request-key: 52549111389893486934626385991395
    response-key: 96103715984234343991809655248883

AES 密钥长度按注释是 16、24 或 32。RSA 的示例公私钥写在同一段 yaml 的注释里,换算法时取消注释并改 algorithm。

名称说明仓库路径
AES前后端 request-key 同一把,response-key 同一把ApiEncryptProperties
RSA后端 request-key 是私钥、response-key 是公钥;前端对调同上

RSA 要两对密钥:请求一对、响应一对。请求和响应不能共用同一对。AES 可以让两边用同一把,也可以分开。

示例密钥不要上生产

yaml 和前端 .env 里的 AES 数字串是仓库自带样例。上线前用单测换一套,前后端一起改。

前端配置 ​

管理端主应用读 VITE_APP_API_ENCRYPT_*。工具类在 @vben/utils 的 createApiEncrypt。

名称说明仓库路径
环境变量ENABLE / HEADER / ALGORITHM / 两个 KEYruoyi-office-vben/apps/web-antd/.env
类型声明五个 VITE_APP_API_ENCRYPT_*ruoyi-office-vben/packages/types/global.d.ts
createApiEncrypt读 env,AES 用 CryptoJS ECB/PKCS7,RSA 用 JSEncryptruoyi-office-vben/packages/@core/base/shared/src/utils/encrypt.ts
请求拦截headers.isEncrypt 为真:加密 data,写加密头ruoyi-office-vben/apps/web-antd/src/api/request.ts
响应拦截响应头为 true 且 body 是字符串,再解密同上
登录显式 isEncrypt: falseruoyi-office-vben/apps/web-antd/src/api/core/auth.ts
UniApp同样的 env 名和 isEncryptruoyi-office-uniapp/env/.env、src/utils/encrypt.ts
env
VITE_APP_API_ENCRYPT_ENABLE = true
VITE_APP_API_ENCRYPT_HEADER = X-Api-Encrypt
VITE_APP_API_ENCRYPT_ALGORITHM = AES
VITE_APP_API_ENCRYPT_REQUEST_KEY = 52549111389893486934626385991395
VITE_APP_API_ENCRYPT_RESPONSE_KEY = 96103715984234343991809655248883

RSA 时:前端 REQUEST_KEY 是请求公钥,RESPONSE_KEY 是响应私钥,和后端刚好对调。

开关打开只表示「具备加解密能力」。真正加密某次请求,还要在调用处加头:

ts
return requestClient.post('/xxx/create', data, {
  headers: {
    isEncrypt: true,
  },
});

前端 enable 关、isEncrypt 开

encryptRequest 在 enable !== true 时原样返回。拦截器仍会写加密头,后端会按密文去解,对不上。两边开关和密钥必须一起改。

PC 端 AES 加密允许密钥 16 或 32 位,解密要求 32 位。当前默认密钥是 32 位数字。仓库里还没有 isEncrypt: true 的调用点。

如何生成密钥 ​

ApiEncryptTest#testGenerateAsymmetric 按算法打印四段。requestClientKey、responseClientKey 给前端;requestServerKey、responseServerKey 给后端。

路径:ruoyi-office/yudao-framework/yudao-spring-boot-starter-web/src/test/java/cn/iocoder/yudao/framework/encrypt/ApiEncryptTest.java。

同文件还有 testEncrypt_aes、testEncrypt_rsa,可拿登录 JSON 样例打出密文,方便抓包对照。

HTTP 签名 ​

给第三方接口防篡改、防重放。切面读 Header 里的 appId、timestamp、nonce、sign,用 Redis 里的 appSecret 重算 SHA256。

管理端请求层没有加签拦截器。签名字符串要调用方自己算,或跑 ApiSignatureTest 对照。

名称说明仓库路径
@ApiSignature标在方法上才生效.../signature/core/annotation/ApiSignature.java
ApiSignatureAspect@Before("@annotation(signature)").../signature/core/aop/ApiSignatureAspect.java
ApiSignatureRedisDAO读密钥、记 nonce.../signature/core/redis/ApiSignatureRedisDAO.java
自动配置Redis 之后装配切面.../signature/config/YudaoApiSignatureAutoConfiguration.java
单测拼串并验 GET 样例.../src/test/java/.../signature/core/ApiSignatureTest.java

单体 yudao-server 已引入 protection。微服务 yudao-module-system-server 的 protection 依赖是注释掉的,那个进程要用签名先打开依赖。

验签过程 ​

示意图:先对时限和随机数,再对签名,最后用 Redis 挡住重放。

签名字符串(顺序固定):

text
排序后的 query/form + 原始 body + 排序后的 appId/nonce/timestamp + appSecret

对应代码:MapUtil.join(parameterMap) + ServletUtils.getBody + MapUtil.join(headerMap) + appSecret,再 DigestUtil.sha256Hex。sign 本身不进拼接。Header 用 TreeMap,所以是 appId、nonce、timestamp 字典序。

四个字段走 Header,避免和 Query、Body 重名。注解可改字段名,默认就是这四个。

名称说明仓库路径
timeout / timeUnit默认 60 秒。`now - timestamp
message验签失败文案,默认「签名不正确」同上;错误码 BAD_REQUEST
nonce长度小于 10 直接失败ApiSignatureAspect#verifyHeaders
重放nonce TTL = timeout * 2setNonce;错误码 900

找不到 appSecret 时,Assert.notNull 直接失败,不会落到业务方法。

加解密和签名一起用

过滤器先解 body,切面再读 body。调用方必须对明文 body 签名,再把 body 加密发出。对密文签名会验不过。

Redis 调用方 ​

名称说明仓库路径
api_signature_appHASH。field 是 appId,value 是 appSecret,不过期ApiSignatureRedisDAO
api_signature_nonce:{appId}:{nonce}用过的随机数,SETNX同上
text
HSET api_signature_app test 123456

本机 Redis 默认 127.0.0.1:6379。没有这条 HASH,对应 appId 验不过。

如何加签 ​

切面只匹配方法上的注解。@Target 含 TYPE,但 @annotation 扫不到类上的声明。

java
@GetMapping("/page")
@PreAuthorize("@ss.hasPermission('system:user:list')")
@ApiSignature
public CommonResult<PageResult<UserRespVO>> getUserPage(@Valid UserPageReqVO pageReqVO) {
    return success(userService.getUserPage(pageReqVO));
}

联调可把超时放宽,例如 @ApiSignature(timeout = 30, timeUnit = TimeUnit.MINUTES)。生产保持默认秒级即可。

http
GET /admin-api/system/user/page?pageNo=1&pageSize=10
Authorization: Bearer {accessToken}
appId: test
timestamp: 1717494535932
nonce: e7eb4265-885d-40eb-ace3-2ecfc34bd639
sign: {sha256hex}

sign 算不准时,在 ApiSignatureAspect#verifySignature 里看 serverSignature,或跑 ApiSignatureTest#testSignatureGet。

当前仓库没有业务方法挂 @ApiSignature。加上之后,缺头、超时、nonce 复用都会在进 Service 之前被挡住。

配置与操作 ​

API 加解密改 yudao.api-encrypt 后重启。签名注解当前没有业务方法在用,加上后缺头会被挡在 Service 前。

开启见 框架层。

相关篇 ​

联系我们

获取报价、演示和二开方案

微信咨询二维码

微信咨询

17156169080

添加时备注「RuoYi Office」

在线体验商业版