SpringBoot+vue集成sm2国密加密解密

前言

博主介绍:✌目前全网粉丝3W+,csdn博客专家、Java领域优质创作者,博客之星、阿里云平台优质作者、专注于Java后端技术领域。

涵盖技术内容:Java后端、大数据、算法、分布式微服务、中间件、前端、运维等。

博主所有博客文件目录索引:博客目录索引(持续更新)

视频平台:b站-Coder长路

本章节相关

本章配套代码:Gitee仓库/demo-exer

  • 说明:前端vue工具类和库在resources目录下。

配套视频讲解(b站):https://www.bilibili.com/video/BV12Qt8eoEbS

本章节实现思路:后端基于Hutool开源工具提供的SmUtil来完成国密加解密,前端使用sm-crypto来实现加解密。

后端:

前端:


认识SM2

认识

SM2是国家密码管理局于2010年12月17日发布的椭圆曲线公钥密码算法。

SM2算法和RSA算法都是公钥密码算法,SM2算法是一种更先进安全的算法,在我们国家商用密码体系中被用来替换RSA算法。

随着密码技术和计算机技术的发展,目前常用的1024位RSA算法面临严重的安全威胁,我们国家密码管理部门经过研究,决定采用SM2椭圆曲线算法替换RSA算法。

对比RSA

SM2性能更优更安全:密码复杂度高、处理速度快、机器性能消耗更小
详细参考: https://www.ecaa.org.cn/667.html

SM2RSA
算法结构基本椭圆曲线(ECC)基于特殊的可逆模幂运算
计算复杂度完全指数级亚指数级
存储空间192-256bit2048-4096bit
秘钥生成速度较RSA算法快百倍以上
解密加密速度较快一般
加密长度限制117

后端工具类实现

引入依赖

<!--    引入Hutool依赖    -->
<dependency>
    <groupId>cn.hutool</groupId>
    <artifactId>hutool-crypto</artifactId>
    <version>5.8.20</version>
</dependency>
<!--    引入Bouncy Castle依赖    -->
<dependency>
    <groupId>org.bouncycastle</groupId>
    <artifactId>bcpkix-jdk18on</artifactId>
    <version>1.78.1</version>
</dependency>

代码实现

工具类:SM2Util

image-20240916214852887.

package com.changlu.springboot.sm.util;
import cn.hutool.crypto.SecureUtil;
import cn.hutool.crypto.asymmetric.KeyType;
import cn.hutool.crypto.asymmetric.SM2;
import org.bouncycastle.crypto.engines.SM2Engine;
import org.springframework.util.StringUtils;

import java.security.KeyPair;


public class SM2Util {

    public static KeyPair generateKeyPair() {
        return SecureUtil.generateKeyPair("SM2");
    }

    /**
     * 加密
     * @param str
     * @return
     */
    public static String encrypt(String str, String privateKey, String publicKey) {
        try {
            if (checkKeyIsEmpty(privateKey, publicKey)) {
                SM2 sm2 = new SM2(privateKey, publicKey);
                sm2.setMode(SM2Engine.Mode.C1C3C2);
                return sm2.encryptHex(str, KeyType.PublicKey);
            }
            return str;
        } catch (Exception e) {
            throw new RuntimeException("sm2加密失败" + e);
        }
    }

    /**
     * 解密
     * @param str 加密之后的字符串
     * @param privateKey 私钥
     * @param publicKey 公钥
     * @return 原文密码
     */
    public static String decrypt(String str, String privateKey, String publicKey) {
        try {
            if (checkKeyIsEmpty(privateKey, publicKey)) {
                SM2 sm2 = new SM2(privateKey, publicKey);
                sm2.setMode(SM2Engine.Mode.C1C3C2);
                return sm2.decryptStr(str, KeyType.PrivateKey);
            }
            return str;
        } catch (Exception e) {
            throw new RuntimeException("sm2解密失败" + e);
        }
    }

    private static boolean checkKeyIsEmpty(String privateKey, String publicKey) {
        if (StringUtils.isEmpty(privateKey) || StringUtils.isEmpty(publicKey)) {
            return false;
        }
        return true;
    }

}

