1. 项目概述与核心价值

最近在做一个前后端分离的项目,前端用的是Vue,后端是Spring Boot,客户对数据安全这块提了硬性要求,点名要用国密算法。说实话,一开始听到“国密SM2”这几个字,我和团队里不少小伙伴都有点懵,毕竟平时RSA、AES用惯了,对国密这套体系接触不多。但需求就是命令,硬着头皮也得啃下来。折腾了小半个月,从查文档、找库、联调测试到最终上线,踩了不少坑,也积累了一些实战心得。今天就把Vue前端配合Spring Boot后端,完整实现国密SM2非对称加密通信的这套方案梳理出来,希望能给遇到同样需求的同行们铺个路。

简单来说,SM2是国家密码管理局发布的椭圆曲线公钥密码算法标准,属于非对称加密,对标的是国际上的RSA和ECC。它在相同安全强度下,密钥长度更短(256位就相当于RSA 2048位的强度),运算速度更快,而且是我们自己的标准,在金融、政务这些对安全自主可控要求高的场景里是硬性要求。我们这个项目就是典型的B/S架构,前端Vue收集用户敏感数据(比如身份证号、银行卡号),需要先加密再传给后端Spring Boot,后端解密后处理。整个过程要保证数据在传输过程中的机密性,防止被窃听。

2. 技术选型与前期准备

2.1 为什么选择SM2而非RSA?

客户要求是一方面,技术上的优势也是我们最终决定投入时间研究SM2的原因。除了前面提到的密钥短、效率高,SM2在签名算法上也有改进,安全性更有保障。RSA算法这些年被挑战的比较多,而基于椭圆曲线的SM2在理论上更抗攻击。当然,最大的推动力还是合规性。在很多行业项目中,使用国密算法不是“加分项”,而是“入场券”。所以,如果你的项目涉及敏感数据且面向国内市场,提前布局国密是很有必要的。

2.2 前端加密库选型: sm-crypto

前端JavaScript环境里实现SM2,经过一番调研,社区里比较成熟、star数高的库是 sm-crypto 。它纯JavaScript实现,不依赖任何原生模块,在浏览器和Node.js环境都能跑,对Vue这种现代前端框架集成起来非常方便。它的API设计得也比较清晰,加密、解密、签名、验签功能都齐全。我们不需要从底层理解椭圆曲线的数学原理,直接调用封装好的方法就行,这对前端开发来说大大降低了门槛。

注意 :市面上也有一些其他带“国密”字样的JS库,但有些可能未经充分审计或已停止维护。选择 sm-crypto 主要是看中其活跃的社区和相对完整的文档,在Github上能查到 issues 和更新记录,用起来更放心。

2.3 后端加密库选型: Bouncy Castle 国密支持包

后端的Java生态里,JDK标准库默认并不支持国密算法。这时候就需要引入强大的第三方密码学提供者——Bouncy Castle。但是,普通的Bouncy Castle(BC)发行版对国密算法的支持是有限的或者不完整的。我们需要一个专门适配了国密算法的BC扩展包。经过对比,我们选择了 org.bouncycastle:bcprov-jdk15to18 这个常用版本,并搭配一个国密补丁包,或者直接使用国内一些厂商维护的、已经集成了国密算法的BC版本。这里有个关键点:前后端使用的椭圆曲线参数必须一致,否则无法互通。国密SM2使用的是固定的曲线参数 sm2p256v1 ,在选型时必须确保后端库使用的也是同一套标准参数。

2.4 环境与依赖确认

在开始敲代码之前,先把环境捋清楚:

  • 前端 (Vue项目) :

    • 项目基于 Vue 2/3 或 Vue CLI 创建均可。
    • 通过 npm 或 yarn 安装 sm-crypto npm install sm-crypto --save
    • 确保你的项目能处理异步操作和Base64编码,这些是现代前端项目的标配。
  • 后端 (Spring Boot项目) :

    • 版本建议 Spring Boot 2.x 或 3.x。
    • pom.xml 中引入 Bouncy Castle 的国密支持依赖。这里以使用一个整合好的依赖为例(具体groupId和artifactId可能需要根据你选择的实际国密BC包调整,以下为示例):
      <dependency>
          <groupId>org.bouncycastle</groupId>
          <artifactId>bcprov-jdk15to18</artifactId>
          <version>1.72</version> <!-- 请使用最新稳定版 -->
      </dependency>
      <!-- 可能需要额外的国密算法包,例如 -->
      <dependency>
          <groupId>cn.hutool</groupId>
          <artifactId>hutool-crypto</artifactId>
          <version>5.8.25</version> <!-- Hutool工具包内置了国密支持,封装得较好 -->
      </dependency>
      
    • 我们最终部分功能选择了Hutool工具包,因为它对国密SM2、SM3、SM4进行了友好封装,API简单,减少了直接操作BC底层API的复杂度。当然,你也可以选择直接使用纯BC API,控制更精细,但代码量会多一些。

