SpringBoot2.x + Vue2 国密接口安全实战(SM2+SM3+SM4 企业级防篡改/防泄露/防重放)
-
- [一、前言 & 应用背景](#一、前言 & 应用背景)
-
- [1.1 传统接口安全痛点](#1.1 传统接口安全痛点)
- [1.2 国密算法优势 & 官方合规依据](#1.2 国密算法优势 & 官方合规依据)
- [1.3 整体实现方案与安全架构](#1.3 整体实现方案与安全架构)
- 二、技术栈介绍
-
- [2.1 前端技术栈](#2.1 前端技术栈)
- [2.2 后端技术栈](#2.2 后端技术栈)
- 三、环境依赖搭建
-
- [3.1 前端依赖安装(Vue2)](#3.1 前端依赖安装(Vue2))
- [3.2 后端核心Maven依赖(精简最终版)](#3.2 后端核心Maven依赖(精简最终版))
- 四、核心原理说明
-
- [4.1 SM2加密原理](#4.1 SM2加密原理)
- [4.2 SM3签名原理](#4.2 SM3签名原理)
- [4.3 SM4加密原理](#4.3 SM4加密原理)
- [4.4 双加密模式适配规则](#4.4 双加密模式适配规则)
- [4.5 防重放+身份认证原理](#4.5 防重放+身份认证原理)
- 五、前端完整代码实现(Vue2)
-
- [5.1 环境变量配置 .env.development](#5.1 环境变量配置 .env.development)
- [5.2 SM2加密工具 sm2.js](#5.2 SM2加密工具 sm2.js)
- [5.3 SM3签名工具 sm3.js](#5.3 SM3签名工具 sm3.js)
- [5.4 SM4签名工具 sm4.js](#5.4 SM4签名工具 sm4.js)
- [5.5 全局请求拦截器 requestSm234.js(双模式自适应)](#5.5 全局请求拦截器 requestSm234.js(双模式自适应))
- [5.6 测试页面 index.vue](#5.6 测试页面 index.vue)
- 六、后端完整代码实现(SpringBoot2.x)
-
- [6.1 配置文件 application.yml](#6.1 配置文件 application.yml)
- [6.2 验签基础实体类 SignBaseBO](#6.2 验签基础实体类 SignBaseBO)
- [6.3 业务参数实体 Sm234BO](#6.3 业务参数实体 Sm234BO)
- [6.4 自定义验签注解 ApiSignCheck](#6.4 自定义验签注解 ApiSignCheck)
- [6.5 密钥生成工具类 SM2KeyGenerator](#6.5 密钥生成工具类 SM2KeyGenerator)
- [6.6 SM2解密工具类 Sm2Util](#6.6 SM2解密工具类 Sm2Util)
- [6.7 SM4加解密工具类 Sm4Util](#6.7 SM4加解密工具类 Sm4Util)
- [6.8 SM3签名验签工具 Sm3SignUtil](#6.8 SM3签名验签工具 Sm3SignUtil)
- [6.9 Redis防重放工具 ReplayUtil](#6.9 Redis防重放工具 ReplayUtil)
- [6.10 验签切面 ApiSignAspect](#6.10 验签切面 ApiSignAspect)
- [6.11 解密切面 Sm2DecryptAspect](#6.11 解密切面 Sm2DecryptAspect)
- [6.12 测试接口 Controller](#6.12 测试接口 Controller)
- 七、AOP执行顺序说明
- 八、核心踩坑&问题总结
-
- [8.1. gm-crypto版本安装报错、版本冲突](#8.1. gm-crypto版本安装报错、版本冲突)
- [8.2. 前后端SM2密文不兼容,解密报错](#8.2. 前后端SM2密文不兼容,解密报错)
- [8.3. SM3签名持续校验失败](#8.3. SM3签名持续校验失败)
- [8.4. Redis静态注入空指针](#8.4. Redis静态注入空指针)
- [8.5. Hutool多版本依赖冲突](#8.5. Hutool多版本依赖冲突)
- [8.6. 空参数、null值导致签名不一致](#8.6. 空参数、null值导致签名不一致)
- [8.7. 时间戳校验失败](#8.7. 时间戳校验失败)
- [8.8. 前端 gm-crypto 安装失败](#8.8. 前端 gm-crypto 安装失败)
- 九、总结
一、前言 & 应用背景
在等级保护 2.0、商用密码应用安全性评估及国产化信创建设要求下,接口通信安全已成为企业级系统建设的核心环节。传统单一加密方式无法满足防泄露、防篡改、防重放、防伪造 的多重安全需求。
本文基于 Java8 + SpringBoot2.x + Vue2 技术栈,采用国密 SM2+SM3+SM4 组合算法,构建一套满足等保、密评、国产化合规要求的企业级接口安全通信方案。
方案实现敏感数据加密、参数签名校验、身份认证、请求防重放等多重防护机制,具备高安全性、低侵入性、易扩展性等特点,可直接应用于政务、金融、能源等关键领域。
1.1 传统接口安全痛点
在常规前后端分离项目中,接口参数明文传输存在三大致命问题,同时无法满足政企合规要求:
- 数据泄露:密码、手机号、身份证等敏感数据明文传输,抓包即可窃取;
- 参数篡改:攻击者拦截请求修改参数,引发越权操作、数据刷取等风险;
- 重放攻击:截取合法请求重复发送,造成重复下单、重复登录、数据重复写入等问题;
- 合规性缺失:传统加密方案无法满足等保2.0、商用密码测评、国产化信创项目的强制要求。
传统方案大多使用 RSA 加密、MD5 签名,存在如下短板:
- RSA:非国产算法、加密效率低、密钥冗长,不符合政企国产化合规要求;
- MD5:哈希算法已被破解,安全性极低,无法抵御碰撞篡改;
- 单一加密模式:仅能实现基础加密,无法同时覆盖精准字段加密、大批量整包加密、防篡改、防重放多重场景。
1.2 国密算法优势 & 官方合规依据
本文采用国家商用密码标准(GM/T)三大核心算法,组成企业级安全黄金组合,全方位覆盖接口安全场景,补齐单一加密方案短板:
- SM2(非对称加密):替代RSA,椭圆曲线加密,密钥更短、安全性更高、国产合规,用于加密手机号、密码等核心敏感字段,保障密钥安全;
- SM3(哈希摘要):替代MD5/SHA,不可逆哈希算法,用于接口参数签名,彻底杜绝参数篡改风险;
- SM4(对称加密):国产主流对称加密算法,加密解密效率极高、适配高并发场景,专门弥补SM2非对称加密大批量数据性能短板,用于表单全量数据、长文本、批量报文整包加密传输。
官方标准依据(密标委正式发布英文标准,合规性可溯源):
- SM2 标准:GM/T 0003-2012 SM2椭圆曲线公钥密码算法标准
- SM3 标准:GM/T 0004-2012 SM3密码杂凑算法标准
- SM4 标准:GM/T 0002-2012 SM4分组密码算法标准
1.3 整体实现方案与安全架构
本文实现六层企业级接口安全防护架构,新增SM4整包加密能力,实现「精准字段加密+全量报文加密」双模式自适应,层层拦截风险请求,兼顾安全性、合规性与可用性:
- 防数据泄露(SM2+SM4双模式):SM2加密单个核心敏感字段,SM4加密大批量业务数据、整包报文,双重保障不同场景数据传输安全;
- 防参数篡改(SM3哈希签名):全参字典排序+固定密钥加盐哈希,后端严格校验签名一致性;
- 防重放攻击(时间戳+随机串):13位时间戳5分钟过期校验 + Redis存储nonce随机串唯一校验,杜绝重复请求;
- 身份认证(AppId+Token):应用唯一标识+用户登录令牌,拦截非法访问、伪造请求;
- 性能优化适配:SM2适配少量敏感数据、SM4适配大批量数据,兼顾极致安全与接口高并发性能;
- 无侵入业务校验:基于AOP切面实现所有安全校验、加解密逻辑,业务代码零改动,解耦性极强。
二、技术栈介绍
2.1 前端技术栈
- Vue2:前端基础框架
- Axios:网络请求拦截器统一处理加密、签名、参数封装
- gm-crypto@0.1.12:Vue2兼容国密加解密库(支持SM2/SM3/SM4全算法)
2.2 后端技术栈
- SpringBoot 2.3.7.RELEASE:后端核心框架
- Spring AOP:切面解耦,实现统一验签、统一解密、安全校验,无侵入业务代码
- Redis:存储随机串,实现防重放机制
- Hutool 5.7.16:国密工具封装(SM2/SM3/SM4全覆盖)
- BouncyCastle 1.70:国密底层加密实现
- FastJSON:参数序列化转Map用于验签
三、环境依赖搭建
3.1 前端依赖安装(Vue2)
bash
# 卸载旧版本
npm uninstall gm-crypto --save
# 安装稳定兼容版(解决版本冲突)
npm install gm-crypto@0.1.12 --save --legacy-peer-deps
# 查看版本验证
npm list gm-crypto
3.2 后端核心Maven依赖(精简最终版)
java
<!-- 1. Web核心 -->
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-web</artifactId>
</dependency>
<!-- 2. AOP切面核心(验签、解密切面依赖) -->
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-aop</artifactId>
</dependency>
<!-- 3. 配置文件属性绑定 -->
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-configuration-processor</artifactId>
<optional>true</optional>
</dependency>
<!-- 4. Redis防重放核心 -->
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-data-redis</artifactId>
</dependency>
<!-- 5. Lombok简化实体 -->
<dependency>
<groupId>org.projectlombok</groupId>
<artifactId>lombok</artifactId>
<optional>true</optional>
</dependency>
<!-- 6. Hutool工具类(含SM2/SM3/SM4全量国密工具) -->
<dependency>
<groupId>cn.hutool</groupId>
<artifactId>hutool-all</artifactId>
<version>5.7.16</version>
</dependency>
<!-- 7. 国密底层BC依赖 -->
<dependency>
<groupId>org.bouncycastle</groupId>
<artifactId>bcprov-jdk15on</artifactId>
<version>1.70</version>
</dependency>
<!-- 8. FastJSON序列化验签 -->
<dependency>
<groupId>com.alibaba</groupId>
<artifactId>fastjson</artifactId>
<version>1.2.73</version>
</dependency>
<!-- 9. 通用工具 -->
<dependency>
<groupId>org.apache.commons</groupId>
<artifactId>commons-lang3</artifactId>
<version>3.20.0</version>
</dependency>
四、核心原理说明
4.1 SM2加密原理
SM2为非对称加密,基于椭圆曲线算法,主打核心敏感数据精准加密:
- 公钥(Public):前端持有,用于加密敏感数据,公开透明无风险;
- 私钥(Private):后端绝密持有,仅后端可解密,杜绝密钥泄露;
- 本文统一使用 C1C3C2 加密模式,完全对齐 gm-crypto 与 Hutool 算法规则;
- 手动补04非压缩公钥头,彻底解决前端库与后端国密框架格式不兼容问题。
4.2 SM3签名原理
为防止参数篡改,采用 参数字典排序+固定密钥加盐哈希 核心机制,保证前后端签名规则绝对统一:
- 所有请求参数按Key字典升序排序,规避参数顺序不同导致签名不一致问题;
- 统一拼接 key=value 格式字符串,标准化签名源串;
- 尾部拼接全局密钥,通过SM3生成不可逆哈希串作为唯一签名;
- 后端重复计算签名并对比,不一致则直接判定参数被篡改,拦截请求。
4.3 SM4加密原理
SM4为国产对称加密算法,是本文方案性能核心补充,加密解密密钥一致、运算效率极高,完美解决SM2非对称加密大批量数据效率极低的痛点,完全契合GM/T 0002-2012官方标准:
- 加密规则:加解密使用同一32位HEX密钥,运算逻辑简单、速度快、资源消耗低;
- 适用场景:大批量业务表单、长文本参数、批量数据上报、文件流摘要加密等场景;
- 核心优势:加密速度远超SM2非对称加密,高并发场景下无性能瓶颈,兼顾国产化合规与接口性能;
- 使用规范:前后端统一使用ECB加密模式、32位标准HEX密钥,保证加解密双向互通、无兼容问题;
- 安全定位:整包报文全覆盖加密,请求无任何明文泄露,适配高密级、高安全要求接口。
4.4 双加密模式适配规则
本文自适应双加密模式,根据接口场景自动切换,无需手动修改代码:
- 普通轻量接口(查询、登录、个人信息修改):SM2字段精准加密 + SM3签名,仅加密密码、手机号等敏感字段,性能最优;
- 高密级/大批量接口(批量上报、表单全量提交、数据同步):SM4整包加密 + SM3签名,全量报文加密,杜绝明文泄露,适配大数据量传输。
4.5 防重放+身份认证原理
- 时间戳防过期:请求携带13位毫秒时间戳,服务端校验5分钟过期,超时直接拦截;
- 随机串nonce防重复:每次请求生成唯一随机串,Redis缓存5分钟,重复请求直接拦截;
- AppId应用校验:固定应用唯一标识,识别合法客户端,拦截伪造应用请求;
- Token身份校验:携带用户登录令牌,未授权用户无法调用接口,实现权限管控。
五、前端完整代码实现(Vue2)
5.1 环境变量配置 .env.development
bash
VUE_APP_BASE_API = http://localhost:8080
# SM2前端公钥
VUE_APP_SM2_PUB_KEY = '04bdea5bbbecc8bc278b0b8ba671036293ed1dd9006b53bb0fcd78dee70fdb2362ac77cf9a31ed229c1ce3ebb0166a738ad00f241edb65343e595cdbf359e019c5'
# SM4对称加密密钥(32位HEX,前后端严格统一)
VUE_APP_SM4_KEY = '31323334353637383930313233343536'
VUE_APP_AUTH_TOKEN = ''
# APPID
VUE_APP_APP_ID = '1001'
# 签名密钥
VUE_APP_APP_SECRET = '1234567890123456'
5.2 SM2加密工具 sm2.js
java
import { SM2 } from 'gm-crypto'
// 后端配置的公钥(04开头非压缩)【环境变量管理公钥,区分开发/生产环境,避免硬编码】
const SM2_PUBLIC_KEY = process.env.VUE_APP_SM2_PUB_KEY || '04bdea5bbbecc8bc278b0b8ba671036293ed1dd9006b53bb0fcd78dee70fdb2362ac77cf9a31ed229c1ce3ebb0166a738ad00f241edb65343e595cdbf359e019c5'
/**
* SM2 加密:C1C3C2 + 非压缩点 + 输出Hex
* @param {string} plainText 明文
* @returns {string} 标准Hex密文
*/
export function sm2Encrypt(plainText) {
if (!plainText || plainText.trim() === '') return ''
try {
const raw = SM2.encrypt(plainText.trim(), SM2_PUBLIC_KEY, {
inputEncoding: 'utf8',
outputEncoding: 'hex',
mode: 'C1C3C2',
useDer: false,
compress: false
})
// 适配后端 Hutool/BC 要求:补 04 非压缩头
const cipher = '04' + raw
// 通过环境变量控制日志,生产关闭日志
if (process.env.NODE_ENV === 'development') {
console.log('最终SM2密文Hex:', cipher)
console.log('密文首两位:', cipher.substring(0, 2))
}
return cipher
} catch (error) {
// 捕获加密异常,防止前端请求直接崩溃
console.error('SM2加密失败:', error)
throw new Error('数据加密失败,请重试')
}
}
5.3 SM3签名工具 sm3.js
java
import { SM3 } from 'gm-crypto'
export default {
/**
* SM3 哈希计算
* @param {String} data 明文字符串
* @returns SM3 哈希值
*/
hash(data) {
if (!data || data.trim() === '') return ''
try {
return SM3.digest(data.trim(), 'utf8', 'hex')
} catch (error) {
console.error('SM3哈希计算失败:', error)
throw new Error('签名计算失败')
}
}
}
5.4 SM4签名工具 sm4.js
适配gm-crypto稳定版,解决密钥报错、格式不统一问题,支持全局默认密钥+自定义密钥双模式,完全对齐后端SM4解密规则:
java
import { SM4 } from 'gm-crypto'
// 全局统一32位HEX标准密钥(与后端严格一致)
const SM4_GLOBAL_KEY = process.env.VUE_APP_SM4_KEY || '31323334353637383930313233343536'
/**
* SM4 ECB模式加密
* @param {string} plainText 明文
* @returns {string} Hex密文
*/
export function sm4Encrypt(plainText) {
if (!plainText || plainText.trim() === '') return ''
try {
return SM4.encrypt(plainText.trim(), SM4_GLOBAL_KEY, {
inputEncoding: 'utf8',
outputEncoding: 'hex',
mode: 'ecb'
})
} catch (error) {
console.error('SM4加密失败:', error)
throw new Error('批量数据加密失败,请重试')
}
}
/**
* SM4 ECB模式解密
* @param {string} plainText Hex密文
* @returns {string} 明文
*/
export function sm4Decrypt(plainText) {
if (!plainText || plainText.trim() === '') return ''
try {
return SM4.decrypt(plainText.trim(), SM4_GLOBAL_KEY, {
inputEncoding: 'hex',
outputEncoding: 'utf8',
mode: 'ecb'
})
} catch (error) {
console.error('SM4解密失败:', error)
throw new Error('批量数据解密失败,请重试')
}
}
/**
* SM4 ECB 加密(支持外部传入密钥,彻底解决密钥非法报错)
* @param {string} plainText 明文
* @param {string} key 32位密钥
* @returns {string} 十六进制密文
*/
export function sm4EncryptKey(plainText, key) {
if (!plainText || plainText.trim() === '') return ''
debugger
// 强制校验密钥,兜底32位标准密钥
const sm4Key = (key && key.length === 32) ? key : SM4_GLOBAL_KEY
try {
return SM4.encrypt(plainText.trim(), sm4Key, {
inputEncoding: 'utf8',
outputEncoding: 'hex',
mode: 'ecb'
})
} catch (error) {
console.error('SM4加密失败:', error)
throw new Error('数据加密失败')
}
}
/**
* SM4 ECB 解密
* @param {string} plainText 密文
* @param {string} key 32位密钥
* @returns {string} 明文
*/
export function sm4DecryptKey(plainText, key) {
if (!plainText) return ''
const sm4Key = (key && key.length === 32) ? key : SM4_GLOBAL_KEY
try {
return SM4.decrypt(plainText.trim(), sm4Key, {
inputEncoding: 'hex',
outputEncoding: 'utf8',
mode: 'ecb'
})
} catch (error) {
console.error('SM4解密失败:', error)
throw new Error('数据解密失败')
}
}
5.5 全局请求拦截器 requestSm234.js(双模式自适应)
升级核心逻辑,自动区分SM2字段加密和SM4整包加密接口,实现无感知自适应切换,统一SM3签名规则:
java
import axios from 'axios'
import { sm2Encrypt } from './sm2'
import { sm4Encrypt } from './sm4'
import SM3 from './sm3'
const service = axios.create({
baseURL: process.env.VUE_APP_BASE_API || '',
timeout: 10000
})
// 环境变量管理Token
const AUTH_TOKEN = process.env.VUE_APP_AUTH_TOKEN || ''
// 环境变量管理AppId
const APP_ID = process.env.VUE_APP_APP_ID || '1001'
// 环境变量管理Secret
const APP_SECRET = process.env.VUE_APP_APP_SECRET || '1234567890123456'
service.interceptors.request.use(config => {
console.log('config.url: ' + config.url)
// 区分两种加密模式接口路由
// 场景1:SM2精准加密敏感字段
const needCommonSign = config.url?.startsWith('/api/test/testSm23')
// 场景2:SM4整包加密专属
const needSm4FullEncrypt = config.url?.startsWith('/api/test/testSm234')
if (config.method === 'post' && config.data && needCommonSign) {
config.headers['Authorization'] = `Bearer ${AUTH_TOKEN}`
// 深拷贝原始数据,避免篡改原对象
const rawData = { ...config.data }
if (process.env.NODE_ENV === 'development') {
console.log('请求原始data: ', rawData)
}
// 追加防重放、身份认证公共参数
rawData.timestamp = new Date().getTime()
// 增强nonce随机性,降低被预测风险
rawData.nonce = Math.random().toString(36).slice(2) + Date.now().toString(36)
rawData.appId = APP_ID
// 场景1:普通接口 - SM2精准加密敏感字段
if (needCommonSign) {
if (rawData.password) {
console.log('原始明文pwd: ', rawData.password)
rawData.password = sm2Encrypt(rawData.password)
console.log('SM2密文pwd: ', rawData.password)
}
if (rawData.phone) {
console.log('原始明文phone: ', rawData.phone)
rawData.phone = sm2Encrypt(rawData.phone)
console.log('SM2密文phone: ', rawData.phone)
}
}
// 场景2:大批量/高密级接口 - SM4整包全量加密
let signData = { ...rawData }
if (needSm4FullEncrypt) {
// 整包序列化为字符串后SM4加密
const fullDataStr = JSON.stringify(rawData)
const sm4Cipher = sm4Encrypt(fullDataStr)
// 仅保留公共验签参数+密文,无任何明文泄露
signData = {
sm4Data: sm4Cipher,
appId: rawData.appId,
timestamp: rawData.timestamp,
nonce: rawData.nonce
}
}
// 字典升序排序,统一签名规则
const sorted = {}
Object.keys(signData).sort().forEach(k => {
// null/undefined 转为空字符串,统一前后端序列化规则
const val = signData[k]
sorted[k] = val === null || val === undefined ? '' : val
})
// 拼接签名字符串(修复末尾多余 & bug)
let signStr = ''
Object.keys(sorted).forEach((k, index) => {
if (k === 'sign') return
signStr += `${k}=${sorted[k]}`
// 不是最后一个字段才拼 &
if (index < Object.keys(sorted).filter(key => key !== 'sign').length - 1) {
signStr += '&'
}
})
// 拼接密钥
signStr += `&secret=${APP_SECRET}`
if (process.env.NODE_ENV === 'development') {
console.log('signStr: ', signStr)
}
// 生成SM3签名
sorted.sign = SM3.hash(signStr)
config.data = sorted
if (process.env.NODE_ENV === 'development') {
console.log(needSm4FullEncrypt ? '当前模式:SM4整包加密' : '当前模式:SM2字段加密')
console.log('最终请求完整报文: ', config.data)
}
}
config.headers['Content-Type'] = 'application/json'
return config
}, err => Promise.reject(err))
// 响应拦截器统一异常处理
service.interceptors.response.use(
res => res.data,
err => {
const msg = err.response?.data?.message || '请求异常,请稍后重试'
console.error('请求失败:', msg)
return Promise.reject(new Error(msg))
}
)
export default service
5.6 测试页面 index.vue
java
<template>
<div style="padding: 50px;">
<h2>国密SM2+SM3+SM4 双模式加密测试</h2>
<div style="margin: 10px 0;">
<input v-model="username" placeholder="用户名">
</div>
<div style="margin: 10px 0;">
<input v-model="password" type="password" placeholder="密码">
</div>
<div style="margin: 10px 0;">
<input v-model="phone" placeholder="手机号">
</div>
<button style="margin-right: 10px;" @click="handleSm23">SM2字段加密+SM3签名测试</button>
<button style="margin-right: 10px;" @click="handleSm234">SM4整包加密+SM3签名测试</button>
<button @click="generate">生成 GM-CRYPTO 专用 SM2 密钥</button>
</div>
</template>
<script>
import requestSm234 from '@/utils/requestSm234'
import { SM2 } from 'gm-crypto'
export default {
data() {
return {
username: 'admin',
password: '123456',
phone: '18888888888'
}
},
methods: {
generate() {
const { publicKey, privateKey } = SM2.generateKeyPair()
console.log('公钥(前端用):', publicKey)
console.log('私钥(后端用):', privateKey)
},
// 单字段加密模式测试
async handleSm23() {
if (!this.password) {
alert('密码不能为空')
return
}
if (!/^1\d{10}$/.test(this.phone)) {
alert('手机号格式错误')
return
}
try {
const res = await requestSm234.post('/api/test/testSm23', {
username: this.username,
password: this.password,
phone: this.phone
})
alert('SM2字段加密请求成功')
console.log('请求成功:', res)
} catch (e) {
alert(e.message || '请求失败')
console.error('请求失败:', e)
}
},
// 整包加密模式测试
async handleSm234() {
if (!this.password) {
alert('密码不能为空')
return
}
if (!/^1\d{10}$/.test(this.phone)) {
alert('手机号格式错误')
return
}
try {
const res = await requestSm234.post('/api/test/testSm234', {
username: this.username,
password: this.password,
phone: this.phone
})
alert('SM4整包加密请求成功')
console.log('请求成功:', res)
} catch (e) {
alert(e.message || '请求失败')
console.error('请求失败:', e)
}
}
}
}
</script>
六、后端完整代码实现(SpringBoot2.x)
6.1 配置文件 application.yml
java
# 国密SM2配置
sm2:
private-key: 00e9c5b041b5fe109f3b22c4a586f95104e3fb8ad2c87e3e7c605646906e5b1e2d
public-key: 04bdea5bbbecc8bc278b0b8ba671036293ed1dd9006b53bb0fcd78dee70fdb2362ac77cf9a31ed229c1ce3ebb0166a738ad00f241edb65343e595cdbf359e019c5
# 国密SM4配置(32位HEX密钥,与前端严格一致)
sm4:
secret-key: 31323334353637383930313233343536
# 接口安全配置
api:
security:
app-secret: 1234567890123456
# 应用身份配置
app:
app-id: 1001
6.2 验签基础实体类 SignBaseBO
java
import lombok.Data;
/**
* 接口验签、防重放、身份认证基础父类
* @author Z
*/
@Data
public class SignBaseBO {
/**
* SM3签名字符串
*/
private String sign;
/**
* 13位毫秒时间戳
*/
private Long timestamp;
/**
* 单次请求唯一随机串
*/
private String nonce;
/**
* 客户端应用标识
*/
private String appId;
}
6.3 业务参数实体 Sm234BO
java
import lombok.Data;
/**
* 国密测试业务参数
* 兼容SM2字段加密、SM4整包加密双模式
* @author Z
*/
@Data
public class Sm234BO extends SignBaseBO {
/**
* 普通业务字段
*/
private String username;
/**
* SM2加密敏感字段-密码
*/
private String password;
/**
* SM2加密敏感字段-手机号
*/
private String phone;
/**
* SM4整包加密密文字段
*/
private String sm4Data;
}
6.4 自定义验签注解 ApiSignCheck
java
import java.lang.annotation.*;
/**
* 接口验签校验注解
* 标记该注解的接口需要执行SM3验签+防重放+身份校验
* @author Z
*/
@Target(ElementType.METHOD)
@Retention(RetentionPolicy.RUNTIME)
public @interface ApiSignCheck {
}
6.5 密钥生成工具类 SM2KeyGenerator
用于生成前后端互通的标准64位私钥、130位04开头公钥:
java
import cn.hutool.core.util.HexUtil;
import cn.hutool.crypto.SmUtil;
import cn.hutool.crypto.asymmetric.SM2;
import org.bouncycastle.jcajce.provider.asymmetric.ec.BCECPrivateKey;
import org.bouncycastle.jcajce.provider.asymmetric.ec.BCECPublicKey;
import org.bouncycastle.math.ec.ECPoint;
import java.math.BigInteger;
/**
* 国密 SM2 密钥生成工具
* 适配 gm-crypto + Hutool 双向互通
* @author Z
*/
public final class SM2KeyGenerator {
private SM2KeyGenerator() {}
public static void main(String[] args) {
SM2 sm2 = SmUtil.sm2();
BCECPrivateKey privateKey = (BCECPrivateKey) sm2.getPrivateKey();
BigInteger privateKeyBigInt = privateKey.getD();
String privateKeyHex = fillZero(privateKeyBigInt.toString(16), 64);
BCECPublicKey publicKey = (BCECPublicKey) sm2.getPublicKey();
ECPoint publicKeyPoint = publicKey.getQ();
byte[] publicKeyEncoded = publicKeyPoint.getEncoded(false);
String publicKeyHex = HexUtil.encodeHexStr(publicKeyEncoded);
System.out.println("════════════════════════════════════════");
System.out.println("【SM2 私钥(Hex 64位)→ 后端配置】");
System.out.println(privateKeyHex);
System.out.println("\n【SM2 公钥(Hex 130位)→ 前端配置】");
System.out.println(publicKeyHex);
System.out.println("════════════════════════════════════════");
validateKeyPair(privateKeyHex, publicKeyHex);
}
private static String fillZero(String hex, int length) {
if (hex.length() >= length) return hex;
StringBuilder sb = new StringBuilder();
while (sb.length() < length - hex.length()) sb.append('0');
return sb.append(hex).toString();
}
private static void validateKeyPair(String privateKeyHex, String publicKeyHex) {
if (privateKeyHex.length() != 64) throw new IllegalStateException("私钥长度必须64位");
if (publicKeyHex.length() != 130) throw new IllegalStateException("公钥长度必须130位");
if (!publicKeyHex.startsWith("04")) throw new IllegalStateException("公钥必须04开头");
System.out.println("\n✅ 密钥格式校验通过,前后端互通!");
}
}
6.6 SM2解密工具类 Sm2Util
java
import cn.hutool.core.util.HexUtil;
import cn.hutool.crypto.asymmetric.KeyType;
import cn.hutool.crypto.asymmetric.SM2;
import org.apache.commons.lang3.StringUtils;
import org.bouncycastle.crypto.engines.SM2Engine;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.springframework.beans.factory.annotation.Value;
import org.springframework.stereotype.Component;
import javax.annotation.PostConstruct;
/**
* SM2国密解密工具
* 适配前端gm-crypto 0.1.12版本,解决前后端格式不兼容问题
* @author Z
*/
@Component
public class Sm2Util {
private static final Logger LOGGER = LoggerFactory.getLogger(Sm2Util.class);
private String publicKey;
private String privateKey;
private static SM2 SM2_INSTANCE;
@Value("${sm2.public-key}")
public void setPublicKey(String publicKey) {
this.publicKey = publicKey;
}
@Value("${sm2.private-key}")
public void setPrivateKey(String privateKey) {
this.privateKey = privateKey;
}
@PostConstruct
public void init() {
SM2_INSTANCE = new SM2(privateKey, publicKey);
// 强制对齐前端C1C3C2模式
SM2_INSTANCE.setMode(SM2Engine.Mode.C1C3C2);
LOGGER.info("SM2工具初始化完成,模式:C1C3C2");
}
/**
* 适配前端gm-crypto的Hex密文解密
*/
public static String decryptHex(String cipherText) {
if (SM2_INSTANCE == null) {
throw new RuntimeException("SM2实例未初始化");
}
if (StringUtils.isBlank(cipherText)) {
return "";
}
// 兜底补04非压缩头,解决前后端格式不统一问题
if (!cipherText.startsWith("04")) {
cipherText = "04" + cipherText;
}
try {
byte[] cipherBytes = HexUtil.decodeHex(cipherText);
byte[] plainBytes = SM2_INSTANCE.decrypt(cipherBytes, KeyType.PrivateKey);
return new String(plainBytes);
} catch (Exception e) {
LOGGER.error("SM2解密失败,密文:{}", cipherText, e);
throw new RuntimeException("数据解密失败");
}
}
}
6.7 SM4加解密工具类 Sm4Util
基于Hutool实现,完全对齐前端ECB模式、HEX密钥规则,适配整包报文解密,全局统一配置、容错性极强:
java
import cn.hutool.core.util.HexUtil;
import cn.hutool.crypto.SmUtil;
import cn.hutool.crypto.symmetric.SymmetricCrypto;
import org.apache.commons.lang3.StringUtils;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.springframework.beans.factory.annotation.Value;
import org.springframework.stereotype.Component;
import javax.annotation.PostConstruct;
/**
* SM4国密对称加密工具类
* 对齐前端gm-crypto ECB模式、32位HEX密钥规则
* 适配整包报文加解密,支撑大批量数据传输
* @author Z
*/
@Component
public class Sm4Util {
private static final Logger LOGGER = LoggerFactory.getLogger(Sm4Util.class);
private static String SM4_KEY;
private static SymmetricCrypto SM4_CRYPTO;
@Value("${sm4.secret-key}")
public void setSm4Key(String sm4Key) {
Sm4Util.SM4_KEY = sm4Key;
}
@PostConstruct
public void init() {
/*
初始化SM4实例,ECB模式对齐前端(前端传入的是32位HEX密钥字符串)
原错误逻辑:直接getBytes()将HEX字符串当明文解析
现正确逻辑:将HEX字符串解码为原始字节,完全对齐gm-crypto底层规则
*/
byte[] keyBytes = HexUtil.decodeHex(SM4_KEY);
SM4_CRYPTO = SmUtil.sm4(keyBytes);
LOGGER.info("SM4工具初始化完成,密钥统一适配前端HEX格式");
}
/**
* SM4整包解密
* @param cipherText 前端传入HEX密文
* @return 明文JSON字符串
*/
public static String decrypt(String cipherText) {
if (StringUtils.isBlank(cipherText)) {
return "";
}
try {
return SM4_CRYPTO.decryptStr(cipherText);
} catch (Exception e) {
LOGGER.error("SM4整包解密失败,密文:{}", cipherText, e);
throw new RuntimeException("批量数据解密异常");
}
}
/**
* 自定义密钥解密(拓展多密钥场景)
*/
public static String decrypt(String cipherText, String key) {
if (StringUtils.isBlank(cipherText) || StringUtils.isBlank(key)) {
return "";
}
try {
SymmetricCrypto customSm4 = SmUtil.sm4(key.getBytes());
return customSm4.decryptStr(cipherText);
} catch (Exception e) {
LOGGER.error("SM4自定义密钥解密失败", e);
throw new RuntimeException("数据解密异常");
}
}
}
6.8 SM3签名验签工具 Sm3SignUtil
java
import cn.hutool.core.util.StrUtil;
import cn.hutool.crypto.SmUtil;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import java.util.Map;
import java.util.TreeMap;
/**
* SM3 签名验签工具(防篡改)
* 规则:参数 KEY 字典序排序 → 拼接 → 哈希
* @author Z
*/
public class Sm3SignUtil {
private static final Logger LOGGER = LoggerFactory.getLogger(Sm3SignUtil.class);
/**
* 生成签名(TreeMap 自动字典排序,保证前后端顺序一致)
*/
public static String createSign(Map<String, Object> paramMap, String appSecret) {
// 使用 TreeMap 按 key 字典升序排序
TreeMap<String, Object> treeMap = new TreeMap<>(paramMap);
StringBuilder sb = new StringBuilder();
treeMap.forEach((key, value) -> {
// 排除 sign 字段不参与签名
if ("sign".equals(key)) {
return;
}
String val = value == null ? "" : String.valueOf(value);
sb.append(key).append("=").append(val).append("&");
});
// 删除最后一个多余 &,和前端签名规则完全一致
if (sb.length() > 0) {
sb.deleteCharAt(sb.length() - 1);
}
sb.append("&secret=").append(appSecret);
String signSource = sb.toString();
LOGGER.debug("签名原串: {}", signSource);
String secretText = SmUtil.sm3(signSource);
LOGGER.debug("签名后: {}", secretText);
return secretText;
}
/**
* 验签
*/
public static boolean verifySign(Map<String, Object> paramMap, String appSecret, String sign) {
if (StrUtil.isBlank(sign)) {
return false;
}
String localSign = createSign(paramMap, appSecret);
return localSign.equals(sign);
}
}
6.9 Redis防重放工具 ReplayUtil
java
import org.apache.commons.lang3.StringUtils;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.springframework.data.redis.core.RedisTemplate;
import org.springframework.stereotype.Component;
import javax.annotation.Resource;
import java.util.concurrent.TimeUnit;
/**
* 防重放攻击工具
* 1. 超过 5 分钟请求拒绝
* 2. nonce 随机串 5 分钟内只能用一次
* @author Z
*/
@Component
public class ReplayUtil {
private static final Logger LOGGER = LoggerFactory.getLogger(ReplayUtil.class);
// 5 分钟有效期
private static final long EXPIRE_MS = 5 * 60 * 1000L;
private static final long EXPIRE_SECOND = EXPIRE_MS / 1000;
private static final String KEY_PREFIX = "api:nonce:repeat:";
// 静态变量
private static RedisTemplate<String, Object> redisTemplate;
// Spring 注入到非静态方法,赋值给静态变量
@Resource
public void setRedisTemplate(RedisTemplate<String, Object> template) {
ReplayUtil.redisTemplate = template;
}
// 校验时间戳
public static boolean checkTimestamp(long timestamp) {
return System.currentTimeMillis() - timestamp <= EXPIRE_MS;
}
/**
* 校验随机串nonce:5分钟内不可重复使用
* @param nonce 前端随机串
* @return true=合法,false=重复请求
*/
public static boolean checkNonce(String nonce) {
// 非空校验
if (StringUtils.isBlank(nonce) || null == redisTemplate) {
LOGGER.error("Redis 未初始化或 nonce 为空");
return false;
}
String key = KEY_PREFIX + nonce;
// setIfAbsent:不存在则写入,同时设置过期时间
Boolean success = redisTemplate.opsForValue()
.setIfAbsent(key, "1", EXPIRE_SECOND, TimeUnit.SECONDS);
return Boolean.TRUE.equals(success);
}
}
6.10 验签切面 ApiSignAspect
java
import cn.hutool.core.util.StrUtil;
import com.alibaba.fastjson.JSON;
import com.zhang.utils.sm234.ReplayUtil;
import com.zhang.utils.sm234.SignBaseBO;
import com.zhang.utils.sm234.Sm3SignUtil;
import org.apache.commons.lang3.StringUtils;
import org.aspectj.lang.ProceedingJoinPoint;
import org.aspectj.lang.annotation.Around;
import org.aspectj.lang.annotation.Aspect;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.springframework.beans.factory.annotation.Value;
import org.springframework.core.annotation.Order;
import org.springframework.stereotype.Component;
import java.util.Map;
/**
* 验签AOP
* @author Z
*/
@Aspect
@Component
@Order(0) // 优先级最高,先执行验签、防重放
public class ApiSignAspect {
private static final Logger LOGGER = LoggerFactory.getLogger(ApiSignAspect.class);
// 从配置文件读取密钥
@Value("${api.security.app-secret:1234567890123456}")
private String appSecret;
@Value("${app.app-id:1001}")
private String validAppId;
// 环绕增强:@ApiSignCheck 注解的方法才会进入验签
@Around("@annotation(com.zhang.annotation.ApiSignCheck)")
public Object checkSign(ProceedingJoinPoint joinPoint) throws Throwable {
Object[] args = joinPoint.getArgs();
// 无参数直接拦截
if (args == null || args.length == 0) {
throw new IllegalArgumentException("请求参数不能为空");
}
// 统一强转为验签基类(所有验签接口入参都继承它)
Object arg = args[0];
// 仅处理继承验签基类的参数实体
if (!(arg instanceof SignBaseBO)) {
return joinPoint.proceed();
}
SignBaseBO baseBO = (SignBaseBO) arg;
// 1. 公共验签字段非空校验
if (StringUtils.isBlank(baseBO.getSign())) {
throw new IllegalArgumentException("签名不能为空");
}
if (baseBO.getTimestamp() == null) {
throw new IllegalArgumentException("时间戳不能为空");
}
if (StrUtil.isBlank(baseBO.getNonce())) {
throw new IllegalArgumentException("随机串 nonce 不能为空");
}
// nonce 重复请求校验
if (!ReplayUtil.checkNonce(baseBO.getNonce())) {
throw new IllegalArgumentException("请求重复,禁止重放攻击");
}
// 应用标识AppId
if (StrUtil.isBlank(baseBO.getAppId())) {
throw new IllegalArgumentException("应用标识appId不能为空");
}
// 【新增】AppId合法身份校验,拦截伪造客户端请求
if (!validAppId.equals(baseBO.getAppId())) {
LOGGER.error("非法客户端请求,无效appId:{}", baseBO.getAppId());
throw new IllegalArgumentException("客户端身份非法,禁止访问");
}
// 2. 全参数转JSON,用于整体验签(包含业务字段+验签字段)
Map<String, Object> paramMap = JSON.parseObject(JSON.toJSONString(arg), Map.class);
// 3. 签名校验
if (!Sm3SignUtil.verifySign(paramMap, appSecret, baseBO.getSign())) {
throw new IllegalArgumentException("签名验证失败,参数已被篡改");
}
// 4. 时间戳,防重放校验
if (!ReplayUtil.checkTimestamp(baseBO.getTimestamp())) {
throw new IllegalArgumentException("请求已过期,禁止重放攻击");
}
LOGGER.info("接口验签通过,完整请求参数:{}", JSON.toJSONString(paramMap));
// 执行目标接口
return joinPoint.proceed();
}
}
6.11 解密切面 Sm2DecryptAspect
java
import cn.hutool.core.util.ReflectUtil;
import com.alibaba.fastjson.JSON;
import com.zhang.utils.sm234.SignBaseBO;
import com.zhang.utils.sm234.Sm234BO;
import com.zhang.utils.sm234.Sm2Util;
import com.zhang.utils.sm234.Sm4Util;
import org.apache.commons.lang3.StringUtils;
import org.aspectj.lang.ProceedingJoinPoint;
import org.aspectj.lang.annotation.Around;
import org.aspectj.lang.annotation.Aspect;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.springframework.core.annotation.Order;
import org.springframework.stereotype.Component;
import java.lang.reflect.Field;
/**
* SM2 自动解密切面
* 执行顺序:晚于验签切面(Order值更大)
* 作用:验签通过后,自动解密敏感字段,业务层直接使用明文
* @author Z
*/
@Aspect
@Component
@Order(10) // 解密切面设置Order=10,此处设置更大值,保证后执行
public class Sm2DecryptAspect {
private static final Logger LOGGER = LoggerFactory.getLogger(Sm2DecryptAspect.class);
// 定义需要自动解密的敏感字段名
private static final String[] DECRYPT_FIELDS = {"password", "phone"};
@Around("@annotation(com.zhang.annotation.ApiSignCheck)")
public Object decryptField(ProceedingJoinPoint joinPoint) throws Throwable {
Object[] args = joinPoint.getArgs();
if (args == null || args.length == 0) {
return joinPoint.proceed();
}
Object paramObj = args[0];
// 仅处理继承验签基类的参数实体
if (!(paramObj instanceof SignBaseBO)) {
return joinPoint.proceed();
}
// TODO 优先处理SM4整包加密解密逻辑(sm4Data:整包加密字符串)
Field sm4DataField = ReflectUtil.getField(paramObj.getClass(), "sm4Data");
if (sm4DataField != null) {
// 开启反射访问
sm4DataField.setAccessible(true);
String sm4Cipher = (String) sm4DataField.get(paramObj);
if (StringUtils.isNotBlank(sm4Cipher)) {
LOGGER.info("SM4整包密文:{}", sm4Cipher);
// SM4解密得到完整明文JSON
String plainJson = Sm4Util.decrypt(sm4Cipher);
LOGGER.info("SM4整包解密明文:{}", plainJson);
// 明文JSON覆盖原有参数对象,自动填充所有业务字段
Sm234BO plainBO = JSON.parseObject(plainJson, Sm234BO.class);
// 替换切面参数数组
args[0] = plainBO;
// 必须传入 args,否则不生效
return joinPoint.proceed(args);
}
}
// 普通接口:SM2单点敏感字段解密
for (String fieldName : DECRYPT_FIELDS) {
Field field = ReflectUtil.getField(paramObj.getClass(), fieldName);
if (field == null) {
continue;
}
// 开启反射访问
field.setAccessible(true);
// 反射取值
String cipherText = "";
try {
cipherText = (String) field.get(paramObj);
} catch (IllegalAccessException e) {
LOGGER.error("反射获取字段失败", e);
throw new RuntimeException("参数解析异常");
}
// 空字符串无需解密
if (StringUtils.isBlank(cipherText)) {
continue;
}
LOGGER.info("{}待解密密文:{}", fieldName, cipherText);
String plainText = Sm2Util.decryptHex(cipherText);
field.set(paramObj, plainText);
LOGGER.info("{}解密明文:{}", fieldName, plainText);
}
// 解密完成,执行目标接口(Controller 拿到明文)
return joinPoint.proceed();
}
}
6.12 测试接口 Controller
java
@RestController
@RequiredArgsConstructor
@RequestMapping("/api/test")
public class ApiTestController {
private static final Logger LOGGER = LoggerFactory.getLogger(ApiTestController.class);
// 注解AOP验签
@ApiSignCheck
@PostMapping("testSm23")
public ResponseEntity<String> testSm23(@RequestBody Sm234BO bo) {
LOGGER.info("验签通过-进入SM2 + SM3 Controller成功!param: {}", JSON.toJSONString(bo));
return ResponseEntity.ok("success");
}
@ApiSignCheck
@PostMapping("testSm234")
public ResponseEntity<String> testSm234(@RequestBody Sm234BO bo) {
LOGGER.info("验签通过-进入 SM2 + SM3 + SM4整包加密接口-param: {}", JSON.toJSONString(bo));
return ResponseEntity.ok("SM4批量加密校验通过,国密SM2+SM3+SM4全体系落地成功");
}
// 不加 = 不测签
@PostMapping("noSign")
public ResponseEntity<String> noSign(@RequestBody Sm234BO bo) {
return ResponseEntity.ok("不需要验签的接口 param: " + JSON.toJSONString(bo));
}
}
七、AOP执行顺序说明
本文基于Spring AOP切面实现所有安全校验、加解密解耦逻辑,通过切面优先级排序实现标准化、流程化的接口安全校验流程,层层拦截风险请求,保障业务执行前所有安全规则校验完毕。
AOP执行顺序图:
配图说明:展示双切面执行优先级:ApiSignAspect(Order=0 最高优先级,负责非空校验、验签、防重放、身份校验) → Sm2DecryptAspect(Order=10 次级优先级,负责敏感字段自动解密) → Controller 核心业务执行。
完整请求执行流程如下:
- 前端完成数据加密、SM3签名、拼接防重放与身份参数后,携带密文、签名、nonce随机串、13位时间戳、AppId、Token发起接口请求;
- 请求抵达后端接口,触发AOP切面拦截机制,优先执行Order优先级最高的安全校验切面;
- 第一层安全校验依次执行:公共参数非空校验、AppId客户端身份合法性校验、Redis随机串nonce防重放校验、13位时间戳超时校验、SM3签名一致性校验,任意环节校验失败直接拦截请求并返回异常;
- 所有安全校验全部通过后,进入解密切面,自动完成密码、手机号等SM2敏感字段解密,若是SM4整包加密请求,同步完成全量报文解密;
- 接口Controller接收完整明文业务参数,执行正常业务逻辑,最终完成请求响应,全程业务代码零侵入、零改动。
八、核心踩坑&问题总结
8.1. gm-crypto版本安装报错、版本冲突
问题现象:Vue2项目安装gm-crypto高版本(0.2.11、1.1.0)提示无对应版本,或安装过程中出现依赖版本冲突、解析失败,终端抛出ERESOLVE依赖解析异常。
问题原因:npm官方无高版本适配Vue2的gm-crypto,新版npm严格校验依赖版本,引发冲突
解决方案:固定安装Vue2专属稳定兼容版,通过忽略版本冲突强制安装,彻底解决所有安装报错,稳定适配本项目所有国密算法逻辑:
bash
# 卸载冲突旧版本
npm uninstall gm-crypto --save
# 稳定版强制安装,忽略依赖版本冲突
npm install gm-crypto@0.1.12 --save --legacy-peer-deps
8.2. 前后端SM2密文不兼容,解密报错
问题现象:前端加密后的密文后端无法解密,抛出椭圆曲线点位异常、密文格式错误等问题,解密逻辑失效。
问题原因:gm-crypto前端库加密输出的密文默认不带04非压缩公钥头,而后端Hutool、BouncyCastle框架的SM2解密逻辑,强制要求密文以04开头,前后端格式不统一导致兼容报错。
解决方案:前后端双向兜底适配,彻底杜绝格式兼容问题,适配所有环境:
- 前端:SM2加密完成后,手动拼接04非压缩公钥头,统一密文格式;
- 后端:解密工具类增加格式判断,自动为无04头的密文兜底补全,双向兼容。
8.3. SM3签名持续校验失败
问题原因:前后端签名字符串拼接规则不统一,核心原因包含三点:
a. 参数遍历拼接后末尾存在多余&符号;
b. 请求参数排序规则不一致;
c. 前端null/undefined与后端null空值序列化格式差异,导致最终签名源串不一致。
解决方案:统一全局签名规则,彻底解决签名不一致问题:
- 两端均采用字典升序排序规则,规避参数顺序差异问题;
- 优化字符串拼接逻辑,自动剔除末尾多余的&分隔符;
- 统一空值处理规则,将所有null、undefined参数强制转为空字符串,保证序列化结果一致。
8.4. Redis静态注入空指针
问题现象:防重放校验逻辑中RedisTemplate实例为null,随机串缓存、过期校验失效,无法拦截重放请求。
问题原因:静态变量无法被Spring容器自动注入,静态方法调用Redis工具类时,依赖未初始化导致空指针。
优化方案:采用setter方法注入替代静态注入,生产环境可改造为非静态成员变量注入,彻底规避空指针问题,保证Redis缓存逻辑稳定生效。
8.5. Hutool多版本依赖冲突
问题原因:项目同时引入hutool-all与hutool-crypto,版本不统一,导致国密算法初始化失败、加解密逻辑异常。
解决方案:统一依赖管理,仅保留hutool-all全局工具依赖,删除独立的hutool-crypto依赖,固定版本为5.7.16,保证国密算法底层逻辑统一。
8.6. 空参数、null值导致签名不一致
问题原因:前端JS的null、undefined空值,与后端Java的null值序列化、拼接规则不同,导致最终生成的签名源串存在差异,引发验签失败。
解决方案:全局统一空值处理策略,前后端所有空参数、空值统一转换为空字符串,再进行排序、拼接与签名计算,彻底规避空值差异问题。
8.7. 时间戳校验失败
问题原因:前端设备时间与服务器系统时间存在微小时钟偏差,即使在有效过期时间内,也会误判为请求超时。
解决方案:优化时间戳校验逻辑,在5分钟基础过期规则上,额外兼容10秒时钟误差,适配前后端时间偏差场景,兼顾安全性与可用性。
8.8. 前端 gm-crypto 安装失败
解决:使用 cnpm 或切换淘宝镜像。
还是报错。版本冲突问题,解决方案:1.升级vue版本;2.忽略版本进行安装。这里我指定版本方式安装,命令如:npm install gm-crypto@0.1.12 --save --legacy-peer-deps
报错信息如下:
bash
npm notice
npm notice New major version of npm available! 8.19.2 -> 11.16.0
npm notice Changelog: https://github.com/npm/cli/releases/tag/v11.16.0
npm notice Run npm install -g npm@11.16.0 to update!
npm notice
npm ERR! code ERESOLVE
npm ERR! ERESOLVE could not resolve
npm ERR!
npm ERR! While resolving: @fullcalendar/vue@5.11.5
npm ERR! Found: vue@2.6.10
npm ERR! node_modules/vue
npm ERR! peer vue@"^2.2.0" from @riophae/vue-treeselect@0.4.0
npm ERR! node_modules/@riophae/vue-treeselect
npm ERR! @riophae/vue-treeselect@"0.4.0" from the root project
npm ERR! peerOptional vue@"*" from @vue/babel-preset-jsx@1.4.0
npm ERR! node_modules/@vue/babel-preset-jsx
npm ERR! @vue/babel-preset-jsx@"^1.0.0" from @vue/babel-preset-app@3.12.1
npm ERR! node_modules/@vue/babel-preset-app
npm ERR! @vue/babel-preset-app@"^3.5.3" from @vue/cli-plugin-babel@3.5.3
npm ERR! node_modules/@vue/cli-plugin-babel
npm ERR! dev @vue/cli-plugin-babel@"3.5.3" from the root project
npm ERR! 6 more (@vue/test-utils, element-ui, v-viewer, vue-echarts, ...)
npm ERR!
npm ERR! Could not resolve dependency:
npm ERR! peer vue@"^2.6.12" from @fullcalendar/vue@5.11.5
npm ERR! node_modules/@fullcalendar/vue
npm ERR! @fullcalendar/vue@"^5.5.0" from the root project
npm ERR!
npm ERR! Conflicting peer dependency: vue@2.7.16
npm ERR! node_modules/vue
npm ERR! peer vue@"^2.6.12" from @fullcalendar/vue@5.11.5
npm ERR! node_modules/@fullcalendar/vue
npm ERR! @fullcalendar/vue@"^5.5.0" from the root project
npm ERR!
npm ERR! Fix the upstream dependency conflict, or retry
npm ERR! this command with --force, or --legacy-peer-deps
npm ERR! to accept an incorrect (and potentially broken) dependency resolution.
npm ERR!
九、总结
本文遵循国家商用密码GM/T 0002-2012、GM/T 0003-2012、GM/T 0004-2012三大官方国密标准体系,基于SpringBoot2.x + Vue2成熟技术栈,落地了一套标准化、国产化、高安全的企业级接口安全防护方案。完美适配等级保护2.0、商用密码安全性评估、国产化信创三大合规体系要求,彻底替代传统RSA、MD5非国产、低安全的加密校验方案。
方案构建了「精准字段加密+整包报文加密+参数防篡改+请求防重放+双向身份认证」的六层闭环安全防护体系,各司其职、互补适配:依托SM2非对称加密实现密码、手机号等核心敏感数据的精准加密传输,杜绝数据泄露;通过SM3哈希摘要算法配合标准化签名规则,彻底解决接口参数篡改风险;借助SM4对称加密的高性能优势,弥补非对称加密大批量数据传输的性能短板,适配高并发、大数据量业务场景。
在安全防护之外,方案基于Spring AOP切面实现所有安全逻辑解耦,做到业务代码零侵入、零改动,极大降低了项目改造与维护成本。同时通过统一前后端算法规则、适配空值与格式差异、规避版本冲突,解决了国密落地过程中的各类兼容问题,让方案具备极强的稳定性与可落地性。
整体方案兼顾国产化合规、数据安全、接口性能、业务解耦、落地便捷性五大核心优势,为前后端分离架构的接口通信安全提供标准化的国产化解决方案。