SpringBoot2.x + Vue2 国密接口安全实战(SM2+SM3+SM4 企业级防篡改/防泄露/防重放)

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 传统接口安全痛点

在常规前后端分离项目中,接口参数明文传输存在三大致命问题,同时无法满足政企合规要求:

  1. 数据泄露:密码、手机号、身份证等敏感数据明文传输,抓包即可窃取;
  2. 参数篡改:攻击者拦截请求修改参数,引发越权操作、数据刷取等风险;
  3. 重放攻击:截取合法请求重复发送,造成重复下单、重复登录、数据重复写入等问题;
  4. 合规性缺失:传统加密方案无法满足等保2.0、商用密码测评、国产化信创项目的强制要求。

传统方案大多使用 RSA 加密、MD5 签名,存在如下短板:

  • RSA:非国产算法、加密效率低、密钥冗长,不符合政企国产化合规要求;
  • MD5:哈希算法已被破解,安全性极低,无法抵御碰撞篡改;
  • 单一加密模式:仅能实现基础加密,无法同时覆盖精准字段加密、大批量整包加密、防篡改、防重放多重场景。

1.2 国密算法优势 & 官方合规依据

本文采用国家商用密码标准(GM/T)三大核心算法,组成企业级安全黄金组合,全方位覆盖接口安全场景,补齐单一加密方案短板:

  • SM2(非对称加密):替代RSA,椭圆曲线加密,密钥更短、安全性更高、国产合规,用于加密手机号、密码等核心敏感字段,保障密钥安全;
  • SM3(哈希摘要):替代MD5/SHA,不可逆哈希算法,用于接口参数签名,彻底杜绝参数篡改风险;
  • SM4(对称加密):国产主流对称加密算法,加密解密效率极高、适配高并发场景,专门弥补SM2非对称加密大批量数据性能短板,用于表单全量数据、长文本、批量报文整包加密传输。

官方标准依据(密标委正式发布英文标准,合规性可溯源):

1.3 整体实现方案与安全架构

本文实现六层企业级接口安全防护架构,新增SM4整包加密能力,实现「精准字段加密+全量报文加密」双模式自适应,层层拦截风险请求,兼顾安全性、合规性与可用性:

  1. 防数据泄露(SM2+SM4双模式):SM2加密单个核心敏感字段,SM4加密大批量业务数据、整包报文,双重保障不同场景数据传输安全;
  2. 防参数篡改(SM3哈希签名):全参字典排序+固定密钥加盐哈希,后端严格校验签名一致性;
  3. 防重放攻击(时间戳+随机串):13位时间戳5分钟过期校验 + Redis存储nonce随机串唯一校验,杜绝重复请求;
  4. 身份认证(AppId+Token):应用唯一标识+用户登录令牌,拦截非法访问、伪造请求;
  5. 性能优化适配:SM2适配少量敏感数据、SM4适配大批量数据,兼顾极致安全与接口高并发性能;
  6. 无侵入业务校验:基于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签名原理

为防止参数篡改,采用 参数字典排序+固定密钥加盐哈希 核心机制,保证前后端签名规则绝对统一:

  1. 所有请求参数按Key字典升序排序,规避参数顺序不同导致签名不一致问题;
  2. 统一拼接 key=value 格式字符串,标准化签名源串;
  3. 尾部拼接全局密钥,通过SM3生成不可逆哈希串作为唯一签名;
  4. 后端重复计算签名并对比,不一致则直接判定参数被篡改,拦截请求。

4.3 SM4加密原理

