环境

jdk8、spring boot 2.3.4、java-jwt 3.11.0、bouncycastle 1.65

背景介绍

在多个系统之间,由于调用链长,使用了jwt token的方式鉴权,然后获取相应的资源,这里用到核心的一点就是jwt的防篡改特性。

以往使用的签名算法大都是HS256(HMAC with SHA-256)、RS256(RSASSA-PKCS1-v1_5 with SHA-256),这次来试试SM3WithSM2签名算法给jwt签名

国密系列简要介绍

国密系列常用的有SM1、SM2、SM3、SM4
SM1 为对称加密。其加密强度与AES相当。该算法不公开,调用该算法时,需要通过加密芯片的接口进行调用。
SM2为非对称加密,基于ECC。该算法已公开。与RSA相比,相同密钥长度下,安全性能更高。计算量小,处理速度快。存储空间占用小 ECC的密钥尺寸和系统参数与RSA、DSA相比要小得多
SM3 消息摘要。可以用MD5作为对比理解。该算法已公开。校验结果为256位。
SM4 无线局域网标准的分组数据算法。对称加密,密钥长度和分组长度均为128位。

参考:
SM2密码算法使用规范
国密算法系列概述

实操

1、首先去git上把开源项目拉下来

https://github.com/ZZMarquis/gmhelper
或直接下载:https://download.csdn.net/download/w57685321/12920144
在github上发现了别人已经实现好了的开源项目,就借鉴借鉴啦,感谢开源项目的分享!

在bouncycastle - 1.57版本之后,加入了对国密SM2、SM3、SM4算法的支持,这个开源项目是个封装或示例
该开源项目具有的功能:
SM2/SM3/SM4算法的简单封装
SM2 X509v3证书的签发
SM2 pfx证书的签发

2、关于曲线参数修改

SM2公钥是SM2曲线上的一个点为Q(x, y),每个分量为256位
如果有修改x或者y参数的需求,那么就在这个SM2Util里面修改这个曲线参数就行了

package org.zz.gmhelper;

public class SM2Util extends GMBaseUtil {
    //
    /*
     * 以下为SM2推荐曲线参数
     */
    public static final SM2P256V1Curve CURVE = new SM2P256V1Curve();
    public final static BigInteger SM2_ECC_P = CURVE.getQ();
    public final static BigInteger SM2_ECC_A = CURVE.getA().toBigInteger();
    public final static BigInteger SM2_ECC_B = CURVE.getB().toBigInteger();
    public final static BigInteger SM2_ECC_N = CURVE.getOrder();
    public final static BigInteger SM2_ECC_H = CURVE.getCofactor();
    public final static BigInteger SM2_ECC_GX = new BigInteger(
            "32C4AE2C1F1981195F9904466A39C9948FE30BBFF2660BE1715A4589334C74C7", 16);
    public final static BigInteger SM2_ECC_GY = new BigInteger(
            "BC3736A2F4F6779C59BDCEE36B692153D0A9877CC62A474002DF32E52139F0A0", 16);
    public static final ECPoint G_POINT = CURVE.createPoint(SM2_ECC_GX, SM2_ECC_GY);
    public static final ECDomainParameters DOMAIN_PARAMS = new ECDomainParameters(CURVE, G_POINT,
            SM2_ECC_N, SM2_ECC_H);
    public static final int CURVE_LEN = BCECUtil.getCurveLength(DOMAIN_PARAMS);
}

3、生成证书

首先进入这个类,我选择的是X509规范的证书
org.zz.gmhelper.cert.test.SM2X509CertMakerTest
testMakeCertificate运行这个方法即可生成证书,可以修改SubjectDN、RootCADN这两个标识信息构造(Distinguished Name)的方法