3. 核心流程与密钥管理设计

3.1 非对称加密通信流程

我们的场景是典型的“前端加密,后端解密”。流程如下:

  1. 后端启动时,生成一对SM2密钥:公钥(Public Key)和私钥(Private Key)。私钥绝对保密,存放在后端服务器安全位置(如配置文件、环境变量或密钥管理服务), 绝不能 发给前端。公钥则可以安全地暴露给前端。
  2. 前端从后端接口(例如 /api/sm2/public-key )获取到SM2公钥(一般是Base64或16进制字符串格式)。
  3. 前端在提交敏感表单数据时,使用获取到的公钥,通过 sm-crypto 对明文数据(如JSON字符串)进行加密,得到密文。
  4. 前端将密文作为请求体(如 {“encryptedData”: “xxxx...”} )发送给后端对应的API。
  5. 后端接收到密文后,使用保管的私钥进行解密,还原出原始明文数据,再进行后续业务处理。

这个流程保证了即使网络请求被截获,攻击者因为没有私钥也无法解密数据内容。

3.2 密钥的生成、存储与分发

密钥管理是安全的核心,这里详细说一下我们的做法:

  • 生成 :后端使用选定的国密库(如Hutool的 SM2 类)生成密钥对。生成后,将公钥和私钥分别转换为Base64编码的字符串,方便存储和传输。

    // 示例:使用Hutool生成SM2密钥对
    import cn.hutool.crypto.asymmetric.KeyType;
    import cn.hutool.crypto.asymmetric.SM2;
    import java.util.Base64;
    
    SM2 sm2 = new SM2(); // 默认使用国密标准曲线参数
    // 获取私钥和公钥的Base64字符串
    String privateKeyBase64 = sm2.getPrivateKeyBase64();
    String publicKeyBase64 = sm2.getPublicKeyBase64();
    System.out.println("私钥(Base64): " + privateKeyBase64);
    System.out.println("公钥(Base64): " + publicKeyBase64);
    
  • 存储

    • 私钥 :这是命根子。我们坚决不把它写在项目的 application.yml 或代码里。生产环境中,我们将其存储在服务器的环境变量中,或者更专业的做法是使用Hashicorp Vault、阿里云KMS等密钥管理服务。Spring Boot可以通过 @Value(“${sm2.private-key}”) 从环境变量中注入。
    • 公钥 :可以存储在配置文件中,或者每次服务启动时动态生成并缓存。我们采用的是后者,并将公钥通过一个安全的、无需认证的接口暴露给前端。因为公钥本身就是可以公开的,所以这个接口不需要担心泄露问题。
  • 分发 :我们专门写了一个 PublicKeyController ,提供一个 GET /public-key 接口。前端在应用初始化时(比如在Vue的 App.vue created mounted 钩子中),调用这个接口获取公钥字符串,并保存在前端的内存或状态管理(如Vuex、Pinia)中,供整个应用在加密时使用。

实操心得 :密钥千万不能写死在代码里!我们吃过亏,早期测试时图省事,把密钥硬编码在常量类里,结果在代码审计时被严重警告。即使是在测试环境,也要养成从环境变量读取的好习惯。另外,可以考虑定期(如每季度)更换密钥对,但要做好新旧密钥的平滑过渡,避免服务中断。

4. 前端Vue实现细节

4.1 安装、引入与封装工具类

首先,在Vue项目中安装 sm-crypto

npm install sm-crypto --save
# 或
yarn add sm-crypto

