【Vue2+SpringBoot+SM2】Vue2 + Spring Boot 实现 SM2 双向非对称加密完整实战

------ 国密算法在前后端真实项目中的落地实践

SM2 作为中国自主可控的椭圆曲线公钥密码算法,已逐步取代 RSA 成为标配。本文将手把手带你实现一套 Vue2 + Spring Boot 项目中真正的「双向」SM2 非对称加密通信机制,做到:

  1. 前端用公钥加密 → 后端私钥解密(保护敏感数据传输)
  2. 后端用私钥签名 → 前端公钥验签(防止响应数据被篡改)
  3. 真正可落地的完整代码(含密钥生成、Hex/C1C3C2 格式处理、异常处理)

一、SM2 算法与常见坑点回顾

项目 说明
密钥格式 SM2 私钥一般 32 字节,公钥未压缩格式 65 字节(04 开头)
常见编码格式 C1C3C2(国密标准,默认)、C1C2C3(部分 js 库使用)
ASN.1 包装 Java 默认使用 PKCS#8 私钥、X509 公钥,需要转原始 04+64 字节
随机数 k 每次加密/签名必须不同,否则泄露私钥

二、后端 Spring Boot 实现(Java 17 + Bouncy Castle 1.78+)

1. 依赖

xml 复制代码
<dependencies>
    <dependency>
        <groupId>org.bouncycastle</groupId>
        <artifactId>bcprov-jdk18on</artifactId>
        <version>1.78.1</version>
    </dependency>
</dependencies>

2. SM2 工具类(完整生产可用)

java 复制代码
@Component
public class Sm2Util {

    private static final String PUB_KEY_HEX = "04a3e2f1b1c5d7...你的公钥04开头130位hex...";
    private static final String PRI_KEY_HEX = "1a2b3c4d5e6f...你的私钥64位hex...";

    private static final ECPublicKey PUBLIC_KEY;
    private static final ECPrivateKey PRIVATE_KEY;

    static {
        Security.addProvider(new BouncyCastleProvider());
        PUBLIC_KEY = loadPublicKey(PUB_KEY_HEX);
        PRIVATE_KEY = loadPrivateKey(PRI_KEY_HEX);
    }

    /** 公钥加密(返回 C1C3C2 格式 Hex) */
    public static String encrypt(String plainText) {
        try {
            SM2Engine engine = new SM2Engine();
            engine.init(true, new ParametersWithRandom(PUBLIC_KEY, SecureRandom.getInstanceStrong()));
            byte[] data = plainText.getBytes(StandardCharsets.UTF_8);
            byte[] c1c3c2 = engine.processBlock(data, 0, data.length);
            return Hex.toHexString(c1c3c2);
        } catch (Exception e) {
            throw new RuntimeException("SM2 encrypt failed", e);
        }
    }

    /** 私钥解密(接收 C1C3C2 Hex) */
    public static String decrypt(String cipherHex) {
        try {
            SM2Engine engine = new SM2Engine();
            engine.init(false, PRIVATE_KEY);
            byte[] c1c3c2 = Hex.decode(cipherHex);
            byte[] plain = engine.processBlock(c1c3c2, 0, c1c3c2.length);
            return new String(plain, StandardCharsets.UTF_8);
        } catch (Exception e) {
            throw new RuntimeException("SM2 decrypt failed", e);
        }
    }

    /** 私钥签名(返回 SM2 标准 r||s 64字节 Hex) */
    public static String sign(String data) {
        try {
            SM2Signer signer = new SM2Signer();
            signer.init(true, new ParametersWithRandom(PRIVATE_KEY, SecureRandom.getInstanceStrong()));
            signer.update(data.getBytes(StandardCharsets.UTF_8), 0, data.length());
            byte[] signature = signer.generateSignature(); // r||s 64字节
            return Hex.toHexString(signature);
        } catch (Exception e) {
            throw new RuntimeException("SM2 sign failed", e);
        }
    }

    /** 公钥验签 */
    public static boolean verify(String data, String signatureHex) {
        try {
            SM2Signer signer = new SM2Signer();
            signer.init(false, PUBLIC_KEY);
            signer.update(data.getBytes(StandardCharsets.UTF_8), 0, data.length());
            byte[] signature = Hex.decode(signatureHex);
            return signer.verifySignature(signature);
        } catch (Exception e) {
            return false;
        }
    }

    // 加载原始 Hex 公私钥
    private static ECPublicKey loadPublicKey(String hex) {
        byte[] pubBytes = Hex.decode(hex);
        ECNamedCurveParameterSpec spec = ECNamedCurveTable.getParameterSpec("sm2p256v1");
        ECPoint q = spec.getCurve().decodePoint(pubBytes);
        return new ECPublicKeyImpl(q, getDomainParameters(spec));
    }

    private static ECPrivateKey loadPrivateKey(String hex) {
        BigInteger d = new BigInteger(1, Hex.decode(hex));
        ECNamedCurveParameterSpec spec = ECNamedCurveTable.getParameterSpec("sm2p256v1");
        return new ECPrivateKeyImpl(d, getDomainParameters(spec));
    }

    private static ECDomainParameters getDomainParameters(ECNamedCurveParameterSpec spec) {
        return new ECDomainParameters(spec.getCurve(), spec.getG(), spec.getN(), spec.getH());
    }
}

3. Controller 示例

java 复制代码
@RestController
@RequestMapping("/api/user")
public class UserController {