关于DN里字段的含义介绍:https://www.ibm.com/support/knowledgecenter/en/SSFKSJ_7.5.0/com.ibm.mq.sec.doc/q009860_.htm
在这里插入图片描述
这就是生成的证书文件和私钥,这是分开的,如果想要不分开可以使用Pfx、Pkcs12等格式,这个开源项目也提供生成这种类型的方法org.zz.gmhelper.cert.test.SM2PfxMakerTest、SM2Pkcs12MakerTest
在这里插入图片描述
点开证书文件可以发现证书的签名算法变成了SM3WithSM2的oid

关于证书oid标识:

对象标识符名称oid
rsaEncryptionRSA算法标识1.2.840.113549.1.1.1
sha1withRSAEncryptionSHA1的RSA签名1.2.840.113549.1.1.5
ECCECC算法标识1.2.840.10045.2.1
SM2SM2算法标识1.2.156.10197.1.301
SM3WithSM2SM3的SM2签名1.2.156.10197.1.501
sha1withSM2SHA1的SM2签名1.2.156.10197.1.502
sha256withSM2SHA256的SM2签名1.2.156.10197.1.503
sm3withRSAEncryptionSM3的RSA签名1.2.156.10197.1.504
commonName主体名2.5.4.3
emailAddress邮箱1.2.840.113549.1.9.1
cRLDistributionPointsCRL分发点2.5.29.31
extKeyUsage扩展密钥用法2.5.29.37
subjectAltName使用者备用名称2.5.29.17
CP证书策略2.5.29.32
clientAuth客户端认证1.3.6.1.5.5.7.3.2

4、引入pom

<dependency>
    <groupId>com.auth0</groupId>
    <artifactId>java-jwt</artifactId>
    <version>3.10.3</version>
</dependency>
<dependency>
    <groupId>org.bouncycastle</groupId>
    <artifactId>bcprov-jdk15on</artifactId>
    <version>1.65</version>
</dependency>
<dependency>
    <groupId>org.bouncycastle</groupId>
    <artifactId>bcpkix-jdk15on</artifactId>
    <version>1.65</version>
</dependency>

引入了jwt和bc库,BouncyCastle是一款开源的密码包,其中包含了大量的密码算法,使用BouncyCastle的目的就是为了扩充算法支持

5、签名验签思路

参考开源项目的org.zz.gmhelper.test.SM2UtilTest类,里面有SM2加密解密,签名验签的方法

根据签名的一般思路: 把需要签名的数据,就是将jwt的header和jwt的payload先base64编码,base64encode(jwt.header) + ‘.’ + base64encode(jwt.payload),然后使用SM3生成它的摘要(tips:如果不生成摘要直接去加密的话,由于加密后密文体积一般都比原文大,特别是非对称加密的情况下,这样很影响性能)

对它的摘要使用SM2算法+私钥进行加密,然后base64编码为可见字符,就得到了我们需要的sign签名值

signature = base64encode(SM2(SM3(base64encode(jwt.header) + ‘.’ + base64encode(jwt.payload)), ‘SECRET_KEY’))

验签: 拿到jwt,用base64解码,再用SM2算法+SM2公钥对signature进行解密,就得到了信息的摘要,然后把信息用相同的算法(SM3)生成摘要与jwt解密后的signature进行对比,一致则验签通过,这样就达到了防篡改的效果

6、编码

有了思路就可以开始编码了,首先我们把开源项目的工具类copy过来
在这里插入图片描述
目录结构就是这样的

首先扩充java-jwt的Algorithm,这些算法它都是调用jce(Java Cryptography Extension) 实现的(我们平常生成AES、DES、MD5等等大都是调用的这个库,还是很强大的)

通过java-jwt的官方git发现它是不支持SM3WithSM2这种签名算法的,那么就自己依葫芦画瓢弄一个

