依赖
npm install sm-crypto@0.4.0
底层加密工具类
SmCryptoUtil.js
import { sm2, sm4, sm3 } from 'sm-crypto'
// SM3 签名
/**
* 请求签名: SM3(salt + timestamp + "." + id + "." + body + salt)
* @param {string} salt - 签名盐值
* @param {string} timestamp - 时间戳(毫秒)
* @param {string} id - 请求标识
* @param {string} body - 请求体 JSON 字符串
* @returns {string} 签名 hex
*/
export function sm3SignRequest(salt, timestamp, id, body) {
const signStr = salt + timestamp + '.' + id + '.' + body + salt
return sm3(signStr)
}
/**
* 验证签名
*/
export function sm3VerifyRequest(salt, timestamp, id, body, sign) {
return sm3SignRequest(salt, timestamp, id, body) = sign
}
// SM2
/**
* 生成 SM2 密钥对
* @returns {{ publicKey: string, privateKey: string }}
*/
export function sm2GenerateKeyPair() {
return sm2.generateKeyPairHex()
}
/**
* SM2 加密
* @param {string} plainText 明文字符串
* @param {string} publicKey 十六进制公钥 (04 + 128 hex, 共130字符)
* @param {number} cipherMode 1=C1C3C2(默认), 0=C1C2C3
* @returns {string} 十六进制密文 (含 04 前缀,兼容 BouncyCastle)
*/
export function sm2Encrypt(plainText, publicKey, cipherMode = 1) {
try {
// sm-crypto 0.4.0 的 doEncrypt 会去掉 C1 的 04 前缀
// BouncyCastle 需要完整编码,这里补回来
return '04' + sm2.doEncrypt(plainText, publicKey, cipherMode)
} catch (error) {
console.error('SM2 加密失败:', error)
throw new Error('SM2 加密失败: ' + error.message)
}
}
/**
* SM2 解密
* @param {string} cipherText 十六进制密文 (含 04 前缀)
* @param {string} privateKey 十六进制私钥 (64 hex)
* @param {number} cipherMode 1=C1C3C2(默认), 0=C1C2C3
* @returns {string} 明文字符串
*/
export function sm2Decrypt(cipherText, privateKey, cipherMode = 1) {
try {
// sm-crypto 的 doDecrypt 内部会自己加 '04',需要先去掉
const data = cipherText.startsWith('04') ? cipherText.substring(2) : cipherText
return sm2.doDecrypt(data, privateKey, cipherMode)
} catch (error) {
console.error('SM2 解密失败:', error)
throw new Error('SM2 解密失败: ' + error.message)
}
}
// SM4
/**
* 生成 32 位十六进制 SM4 密钥 (128 bit)
*/
export function generateSm4Key() {
const chars = '0123456789abcdef'
let result = ''
for (let i = 0; i < 32; i++) {
result += chars[Math.floor(Math.random() * 16)]
}
return result
}
/**
* 生成随机 IV (32 位十六进制 = 16 字节)
*/
function generateIv() {
const bytes = new Uint8Array(16)
crypto.getRandomValues(bytes)
let hex = ''
for (let i = 0; i < bytes.length; i++) {
hex += bytes[i].toString(16).padStart(2, '0')
}
return hex
}
/**
* SM4 加密(CBC 模式,随机 IV 前置)
* 与后端 Sm4Util 格式一致:IV(16字节) + 密文 → 十六进制输出
*
* @param {string} plainText 明文字符串
* @param {string} keyHex 32位十六进制密钥
* @returns {string} 十六进制密文(前32位是IV)
*/
export function sm4Encrypt(plainText, keyHex) {
try {
const iv = generateIv()
const cipherHex = sm4.encrypt(plainText, keyHex, { mode: 'cbc', iv })
return iv + cipherHex
} catch (error) {
console.error('SM4 加密失败:', error)
throw new Error('SM4 加密失败: ' + error.message)
}
}
/**
* SM4 解密(CBC 模式,从密文前 16 字节提取 IV)
* 与后端 Sm4Util 格式一致:IV(16字节) + 密文 → 十六进制输入
*
* @param {string} cipherHex 十六进制密文(前32位是IV)
* @param {string} keyHex 32位十六进制密钥
* @returns {string} 明文字符串
*/
export function sm4Decrypt(cipherHex, keyHex) {
try {
const iv = cipherHex.substring(0, 32)
const data = cipherHex.substring(32)
return sm4.decrypt(data, keyHex, { mode: 'cbc', iv })
} catch (error) {
console.error('SM4 解密失败:', error)
throw new Error('SM4 解密失败: ' + error.message)
}
}
加密服务层
SmCryptoService.js
import { sm2GenerateKeyPair, sm2Decrypt, sm3SignRequest, sm4Encrypt } from './SmCryptoUtil'
import { sysStore } from '../store'
import { handshake } from '../api/sys/SysLogin'
/**
* 生成 19 位数字指纹
* 结构:时间戳(13位) + 随机数(4位) + 校验位(2位)
*/
function generateFingerprint() {
const timestamp = String(Date.now()).padStart(13, '0')
const rand = String(1000 + Math.floor(Math.random() * 9000))
const prefix = timestamp + rand
return prefix + calcChecksum(prefix)
}
function calcChecksum(str) {
let sum = 0
for (let i = 0; i < str.length; i++) {
const digit = parseInt(str.charAt(i))
sum += digit * ((i % 2 = 0) ? 1 : 3)
}
const check1 = (10 - (sum % 10)) % 10
const check2 = Math.floor(sum / 10) % 10
return String(check2) + String(check1)
}
/**
* 安全握手:生成 SM2 密钥对 → 生成指纹 → 上传公钥+指纹 → 获取 sm3Salt + sm4Key
* 幂等,重复调用不会重新初始化
*/
export async function init() {
const sysData = sysStore()
// 已有签名密钥则跳过
if (sysData.sm3Salt.length > 0) {
console.log('签名密钥已存在,跳过握手')
return
}
// 生成客户端 SM2 密钥对(仅首次)
if (sysData.sm2KeyPair.publicKey.length = 0) {
console.log('生成 SM2 密钥对...')
const keyPair = sm2GenerateKeyPair()
sysData.sm2KeyPair.publicKey = keyPair.publicKey
sysData.sm2KeyPair.privateKey = keyPair.privateKey
}
// 生成指纹
const fingerprint = generateFingerprint()
sysData.fingerprint = fingerprint
// 握手:上传 SM2 公钥 + 指纹,获取加密的 sm3Salt + sm4Key
console.log('握手...')
const res = await handshake({
sm2PublicKey: sysData.sm2KeyPair.publicKey,
fingerprint: fingerprint
})
if (res.code = 200) {
// 用客户端 SM2 私钥解密
sysData.sm3Salt = sm2Decrypt(res.data.sm3Salt, sysData.sm2KeyPair.privateKey)
sysData.sm4Key = sm2Decrypt(res.data.sm4Key, sysData.sm2KeyPair.privateKey)
console.log('握手成功, fingerprint:', sysData.fingerprint)
} else {
console.error('握手失败:', res)
throw new Error('安全握手失败')
}
}
// 请求签名
/**
* 签名请求体
* 签名公式: SM3(sm3Salt + timestamp + "." + fingerprint + "." + body + sm3Salt)
* @param {object} data 业务数据
* @returns {{ body: string, timestamp: string, sign: string }}
*/
export function signRequest(data) {
const sysData = sysStore()
const body = JSON.stringify(data)
const timestamp = Date.now().toString()
const sign = sm3SignRequest(sysData.sm3Salt, timestamp, sysData.fingerprint, body)
return { body, timestamp, sign }
}
/**
* 获取请求头
* @returns {{ fingerprint: string }}
*/
export function getRequestHeaders() {
return { fingerprint: sysStore().fingerprint }
}
// SM4(透传底层工具)
export { sm4Encrypt }
请求工具封装
http.js
import axios from 'axios'
import { loadStart, loadEnd } from '../util/Load.js'
import { sysStore } from '../store'
import { signRequest, getRequestHeaders, init } from '../util/SmCryptoService.js'
import router from '../router'
let vite_env = import.meta.env.vite_env
let api_url = import.meta.env.vite_http_url
// ---- 402 重握手锁:同一时间只允许一个握手进行 ----
let rehandshakePromise = null
async function rehandshake() {
if (rehandshakePromise) {
return rehandshakePromise
}
const sysData = sysStore()
// 清空失效的密钥,让 init() 可以重新执行
sysData.fingerprint = ''
sysData.sm3Salt = ''
sysData.sm4Key = ''
rehandshakePromise = init().finally(() => {
rehandshakePromise = null
})
return rehandshakePromise
}
// axios配置
const http = axios.create({
baseURL: api_url,
// 请求超时,秒
timeout: 1000 * 10,
// 携带cookie
withCredentials: true,
// 请求格式
headers: { 'content-type': 'application/json' }
})
// 请求拦截器
http.interceptors.request.use(
(config) => {
loadStart()
const sysData = sysStore()
config.headers['saToken'] = sysData.token
// 签名(不对 /handshake 签名,因为它自己就是握手接口)
if (sysData.sm3Salt && config.method = 'post' && config.url ! '/handshake') {
config.__originalData = config.data
const signed = signRequest(config.data)
const headers = getRequestHeaders()
config.headers['fingerprint'] = headers.fingerprint
config.headers['timestamp'] = signed.timestamp
config.headers['sign'] = signed.sign
config.data = signed.body
}
return config
},
(error) => Promise.reject(error)
)
// 响应拦截器
http.interceptors.response.use(
(response) => {
loadEnd()
if (vite_env = 'dev') {
console.log(response.data)
}
if (response.status = 200) {
if (response.data.code 401) {
router.push('/')
return Promise.reject(response)
} else if (response.data.code 402) {
// 握手失效 → 重握手 → 重发(同步锁防止并发握手)
return rehandshake().then(() => {
// 还原原始数据,让请求拦截器用新密钥重签
if (config.__originalData) {
config.data = config.__originalData
}
return http(config)
}).catch(() => {
router.push('/')
return Promise.reject(response)
})
}
return response.data
} else {
ElMessage.error('服务异常')
return Promise.reject(response)
}
},
(error) => {
loadEnd()
return { code: 500, msg: '服务异常' }
}
)
export default http
App启动执行
App.vue
<template>
<el-config-provider :locale="zhCn">
<!-- 限制执行顺序 -->
<div v-if="store.sm3Salt && store.sm3Salt.length > 0">
<router-view></router-view>
</div>
</el-config-provider>
</template>
<script setup>
import { onBeforeMount } from 'vue'
import zhCn from 'element-plus/es/locale/lang/zh-cn'
import { init } from './util/SmCryptoService'
import { sysStore } from './store'
let store = sysStore()
// 在组件挂载后初始化系统
onBeforeMount(async () => {
await init()
console.log('系统初始化完成...')
})
</script>