我们不建议在每个需要加密的Vue组件里直接 import smCrypto from ‘sm-crypto’ 然后写加密逻辑。更好的做法是封装一个专用的工具模块(如 utils/sm2Encrypt.js ),实现关注点分离,也便于统一维护和更新。

// utils/sm2Encrypt.js
import { sm2 } from 'sm-crypto';

// 这里存储从后端获取的公钥
let publicKey = '';

/**
 * 设置公钥(通常在应用初始化时调用)
 * @param {string} key - Base64格式的公钥字符串
 */
export function setPublicKey(key) {
  // 后端传来的可能是Base64,sm-crypto通常需要16进制字符串
  // 这里假设后端传的是Base64,我们需要转换
  // 注意:实际转换取决于后端返回的格式,需与后端约定一致
  publicKey = `04${Buffer.from(key, 'base64').toString('hex').substring(2)}`;
  // 更常见的做法是后端直接返回16进制公钥(04开头),前端直接使用
  // publicKey = key;
}

/**
 * 使用SM2公钥加密明文
 * @param {string} plainText - 需要加密的原始文本
 * @param {string} cipherMode - 加密模式,默认 'C1C3C2'
 * @returns {string} 加密后的密文(16进制字符串)
 */
export function encrypt(plainText, cipherMode = 'C1C3C2') {
  if (!publicKey) {
    throw new Error('SM2公钥未初始化,请先调用 setPublicKey 方法。');
  }
  // sm2.doEncrypt 默认输出16进制字符串
  const encryptedData = sm2.doEncrypt(plainText, publicKey, cipherMode);
  return encryptedData;
}

/**
 * 注意:前端通常只加密,不解密。解密由后端私钥完成。
 */

4.2 在应用生命周期中获取并设置公钥

在应用入口(如 main.js 或根组件 App.vue ),调用后端接口获取公钥并设置到工具类中。

// App.vue 或专门的初始化脚本中
import { setPublicKey } from '@/utils/sm2Encrypt';
import axios from 'axios'; // 假设使用axios

export default {
  name: 'App',
  created() {
    this.fetchPublicKey();
  },
  methods: {
    async fetchPublicKey() {
      try {
        const response = await axios.get('/api/sm2/public-key');
        // 假设后端返回格式为 { code: 200, data: { publicKey: '...' } }
        if (response.data.code === 200) {
          setPublicKey(response.data.data.publicKey);
          console.log('SM2公钥初始化成功');
        } else {
          console.error('获取公钥失败:', response.data.message);
        }
      } catch (error) {
        console.error('获取公钥接口异常:', error);
        // 这里可以根据业务需求决定是否阻止应用运行
      }
    }
  }
}

4.3 在表单提交时加密数据

现在,在任何一个需要提交敏感数据的组件里,就可以方便地使用加密功能了。

<template>
  <div>
    <form @submit.prevent="handleSubmit">
      <input v-model="formData.idCard" placeholder="请输入身份证号" />
      <input v-model="formData.bankCard" placeholder="请输入银行卡号" />
      <button type="submit">提交</button>
    </form>
  </div>
</template>

<script>
import { encrypt } from '@/utils/sm2Encrypt';
import axios from 'axios';

export default {
  data() {
    return {
      formData: {
        idCard: '',
        bankCard: ''
      }
    };
  },
  methods: {
    async handleSubmit() {
      // 1. 将表单数据转为JSON字符串
      const plainText = JSON.stringify(this.formData);

      // 2. 使用SM2加密
      let encryptedHex;
      try {
        encryptedHex = encrypt(plainText);
        console.log('加密结果(16进制):', encryptedHex);
      } catch (error) {
        console.error('数据加密失败:', error);
        alert('数据加密失败,请检查公钥配置或联系管理员。');
        return;
      }

      // 3. 将密文发送给后端
      try {
        const response = await axios.post('/api/secure/submit', {
          encryptedData: encryptedHex
          // 可以同时传递其他非敏感字段,如业务类型
          // bizType: 'user_register'
        });
        if (response.data.code === 200) {
          alert('提交成功!');
          // 处理成功逻辑...
        } else {
          alert(`提交失败: ${response.data.message}`);
        }
      } catch (error) {
        console.error('请求发送失败:', error);
        alert('网络请求异常,请重试。');
      }
    }
  }
};
</script>