单元测试

image-20240916214837775

案例1:生成服务端公钥、私钥,前端js公钥、私钥

package com.changlu.springboot.sm;
import cn.hutool.core.util.HexUtil;
import cn.hutool.core.util.StrUtil;
import cn.hutool.crypto.BCUtil;
import cn.hutool.crypto.SecureUtil;
import cn.hutool.crypto.SmUtil;
import cn.hutool.crypto.asymmetric.KeyType;
import cn.hutool.crypto.asymmetric.SM2;
import com.changlu.springboot.sm.util.SM2Util;
import org.bouncycastle.jcajce.provider.asymmetric.ec.BCECPublicKey;
import org.junit.jupiter.api.Test;

import java.security.KeyPair;

public class SMUtilTest {

    // 案例1:生成服务端公钥、私钥,前端js公钥、私钥
    @Test
    public void testDiySM() {
        String text = "我是一段测试aaaa";
        // 生成密钥对
        KeyPair keyPair = SM2Util.generateKeyPair();
        // 服务器端使用
        // 生成私钥
        String privateKey = HexUtil.encodeHexStr(keyPair.getPrivate().getEncoded());
        // 生成公钥
        String publicKey = HexUtil.encodeHexStr(keyPair.getPublic().getEncoded());
        System.out.println("privateKey=>" + privateKey);
        System.out.println("publicKey=>" + publicKey);

        // 前端使用
        // 生成公钥 Q,以Q值做为js端的加密公钥
        String publicKeyQ = HexUtil.encodeHexStr(((BCECPublicKey) keyPair.getPublic()).getQ().getEncoded(false));
        System.out.println("公钥Q:"+ publicKeyQ);
        // 生成私钥 D,以D值做为js端的解密私钥
        String privateKeyD = HexUtil.encodeHexStr(BCUtil.encodeECPrivateKey(keyPair.getPrivate()));
        System.out.println("私钥D:"+ privateKeyD);

        // 服务端加解密
        String encodeStr = SM2Util.encrypt(text, privateKey, publicKey);
        String formatStr = SM2Util.decrypt(encodeStr, privateKey, publicKey);
        System.out.println("encodeStr=>" + encodeStr);
        System.out.println("formatStr=>" + formatStr);
    }

}

效果:该单元测试得到的服务端公钥、私钥,前端js公钥、私钥,可用于服务器端、前端使用。

image-20240916214342674


案例2:客户端加密,服务端完成解密

场景:前端客户端使用前端公钥加密后(可用vue中加密函数生成的加密内容),我们在服务器端进行解密。

package com.changlu.springboot.sm;
import cn.hutool.core.util.HexUtil;
import cn.hutool.core.util.StrUtil;
import cn.hutool.crypto.BCUtil;
import cn.hutool.crypto.SecureUtil;
import cn.hutool.crypto.SmUtil;
import cn.hutool.crypto.asymmetric.KeyType;
import cn.hutool.crypto.asymmetric.SM2;
import com.changlu.springboot.sm.util.SM2Util;
import org.bouncycastle.jcajce.provider.asymmetric.ec.BCECPublicKey;
import org.junit.jupiter.api.Test;

import java.security.KeyPair;

public class SMUtilTest {

    // 生成的一组私钥、公钥进行测试
    private String privateKey = "308193020100301306072a8648ce3d020106082a811ccf5501822d047930770201010420057ab3e1e512e970023c16c545289ecf37dd2cb202daa24c42936f21daa061aca00a06082a811ccf5501822da14403420004981070e26f624917f2717bcaadc000c928c91b49c9c218df33260cafa1d2243c2427fd3486884a67d390751ff4956e35466fb4b925a666229b22d36c26267d67";
    private String publicKey = "3059301306072a8648ce3d020106082a811ccf5501822d03420004981070e26f624917f2717bcaadc000c928c91b49c9c218df33260cafa1d2243c2427fd3486884a67d390751ff4956e35466fb4b925a666229b22d36c26267d67";
    private String publicKeyQ = "04981070e26f624917f2717bcaadc000c928c91b49c9c218df33260cafa1d2243c2427fd3486884a67d390751ff4956e35466fb4b925a666229b22d36c26267d67";
    private String privateKeyD = "057ab3e1e512e970023c16c545289ecf37dd2cb202daa24c42936f21daa061ac";

