Heartsuit's Simple Blog

A place to hold mainly reading notes, and some technical stuff occasionally. 这里主要是一些读书笔记、感悟;还有部分技术相关的内容。


Project maintained by heartsuit Hosted on GitHub Pages — Theme by mattgraham

SpringSecurity:前后端分离项目中用户名与密码通过国密算法SM2加密传输

目录[-]

背景

国密算法是我国自主研发创新的一套数据加密处理系列算法,包括SM1, SM2, SM3, SM4, SM7, SM9, 祖冲之密码算法等。从SM1-SM4分别实现了对称、非对称、摘要等算法功能,完成身份认证和数据加解密等功能。我们这里使用的 SM2 非对称加密算法,对标替代的是大名鼎鼎的 RSA 算法。对于使用过 RSA 算法的同学, SM2 的用法基本类似,如果您想了解前后端分离项目中使用 SpringSecurityRSA 算法对用户信息的加密传输,可以参考14-SpringSecurity:前后端分离项目中用户名与密码通过RSA加密传输

这篇文章依然基于一个简单的前后端分离项目,将以前的 RSA 替换为 SM2 即可,除此之外,还记录了使用 OpenSSLSM2 以及 sm-crypto 时遇到的问题,文末有源码地址。

[root@hadoop1 local]# uname -a
Linux hadoop1 3.10.0-1127.el7.x86_64 #1 SMP Tue Mar 31 23:36:51 UTC 2020 x86_64 x86_64 x86_64 GNU/Linux
[root@hadoop1 local]# cat /proc/version
Linux version 3.10.0-1127.el7.x86_64 (mockbuild@kbuilder.bsys.centos.org) (gcc version 4.8.5 20150623 (Red Hat 4.8.5-39) (GCC) ) #1 SMP Tue Mar 31 23:36:51 UTC 2020
[root@hadoop1 local]# cat /etc/redhat-release
CentOS Linux release 7.8.2003 (Core)
内存:4G
处理器:2*2
硬盘:100G

SM2秘钥对从哪里来?

在通过 SpringBoot 集成 SM2 之前,我们要解决的第一个问题是,秘钥对从哪里来?我们有以下几种方式来得到 SM2 的秘钥对。

  1. 使用OpenSSL命令行工具;
  2. 使用HuTool工具类(见文末源码中的/sm2接口);
  3. 使用在线工具(见文末链接);

这里主要使用 OpenSSL 命令行工具来生成 SM2 的秘钥对,需要注意的是,目前的 Centos 7 一般搭配的版本是 openssl 1.0.2 ,如果要使用 OpenSSL 生成的话,需要先升级到 openssl 1.1.1 以上,具体的升级方法见文末。与生成 RSA 秘钥对类似,我们同样需要用到以下三条命令。

# 生成SM2私钥
openssl ecparam -genkey -name SM2 -out sm2_private_key.key
# 把SM2私钥转换成PKCS8格式
openssl pkcs8 -topk8 -inform PEM -in sm2_private_key.key -outform PEM -nocrypt
# 使用原始私钥生成SM2公钥
openssl ec -in sm2_private_key.key -pubout -out sm2_public_key.key
[root@hadoop1 ~]# openssl ecparam -genkey -name SM2 -out sm2_private_key.key
[root@hadoop1 ~]# openssl pkcs8 -topk8 -inform PEM -in sm2_private_key.key -outform PEM -nocrypt
-----BEGIN PRIVATE KEY-----
MIGHAgEAMBMGByqGSM49AgEGCCqBHM9VAYItBG0wawIBAQQgHXugwQeFgF6ELKVH
l28evMOM/Cc2H/EGwv0wcIVYkUShRANCAASrr2gtqvS+u5A1Y7ywDh+6LnxYzum9
sypgJ1bj0S+LyxQJCqg1jE7i/i4LADFJLvO1HY1LQW+6cIM2rLWXzQQG
-----END PRIVATE KEY-----
[root@hadoop1 ~]# openssl ec -in sm2_private_key.key -pubout -out sm2_public_key.key
read EC key
writing EC key

这时,得到的秘钥对如下:

MFkwEwYHKoZIzj0CAQYIKoEcz1UBgi0DQgAEq69oLar0vruQNWO8sA4fui58WM7p
vbMqYCdW49Evi8sUCQqoNYxO4v4uCwAxSS7ztR2NS0FvunCDNqy1l80EBg==
MIGHAgEAMBMGByqGSM49AgEGCCqBHM9VAYItBG0wawIBAQQgHXugwQeFgF6ELKVH
l28evMOM/Cc2H/EGwv0wcIVYkUShRANCAASrr2gtqvS+u5A1Y7ywDh+6LnxYzum9
sypgJ1bj0S+LyxQJCqg1jE7i/i4LADFJLvO1HY1LQW+6cIM2rLWXzQQG

