赞
踩
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密码算法使用规范
国密算法系列概述
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证书的签发
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); }
首先进入这个类,我选择的是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 |
---|---|---|
rsaEncryption | RSA算法标识 | 1.2.840.113549.1.1.1 |
sha1withRSAEncryption | SHA1的RSA签名 | 1.2.840.113549.1.1.5 |
ECC | ECC算法标识 | 1.2.840.10045.2.1 |
SM2 | SM2算法标识 | 1.2.156.10197.1.301 |
SM3WithSM2 | SM3的SM2签名 | 1.2.156.10197.1.501 |
sha1withSM2 | SHA1的SM2签名 | 1.2.156.10197.1.502 |
sha256withSM2 | SHA256的SM2签名 | 1.2.156.10197.1.503 |
sm3withRSAEncryption | SM3的RSA签名 | 1.2.156.10197.1.504 |
commonName | 主体名 | 2.5.4.3 |
emailAddress | 邮箱 | 1.2.840.113549.1.9.1 |
cRLDistributionPoints | CRL分发点 | 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 |
<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的目的就是为了扩充算法支持
参考开源项目的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进行对比,一致则验签通过,这样就达到了防篡改的效果
有了思路就可以开始编码了,首先我们把开源项目的工具类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配置里面
编码完成后就可以进行愉快的单元测试了
/** * @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值,验签失败,就会抛异常,所以在业务中捕获一下异常就可以判断是否验签成功
Copyright © 2003-2013 www.wpsshop.cn 版权所有,并保留所有权利。