    // 案例2:客户端加密,服务端完成解密
    @Test
    public void testDecrypt() {
        String encodeStr = "04badafbddce5f728fb11c2007f2230b2fcd0ecf019ac4536370c75dc2e222ca696d20033ab8f76965bd1a9e2691b7a6e4e62d71627874cedd6138453444e1868881e69dbcd3ca13818d6db061561fb87da14e061d9d1c82d550322b2e04c60bcca7998ac51059";
        String formatStr = SM2Util.decrypt(encodeStr, privateKey, publicKey);
        System.out.println("formatStr=>" + formatStr);
    }
}

image-20240916214659774


案例3:服务端进行加密(可用于后面前端测试解密操作)

package com.changlu.springboot.sm;
import cn.hutool.core.util.HexUtil;
import cn.hutool.core.util.StrUtil;
import cn.hutool.crypto.BCUtil;
import cn.hutool.crypto.SecureUtil;
import cn.hutool.crypto.SmUtil;
import cn.hutool.crypto.asymmetric.KeyType;
import cn.hutool.crypto.asymmetric.SM2;
import com.changlu.springboot.sm.util.SM2Util;
import org.bouncycastle.jcajce.provider.asymmetric.ec.BCECPublicKey;
import org.junit.jupiter.api.Test;
import java.security.KeyPair;

public class SMUtilTest {

    // 生成的一组私钥、公钥进行测试
    private String privateKey = "308193020100301306072a8648ce3d020106082a811ccf5501822d047930770201010420057ab3e1e512e970023c16c545289ecf37dd2cb202daa24c42936f21daa061aca00a06082a811ccf5501822da14403420004981070e26f624917f2717bcaadc000c928c91b49c9c218df33260cafa1d2243c2427fd3486884a67d390751ff4956e35466fb4b925a666229b22d36c26267d67";
    private String publicKey = "3059301306072a8648ce3d020106082a811ccf5501822d03420004981070e26f624917f2717bcaadc000c928c91b49c9c218df33260cafa1d2243c2427fd3486884a67d390751ff4956e35466fb4b925a666229b22d36c26267d67";
    private String publicKeyQ = "04981070e26f624917f2717bcaadc000c928c91b49c9c218df33260cafa1d2243c2427fd3486884a67d390751ff4956e35466fb4b925a666229b22d36c26267d67";
    private String privateKeyD = "057ab3e1e512e970023c16c545289ecf37dd2cb202daa24c42936f21daa061ac";

    // 案例3:服务端进行加密
    @Test
    public void testEncrypt() {
        String str = "changlu test test";
        String encodeStr = SM2Util.encrypt(str, privateKey, publicKey);
        System.out.println("encodeStr=>" + encodeStr);
    }

}

image-20240916214800810


前端vue2实现

工具类构建:sm2.js

导入依赖:

cnpm i sm-crypto --save

工具类utils目录中创建sm2.js:

image-20240916214949913

import { sm2 } from 'sm-crypto';

// 公钥
const PUBLIC_KEY = '04981070e26f624917f2717bcaadc000c928c91b49c9c218df33260cafa1d2243c2427fd3486884a67d390751ff4956e35466fb4b925a666229b22d36c26267d67'
const PRIVATE_KEY = '057ab3e1e512e970023c16c545289ecf37dd2cb202daa24c42936f21daa061ac'

// 可配置参数
// 1 - C1C3C2;	0 - C1C2C3;  默认为1
const cipherMode = 1