SpringBoot后端服务如何集成SM2?

添加依赖

在pom.xml中添加依赖。

		<!--SM2相关-->
		<dependency>
			<groupId>org.bouncycastle</groupId>
			<artifactId>bcprov-jdk15on</artifactId>
			<version>1.68</version>
		</dependency>

配置文件

server:
  port: 8000

# 密码加密传输,前端公钥加密,后端私钥解密
sm2:
  private_key: |
    MIGHAgEAMBMGByqGSM49AgEGCCqBHM9VAYItBG0wawIBAQQgHXugwQeFgF6ELKVH
    l28evMOM/Cc2H/EGwv0wcIVYkUShRANCAASrr2gtqvS+u5A1Y7ywDh+6LnxYzum9
    sypgJ1bj0S+LyxQJCqg1jE7i/i4LADFJLvO1HY1LQW+6cIM2rLWXzQQG

测试加解密

    /**
     * 测试SM2公钥加密与私钥解密
     *
     * @param formUser
     * @return
     */
    @PostMapping("/sm2")
    public String sm2(@RequestBody FormUser formUser) {
        String publicKey = "MFkwEwYHKoZIzj0CAQYIKoEcz1UBgi0DQgAEq69oLar0vruQNWO8sA4fui58WM7p\n" +
                "vbMqYCdW49Evi8sUCQqoNYxO4v4uCwAxSS7ztR2NS0FvunCDNqy1l80EBg==";

        // 基本的加密与解密操作
        SM2 sm2 = new SM2(privateKey, publicKey);
        String encryptedString = sm2.encryptBcd(formUser.getUsername(), KeyType.PublicKey);
        String decryptedString = StrUtil.utf8Str(sm2.decryptFromBcd(encryptedString, KeyType.PrivateKey));

        log.info("密文:" + encryptedString);
        log.info("明文:" + decryptedString);

        // 不同格式的秘钥
        String publicKeyBase64 = sm2.getPublicKeyBase64();
        log.info("Base64公钥:" + publicKeyBase64);
        String privateKeyBase64 = sm2.getPrivateKeyBase64();
        log.info("Base64私钥:" + privateKeyBase64);

        String hexPublicKey = HexUtil.encodeHexStr(((BCECPublicKey) sm2.getPublicKey()).getQ().getEncoded(false));
        log.info("16进制的公钥:" + hexPublicKey);
        String hexPrivateKey = HexUtil.encodeHexStr(sm2.getPrivateKey().getEncoded());
        log.info("16进制的私钥:{},格式:{}", hexPrivateKey, sm2.getPrivateKey().getFormat());

        // 使用Hutool生成秘钥对
        KeyPair pair = SecureUtil.generateKeyPair("SM2");
        PublicKey pub = pair.getPublic();
        byte[] pubKey = pub.getEncoded();
        log.info("Hutool生成公钥:{},格式:{}", HexUtil.encodeHexStr(pubKey), pub.getFormat());

        PrivateKey pri = pair.getPrivate();
        byte[] priKey = pri.getEncoded();
        log.info("Hutool生成私钥:{},格式:{}", HexUtil.encodeHexStr(priKey), pri.getFormat());

        //提取Q值转为16进制
        String qHex = HexUtil.encodeHexStr(((BCECPublicKey) pub).getQ().getEncoded(false));
        log.info("qHex:" + qHex);

        return decryptedString;
    }

2022-10-15-SM2Test.jpg

如果对 SM2 的秘钥格式有疑问,可参考这个 GitHub 示例:https://github.com/luanxuechao/hutool-js-sm2-demo/blob/main/java-sm2/src/TestClass.java,应该可以解答您的困惑。

Note:记得在 SpringSecurity 的配置文件中放行新增的测试接口。

     http.authorizeRequests()
                // 记得放行匿名测试接口与认证接口
                .antMatchers("/auth/login").permitAll()
                .antMatchers("/auth/sm2").permitAll()
                .anyRequest().authenticated()

认证接口

  /**
     * 认证接口,其中使用SM2国密算法进行私钥解密
     *
     * @param formUser 加密后的用户信息
     * @param request
     * @return 认证后的用户
     */
    @PostMapping("/login")
    public String login(@RequestBody FormUser formUser, HttpServletRequest request) {
        log.info("formUser encrypted: {}", formUser);

        // 用户信息SM2私钥解密,使用hutool中的工具类进行解密
        SM2 sm2 = new SM2(privateKey, null);
        String username = StrUtil.utf8Str(sm2.decryptFromBcd(formUser.getUsername(), KeyType.PrivateKey));
        log.info("Username decrypted: {}", username);

        String password = StrUtil.utf8Str(sm2.decrypt(formUser.getPassword(), KeyType.PrivateKey));
        log.info("Password decrypted: {}", password);

        log.info("Userinfo decrypted: {}, {}", username, password);

        // 核验用户名密码
        UsernamePasswordAuthenticationToken authenticationToken = new UsernamePasswordAuthenticationToken(username, password);
        Authentication authentication = authenticationManagerBuilder.getObject().authenticate(authenticationToken);
        SecurityContextHolder.getContext().setAuthentication(authentication);
        log.info("authentication: {}", authentication);

        return SecurityContextHolder.getContext().getAuthentication().getPrincipal().toString();
    }

