uni-app + Vue3 微信小程序实现 SM2/SM4 接口加密解密实践
最近在 uni-app(unibest + Vue3 + Vite)微信小程序项目中,完成了一套基于 SM2 + SM4 的接口加密方案。
整体目标:
-
请求参数自动加密
-
接口响应自动解密
-
支持 GET / POST
-
支持中文参数
-
与 PC Web 端完全兼容
-
使用方式尽量无侵入
最终实现后,接口层调用方式非常简单。
一、整体加密交互流程
整个流程本质是:
前端随机生成 SM4 Key
↓
使用后端 SM2 公钥加密 SM4 Key
↓
放入请求头 Web-Encrypt
↓
请求数据使用 SM4 加密
↓
后端使用私钥解出 SM4 Key
↓
再使用 SM4 解密请求数据
响应时:
后端返回 SM4 加密数据
↓
前端使用同一个 SM4 Key 解密
↓
恢复 JSON 数据
二、为什么使用 SM2 + SM4 混合加密
这是典型的:
非对称加密 + 对称加密
组合方案。
原因:
| 算法 | 用途 | 特点 |
|---|---|---|
| SM2 | 加密 SM4 Key | 安全,但性能低 |
| SM4 | 加密业务数据 | 性能高,适合大数据 |
因此:
SM2 只负责“密钥交换”
SM4 负责“业务数据加密”
这是标准方案。
三、前端核心设计
项目中主要分为:
1、store 管理加密会话
2、smEncrypt.ts 负责加解密
3、api 层按需调用
4、http 层统一响应解密
四、Store:维护加密会话
核心职责:
1、拉取后端加密配置
2、生成前端 SM4 Key
3、生成 Web-Encrypt
4、缓存整个加密会话
后端会返回:
{
"publicKey": "SM2公钥",
"iv": "SM4 iv",
"enabled": true
}
前端收到后:
// 前端随机生成 SM4 Key
const sm4Key = generateRandomHex(16)
// SM2 加密 SM4 Key
const encryptHex = sm2.doEncrypt(sm4Key, sm2PubKey, 1)
// 转 base64
const encryptedSm4KeyBase64 = hexToBase64(encryptHex)
最终:
config = {
publicKey,
iv,
sm4Key,
encryptedSm4KeyBase64,
}
后续所有接口都复用这一套会话。
五、API 使用方式
整个项目采用:
局部渐进式替换
即:
-
哪些接口需要加密
-
哪些接口不加密
完全由 API 层控制。
POST 请求加密
export function postSmDemoDto(params: any) {
return http.post(
'/api/v1/biz/demo/postSmDemoDto',
entryParams(params),
entryHeader(),
)
}
GET Query 加密
export function getSmByNameCode(params: any) {
return http.get(
'/api/v1/biz/demo/getSmByNameCode',
entryQuery(params),
entryHeader(),
)
}
这样最大的优点:
不改 http 核心逻辑
不影响旧接口
支持逐步迁移
非常适合存量项目。
六、请求加密原理
1、POST Body 加密
POST 最简单:
JSON -> SM4 -> Base64
核心逻辑:
const dataStr = JSON.stringify(params)
const encryptData = sm4Encrypt(dataStr)
最终请求体:
加密后的字符串
2、GET Query 加密
GET 比 POST 麻烦。
因为:
query 参数涉及 URL 编码
尤其中文容易乱码。
因此最终方案:
query
→ json
→ utf8
→ base64
→ sm4
核心:
const jsonStr = JSON.stringify(queryMap)
const base64Str = utf8ToBase64(jsonStr)
const encryptData = sm4Encrypt(base64Str)
最终:
{
encryptPayload: 'xxxxxx'
}
七、中文乱码问题(重点)
这是整个过程中最容易踩坑的地方。
问题现象
下面参数:
{
name: '王五',
code: 'wangwu'
}
正常。
但:
{
name: '赵六',
code: '赵六'
}
后端解密后为空。
根本原因
最开始小程序使用:
TextEncoder + base64
但 PC Web 使用的是:
btoa(encodeURIComponent(str)...)
两边 UTF8 处理并不一致。
尤其中文字符:
赵六
在不同平台编码结果不同。
最终导致:
SM4 加密结果不同
后端无法正确解码
八、最终兼容方案
最终统一:
前端 PC
前端小程序
后端 Java
全部采用:
UTF8 → Base64 → SM4
且:
Base64URL 标准
小程序最终实现:
const encoded = encodeURIComponent(str).replace(
/%([0-9A-F]{2})/g,
(_, p1) => String.fromCharCode(Number.parseInt(p1, 16)),
)
let result = uni.arrayBufferToBase64(bytes.buffer)
result = result
.replace(/\+/g, '-')
.replace(/\//g, '_')
这样最终:
PC 与 小程序 完全一致
九、响应解密
请求加密后:
响应也需要解密。
项目里统一在 http 层处理:
const isEnc = res.header['web-encrypt']
if (isEnc) {
res.data = decryptData(res.data)
}
这样业务层完全无感知。
十、SM4 解密流程
响应解密流程:
base64
→ hex
→ sm4 decrypt
→ json parse
核心:
const hex = base64ToHex(data)
const decryptStr = sm4.decrypt(hex, sm4Key, {
mode: 'cbc',
iv,
})
再:
JSON.parse(decryptStr)
恢复对象。
十一、为什么不做全局拦截器自动加密
最终没有采用:
request interceptor 全局自动加密
原因:
1、老项目改造风险大
很多接口:
不需要加密
全局处理风险高。
2、文件上传特殊
上传接口:
multipart/form-data
不能直接 SM4。
3、调试困难
局部调用:
entryParams()
entryQuery()
更容易定位问题。
十二、最终项目结构
store/
encryptConfig.ts
utils/
smEncrypt.ts
api/
xxx.ts
http/
http.ts
职责清晰:
| 模块 | 职责 |
|---|---|
| encryptConfig.ts | 管理加密会话 |
| smEncrypt.ts | SM2/SM4工具 |
| api | 控制接口是否加密 |
| http | 统一响应解密 |
十三、最终效果
目前已经实现:
-
uni-app 微信小程序
-
Vue3 + Vite
-
unibest
-
SM2 + SM4
-
GET/POST 加密
-
中文兼容
-
与 PC Web 完全一致
-
渐进式接入
-
低侵入改造
并已稳定运行。
十四、总结
这套方案核心其实只有一句话:
SM2 做密钥交换
SM4 做业务加密
UTF8/Base64 必须全端统一
真正难点不在 SM2/SM4:
而在:
不同平台 UTF8/Base64 编码兼容
尤其:
Web
小程序
Java 后端
三端一致性。
一旦编码统一,整个链路就会稳定很多。


浙公网安备 33010602011771号