注意事项 sm-crypto doEncrypt 方法默认输出是16进制字符串。有些后端库可能期望接收Base64格式的密文。这里需要前后端对齐,要么前端加密后转Base64再传,要么后端适配16进制解密。我们选择传16进制,因为 sm-crypto 直接输出就是16进制,减少一次转换。 务必与后端同事确认好密文的格式 ,这是联调时最常见的坑。

5. 后端Spring Boot实现细节

5.1 配置国密算法提供者

首先,我们需要在应用启动时,将支持国密的Bouncy Castle提供者注册到Java安全体系中。可以写一个配置类来完成。

import org.bouncycastle.jce.provider.BouncyCastleProvider;
import org.springframework.context.annotation.Configuration;
import javax.annotation.PostConstruct;
import java.security.Security;

@Configuration
public class CryptoConfig {

    @PostConstruct
    public void init() {
        // 防止重复添加
        if (Security.getProvider(BouncyCastleProvider.PROVIDER_NAME) == null) {
            Security.addProvider(new BouncyCastleProvider());
            System.out.println("BouncyCastle Provider (国密支持) 注册成功。");
        }
    }
}

5.2 密钥对管理服务

我们创建一个服务来管理SM2密钥对的生成、获取和私钥的解密操作。

import cn.hutool.core.codec.Base64;
import cn.hutool.crypto.asymmetric.KeyType;
import cn.hutool.crypto.asymmetric.SM2;
import lombok.extern.slf4j.Slf4j;
import org.springframework.beans.factory.annotation.Value;
import org.springframework.stereotype.Service;
import javax.annotation.PostConstruct;
import java.nio.charset.StandardCharsets;

@Service
@Slf4j
public class Sm2Service {

    /**
     * 从环境变量或配置中心读取Base64编码的私钥
     * 生产环境务必使用外部配置!
     */
    @Value("${sm2.private-key:}")
    private String privateKeyBase64Config;

    /**
     * SM2实例,持有密钥对
     */
    private SM2 sm2;

    /**
     * 初始化SM2实例
     * 如果配置了私钥,则使用配置的密钥对;否则生成新的。
     */
    @PostConstruct
    public void init() {
        if (privateKeyBase64Config != null && !privateKeyBase64Config.trim().isEmpty()) {
            try {
                // 使用配置的私钥(和对应的公钥)创建SM2实例
                // 注意:这里需要根据你存储密钥对的方式调整
                // 假设我们存储了完整的密钥对信息,Hutool的SM2可以重建
                // 更常见的做法是存储私钥,公钥可以从私钥推导。这里简化处理。
                // 实际情况可能需要从文件或KMS加载完整的密钥对对象。
                log.info("从配置加载SM2密钥对...");
                // 示例:如果配置的是PEM格式或特定字符串,需要解析
                // 这里假设配置的就是Hutool SM2生成的Base64私钥字符串
                sm2 = new SM2(privateKeyBase64Config, null); // 仅私钥,公钥自动推导
            } catch (Exception e) {
                log.error("加载配置的SM2私钥失败,将生成新密钥对。", e);
                generateNewKeyPair();
            }
        } else {
            log.warn("未配置SM2私钥,将生成新的临时密钥对。生产环境请务必配置!");
            generateNewKeyPair();
        }
        log.info("SM2服务初始化完成。公钥Base64: {}", this.getPublicKeyBase64());
    }

    /**
     * 生成新的SM2密钥对
     */
    private void generateNewKeyPair() {
        this.sm2 = new SM2(); // Hutool默认使用国密标准曲线 sm2p256v1
        // 可以将新生成的密钥对输出到日志(仅限测试环境!)
        log.info("生成新的SM2密钥对 - 私钥: {}, 公钥: {}",
                sm2.getPrivateKeyBase64(),
                sm2.getPublicKeyBase64());
    }

    /**
     * 获取Base64编码的公钥,供前端使用
     * @return 公钥Base64字符串
     */
    public String getPublicKeyBase64() {
        return sm2.getPublicKeyBase64();
    }