SM4为国产对称加密算法,是本文方案性能核心补充,加密解密密钥一致、运算效率极高,完美解决SM2非对称加密大批量数据效率极低的痛点,完全契合GM/T 0002-2012官方标准:

  • 加密规则:加解密使用同一32位HEX密钥,运算逻辑简单、速度快、资源消耗低;
  • 适用场景:大批量业务表单、长文本参数、批量数据上报、文件流摘要加密等场景;
  • 核心优势:加密速度远超SM2非对称加密,高并发场景下无性能瓶颈,兼顾国产化合规与接口性能;
  • 使用规范:前后端统一使用ECB加密模式、32位标准HEX密钥,保证加解密双向互通、无兼容问题;
  • 安全定位:整包报文全覆盖加密,请求无任何明文泄露,适配高密级、高安全要求接口。

4.4 双加密模式适配规则

本文自适应双加密模式,根据接口场景自动切换,无需手动修改代码:

  1. 普通轻量接口(查询、登录、个人信息修改):SM2字段精准加密 + SM3签名,仅加密密码、手机号等敏感字段,性能最优;
  2. 高密级/大批量接口(批量上报、表单全量提交、数据同步):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 核心业务执行。

完整请求执行流程如下:

  1. 前端完成数据加密、SM3签名、拼接防重放与身份参数后,携带密文、签名、nonce随机串、13位时间戳、AppId、Token发起接口请求;
  2. 请求抵达后端接口,触发AOP切面拦截机制,优先执行Order优先级最高的安全校验切面;
  3. 第一层安全校验依次执行:公共参数非空校验、AppId客户端身份合法性校验、Redis随机串nonce防重放校验、13位时间戳超时校验、SM3签名一致性校验,任意环节校验失败直接拦截请求并返回异常;
  4. 所有安全校验全部通过后,进入解密切面,自动完成密码、手机号等SM2敏感字段解密,若是SM4整包加密请求,同步完成全量报文解密;
  5. 接口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开头,前后端格式不统一导致兼容报错。

解决方案:前后端双向兜底适配,彻底杜绝格式兼容问题,适配所有环境:

  1. 前端:SM2加密完成后,手动拼接04非压缩公钥头,统一密文格式;
  2. 后端:解密工具类增加格式判断,自动为无04头的密文兜底补全,双向兼容。

8.3. SM3签名持续校验失败

问题原因:前后端签名字符串拼接规则不统一,核心原因包含三点:

a. 参数遍历拼接后末尾存在多余&符号;

b. 请求参数排序规则不一致;

c. 前端null/undefined与后端null空值序列化格式差异,导致最终签名源串不一致。

解决方案:统一全局签名规则,彻底解决签名不一致问题:

  1. 两端均采用字典升序排序规则,规避参数顺序差异问题;
  2. 优化字符串拼接逻辑,自动剔除末尾多余的&分隔符;
  3. 统一空值处理规则,将所有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-2012GM/T 0003-2012GM/T 0004-2012三大官方国密标准体系,基于SpringBoot2.x + Vue2成熟技术栈,落地了一套标准化、国产化、高安全的企业级接口安全防护方案。完美适配等级保护2.0、商用密码安全性评估、国产化信创三大合规体系要求,彻底替代传统RSA、MD5非国产、低安全的加密校验方案。

方案构建了「精准字段加密+整包报文加密+参数防篡改+请求防重放+双向身份认证」的六层闭环安全防护体系,各司其职、互补适配:依托SM2非对称加密实现密码、手机号等核心敏感数据的精准加密传输,杜绝数据泄露;通过SM3哈希摘要算法配合标准化签名规则,彻底解决接口参数篡改风险;借助SM4对称加密的高性能优势,弥补非对称加密大批量数据传输的性能短板,适配高并发、大数据量业务场景。

在安全防护之外,方案基于Spring AOP切面实现所有安全逻辑解耦,做到业务代码零侵入、零改动,极大降低了项目改造与维护成本。同时通过统一前后端算法规则、适配空值与格式差异、规避版本冲突,解决了国密落地过程中的各类兼容问题,让方案具备极强的稳定性与可落地性。

整体方案兼顾国产化合规、数据安全、接口性能、业务解耦、落地便捷性五大核心优势,为前后端分离架构的接口通信安全提供标准化的国产化解决方案。