2022-10-15-FrontEncrypt.jpg

2022-10-15-BackDecrypt.jpg

Vue前端服务如何集成SM2?

安装依赖

npm install sm-crypto

此时, package.json 的依赖变为:

  "dependencies": {
    "axios": "^0.21.1",
    "core-js": "^3.6.5",
    "sm-crypto": "^0.3.11",
    "vue": "^3.0.0"
  },

具体用法可参考 NPM 文档:https://www.npmjs.com/package/sm-crypto

集成SM2:sm-crypto

在需要使用 SM2 的组件中引入 import { sm2 } from "sm-crypto"; 。最终的前端登录组件代码如下:

Note:前端库 sm-crypto 对公钥的格式要求为04开头的16进制公钥,不可以使用 OpenSSL 直接生成的带换行的公钥形式。

<template>
  <div>
    <span>用户名</span><input type="text" v-model="user.username" />
    <span>密码</span><input type="password" v-model="user.password" />
    <input type="submit" v-on:click="login" value="登录" />
  </div>
</template>
<script>
import { defineComponent } from "vue";
import axios from "axios";
import { sm2 } from "sm-crypto";

export default defineComponent({
  name: "SM2Demo",
  setup() {},
  data() {
    return {
      user: { username: "dev", password: "123" },
//       publicKey: `MFkwEwYHKoZIzj0CAQYIKoEcz1UBgi0DQgAEq69oLar0vruQNWO8sA4fui58WM7p
// vbMqYCdW49Evi8sUCQqoNYxO4v4uCwAxSS7ztR2NS0FvunCDNqy1l80EBg==`,
      publicKey: '04abaf682daaf4bebb903563bcb00e1fba2e7c58cee9bdb32a602756e3d12f8bcb14090aa8358c4ee2fe2e0b0031492ef3b51d8d4b416fba708336acb597cd0406'
    };
  },
  mounted() {
    this.login();
  },
  methods: {
    login: function () {
      // 密文前面需要加上04标志位,否则后端解密失败
      let userinfo = {
        username: "04" + sm2.doEncrypt(this.user.username, this.publicKey, 1),
        password: "04" + sm2.doEncrypt(this.user.password, this.publicKey, 1),
      };
      console.log(userinfo);

      axios.post("http://localhost:8000/auth/login", userinfo).then(
        function (res) {
          if (res.status == 200) {
            console.log(res.data);
          } else {
            console.error(res);
          }
        },
        function (res) {
          console.error(res);
        }
      );
    }
  },
});
</script>

我遇到了哪些问题?

如何升级OpenSSL?

openssl1.1.1 以上的版本提供了对国密 SM2 算法的支持。 https://www.openssl.org/news/cl111.txt 。若版本低于 1.1.1 ,则进行以下升级过程:

# 先查看openssl版本
[root@hadoop1 ~]# openssl version
OpenSSL 1.0.2k-fips  26 Jan 2017
[root@hadoop1 ~]# cd /opt/
[root@hadoop1 opt]# wget https://www.openssl.org/source/openssl-1.1.1q.tar.gz
--2022-10-07 15:39:22--  https://www.openssl.org/source/openssl-1.1.1q.tar.gz
正在解析主机 www.openssl.org (www.openssl.org)... 104.69.242.206, 2600:140b:2:9a6::c1e, 2600:140b:2:9a4::c1e
正在连接 www.openssl.org (www.openssl.org)|104.69.242.206|:443... 已连接。
错误: 无法验证 www.openssl.org 的由 “/C=US/O=Let's Encrypt/CN=R3” 颁发的证书:
  颁发的证书已经过期。
要以不安全的方式连接至 www.openssl.org,使用“--no-check-certificate”。

[root@hadoop1 opt]# wget https://www.openssl.org/source/openssl-1.1.1q.tar.gz --no-check-certificate
--2022-10-07 15:40:31--  https://www.openssl.org/source/openssl-1.1.1q.tar.gz
正在解析主机 www.openssl.org (www.openssl.org)... 104.69.242.206, 2600:140b:2:9a4::c1e, 2600:140b:2:9a6::c1e
正在连接 www.openssl.org (www.openssl.org)|104.69.242.206|:443... 已连接。
警告: 无法验证 www.openssl.org 的由 “/C=US/O=Let's Encrypt/CN=R3” 颁发的证书:
  颁发的证书已经过期。
