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

本文涉及的产品
密钥管理服务KMS,1000个密钥,100个凭据,1个月
简介: SpringSecurity:前后端分离项目中用户名与密码通过国密算法SM2加密传输

C(G2QR@F%EMC7O{]QNT(C%M.png

背景


国密算法是我国自主研发创新的一套数据加密处理系列算法,包括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;
    }

XO}8[])YZESTE@H}UF)ZV@S.png

如果对 SM2 的秘钥格式有疑问,可参考这个 GitHub 示例:github.com/luanxuechao…,应该可以解答您的困惑。


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();
    }

DJC3UXONFOEZQG927$G3G7F.png

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 文档:www.npmjs.com/package/sm-…

集成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 ,则进行以下升级过程:

  • 下载
bash
复制代码
# 先查看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依赖。javadox.com/maven/depen…


<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


5U9VZ~]W]8{MV3)KA0~HI}B.png

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

前端加密失败


报错:Uncaught TypeError: publicKey is null

O[P0}75356ITBI)3VAW3{O6.png

公钥格式不正确,前端库 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!

目录
相关文章
|
2月前
|
存储 缓存 NoSQL
【Azure Redis 缓存】关于Azure Cache for Redis 服务在传输和存储键值对(Key/Value)的加密问题
【Azure Redis 缓存】关于Azure Cache for Redis 服务在传输和存储键值对(Key/Value)的加密问题
|
6天前
|
存储 算法 安全
超级好用的C++实用库之国密sm4算法
超级好用的C++实用库之国密sm4算法
16 0
|
2月前
|
网络协议 安全 网络安全
DNS服务器加密传输
【8月更文挑战第18天】
95 15
|
2月前
|
算法 安全 数据安全/隐私保护
Android经典实战之常见的移动端加密算法和用kotlin进行AES-256加密和解密
本文介绍了移动端开发中常用的数据加密算法,包括对称加密(如 AES 和 DES)、非对称加密(如 RSA)、散列算法(如 SHA-256 和 MD5)及消息认证码(如 HMAC)。重点讲解了如何使用 Kotlin 实现 AES-256 的加密和解密,并提供了详细的代码示例。通过生成密钥、加密和解密数据等步骤,展示了如何在 Kotlin 项目中实现数据的安全加密。
59 1
|
2月前
|
算法 安全 数据安全/隐私保护
Android经典实战之常见的移动端加密算法和用kotlin进行AES-256加密和解密
本文介绍了移动端开发中常用的数据加密算法,包括对称加密(如 AES 和 DES)、非对称加密(如 RSA)、散列算法(如 SHA-256 和 MD5)及消息认证码(如 HMAC)。重点展示了如何使用 Kotlin 实现 AES-256 的加密和解密,提供了详细的代码示例。
39 2
|
2月前
|
算法 JavaScript 前端开发
国标非对称加密:RSA算法、非对称特征、js还原、jsencrypt和rsa模块解析
国标非对称加密:RSA算法、非对称特征、js还原、jsencrypt和rsa模块解析
131 1
|
2月前
|
算法 JavaScript 前端开发
消息摘要算法:MD5加密
消息摘要算法:MD5加密
47 1
|
2月前
|
存储 算法 安全
密码算法的分类
【8月更文挑战第23天】
41 0
|
2月前
|
安全 Nacos 数据安全/隐私保护
【技术干货】破解Nacos安全隐患:连接用户名与密码明文传输!掌握HTTPS、JWT与OAuth2.0加密秘籍,打造坚不可摧的微服务注册与配置中心!从原理到实践,全方位解析如何构建安全防护体系,让您从此告别数据泄露风险!
【8月更文挑战第15天】Nacos是一款广受好评的微服务注册与配置中心,但其连接用户名和密码的明文传输成为安全隐患。本文探讨加密策略提升安全性。首先介绍明文传输风险,随后对比三种加密方案:HTTPS简化数据保护;JWT令牌减少凭证传输,适配分布式环境;OAuth2.0增强安全,支持多授权模式。每种方案各有千秋,开发者需根据具体需求选择最佳实践,确保服务安全稳定运行。
103 0
|
3月前
|
算法 安全 数据安全/隐私保护
支付系统---微信支付09------数字签名,现在Bob想要给Pink写一封信,信件的内容不需要加密,怎样能够保证信息的完整性,使用信息完整性的主要手段是摘要算法,散列函数,哈希函数,H称为数据指纹
支付系统---微信支付09------数字签名,现在Bob想要给Pink写一封信,信件的内容不需要加密,怎样能够保证信息的完整性,使用信息完整性的主要手段是摘要算法,散列函数,哈希函数,H称为数据指纹
下一篇
无影云桌面