加密算法类com.auth0.jwt.algorithms.Algorithm
支持的加密算法:
JWS Algorithm Description
HS256 HMAC256 HMAC with SHA-256
HS384 HMAC384 HMAC with SHA-384
HS512 HMAC512 HMAC with SHA-512
RS256 RSA256 RSASSA-PKCS1-v1_5 with SHA-256
RS384 RSA384 RSASSA-PKCS1-v1_5 with SHA-384
RS512 RSA512 RSASSA-PKCS1-v1_5 with SHA-512
ES256 ECDSA256 ECDSA with curve P-256 and SHA-256
ES256K ECDSA256 ECDSA with curve secp256k1 and SHA-256
ES384 ECDSA384 ECDSA with curve P-384 and SHA-384
ES512 ECDSA512 ECDSA with curve P-521 and SHA-512

import com.auth0.jwt.algorithms.Algorithm;
import com.auth0.jwt.exceptions.SignatureGenerationException;
import com.auth0.jwt.exceptions.SignatureVerificationException;
import com.auth0.jwt.interfaces.DecodedJWT;
import lombok.extern.slf4j.Slf4j;
import org.apache.commons.codec.binary.Base64;
import org.bouncycastle.crypto.CryptoException;
import org.bouncycastle.jcajce.provider.asymmetric.ec.BCECPrivateKey;
import org.bouncycastle.jcajce.provider.asymmetric.ec.BCECPublicKey;

/**
 * 扩充auth0.java-jwt的签名算法
 * SM2是国家密码管理局于2010年12月17日发布的椭圆曲线公钥密码算法
 * 是一种非对称加密算法,证书保存在 /resources/jwt.sm2.cer
 * SM3是中华人民共和国政府采用的一种密码散列函数标准,由国家密码管理局于2010年12月17日发布
 *
 * QA: 为什么使用该系列算法 ===> 支持国产!
 * 基于ECC的SM2证书普遍采用256位密钥长度,加密强度等同于3072位RSA证书,远高于业界普遍采用的2048位RSA证书
 * 测基准试:com.ai.base.tool.JwtTest、com.ai.base.tool.JwtTestSm3WithSm2
 * 对各种算法进行简单的性能测试,SM3WithSM2速度大大快于ECDSA256

 * @see com.auth0.jwt.algorithms.Algorithm
 * 这里使用SM3WithSM2的方式签名、验签,对标SHA256withRSA(RS256)
 * signature = SM2(SM3(base64encode(header) + '.' + base64encode(payload)), 'SECRET_KEY')
 * <p>
 * 签名:用SM3对jwt生成摘要, 再用SM2的私钥对其进行加密(如上面的公式),完成后即生成jwt的signature
 * 验签:拿到jwt,用base64解码,再用SM2算法+SM2公钥对signature进行解密,就得到了信息的摘要,然后把信息用相同的算法(SM3)生成摘要与jwt解密后的signature进行对比,一致则验签通过,这样就达到了防篡改的效果
 *
 * @author Created by zkk on 2020/9/23
 **/
@Slf4j
public class SMAlgorithm extends Algorithm {

    private final BCECPublicKey publicKey;
    private final BCECPrivateKey privateKey;

    private static final byte JWT_PART_SEPARATOR = (byte) 46;

    protected SMAlgorithm(BCECPublicKey publicKey, BCECPrivateKey privateKey) {
        super("SM3WithSM2", "SM3WithSM2");
        this.publicKey = publicKey;
        this.privateKey = privateKey;
        if (publicKey == null || privateKey == null) {
            throw new IllegalArgumentException("The Key Provider cannot be null.");
        }
    }

    @Override
    public void verify(DecodedJWT jwt) throws SignatureVerificationException {
        byte[] signatureBytes = Base64.decodeBase64(jwt.getSignature());
        byte[] data = combineSignByte(jwt.getHeader().getBytes(), jwt.getPayload().getBytes());
        try {
            if(!SM2Util.verify(publicKey, data, signatureBytes)) {
                throw new SignatureVerificationException(this);
            }
        } catch (Exception e) {
            throw new SignatureVerificationException(this);
        }
    }

    @Override
    @Deprecated
    public byte[] sign(byte[] contentBytes) throws SignatureGenerationException {
        // 不支持该方法
        throw new RuntimeException("该方法已过时");
    }