    /**
     * 使用私钥解密数据
     * @param encryptedHex 前端传来的16进制格式密文
     * @return 解密后的原始明文
     */
    public String decrypt(String encryptedHex) {
        try {
            // Hutool的SM2解密方法默认支持16进制密文输入
            // 注意:加密模式需与前端一致,默认是 C1C3C2
            byte[] decryptedBytes = sm2.decrypt(encryptedHex, KeyType.PrivateKey);
            return new String(decryptedBytes, StandardCharsets.UTF_8);
        } catch (Exception e) {
            log.error("SM2解密失败,密文: {}", encryptedHex, e);
            throw new RuntimeException("数据解密失败,请检查密文格式或密钥是否正确。", e);
        }
    }
}

5.3 提供公钥的控制器

创建一个简单的REST接口,让前端能获取到公钥。

import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.web.bind.annotation.GetMapping;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.RestController;
import java.util.HashMap;
import java.util.Map;

@RestController
@RequestMapping("/api/sm2")
public class PublicKeyController {

    @Autowired
    private Sm2Service sm2Service;

    @GetMapping("/public-key")
    public Map<String, Object> getPublicKey() {
        Map<String, Object> result = new HashMap<>();
        result.put("code", 200);
        result.put("message", "success");
        Map<String, String> data = new HashMap<>();
        data.put("publicKey", sm2Service.getPublicKeyBase64());
        result.put("data", data);
        return result;
    }
}

5.4 接收加密数据并解密的控制器

最后,创建处理业务请求的控制器,它接收前端发来的密文,解密后再处理业务逻辑。

import com.alibaba.fastjson.JSON;
import com.alibaba.fastjson.JSONObject;
import lombok.extern.slf4j.Slf4j;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.web.bind.annotation.PostMapping;
import org.springframework.web.bind.annotation.RequestBody;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.RestController;
import java.util.Map;

@RestController
@RequestMapping("/api/secure")
@Slf4j
public class SecureDataController {

    @Autowired
    private Sm2Service sm2Service;

    @PostMapping("/submit")
    public Map<String, Object> handleEncryptedData(@RequestBody Map<String, String> requestBody) {
        Map<String, Object> response = new HashMap<>();
        try {
            // 1. 获取密文
            String encryptedHex = requestBody.get("encryptedData");
            if (encryptedHex == null || encryptedHex.isEmpty()) {
                response.put("code", 400);
                response.put("message", "请求参数缺失: encryptedData");
                return response;
            }

            // 2. 使用SM2服务解密
            String decryptedText = sm2Service.decrypt(encryptedHex);
            log.info("解密成功,明文数据: {}", decryptedText); // 生产环境切勿日志记录敏感明文!

            // 3. 将解密后的JSON字符串解析为对象
            JSONObject formData = JSON.parseObject(decryptedText);
            String idCard = formData.getString("idCard");
            String bankCard = formData.getString("bankCard");

            // 4. 此处编写你的业务逻辑,例如数据验证、存储等
            // ...

            // 5. 返回成功响应
            response.put("code", 200);
            response.put("message", "数据处理成功");
            // 可以返回处理后的业务数据(非敏感信息)
            // response.put("data", someResult);

        } catch (RuntimeException e) {
            log.error("处理加密数据时发生解密或业务错误", e);
            response.put("code", 500);
            response.put("message", "服务器内部错误: " + e.getMessage());
        } catch (Exception e) {
            log.error("处理加密数据时发生未知错误", e);
            response.put("code", 500);
            response.put("message", "系统异常");
        }
        return response;
    }
}

6. 联调测试、常见问题与优化

6.1 完整联调测试步骤

  1. 启动后端 :确保Spring Boot应用成功启动,Bouncy Castle提供者注册成功,SM2密钥对生成或加载成功。
  2. 获取公钥 :使用Postman或浏览器访问 GET http://localhost:8080/api/sm2/public-key ,确认能正确返回公钥字符串。记录下这个公钥。
  3. 前端配置 :在Vue项目中,手动或在初始化时代码里,将上一步获取的公钥设置到 sm2Encrypt.js 工具类中。
  4. 前端加密测试 :在浏览器控制台,尝试调用封装好的 encrypt 方法,对一个测试字符串(如 “Hello SM2” )进行加密,观察输出的16进制密文是否为一长串有规律的字符。
  5. 后端解密测试 :将上一步前端加密得到的密文,通过Postman构造一个POST请求,发送到 POST http://localhost:8080/api/secure/submit ,请求体为 {“encryptedData”: “你的密文”} 。观察后端日志,看是否能成功解密出 “Hello SM2” ,并返回成功响应。
  6. 集成测试 :在前端Vue页面真实提交表单,通过浏览器开发者工具的Network面板查看发送的请求,确认请求体中是密文。同时查看后端应用日志,确认解密和业务逻辑执行成功。

