Fork me on GitHub

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 后端

三端一致性。

一旦编码统一,整个链路就会稳定很多。

posted @ 2026-05-23 19:51  极度恐慌_JG  阅读(80)  评论(0)    收藏  举报