    @Override
    public byte[] sign(byte[] headerBytes, byte[] payloadBytes) throws SignatureGenerationException {
        byte[] hash = combineSignByte(headerBytes, payloadBytes);
        byte[] signatureByte;
        try {
            signatureByte = SM2Util.sign(privateKey, hash);
        } catch (CryptoException e) {
            throw new SignatureGenerationException(this, e);
        }

        return signatureByte;
    }

    /**
     * 拼接签名部分 header + . + payload
     *
     * @param headerBytes  header
     * @param payloadBytes payload
     * @return bytes
     */
    private byte[] combineSignByte(byte[] headerBytes, byte[] payloadBytes) {
        // header + payload
        byte[] hash = new byte[headerBytes.length + payloadBytes.length + 1];
        System.arraycopy(headerBytes, 0, hash, 0, headerBytes.length);
        hash[headerBytes.length] = JWT_PART_SEPARATOR;
        System.arraycopy(payloadBytes, 0, hash, headerBytes.length + 1, payloadBytes.length);
        return hash;
    }

    /**
     * builder
     */
    public static class SMAlogrithmBuilder {
        private BCECPublicKey publicKey;
        private BCECPrivateKey privateKey;

        SMAlogrithmBuilder() {
        }

        public SMAlgorithm.SMAlogrithmBuilder publicKey(final BCECPublicKey publicKey) {
            this.publicKey = publicKey;
            return this;
        }

        public SMAlgorithm.SMAlogrithmBuilder privateKey(final BCECPrivateKey privateKey) {
            this.privateKey = privateKey;
            return this;
        }

        public SMAlgorithm build() {
            return new SMAlgorithm(this.publicKey, this.privateKey);
        }
    }

    public static SMAlgorithm.SMAlogrithmBuilder builder() {
        return new SMAlgorithm.SMAlogrithmBuilder();
    }
}

直接调用了SM2Util.这个开源项目提供的工具类签名、验签了
最开始自己写的签名和验签过程,先SM3取摘要然后SM2加密,但是后面发现这个Util提供了这个方法,它是调用的bc框架的org.bouncycastle.crypto.signers.SM2Signer

public class SM2Signer
    implements Signer, ECConstants
{
	…………
    public SM2Signer()
    {
        this(StandardDSAEncoding.INSTANCE, new SM3Digest());
    }
    …………
    }

发现它这个签名算法就是用的SM3取的摘要,所以效果是一样的

有了签名算法就可以封装我们的jwt工具类了

import com.auth0.jwt.JWT;
import com.auth0.jwt.JWTCreator;
import com.auth0.jwt.interfaces.Claim;
import com.auth0.jwt.interfaces.DecodedJWT;
import com.auth0.jwt.interfaces.JWTVerifier;
import com.google.common.collect.Maps;
import lombok.extern.slf4j.Slf4j;
import org.bouncycastle.jcajce.provider.asymmetric.ec.BCECPrivateKey;
import org.bouncycastle.jcajce.provider.asymmetric.ec.BCECPublicKey;
import org.bouncycastle.jce.provider.BouncyCastleProvider;

import java.io.InputStream;
import java.security.Security;
import java.security.cert.X509Certificate;
import java.util.Map;
import java.util.Objects;

/**
 * 生成jwt的工具类,基于auth0.java-jwt封装
 * 签名算法使用SM3WithSM2
 * payload统一使用Map<String, String>类型
 * @author Created by zkk on 2020/9/22
 **/
@Slf4j
public class JwtHelper {