//加密
export function doSM2Encrypt(str) {
    let msg = str
    if (typeof str !== 'string') {
      msg = JSON.stringify(str)
    }
    // console.log(msg,'加密前')
    let publicKey = PUBLIC_KEY
    // 加密结果
    let encryptData = sm2.doEncrypt(msg, publicKey, cipherMode)
    //Base64编码 自行选择是否使用
    //let baseEncode = Base64.encode(encryptData)
    // 加密后的密文前需要添加04,后端才能正常解密 (不添加04,后端处理也可以)
    let encrypt = '04' + encryptData
    return encrypt
}

// 解密
export function doSM2DecryptStr(enStr) {
    let msg = enStr
    if (typeof enStr !== 'string') {
      msg = JSON.stringify(enStr)
    }
    let privateKey = PRIVATE_KEY
    let enval = enStr.substring(2)
    // 解密结果
    let doDecrypt = sm2.doDecrypt(enval , privateKey, cipherMode)
    console.log("doDecrypt=>", doDecrypt)
    // 解密后类型转换
    return doDecrypt;
}

页面vue测试

任意找一个vue页面来进行测试。

首先引入工具类:

import { doSM2Encrypt, doSM2DecryptStr } from '@/utils/sm2'

编写测试函数:

export default {
    created() {
      //测试编码
      this.testEncode()
    },
    methods: {
      testEncode() {
        // 案例1:前端进行加密
        let str = "123456"
        let encodeStr = doSM2Encrypt(str)
        console.log("前端明文加密之后=>", encodeStr)
        // 案例2:前端自己加密之后的内容进行解密
        let originStr = doSM2DecryptStr(encodeStr)
        console.log("前端自行加密,解密之后=>", originStr)
        // 案例3:服务器端内容加密后进行解密
        let testEncodeStr = '04c719fa9dff41a22b8119a3f1ba984303d19c295f1f6a6b8196c7a330cf0e9e1830cbd3cd949c49be0681fd2fa9abca3ed14f8f8d4111c552ef7603793c0a2ae344e9072b7dcaefaf5785634a624d4ca7addcf4ab9ff37abe4ec69847ee5e24a65ce74e8a5b1aecdc467d18b36fba22a8e8'
        originStr = doSM2DecryptStr(testEncodeStr)
        console.log("服务器端进行加密,解密之后=>", originStr)
      },
}

image-20240916220322600


常见报错

1、前端加密出现:TypeError: Cannot read properties of null (reading ‘multiply

出现异常:

image-20240916172117381

解决方式:

错误:一开始直接将服务端生成的公钥作为前端的公钥,该公钥的前缀没有04,此时就会报错。

image-20240916181159848

3059301306072a8648ce3d020106082a811ccf5501822d03420004981070e26f624917f2717bcaadc000c928c91b49c9c218df33260cafa1d2243c2427fd3486884a67d390751ff4956e35466fb4b925a666229b22d36c26267d67

正确方式:

image-20240916181217277

此时得到的公钥为:

04981070e26f624917f2717bcaadc000c928c91b49c9c218df33260cafa1d2243c2427fd3486884a67d390751ff4956e35466fb4b925a666229b22d36c26267d67

2、后端解密出现:InvalidCipherTextException: invalid cipher text

问题描述:

image-20240916171945532

解决方案:

和问题1一致,一开始前端使用的公钥并不是D值作为js端的解密私钥,此时生成出来的自然在解密端无法解除。


参考文章

[1]. 使用sm2出现报错 “TypeError: Cannot read properties of null (reading ‘multiply‘)”:https://blog.csdn.net/ciwei0605/article/details/125844154

[2]. BC库实现SM2解密时InvalidCipherTextException:https://blog.csdn.net/weixin_43504369/article/details/132739118

[3]. 从零玩转前后端加解密之SM2-sm2:https://www.cnblogs.com/yby6/p/17414766.html

[4]. 适用于前后端的SM2国密加密解密:https://blog.csdn.net/weixin_53021967/article/details/131594733

评论 1
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包

打赏作者

长路 ㅤ   

你的鼓励将是我创作的最大动力

¥1 ¥2 ¥4 ¥6 ¥10 ¥20
扫码支付:¥1
获取中
扫码支付

您的余额不足,请更换扫码支付或充值

打赏作者

实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值