    @PostMapping("/login")
    public Result<LoginVO> login(@RequestBody LoginDTO dto) {
        // 1. 前端传过来的密码是 SM2 加密后的 Hex
        String encryptedPwd = dto.getPassword();
        String plainPwd = Sm2Util.decrypt(encryptedPwd);

        // 2. 业务校验(这里简化)
        if (!"123456".equals(plainPwd)) {
            throw new BizException("密码错误");
        }

        // 3. 组装返回数据并签名
        LoginVO vo = new LoginVO();
        vo.setToken(JwtUtil.generate(userId));
        vo.setUserId(userId);

        String dataToSign = vo.getToken() + vo.getUserId(); // 建议签名业务关键字段
        String signature = Sm2Util.sign(dataToSign);
        vo.setSign(signature);

        return Result.success(vo);
    }
}

三、前端 Vue2 实现(gm-crypto-js + vue-cli)

1. 安装依赖

bash 复制代码
npm install gm-crypto-js
# 或 yarn add gm-crypto-js

2. 封装 sm2.js

javascript 复制代码
// src/utils/sm2.js
import { sm2 } from 'gm-crypto-js'

// 必须与后端一致:04 + 64字节未压缩公钥
const PUBLIC_KEY = '04a3e2f1b1c5d7...同后端公钥...'

const sm2Encrypt = (plainText) => {
  return sm2.encrypt(plainText, PUBLIC_KEY, {
    inputEncoding: 'utf8',
    outputEncoding: 'hex',
    // 关键:gm-crypto-js 默认 C1C2C3,这里要转成 C1C3C2 与 Java 一致
    asn1: false
  })
}

// gm-crypto-js 加密出来是 C1C2C3,需要转换为 C1C3C2
const toC1C3C2 = (c1c2c3Hex) => {
  const hex = c1c2c3Hex.toUpperCase()
  const c1 = hex.substr(0, 66)        // 04 + 32*2
  const c2 = hex.substr(-64)          // 最后32字节
  const c3 = hex.substr(66, 64)       // 中间32字节
  return (c1 + c3 + c2).toLowerCase()
}

export const encryptPassword = (pwd) => {
  const c1c2c3 = sm2Encrypt(pwd)
  return toC1C3C2(c1c2c3)
}

// 验签
export const verifySign = (data, signatureHex) => {
  return sm2.verify(data, signatureHex, PUBLIC_KEY, {
    inputEncoding: 'utf8'
  })
}

3. 登录页面使用

vue 复制代码
<script>
import { encryptPassword, verifySign } from '@/utils/sm2'
import axios from 'axios'

export default {
  data() {
    return {
      form: { username: '', password: '' }
    }
  },
  methods: {
    async login() {
      const encryptedPwd = encryptPassword(this.form.password)
      
      const res = await axios.post('/api/user/login', {
        username: this.form.username,
        password: encryptedPwd   // 明文绝不上网
      })

      const { token, userId, sign } = res.data.data
      
      // 验签防止伪造响应
      const dataToVerify = token + userId
      const ok = verifySign(dataToVerify, sign)
      if (!ok) {
        this.$message.error('响应数据异常,请重试')
        return
      }

      this.$store.commit('SET_TOKEN', token)
      this.$router.push('/dashboard')
    }
  }
}
</script>

四、密钥生成(只执行一次)

java 复制代码
// 生成一对 SM2 密钥(Java)
public static void generateKeyPair() {
    ECNamedCurveParameterSpec spec = ECNamedCurveTable.getParameterSpec("sm2p256v1");
    ECKeyPairGenerator generator = new ECKeyPairGenerator();
    generator.init(new ECKeyGenerationParameters(new ECDomainParameters(spec.getCurve(), spec.getG(), spec.getN()), new SecureRandom()));
    AsymmetricKeyParameter keyPair = generator.generateKeyPair();

    ECPublicKey pub = (ECPublicKey) keyPair.getPublic();
    ECPrivateKey pri = (ECPrivateKey) keyPair.getPrivate();

    System.out.println("公钥: " + Hex.toHexString(pub.getQ().getEncoded(false))); // 04...
    System.out.println("私钥: " + pri.getD().toString(16));
}

五、生产注意事项

  1. 公钥硬编码到前端,私钥必须放在服务器环境变量或配置中心
  2. 每次加密必须使用不同随机数 k(BouncyCastle 已自动处理)
  3. 建议对敏感接口全部启用双向 SM2(登录、支付、修改密码等)
  4. C1C3C2 与 C1C2C3 转换是最大坑点,已在上面代码解决
  5. 签名建议只签业务关键字段,防止重放攻击仍需配合 timestamp + nonce + JWT
相关推荐
北郭guo1 小时前
MyBatis框架讲解,工作原理、核心内容、如何实现【从浅入深】让你看完这篇文档对于MyBatis的理解更加深入
java·数据库·mybatis
Predestination王瀞潞1 小时前
Java EE开发技术(第七章:JSTL标签库)
java·java-ee
信仰_2739932431 小时前
Java面试题
java·开发语言
A***F1571 小时前
使用 Spring Boot 实现图片上传
spring boot·后端·状态模式
间彧1 小时前
分享一些ServBay和Docker混合使用的最佳实践?
后端
间彧1 小时前
一个典型的SpringBoot Web项目在ServBay和Docker中分别的完整开发部署流程
后端
间彧2 小时前
ServBay如何与IDE(如IntelliJ IDEA)深度集成,实现一键调试和热部署?
后端
万少2 小时前
流碧卡片 6 小时闪电开发 AI gemini-3-pro-preview ! 秒出小红书爆款图,免下载直接用
前端·后端·ai编程
间彧2 小时前
ServBay与Docker在具体使用场景和性能表现上有哪些详细对比?
后端