    static {
        Security.addProvider(new BouncyCastleProvider());
        X509Certificate cert;
        try {
            // 从yml中读取配置
            PropertiesTool propertiesTool = ApplicationContextProvider.getBean(PropertiesTool.class);
            InputStream streamCer = JwtHelper.class.getClassLoader().getResourceAsStream(propertiesTool.getCerFilePath());
            InputStream streamPri = JwtHelper.class.getClassLoader().getResourceAsStream(propertiesTool.getCerPriKeyPath());
            int streamPriLen = Objects.requireNonNull(streamPri).available();

            cert = SM2CertUtil.getX509Certificate(streamCer);

            byte[] priKeyData = new byte[streamPriLen];
            streamPri.read(priKeyData);
            // 从证书中获取公钥,从私钥文件中获取私钥
            publicKey = SM2CertUtil.getBCECPublicKey(cert);
            privateKey = BCECUtil.convertSEC1ToBCECPrivateKey(priKeyData);

        } catch (Exception e) {
            log.error("JWT工具初始化异常", e);
        }

    }

    /**
     * 设置发行人
     */
    private static final String ISSUER = "zzz";

    /**
     * SM2需要的公钥和私钥
     */
    private static BCECPublicKey publicKey;
    private static BCECPrivateKey privateKey;

    /**
     * 初始化SM3WithSM2算法
     */
    private static final SMAlgorithm ALGORITHM = SMAlgorithm.builder().publicKey(publicKey).privateKey(privateKey).build();

    /**
     * 生成jwt
     * @param claims 携带的payload
     * @return jwt token
     */
    public static String genToken(Map<String, String> claims){
        try {
            JWTCreator.Builder builder = JWT.create()
                    .withIssuer(ISSUER);
            claims.forEach(builder::withClaim);
            return builder.sign(ALGORITHM);
        } catch (IllegalArgumentException e) {
            log.error("jwt生成失败", e);
        }
        return null;
    }

    /**
     * 验签方法
     * @param token jwt token
     * @return jwt payload
     */
    public static Map<String, String> verifyToken(String token) {
        JWTVerifier verifier = JWT.require(ALGORITHM).withIssuer(ISSUER).build();
        DecodedJWT jwt =  verifier.verify(token);
        Map<String, Claim> map = jwt.getClaims();
        Map<String, String> resultMap = Maps.newHashMap();
        map.forEach((k,v) -> resultMap.put(k, v.asString()));
        return resultMap;
    }
}

ApplicationContextProvider是实现的ApplicationContextAware接口,用于获取bean

通过这个工具类就可以生成和解析jwt了

# jwt需要的证书路径
app:
  jwt:
    certificate:
      filePath: jwt.sm2.cer
      priKeyPath: jwt.sm2.pri

证书文件我直接放在resources目录下的,然后写在yml配置里面

7、单元测试

编码完成后就可以进行愉快的单元测试了

/**
 * @author Created by zkk on 2020/9/24
 **/
@SpringBootTest
class JwtHelperTest {

    @Test
    void signToken() {
        HashMap<String, String> map = new HashMap<>();
        map.put("test","test");
        map.put("test4","test");
        map.put("test5","test");
        String token = JwtHelper.genToken(map);
        System.out.println(token);
    }

    @Test
    void verifyToken() {
        String token = "eyJ0eXAiOiJKV1QiLCJhbGciOiJTTTNXaXRoU00yIn0.eyJ0ZXN0NCI6InRlc3QiLCJ0ZXN0NSI6InRlc3QiLCJ0ZXN0IjoidGVzdCIsImlzcyI6Inp6eiJ9.MEQCICkcIuJ3cOYCd2wKHOwnt9ZnGcM_6xrNgRy3Bzq905s9AiAc0zzNG4_OhxCCZHMCB9Bg8vSBcLnX5jU1JUS56Hb6fg";
        Map<String, String> map1 = JwtHelper.verifyToken(token);
        System.out.println(map1);
    }
}

在这里插入图片描述
base64解码后发现就是SM3WithSM2的签名算法了
在这里插入图片描述
验签通过,然后获取到payload
在这里插入图片描述
修改sign值,验签失败,就会抛异常,所以在业务中捕获一下异常就可以判断是否验签成功

Logo

旨在为数千万中国开发者提供一个无缝且高效的云端环境,以支持学习、使用和贡献开源项目。

更多推荐