6.2 常见问题与排查表

问题现象 可能原因 排查步骤与解决方案
前端加密时报错: Invalid public key 公钥格式不正确。 sm-crypto 需要的公钥是16进制格式,且可能要求包含 04 前缀。 1. 检查从后端获取的公钥字符串。2. 确认前端 setPublicKey 方法中的格式转换逻辑是否正确。3. 最简单的方法:让后端直接返回16进制格式的公钥字符串(以 04 开头),前端无需转换直接使用。
后端解密失败,抛出异常如 Invalid point encoding Unable to process key 1. 前后端使用的椭圆曲线参数不一致。2. 密文格式不符。3. 私钥与加密公钥不匹配。 1. 确认曲线 :确保前后端库都使用国标 sm2p256v1 曲线。2. 确认密文格式 :前端加密模式( C1C3C2 )与后端解密期望的模式必须一致。Hutool默认也是 C1C3C2 。3. 确认密钥配对 :确保后端解密的私钥,就是生成提供给前端那个公钥所对应的私钥。重启服务后密钥对是否变化?4. 检查密文传输 :确保网络传输中密文没有被截断或修改。
加解密过程很慢 加密的数据块太大。SM2作为非对称加密,适合加密小数据(如密钥、敏感字段),不适合加密大文件。 对于大数据,应采用混合加密:1. 前端随机生成一个对称密钥(如AES密钥)。2. 用SM2公钥加密这个对称密钥。3. 用对称密钥加密大数据。4. 将加密后的对称密钥和加密后的大数据一起传给后端。后端先用SM2私钥解出对称密钥,再用对称密钥解密数据。
后端无法加载Bouncy Castle Provider 依赖冲突或版本问题。 1. 检查 pom.xml ,排除其他依赖引入的老版本BC。2. 确认引入的BC版本支持国密算法。3. 在 CryptoConfig init 方法中打印所有已注册的Provider,看BC是否在其中。
生产环境私钥泄露风险 私钥硬编码在代码或配置文件中。 1. 立即移除 代码中的硬编码私钥。2. 将私钥存入服务器环境变量。3. 使用Spring Cloud Config、Apollo等配置中心,并开启加密。4. 强烈建议 使用专业的密钥管理服务(KMS),私钥根本不落地到应用服务器。

6.3 性能与安全优化建议

  1. 公钥缓存 :前端获取公钥后,可以将其存储在 localStorage sessionStorage 中,并设置合理的过期时间,避免每次页面加载都请求接口。后端公钥一般不变,除非密钥轮换。
  2. HTTPS是基础 :SM2保证了数据本身的机密性,但传输过程仍需HTTPS(TLS)来防止中间人攻击、篡改和重放。 绝对不能因为用了SM2加密就省略HTTPS
  3. 数据签名防篡改 (可选但推荐):上述流程只保证了机密性。为了确保数据在传输过程中未被篡改,可以考虑加入SM2签名机制。前端用另一对密钥(或同一对)的私钥对数据(或数据的摘要)签名,后端用公钥验签。这样实现了“加密+签名”,同时满足机密性、完整性和不可否认性。
  4. 密钥轮换 :制定密钥轮换策略,定期更换SM2密钥对。轮换时需要有一个重叠期,新旧公钥同时有效,前端逐步升级,确保服务不间断。
  5. 异常监控与告警 :在后端的解密服务中,对频繁的解密失败请求进行监控和告警,这可能是攻击者在进行盲测或密钥已泄露的迹象。

这套方案从零到一跑通后,你会发现国密集成并没有想象中那么困难。核心在于理解非对称加密的流程,选对经过验证的库,并仔细处理好前后端之间密钥格式、数据格式的约定。最深的体会就是, 联调阶段“对齐”二字值千金 ——公钥格式、加密模式、密文编码,任何一个细节不一致都会导致失败。希望这篇长文能帮你避开我们踩过的那些坑,顺利在Vue+SpringBoot项目中驾驭国密SM2加密。

更多推荐