已发出 HTTP 请求,正在等待回应... 200 OK
长度:9864061 (9.4M) [application/x-gzip]
正在保存至: “openssl-1.1.1q.tar.gz”

100%[=================================================================>] 9,864,061   3.58MB/s 用时 2.6s
# 下载
wget https://www.openssl.org/source/openssl-1.1.1q.tar.gz
# 解压
tar -xvf openssl-1.1.1q.tar.gz
# 进入
cd openssl-1.1.1q
# 配置
./config
# 编译
make
# 安装
make install

#备份旧的命令 
mv /usr/bin/openssl /usr/bin/openssl.old
mv /usr/include/openssl /usr/include/openssl.old

# 添加软连接
ln -s /usr/local/ssl/bin/openssl /usr/bin/openssl
ln -s /usr/local/ssl/include/openssl /usr/include/openssl

# 写入openssl库文件的搜索路径
echo "/usr/local/lib64" >> /etc/ld.so.conf

# 使修改后的搜索路径生效 
ldconfig -v

# 再次查看openssl版本,确认是否更新
openssl version
[root@hadoop1 ~]# openssl version
OpenSSL 1.1.1q  5 Jul 2022

YAML中如何换行?

前面知道,我们是通过 OpenSSL 生成的私钥,这种格式是带换行符的,如果我配置到了 YAML 描述文件中,则需要对换行进行特殊处理, YAML 描述文件中的多行字符串可以使用 | 保留换行符,使用 > 将换行符替换为空格。

# 使用竖线 | 保留换行符
sm2:
  private_key: |
    MIGHAgEAMBMGByqGSM49AgEGCCqBHM9VAYItBG0wawIBAQQgHXugwQeFgF6ELKVH
    l28evMOM/Cc2H/EGwv0wcIVYkUShRANCAASrr2gtqvS+u5A1Y7ywDh+6LnxYzum9
    sypgJ1bj0S+LyxQJCqg1jE7i/i4LADFJLvO1HY1LQW+6cIM2rLWXzQQG

后端报错

java.security.InvalidKeyException: IOException : Unknown named curve: 1.2.156.10197.1.301

国密证书使用了自有的椭圆曲线,所以无法使用JDK自带的java.security解析证书,需要引入BouncyCastle的bcprov-jdk15on依赖。http://javadox.com/maven/dependecy/org.bouncycastle/bcprov-jdk15on/1.53.html

		<dependency>
			<groupId>org.bouncycastle</groupId>
			<artifactId>bcprov-jdk15on</artifactId>
			<version>1.53</version>
		</dependency>

java.security.spec. InvalidKeySpecException: encoded key spec not recognised

升级版本 1.53 ——> 1.68

2022-10-15-AliCloud.jpg

		<dependency>
			<groupId>org.bouncycastle</groupId>
			<artifactId>bcprov-jdk15on</artifactId>
			<version>1.68</version>
		</dependency>		

前端加密失败

报错:Uncaught TypeError: publicKey is null

2022-10-15-Exception.jpg

公钥格式不正确,前端库 sm-crypto 对公钥的格式要求为04开头的16进制公钥,可在后端通过 String hexPublicKey = HexUtil.encodeHexStr(((BCECPublicKey) sm2.getPublicKey()).getQ().getEncoded(false));

MFkwEwYHKoZIzj0CAQYIKoEcz1UBgi0DQgAEq69oLar0vruQNWO8sA4fui58WM7p
vbMqYCdW49Evi8sUCQqoNYxO4v4uCwAxSS7ztR2NS0FvunCDNqy1l80EBg==

转换为16进制格式公钥

04abaf682daaf4bebb903563bcb00e1fba2e7c58cee9bdb32a602756e3d12f8bcb14090aa8358c4ee2fe2e0b0031492ef3b51d8d4b416fba708336acb597cd0406

后端解密失败

后端解密失败,报错: java.lang.IllegalArgumentException: Invalid point encoding 0x-5

后端解密失败,报错: cn.hutool.crypto. CryptoException: invalid cipher text

这两个问题比较诡异,可能是后端解密问题:后端解密时,要在密文前面加上04(或者前端加密后在密文前直接加上04)。 但主要是因为我的前端加密有问题,因为我对数字类型加密,前端使用公钥加密成功,导致后端无法解密。这应该是 sm-crypto 的一个Bug。


Source Code

Reference

If you have any questions or any bugs are found, please feel free to contact me.

Your comments and suggestions are welcome!


「说点什么吧😊~~😊」: