密码学总结,实现开放接口验签和加密

2024-01-17 01:50

本文主要是介绍密码学总结,实现开放接口验签和加密,希望对大家解决编程问题提供一定的参考价值,需要的开发者们随着小编来一起学习吧!

前言

最近刚看完《图解密码技术》,这本书挺不错的,非常适合对密码学感兴趣入门小白。最大的收获就是产生了意识,就是我们常识里认识的理所当然安全意识,实际是安全陷阱。这本书很浅显,对于里面很深的密码学数学原理,没有过多介绍。我想更多是帮小白打开密码学大门,对现有的认识有个初步认识。实际密码学后面就是数学,貌似很多科学最后都是数学。加上前不久公司还遭受了黑客攻击,产生了非常很严重的影响。安全这种事就是不发生什么事都没有,发生了那么就是重大安全事故。

既然学习了密码学,想到之前自己也写过开放接口验签功能。之前是用MD5生成签名,然而现在MD5已经被弃用了不安全。不过如果你到网上查一下依然还大部分MD5,学以致用那么就实现一套,做个总结将来说不定也能用到。

密码学简介

在实现具体功能前,先对密码学做个简要介绍。
密码学一共分成:对称密码(AES)、公钥密码(RSA,椭圆曲线密码)、单向散列函数(MD5,SHA256) 、随机数生成器

实际上会对以上密码分类混合扩展出以下两个分类:消息认证码(HMAC):单向散列函数+密钥,数字签名:单向散列函数+公钥密码

对称密码

对称密码就是加密和解密用的一样的密钥,代表密码有AES,DES。

DES(Data Encryption Standard)是一种对称密钥加密算法,最初由IBM设计,并在1977年被美国国家标准局(NIST)发布为联邦信息处理标准(FIPS PUB 46)。(DES现在不安全了)
AES(Advanced Encryption Standard,高级加密标准,又称Rijndael加密法)是一种对称密钥加密算法,它是由比利时密码学家Joan Daemen和Vincent Rijmen设计的。2001年NIST选取AES取代了原来的DES作为美国政府采用的加密标准,并且在全球范围内得到了广泛的应用。

在使用到对称密码,我们都会遇到工作模式选择。以AES为例子,有时候需要iv变量,有时候不需要。有时候还需要选择填充模式。
image.png{:width=400}

这是分组密码的工作模式,决定要使用这些变量。
分组密码(Block Cipher)是一类对数据进行加密的对称密码算法。在分组密码中,明文按照固定长度的块(比特块)进行分割,每个块被独立地加密。(DES,AES)

AES(Advanced Encryption Standard)的加密过程:文本填充:把明文拆分成128bit分组,每组16个字节。当最后一个分组不足16个字节,进行填充处理PKCS5PADDING密钥扩展(Key Expansion):在这一步骤中,使用初始密钥生成轮密钥,以备后续的加密轮使用。密钥扩展算法将初始密钥扩展为一系列轮密钥(128位密钥生成11个伦密钥),这些轮密钥将在加密的每一轮中使用。(密钥长度有128位加密轮数10,192位 轮数12,256位轮数14)初始轮密钥加法(Initial Round Key Addition):将明文与第一轮的轮密钥进行异或运算。这个步骤旨在引入密钥的影响,增加加密的复杂性。多轮加密(Rounds):a. SubBytes(字节替代):对状态矩阵的每个字节进行字节替代,使用一个固定的S盒(Substitution Box)。这个S盒将每个字节映射到另一个字节,增加了非线性性质。b. ShiftRows(行移位):对状态矩阵的行进行循环左移。不同的行有不同的循环位移量,引入了行间的混淆。 (例如,状态矩阵的第0行左移0字节,第1行左移1字节,第2行左移2字节,第3行左移3字节)c. MixColumns(列混淆):对状态矩阵的列进行线性变换。这个步骤引入了列间的混淆,增强了AES的抗线性攻击性能。(经行移位后的状态矩阵与固定的矩阵相乘,得到混淆后的状态矩阵)d. AddRoundKey(轮密钥加法):将当前状态矩阵与当前轮的轮密钥进行异或运算。这个步骤引入了轮密钥的影响。最终轮(Final Round):省略MixColumns操作,只执行SubBytes、ShiftRows和AddRoundKey输出:经过多轮的加密后,最终得到加密后的密文工作模式(Mode)主要是指分组密码工作模式:ECB(Electronic Codebook,电子密码本),将每个数据块独立加密,不考虑前后关系。如果明文全是1那么出现的密文也是重复的,在这个模式下。同时可以将密文分组内容顺序对调和删除,不破解密码实现了攻击。(不需要初始向量,剩下其他模式都需要)CBC(Cipher Block Chaining,密码分组连接) 明文分组与前一个密文分组进行异或运算,然后进行加密。每个块的加密都依赖于前一个块的密文,引入了初始向量(Initialization Vector,IV),第一个分组没有前一个块密文那么使用初始向量和他异或(一般使用这个模式),解密支持并行运算CFB(Cipher Feedback,密文反馈)将前一个密文块加密,然后与明文块异或,得到密文块。与 CBC 类似(错误通常只在影响到错误块和后续块的密文中传播,不会影响前面的块,适合流加密)OFB(Output Feedback,输出反馈)与 CFB 类似,但加密前一个密文块得到的密文块并不参与后续块的加密,而是直接输出。CTR(Counter,计数器)使用计数器作为初始向量,每个块加密计数器的值,然后与明文块异或。加解密可以并行计算。GCM (Galois/Counter Mode,伽罗瓦/计数器模式)并行处理能力和提供的完整性验证。(TLS和微信支付都用这个)
CBC 模式下,每个明文块在加密之前都要与前一个密文块进行异或操作,如果最后一个明文块的长度不是块大小的整数倍,那么就无法进行异或操作。其他一些分组密码工作模式,如 ECB(Electronic Codebook)和 CFB(Cipher Feedback)每个块独立加密,不会涉及到块与块之间的异或操作,因此不需要填充。

工作模式使用比较多是CBC和GCM,我们知道这两个就好了。

填充模式(Padding):填充明文不足分组长度的数据,使之可以被分组成相等长度:NOPADDING(不填充), PKCS5PADDING(缺少的字节,都填缺少的字符数。比如少6个字节那么{1,2,3,4,5,a,b,c,d,e,6,6,6,6,6,6}), ISO10126PADDING(填充最后一个字节是缺少的字符数,剩下都是随机数)。

这边iv变量通常是和报文一起传输,也就是iv变量是公开。这边密码学有个原则就是发送内容一样,但是密文内容要求不一样。也就是说这边业务内容发生的数据一模一样,这边生成的密文需要不一样。这边处理方式基本都是加上随机字符串。(注:这边随机数需使用随时生成器生成的随机字符串,而不能使用uuid和雪花id等有规律的字符串)

因为如果相同业务数据,密文也一样。那么就可以通过密码差分析攻击。

密码差分分析是一种针对加密算法的攻击方法,它利用输入和输出之间的差分特征。攻击者观察一系列不同输入之间的差异,然后分析相应的输出之间的差异,以推导出加密密钥或其他关键信息。
线性分析是一种基于线性逼近的攻击方法,它尝试建立输入、输出和密钥之间的线性关系。攻击者使用已知的明文-密文对,通过构建一些线性逼近来猜测密钥位。

这边对称密码,遇到一个问题就是密钥怎么发给对方。在没有密钥之前,双方的通信都是公开的。如果用明文发生密钥,如果密钥被窃听。那么接下来的加密,将毫无意义。
接下来介绍的公钥密钥就是为了解决密钥配送问题,而诞生。

密钥实际就是一串比较长无规则随机数。

公钥密码

在上面我们解决了内容加密问题,但是密钥配送问题还没解决。
上面的AES加密过程是需要将密钥加密后发送给对方,也就是一方需要加密,而另一方只需要解密。发明一种密码加密用加密密钥,解密用解密密钥。发送者只需要加密密钥(公钥),解密者只需要解密密钥(私钥)。那么我将公钥发给对方,让他用AES密钥用公钥密码的公钥加密后发给我,而我再用私钥解密。
非对称加密解决密钥配送问题(key distribution problem,密钥分发问题)

和前面的对称密码实现过程是填充,然后分组再通过异或,混淆等操作。接下来的公钥密码实现就是纯数学。

RSA

RSA(Rivest-Shamir-Adleman)是一种非对称加密算法,广泛用于信息安全领域,特别是在公开密钥加密和数字签名中。RSA算法的安全性基于大整数因子分解的难度,即将大合数分解成其质数因子的难度。RSA是由罗纳德·李维斯特(Ron Rivest)、阿迪·萨莫尔(Adi Shamir)和伦纳德·阿德曼(Leonard Adleman)在1977年一起提出的。当时他们三人都在麻省理工学院工作。RSA 就是他们三人姓氏开头字母拼在一起组成的。

RSA利用下面两个公式:
将待加密的消息表示为一个整数M,满足0 ≤ M < n。加密操作使用公钥进行,计算密文C = M e m o d n M^emod n Memodn
接收到密文C后,使用私钥进行解密,计算明文 M ≡ C d m o d n M \equiv C^d mod n MCdmodn
代码实现比较简单可以参考:RSA非对称加密原理

但是这个RSA有个缺点,处理速度比对称加密慢几百分之一,处理明文长度有限制(明文长度不能超过密钥长度),密钥长度特别长,大家如果有用github的ssh打开那密钥就看到非常长的一串。

椭圆曲线密码学

很多技术都是为了解决现有问题而诞生,这不椭圆曲线来了。
椭圆曲线密码学(英语:Elliptic Curve Cryptography,缩写:ECC)是一种基于椭圆曲线数学的公开密钥加密算法。ECC的主要优势是它相比RSA加密算法使用较小的密钥长度并提供相当等级的安全。(ECC证书无法兼容XP和Android 2.3以下)
椭圆曲线的图像不是椭圆形,椭圆曲线来源于求椭圆弧长的椭圆积分的反函数。
ECC是一种基础的椭圆曲线密码学技术,而ECDH和ECDSA是基于这种技术的两个具体应用。ECDH用于密钥交换,而ECDSA用于数字签名。

椭圆曲线密码是下一代公钥密码,为了让大家直观感受密钥长度。这边生成同等抵御暴力破解强度RSA和ECC密钥

RSA公钥
MIIBIjANBgkqhkiG9w0BAQEFAAOCAQ8AMIIBCgKCAQEAmk7yrp2kwDpNhpX2jtwNt0EXn+mtXXPOoQNOmsXuAt0cSYtEYhIH7nkUG73TNVGdOMHlgtTatkApL+7SPQeuB1R91pVMdU0PMNWKs+WbVHyca7Ek6j2Csj6VhTKcTtuJOZTKGHuLo3O0/VmEOXEhwg+t3bd9vKYcrCr/Ij0kfxZIVLJR1Lf4UjUIANvp3sVUHqwsHb0S0PC3cYvS2vcNvajyAvqTSNCJgfy9FrogrtcOBU5F2Y41F2KXZ3KCxErTF3ks/+m8CPl89v0GzlHpoL0QrAxss3l0fNjhrp+wCsCqn6aDniw+voKERUV/F+xOtByBs32LGAz6KKFvb17Z+wIDAQAB

RSA私钥
MIIEvgIBADANBgkqhkiG9w0BAQEFAASCBKgwggSkAgEAAoIBAQCaTvKunaTAOk2GlfaO3A23QRef6a1dc86hA06axe4C3RxJi0RiEgfueRQbvdM1UZ04weWC1Nq2QCkv7tI9B64HVH3WlUx1TQ8w1Yqz5ZtUfJxrsSTqPYKyPpWFMpxO24k5lMoYe4ujc7T9WYQ5cSHCD63dt328physKv8iPSR/FkhUslHUt/hSNQgA2+nexVQerCwdvRLQ8Ldxi9La9w29qPIC+pNI0ImB/L0WuiCu1w4FTkXZjjUXYpdncoLEStMXeSz/6bwI+Xz2/QbOUemgvRCsDGyzeXR82OGun7AKwKqfpoOeLD6+goRFRX8X7E60HIGzfYsYDPoooW9vXtn7AgMBAAECggEAKZtV9l1NWgOeecafx59wF1R20UiJ1WPveBzTKgpqhd0mznG9y4+/Eb8V+/DwLTVHZlFp2CAapP+h22nSyF/0vcc7cwuopQxVJz68+orTmjFLYAJQyNQzFGqpmFOwL9ckJiGHaJiPa1A8of9sFqz5hEEECX97aM2Fd6rURmAkIhvtZ5rKBeHmejF1qW5Qce6CNMiz29/YZ4QkY9yGPFgG4pqRmf2OEEuABgIuD3wR+vlNvDgHVmBQzXkhpcDq6SfBm6Qymn/vps1MTSrx6cuuVZs1qxXTTaMBt+qkY+cSh24xxZj4kxu+BrAOAhxhCPsZIH9aRuml4HLUJz2jUBHo2QKBgQDY3cPxa8rjt9EW5kpl2ocvY8kwpbby0jZBseB+mWO7z9Yp56LyxslM2Sw+N0Z57tnmj0P2cnZgUYgsPahI/a8BpEY1C6LemoTJ3eqmuA6X+1za/N0cIgMMSmCL/BWOWG1ycGHjr3oLxtjkjTNSuO3SniKEi2+JUYPwORZD3+7ztwKBgQC2J0sR5R/Wd4kaZLLlQ6rlPmL3OvNyepWbOf+zOG4sCUx0gjZZevNQDNgkzCc2DgpxZS5JsF+Y4vkZsKB6nl5aaz+gbZVPWL2VD0KwN+heGcVlwy8qCmGRzhHgzu1rWxLk6pMKWlR7DG+Qee46m0l/NNdlrSmX38LvR1LOwr8z3QKBgENG0OGVShTI0pGjEZN1bpiyARkZX58GvZQ1xWog1cLO4CaR2IguSQaRHNuUNEXhZjHTYgcTTUvmHzNyUUGLiX4d4zXTjUw3bVhegswDr2tQrGa3KTTwDDgE3tyucFV/czycRpHmvxrmLiUA9NoFyqmbKyQpSYm1AxRD8XdPAgcTAoGBAJFtL5ca6d9NUi27RcSa0L7znwmpjCHS0sy6cnZA0FziE6NVLlUkP0ui5ZIDWC7k6Nt9n4X/hWNHmv8yr/0VoVjpFURdGP7fZ4SxSVntWNyAEMRdH7Od1CYXctib/Jtge+Y9jaWPVrFizVN4tYUe43/mzS2FIZb6c+SbefZPL14VAoGBAKT+KJ4yYGrtYvsHcPcLM1WGWZj/ENKXM15v28TQHmKEjTCe83MYZCkAZoUaPrGWPjvxg0VMGsd1jcPy4pbxK4VO4Mml2h76W7a/SkJbDxn/WSAR47aJ5FzZ4pZCINEs4K3U8zkh16EtT1OhoT7REE9xOcS30Et3n8YT4prnzb6/

椭圆曲线密钥

MFkwEwYHKoZIzj0CAQYIKoZIzj0DAQcDQgAEQt9CGT3M31bg/qIWiUJcy43mwnt4/m3yoGFuJuNlWkunhCYmdJEzvmJQtE1ap8AvVbvzDvAnhBNLqlclObL0KA==

MEECAQAwEwYHKoZIzj0CAQYIKoZIzj0DAQcEJzAlAgEBBCDPJcpbNzEZ7LAEGpxeAdCPI997WECXOfUFF0GsV7TeLQ==

公钥密钥目前RSA用的还是比较多,但是渐渐的ECC会取代RSA。大家如果查看比较新的网站证实,会发现他们已经开始用ECC了。比如chatgpt网站。
image.png{:width=400}

而比较早的网站还是RSA
image.png{:width=400}

这边解决了密钥配送问题,还有一个问题就是。你怎么知道我就是我,你怎么认为你收到的公钥就是我发给你的公钥。
中间人攻击(Man-in-the-Middle Attack,缩写为MitM或MiTM)是一种网络安全攻击,攻击者在通信的两个实体之间插入自己,以监听、篡改或劫持通信。(代理服务器,Wi-Fi劫持,DNS欺骗攻,恶意软件,SSL劫持)
中间人攻击例子:Mallory拦截了Bob发给Alice公钥,Mallory将自己生成的公钥伪装成是Bob的公钥发给Alice。那么Alice用的公钥加密是Mallory,而不是Bob。那么Mallory能解密Alice的内容了。

要解决公钥配送问题,那么就需要证书了。

单向散列函数

还有一个问题,就是你收到别人的密文,那你有没怀疑过你收到的内容是否被篡改了。那么你如何知道自己内容被串改了,那么就要使用到摘要密码也就是单向散列函数。就是把内容+密钥生成一个签名,双方都用相同的方式,查看签名是否一样。
One-way hash function(单向散列函数)又称为信息摘要函数(message digest function ),有一个输入和输出,输入称为消息(message),输出称为散列值(hash value)又称为信息摘要。(message digest)。
两个不同的消息产生同一个散列值的情况称为碰撞(collision)。
MD5(Message Digest 5) 消息摘要算法是一种广泛使用的哈希函数,可生成 128位哈希值。MD5 由Ronald Rivest 罗纳德·里维斯特于 1991年设计,用于取代早期的哈希函数MD4.MD5的强抗碰撞性已经被攻破,已经能够产生相同散列值不同消息。
任何加密哈希函数的一个基本要求是,在计算上,找到两个散列为相同值的不同消息应该是不可行的。(MD5已经被弃用了不安全,推荐SHA256)

SHA-2,名称来自于安全散列算法2(英语:Secure Hash Algorithm 2),一种密码散列函数算法标准,由美国国家安全局研发,由美国国家标准与技术研究院(NIST)在2001年发布。是SHA-1的后继者。(sha2还是安全算法)

RIPEMD(RACE原始完整性校验讯息摘要)是一种加密哈希函数,由鲁汶大学Hans Dobbertin,Antoon Bosselaers 和 Bart Prenee组成的COSIC 研究小组发布于1996年。 (比特币使用RIPEMD-160)

上面说到用内容+密钥生成签名,就是消息认证码。
消息认证码(Message Authentication Code,简称MAC)是一种用于对消息进行认证和完整性验证的算法。它是通过对消息和一个密钥进行计算而生成的固定长度的字符串,这个字符串被发送方附加到消息上,并随消息一起传输给接收方。(就是签名)
HMAC(Hash-based Message Authentication Code)是一种用于消息认证的算法,其工作原理基于哈希函数和密钥的组合。HMAC构建在哈希函数的基础上,通过引入密钥来增加消息的认证性和完整性。

散列值攻击
生日攻击(Birthday Attack)是一种密码学攻击,它利用生日悖论的原理,试图找到两个不同的输入,使得它们经过哈希函数后的输出相同。生日攻击的名称来源于生日悖论,即在一个房间里的人数超过23人时,存在两个人生日相同的概率超过50%.
散列值一致的概率比我们想象中高,这就是生日悖论。也就是说生成N份100万合同和N份1亿合同,最后得到找出存在一个相同哈希值即可,那么sha256概率就是√2256=2128,相比暴力破解少了一般。

数字签名

数字签名使用的也是公钥密码技术
数字签名(Digital Signature)是私钥加密只有私人才可以签名,公钥解密任何人都可以看到自己签名。非对称加密是公钥加密,私钥解密。(他们两个是反过来)
举个例子,如果用Hmac就行签名验证,你和他都用相同的密钥。那么你向张三借了100万,用Hmac签名。过几天你想赖掉,说这个你自己用密钥生成。不是我生成的。而如果用RSA进数字签名,那么私钥一直在你自己手上。那么想赖掉,你就不能说签名也有可能是你生成的。

RSA和SHA256数字签名流程:
(1)内容使用SHA256生成信息摘要。
(2)用私钥将摘要加密,生成的内容就是签名。
(3)验签就是将收到的内容生成信息摘要
(4)将收到的签名,用公钥解密。对比两个摘要内容是否一致。

伪随机数

密码的本质就是将较长的秘密——消息,变成较短的秘密——密钥。
密钥就是一串数字。
也就是如果知道密钥那么也就是破译了密码,密钥是个命门却容易被忽视的地方。

真随机数(True Random Number):真随机数是通过真实的物理过程或自然现象产生的随机数。例如,利用大气噪声、放射性衰变或其他物理过程的变化来生成随机数。
伪随机数(Pseudo-Random Number):伪随机数是通过算法生成的数列,看起来在统计上类似于真随机数序列。这个算法接受一个称为种子(seed)的初始值,并基于该种子生成一系列看似随机的数字。(Java随机数都是伪随机数)

在 Java 中,java.util.Random 类主要使用线性同余法(linear congruential generator,LCG)作为其伪随机数生成算法。LCG 是一种简单而快速的伪随机数生成算法,其基本形式如下

X=(aX+c)%m (a=25214903917(常数),c=11(常数),m=2^48) 种子就是第一个随机数x的值,默认取当前时间 (不能将线性同余法用于密码技术)

种子就是产生随机数的第一次使用值,机制是通过一个函数,将这个种子的值转化为随机数空间中的某一个点上,并且产生的随机数均匀的散布在空间中,以后产生的随机数都与前一个随机数有关。
在Java中生成密钥使用SecureRandom,Java SecureRandom 使用一个或多个熵源(entropy sources)来收集真正的随机性。熵源可能包括操作系统的硬件设备、鼠标和键盘的事件、磁盘访问模式、系统启动时的时间戳等。这些熵源的变化和不可预测性为生成随机数提供了更高的安全性。(密钥只能用这个方法生成)。这个加入了物理性随机,保证了

密钥的三个属性:随机性(不存在统计学偏差,是完全杂乱无章的数列)、不可预测性(不知道下一个会是什么样)、不可重现性(生成的随机数不会重复)

生成密钥是个比较容易被忽视的安全问题,我之前一直用的是Random类生成。这个是不符合安全规范。

现在我们谈下,在同等强度下各个密钥的长度要求。

同等抵御暴力破解强度,密钥长度比较。
AES:128 RSA:3072 散列:256 ECC:256,
AES:256 RSA:15360 散列:512 ECC:521 (单位bit)
从上面我们可以看出,RSA是真的长。一般实际使用是2048或者4096长度,Java默认是2048长度。

代码实现

签名和加密工具类

实现了Hmac生成消息认证码,RSA_SHA生成签名和验签,ECC_SHA签名和验签,AES加解密,密钥生成。
在生成签名有使用到AuthStringPrefix字段,这个是签名的一些关键信息。

AuthStringPrefix 格式为:ji-HmacSHA256-v1/{appKey}/{timestamp}/{nonce}
{signVersion} :ji-HmacSHA256-v1是声明签名的类型,这样方便扩展以及将来升级签名版本
{appKey}:当前请求的身份信息,通过appKey找到它对应的密钥
{timestamp}:生成签名时间戳,为了防止重放攻击验签会比对当前时间和{timestamp},两者时间差不能超过5分钟
{nonce}:随机数,保证相同业务内容最后生成的密文和签名都不一样。防止重放攻击。

重放攻击(Replay Attack)是一种网络攻击,攻击者通过在通信中重新播放已经截获的合法通信数据,试图欺骗系统,使系统接受重复的请求或命令。(请求中加入时间戳和实际时间不超过5分钟和 nonce(随机数))
Nonce是Number once的缩写,在密码学中Nonce是一个只被使用一次的任意或非重复的随机数值。

在HmacSHA256模式中签名过程是:
先使用 authStringPrefix和secretKey生成signingKey,再用signingKey作为密钥对业务内容生成认证信息摘要。这样保证业务内容一样,每次的签名不一样。(参考的是百度云加密)

    public static String hmacSha256(String secretKey, String authStringPrefix, Map<String, Object> data) {
//        生成signingKeyString signingKey = hmacSha256(secretKey, authStringPrefix);
//        内容生成摘要return hmacSha256(signingKey, getBodyDataStr(data));}

在SHA256WithRSa和SHA384withECDSA中也是一样,将authStringPrefix和业务内容合并生成签名。

    public static String signRsa(String authStringPrefix, Map<String, Object> bodyData, String privateKeyBase64) {
//        data=authStringPrefix+bodyDatareturn sign(authStringPrefix + getBodyDataStr(bodyData), privateKeyBase64, RSA_ALGORITHM, RSA_SHA_ALGORITHM);}

这边在签名都是用一个方法,getBodyDataStr(Map<String, Object> data)。这边是将实体对象转成一个string字符串,因为要让平台和商户产生相同的签名。那么明文内容要求是一样,比较常用有两种方案,一种是通过TreeMap将key从小到大排序,拼接成一样的。还有一种是直接转成json串。一般有个原则是为null的字段不输出。转json串有两个好处,可以处理比较复杂结构的对象,向对象套对象,套数组等。还有就是将加密后的内容还原比较好还原。
转json,不同语言输出内容会有差距。记住一个前提,开放接口是需要满足不同语言的对接。(之前遇到公司其他部门写得开放接口只支持Java,为了满足其他语言还特地再写一套验签功能)。

举个例子:Java和python的转成json字符串结构不太一样,python字符串有空格需要特殊处理。还有对于不输出null的内容也需要特殊处理,还有中文字符也需要处理。很多对接商户签名错误,大部分都是明文数据不一致导致的。

    public static String getBodyDataStr(Map<String, Object> data) {String jsonString = JSON.toJSONString(data, SerializerFeature.MapSortField);System.out.println("待加密数据:" + jsonString);return jsonString;}

python的处理方式

    @staticmethoddef get_request_body_str(data:dict)->str:# 过滤值为null的keyfiltered_data = {k: v for k, v in data.items() if v is not None}# ensure_ascii=False 参数用于处理非 ASCII 字符,以确保中文字符正常输出jsonStr= json.dumps(filtered_data, ensure_ascii=False,sort_keys=True).replace(" ","")print("待加密数据:", jsonStr)return jsonStr
package demo.code.utils;import com.alibaba.fastjson.JSON;
import com.alibaba.fastjson.serializer.SerializerFeature;import javax.crypto.Cipher;
import javax.crypto.KeyGenerator;
import javax.crypto.Mac;
import javax.crypto.SecretKey;
import javax.crypto.spec.IvParameterSpec;
import javax.crypto.spec.SecretKeySpec;
import java.nio.charset.StandardCharsets;
import java.security.*;
import java.security.spec.InvalidKeySpecException;
import java.security.spec.PKCS8EncodedKeySpec;
import java.security.spec.X509EncodedKeySpec;
import java.util.Base64;
import java.util.Map;/*** Ji签名工具类** @author jisl on 2023/6/8 14:03*/
public class JiDigitUtil {private static final String HMAC_SHA_256_ALGORITHM = "HmacSHA256";private static final String RSA_ALGORITHM = "RSA";private static final String RSA_SHA_ALGORITHM = "SHA256WithRSA";private static final String ECC_ALGORITHM = "EC";private static final String ECDSA_SHA_ALGORITHM = "SHA384withECDSA";private static final String AES_ALGORITHM = "AES";private static final String AES_CIPHER_TRANSFORMATION = "AES/CBC/PKCS5Padding";public static final String AUTH_STRING_PREFIX = "AuthStringPrefix";public static final String AUTHORIZATION = "Authorization";public static final String ENCRYPT_VERSION_FIELD = "jiEncryptVersion";public static final String CIPHER_TEXT_FIELD = "ciphertext";/*** * HMAC(Hash-based Message Authentication Code,散列消息认证码)是一种使用密码散列函数,同时结合一个加密密钥,通过特别计算方式之后产生的消息认证码(MAC)。它可以用来保证数据的完整性,同时可以用来作某个消息的身份验证。* * JWT 默认的签名算法 HMAC SHA256** @param secretKey secretKey* @param data      内容* @return java.lang.String* @author jisl on 2023/12/22 19:09**/private static String hmacSha256(String secretKey, String data) {try {// 2. 初始化Mac对象Mac hmacSha256 = Mac.getInstance(HMAC_SHA_256_ALGORITHM);SecretKeySpec secretKeySpec = new SecretKeySpec(secretKey.getBytes(StandardCharsets.UTF_8), HMAC_SHA_256_ALGORITHM);hmacSha256.init(secretKeySpec);// 3. 计算HMACbyte[] hmacBytes = hmacSha256.doFinal(data.getBytes(StandardCharsets.UTF_8));return bytesToHex(hmacBytes);} catch (NoSuchAlgorithmException | InvalidKeyException e) {e.printStackTrace();throw new RuntimeException(e.getMessage());}}public static String hmacSha256(String secretKey, String authStringPrefix, Map<String, Object> data) {
//        生成signingKeyString signingKey = hmacSha256(secretKey, authStringPrefix);
//        内容生成摘要return hmacSha256(signingKey, getBodyDataStr(data));}public static String getBodyDataStr(Map<String, Object> data) {// 使用 TreeMap 保证key从小到大排序String jsonString = JSON.toJSONString(data, SerializerFeature.MapSortField);System.out.println("待加密数据:" + jsonString);return jsonString;}/*** 私钥签名** @param data             内容* @param privateKeyBase64 私钥* @param algorithm        算法* @param signatureName    签名算法* @return java.lang.String* @author jisl on 2023/12/23 14:07**/private static String sign(String data, String privateKeyBase64, String algorithm, String signatureName) {byte[] privateKeyBytes = Base64.getDecoder().decode(privateKeyBase64);PKCS8EncodedKeySpec keySpec = new PKCS8EncodedKeySpec(privateKeyBytes);try {KeyFactory keyFactory = KeyFactory.getInstance(algorithm);PrivateKey privateKey = keyFactory.generatePrivate(keySpec);Signature signature = Signature.getInstance(signatureName);signature.initSign(privateKey);signature.update(data.getBytes(StandardCharsets.UTF_8));byte[] signatureBytes = signature.sign();return Base64.getEncoder().encodeToString(signatureBytes);} catch (NoSuchAlgorithmException | InvalidKeySpecException | InvalidKeyException | SignatureException e) {e.printStackTrace();throw new RuntimeException(e.getMessage());}}/*** RSA私钥签名** @param authStringPrefix 签名关键信息前缀* @param bodyData              内容* @param privateKeyBase64      私钥* @return java.lang.String* @author jisl on 2023/12/22 19:41**/public static String signRsa(String authStringPrefix, Map<String, Object> bodyData, String privateKeyBase64) {
//        data=authStringPrefix+bodyDatareturn sign(authStringPrefix + getBodyDataStr(bodyData), privateKeyBase64, RSA_ALGORITHM, RSA_SHA_ALGORITHM);}/*** ECDSA私钥签名** @param authStringPrefix 签名关键信息前缀* @param bodyData              内容* @param privateKeyBase64      私钥* @return java.lang.String* @author jisl on 2023/12/22 19:41**/public static String signEcdsa(String authStringPrefix, Map<String, Object> bodyData, String privateKeyBase64) {
//        data=authStringPrefix+bodyDatareturn sign(authStringPrefix + getBodyDataStr(bodyData), privateKeyBase64, ECC_ALGORITHM, ECDSA_SHA_ALGORITHM);}/*** 使用公钥进行验证签名** @param data            内容* @param signature       签名* @param publicKeyBase64 公钥* @param algorithm       算法* @param signatureName   签名类型* @return boolean* @author jisl on 2023/12/23 14:05**/private static boolean verify(String data, String signature, String publicKeyBase64, String algorithm, String signatureName) {byte[] publicKeyBytes = Base64.getDecoder().decode(publicKeyBase64);X509EncodedKeySpec keySpec = new X509EncodedKeySpec(publicKeyBytes);try {KeyFactory keyFactory = KeyFactory.getInstance(algorithm);PublicKey publicKey = keyFactory.generatePublic(keySpec);Signature signatureVerifier = Signature.getInstance(signatureName);signatureVerifier.initVerify(publicKey);signatureVerifier.update(data.getBytes());byte[] signatureBytes = Base64.getDecoder().decode(signature);return signatureVerifier.verify(signatureBytes);} catch (NoSuchAlgorithmException | InvalidKeySpecException | InvalidKeyException | SignatureException e) {e.printStackTrace();throw new RuntimeException(e.getMessage());}}/*** RSA公钥验证签名** @param authStringPrefix 签名关键信息前缀* @param bodyData              bodyData* @param signature             签名* @param publicKeyBase64       公钥* @return boolean* @author jisl on 2023/12/22 19:43**/public static boolean verifyRsa(String authStringPrefix, Map<String, Object> bodyData, String signature, String publicKeyBase64) {
//        data=authStringPrefix+bodyDatareturn verify(authStringPrefix + getBodyDataStr(bodyData), signature, publicKeyBase64, RSA_ALGORITHM, RSA_SHA_ALGORITHM);}/*** ECDSA公钥验证签名** @param authStringPrefix 签名关键信息前缀* @param bodyData              bodyData* @param signature             签名* @param publicKeyBase64       公钥* @return boolean* @author jisl on 2023/12/22 19:43**/public static boolean verifyEcdsa(String authStringPrefix, Map<String, Object> bodyData, String signature, String publicKeyBase64) {
//        data=authStringPrefix+bodyDatareturn verify(authStringPrefix + getBodyDataStr(bodyData), signature, publicKeyBase64, ECC_ALGORITHM, ECDSA_SHA_ALGORITHM);}/*** AES 加密操作** @param plaintext       明文* @param secretKeyBase64 密钥* @param ivStr           偏移量  偏移量通常称为初始化向量(Initialization Vector,IV)。初始化向量是在使用某些分组密码工作模式时引入的一种附加输入,用于增加加密的随机性和安全性。随密文一起传输。(随机数)* @return java.lang.String 密文* @author jisl on 2022/2/16 13:53**/public static String encryptAes(String plaintext, String secretKeyBase64, String ivStr) {try {// 创建密码器 算法/工作模式/填充模式(algorithm/mode/padding)Cipher cipher = Cipher.getInstance(AES_CIPHER_TRANSFORMATION);byte[] byteContent = plaintext.getBytes(StandardCharsets.UTF_8);// 初始化为加密模式的密码器cipher.init(Cipher.ENCRYPT_MODE, new SecretKeySpec(Base64.getDecoder().decode(secretKeyBase64), AES_ALGORITHM), new IvParameterSpec(ivStr.getBytes()));// 加密byte[] result = cipher.doFinal(byteContent);//通过Base64转码返回return Base64.getEncoder().encodeToString(result);} catch (Exception ex) {throw new RuntimeException(ex.getMessage());}}/*** AES 解密操作** @param ciphertext      密文* @param secretKeyBase64 密钥* @param ivStr           iv* @return java.lang.String* @author jisl on 2023/12/23 14:03**/public static String decryptAes(String ciphertext, String secretKeyBase64, String ivStr) {try {//实例化Cipher cipher = Cipher.getInstance(AES_CIPHER_TRANSFORMATION);//使用密钥初始化,设置为解密模式cipher.init(Cipher.DECRYPT_MODE, new SecretKeySpec(Base64.getDecoder().decode(secretKeyBase64), AES_ALGORITHM), new IvParameterSpec(ivStr.getBytes()));//执行操作byte[] result = cipher.doFinal(Base64.getDecoder().decode(ciphertext));return new String(result, StandardCharsets.UTF_8);} catch (Exception ex) {throw new RuntimeException(ex.getMessage());}}/*** 生成Appkey (前两位组织代码)** @return java.lang.String* @author jisl on 2023/12/22 19:57**/private static String generateAppKey() {return "ji" + createNonce(16);}private static String genSecretKey(String algorithmName) {try {// 创建一个安全随机数生成器KeyGenerator keyGenerator = KeyGenerator.getInstance(algorithmName);// 初始化密钥生成器,使用安全随机数生成器SecretKey secretKey = keyGenerator.generateKey();// 打印生成的密钥return Base64.getEncoder().encodeToString(secretKey.getEncoded());} catch (NoSuchAlgorithmException e) {e.printStackTrace();throw new RuntimeException(e.getMessage());}}private static String[] genKeyPair(String algorithm) {// KeyPairGenerator类用于生成公钥和私钥对,基于RSA算法生成对象KeyPairGenerator keyPairGen;try {keyPairGen = KeyPairGenerator.getInstance(algorithm);} catch (NoSuchAlgorithmException e) {throw new RuntimeException(e.getMessage());}// 生成一个密钥对,保存在keyPair中KeyPair keyPair = keyPairGen.generateKeyPair();// 得到公钥PublicKey publicKey = keyPair.getPublic();// 得到私钥PrivateKey privateKey = keyPair.getPrivate();String publicKeyString = Base64.getEncoder().encodeToString(publicKey.getEncoded());String privateKeyString = Base64.getEncoder().encodeToString(privateKey.getEncoded());return new String[]{publicKeyString, privateKeyString};}private static final char[] HEX_ARRAY = "0123456789abcdef".toCharArray();private static String bytesToHex(byte[] bytes) {char[] hexChars = new char[bytes.length * 2];for (int i = 0; i < bytes.length; i++) {int v = bytes[i] & 0xFF;hexChars[i * 2] = HEX_ARRAY[v >>> 4];hexChars[i * 2 + 1] = HEX_ARRAY[v & 0x0F];}return new String(hexChars);}private static final char[] SYMBOLS ="0123456789abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ".toCharArray();private static final SecureRandom RANDOM = new SecureRandom();/*** 使用SecureRandom生成随机串** @param length 随机串长度* @return nonce 随机串*/public static String createNonce(int length) {char[] buf = new char[length];for (int i = 0; i < length; ++i) {buf[i] = SYMBOLS[RANDOM.nextInt(SYMBOLS.length)];}return new String(buf);}private static void genJiKey() {System.out.println("AppKey:" + generateAppKey());System.out.println("AppSecret:" + genSecretKey(HMAC_SHA_256_ALGORITHM));System.out.println("AESSecretKey:" + genSecretKey(AES_ALGORITHM));String[] keyPair = genKeyPair(RSA_ALGORITHM);System.out.println("publicKey:" + keyPair[0]);System.out.println("privateKey:" + keyPair[1]);String[] eccKeyPair = genKeyPair(ECC_ALGORITHM);System.out.println("ECC_publicKey:" + eccKeyPair[0]);System.out.println("ECC_privateKey:" + eccKeyPair[1]);}public static void main(String[] args) {genJiKey();}}

验签和解密拦截器

用拦截器实现对开放接口签名验证和内容解密。在Java的Servlet中,HttpServletRequest对象中的getReader()和getInputStream()方法用于读取请求体(Request Body)中的数据,只能读取一次。也就是拦截器读取了Request Body内容,实际业务接口Controller就会读不到内容,这边需要重写HttpServletRequest将Request Body存起来。具体实现查看Request Body内容多次读取实现

同时如果使用AES加密,希望在接口那看到是解密后的业务内容。也需要将Request Body内容修改。

为了将业务功能和验签功能解耦,将所有验签的功能字段放在Http请求的requestHeader中。使用两个字段AuthStringPrefix和Authorization(签名)

考虑到有时候业务内容并没有敏感字段,比如用户手机号,银行卡号等敏感字段,有时候并不需要对内容进行加密。这边通过报文内容是否有“jiEncryptVersion”字段来判断内容是否有加密,同时根据“jiEncryptVersion”值来判断使用的加密算法。

这边验签和加密都比较灵活都可以判断字段修改来进行扩展。

package demo.code.interceptor;import cn.hutool.core.util.ArrayUtil;
import com.alibaba.fastjson.JSON;
import demo.code.annotation.JiSign;
import demo.code.config.ReReadableHttpServletRequest;
import demo.code.utils.JiDigitUtil;
import lombok.extern.slf4j.Slf4j;
import org.apache.commons.lang3.StringUtils;
import org.springframework.stereotype.Component;
import org.springframework.web.method.HandlerMethod;
import org.springframework.web.servlet.HandlerInterceptor;import javax.servlet.http.HttpServletRequest;
import javax.servlet.http.HttpServletResponse;
import java.util.Map;
import java.util.concurrent.TimeUnit;/*** 统一处理过滤请求:校验签名是否合法、校验时间戳是否超过30s** @author jisl on 2023/12/23 9:56**/
@Component
@Slf4j
public class SignInterceptor implements HandlerInterceptor {private static final String[] APP_KEYS = {"jixcpXjOphmNQ9hI44"};private static final String[] APP_SECRETS = {"MD8ACw7xVEmBl+deSo9pLyJo3bccRJe+rFGhXvbDxhs="};private static final String APP_CIPHER_SECRET = "2sYjMi6eo8349FIqZckLOA==";private static final String PUBLIC_KEY = "MIIBIjANBgkqhkiG9w0BAQEFAAOCAQ8AMIIBCgKCAQEAnVm37WXNHicyB9J8Deb/vgw156OKiTbB5HwDOiwkbhckIrayDCQCVGDN2xM1C2NGBYjZYRlz9EnxB7u1JgaANA20nHsddVg8RDZgwOdAq3UqFSQ1gNTpHjqkJFcczLw6gtIMg7oMcSfKBasIHdhS9/MS4C8GmfllWQhIq/wpjMXau88aciluhU7AYjD10LnyeUC8k7plzBaAH2gDuRoa02UYGRbOvsbkzqPQWeCwaKbjc8iKiTgPnge5sTPC4qar46fpn0caJLvzhgNAfx0SsxC8VmmUl33WSqL2JhhRKBClm35x58/FuGNeavdi6ohijBbFPZkWnEd5mPd4TFE1WwIDAQAB";private static final String ECC_PUBLIC_KEY = "MFkwEwYHKoZIzj0CAQYIKoZIzj0DAQcDQgAEhiq5DUdt3RxE4+uPN1Mct0OB7C1mYlnVz29/Ud5/8yXenWxPc5ApAhOcQkbOxsYuzLjjjpJ+RvVoJSuldxP+fQ==";private static final String HMAC_SHA_SIGN_VERSION = "ji-HmacSHA256-v1";private static final String RSA_SHA_SIGN_VERSION = "ji-SHA256WithRSA-v1";private static final String ECDSA_SHA_SIGN_VERSION = "ji-SHA384withECDSA-v1";private static final String AES_ENCRYPT_VERSION = "AES-v1";@Overridepublic boolean preHandle(HttpServletRequest request, HttpServletResponse response, Object handler) {HandlerMethod method = (HandlerMethod) handler;//获取方法上的注解JiSign nldSign = method.getMethodAnnotation(JiSign.class);if (nldSign == null) {return true;}ReReadableHttpServletRequest reReadableHttpServletRequest = (ReReadableHttpServletRequest) request;String requestBody = reReadableHttpServletRequest.getBody();if (StringUtils.isBlank(requestBody)) {throw new RuntimeException("签名参数异常");}Map<String, Object> bodyData;try {bodyData = JSON.parseObject(requestBody, Map.class);} catch (Exception ex) {log.error("签名全局拦截器请求消息转化出现异常", ex);throw new RuntimeException("签名异常");}final String authStringPrefix = request.getHeader(JiDigitUtil.AUTH_STRING_PREFIX);final String signature = request.getHeader(JiDigitUtil.AUTHORIZATION);if (StringUtils.isEmpty(signature) || StringUtils.isEmpty(authStringPrefix)) {throw new RuntimeException("签名参数不正确");}final String[] prefixes = authStringPrefix.split("/");if (prefixes.length != 4) {throw new RuntimeException("签名格式不正确");}//        authStringPrefix,格式为:ji-HmacSHA256-v1/{appKey}/{timestamp}/{nonce}String jiSignVersion = prefixes[0], appKey = prefixes[1], timestampStr = prefixes[2], nonce = prefixes[3];if (StringUtils.isBlank(nonce) || nonce.length() != 16) {
//            nonce同时需要用来当作AES的 iv值,所以长度一定要16字节throw new RuntimeException("签名格式不正确");}if (!ArrayUtil.contains(APP_KEYS, appKey)) {
//            实际需要从数据库或者配置文件读取,测试环境和生产环境密钥不一样。throw new RuntimeException("AppKey不正确");}if (TimeUnit.MILLISECONDS.toMinutes(Math.abs(System.currentTimeMillis() - Long.parseLong(timestampStr))) > 5) {throw new RuntimeException("签名时间超过5分钟");}verifySignature(bodyData, authStringPrefix, signature, jiSignVersion);decryptData(reReadableHttpServletRequest, bodyData, nonce);return true;}/*** 验证签名** @author jisl on 2023/12/24 9:02**/private void verifySignature(Map<String, Object> bodyData, String authStringPrefix, String signature, String jiSignVersion) {boolean verify;if (jiSignVersion.equalsIgnoreCase(HMAC_SHA_SIGN_VERSION)) {final String appSecret = APP_SECRETS[0];final String mySignature = JiDigitUtil.hmacSha256(appSecret, authStringPrefix, bodyData);verify = mySignature.equalsIgnoreCase(signature);} else if (jiSignVersion.equalsIgnoreCase(RSA_SHA_SIGN_VERSION)) {verify = JiDigitUtil.verifyRsa(authStringPrefix, bodyData, signature, PUBLIC_KEY);} else if (jiSignVersion.equalsIgnoreCase(ECDSA_SHA_SIGN_VERSION)) {verify = JiDigitUtil.verifyEcdsa(authStringPrefix, bodyData, signature, ECC_PUBLIC_KEY);} else {throw new RuntimeException("签名类型不正确");}if (!verify) {throw new RuntimeException("签名不正确");}}/*** 解密报文** @author jisl on 2023/12/24 9:03**/private void decryptData(ReReadableHttpServletRequest reReadableHttpServletRequest, Map<String, Object> bodyData, String nonce) {String encryptVersion = (String) bodyData.getOrDefault(JiDigitUtil.ENCRYPT_VERSION_FIELD, "");if (AES_ENCRYPT_VERSION.equals(encryptVersion)) {String ciphertext = (String) bodyData.get(JiDigitUtil.CIPHER_TEXT_FIELD);String plaintext = JiDigitUtil.decryptAes(ciphertext, APP_CIPHER_SECRET, nonce);reReadableHttpServletRequest.setBody(plaintext);}}}

请求开放接口

实际使用如果按照规范是Hmac验签那么密钥由平台生成,而如果是公钥密码(RSA,ECC)实际由商户生成密钥对。商户保存私钥,将公钥发给平台。这样才达到密钥配送问题,而之前对接过的平台都是自己生成。那么直接用Hmac就好了,失去了意义公钥密码的意义。
平台不负责生商户签名的密钥对,同时也不能知道商户的私钥。

这边实现的验签有HmacSHA256,SHA256WithRSa,SHA384withECDSA。加密算法:AES。大家可以选择适合自己的方式使用。目前HmacSHA256,SHA256WithRSa用的比较多,将来会是HmacSHA256,SHA384withECDSA。

package demo.manager.sign;import com.alibaba.fastjson.JSON;
import com.alibaba.fastjson.JSONObject;
import demo.code.utils.JiDigitUtil;
import org.junit.jupiter.api.Test;
import org.springframework.http.*;
import org.springframework.web.client.RestTemplate;import java.util.HashMap;
import java.util.Map;
import java.util.TreeMap;/*** 简单的java demo程序,演示如何调用Ji API接口。** @author jisl on 2023/12/22 19:46**/
public class JiSignDemo {private static final String APP_KEY = "jixcpXjOphmNQ9hI44";private static final String APP_SECRET = "MD8ACw7xVEmBl+deSo9pLyJo3bccRJe+rFGhXvbDxhs=";private static final String APP_CIPHER_SECRET = "2sYjMi6eo8349FIqZckLOA==";private static final String HMAC_SHA_SIGN_VERSION = "ji-HmacSHA256-v1";private static final String RSA_SHA_SIGN_VERSION = "ji-SHA256WithRSa-v1";private static final String ECC_SHA_SIGN_VERSION = "ji-SHA384withECDSA-v1";private static final String AES_ENCRYPT_VERSION = "AES-v1";private static final String PUBLIC_KEY = "MIIBIjANBgkqhkiG9w0BAQEFAAOCAQ8AMIIBCgKCAQEAnVm37WXNHicyB9J8Deb/vgw156OKiTbB5HwDOiwkbhckIrayDCQCVGDN2xM1C2NGBYjZYRlz9EnxB7u1JgaANA20nHsddVg8RDZgwOdAq3UqFSQ1gNTpHjqkJFcczLw6gtIMg7oMcSfKBasIHdhS9/MS4C8GmfllWQhIq/wpjMXau88aciluhU7AYjD10LnyeUC8k7plzBaAH2gDuRoa02UYGRbOvsbkzqPQWeCwaKbjc8iKiTgPnge5sTPC4qar46fpn0caJLvzhgNAfx0SsxC8VmmUl33WSqL2JhhRKBClm35x58/FuGNeavdi6ohijBbFPZkWnEd5mPd4TFE1WwIDAQAB";private static final String PRIVATE_KEY = "MIIEwAIBADANBgkqhkiG9w0BAQEFAASCBKowggSmAgEAAoIBAQCdWbftZc0eJzIH0nwN5v++DDXno4qJNsHkfAM6LCRuFyQitrIMJAJUYM3bEzULY0YFiNlhGXP0SfEHu7UmBoA0DbScex11WDxENmDA50CrdSoVJDWA1OkeOqQkVxzMvDqC0gyDugxxJ8oFqwgd2FL38xLgLwaZ+WVZCEir/CmMxdq7zxpyKW6FTsBiMPXQufJ5QLyTumXMFoAfaAO5GhrTZRgZFs6+xuTOo9BZ4LBopuNzyIqJOA+eB7mxM8Lipqvjp+mfRxoku/OGA0B/HRKzELxWaZSXfdZKovYmGFEoEKWbfnHnz8W4Y15q92LqiGKMFsU9mRacR3mY93hMUTVbAgMBAAECggEBAJM5Kc12bbfjxnzpJOdBFlI0TI9iRjHiSQJGQiLAwIk9H7NXKzoOyxs1xAZRb1UrWo29IF8omISeVmX87B4bAQMzO6UZ0FNF7oWIN3jmJm0GYAyN532WOFPvXDsgQrMQ/tH+RHVPs2qAZxZsk1iRWffit+uING4Gmhv2k0BP3HR4BUZ3V1haBBl/aD2zNZ/HEdeNT373xKoHpB7L/ExPA2xUBdfYUsDjXAu9OTOEypqBogMki3B8UzyeXckRVB7FRlc4ro9RRSyMhDt3CcbnQ2DCAEBkNDnwmpR9kgD3A3iWb8gDChhgBukGNKytl+nKD4WBCPzGB2nW9Am6LjL0RZECgYEA38SZWiLFSJ6dz4T+L7CkemyLBC4q3N37xqVrCo/SyAU8oWt500p10Ht3O2YDsifCh4ZvKZMRaBnESsn8i305lhbnjYas57NIZm10Hv9x3awJlJ/zFm8rYsLsMSuosM7S+aEVFzm5Y6DlzKtefLOurOUcOTZt1KB3UCulWjjsHbMCgYEAtAP8mhlBAIAWRUvrLnwt06tODJyFoSDrfCqcTrlvq7MlCQibv4bx0SrPaT4Z5VyO2u6v2/DrvD20jNXX77ErYmx0DVMFssi/5OK0whPjwk47t4k4zEK+4z7ervlwK+RJNJl6zxloF8hfKBHogw4MojHmZIruvN8+Kp5DF6VXxbkCgYEAx9jK6SByt9fJs4PgjHEjhJ8aTSJ6b4XmDlTAU899fdyHeNcHF7jBnfAW3brPhDZUWzuqXiQWALY6hFz/Kwks/Cn6pYBpPgn1mF0av3B+nm6+o7lynk+tHOhfj86hOz+MVxwRPQv10c/qTK+klJTZZVq9qS8+Sg8CYFyKshhycFcCgYEAihScyB8i6x1U9+aaCVgbVseJ4MaXAddciiutJf27mLqbZ+iAf1MlXco2uoV2G6b5tRltL4oHaKb0PSsyrZr6qk6CXk4WiuLNvuXevRfAlqSnzcqmFJgGEA2DhjezQRekx4IK35yfac66nuPNs+ks66TXErw3EA01hD9NO3AKeZECgYEAmQhzScIuM06qF/NX2WZ6p9TGZRFC7g/ueqBxS/2c9jpzUFRUY38M4fUQfj7w+gW5j95dMK+l4SHaP9PJOK8CFxjDoehAyR+G3TAFjRywEW7vaklHNNr01t/pp3vnkSvFZOtCIsyXD+QFJH5iMGVJMoXB0pBjRGqDmH91a4fTn/Q=";private static final String ECC_PUBLIC_KEY = "MFkwEwYHKoZIzj0CAQYIKoZIzj0DAQcDQgAEhiq5DUdt3RxE4+uPN1Mct0OB7C1mYlnVz29/Ud5/8yXenWxPc5ApAhOcQkbOxsYuzLjjjpJ+RvVoJSuldxP+fQ==";private static final String ECC_RIVATE_KEY = "MEECAQAwEwYHKoZIzj0CAQYIKoZIzj0DAQcEJzAlAgEBBCCnAVgWbOaERLZqi96mmmMl3eyGUmr4PfgzhaXeEq6tHg==";@Testpublic void helloSignByHmac() {Map<String, Object> param = new HashMap<>();param.put("couponAmount", 1500);param.put("openid", "m6VOUP8x7DNeK5svrq5uo5Ak+ekSo2Clu27DD2eue5c");param.put("outTradeSn", "60938d0cf36577cd35e5a5e8");param.put("couponType", 1);request("http://localhost:8091/api/v1/hello", param, HMAC_SHA_SIGN_VERSION, "");}@Testpublic void helloSignByHmacAES() {Map<String, Object> param = new HashMap<>();param.put("couponAmount", 1500);param.put("openid", "m6VOUP8x7DNeK5svrq5uo5Ak+ekSo2Clu27DD2eue5c");param.put("outTradeSn", "60938d0cf36577cd35e5a5e8");param.put("couponType", 1);param.put("enstr", "AES加密");param.put("nullStr", null);param.put("emptyStr", "");request("http://localhost:8091/api/v1/hello", param, HMAC_SHA_SIGN_VERSION, AES_ENCRYPT_VERSION);}@Testpublic void helloSignByRsa() {Map<String, Object> param = new HashMap<>();param.put("couponAmount", 1500);param.put("openid", "m6VOUP8x7DNeK5svrq5uo5Ak+ekSo2Clu27DD2eue5c");param.put("outTradeSn", "60938d0cf36577cd35e5a5e8");param.put("couponType", 1);param.put("payTime", System.currentTimeMillis());request("http://localhost:8091/api/v1/hello", param, RSA_SHA_SIGN_VERSION, "");}@Testpublic void helloSignByEcdsa() {Map<String, Object> param = new HashMap<>();param.put("couponAmount", 1500);param.put("openid", "m6VOUP8x7DNeK5svrq5uo5Ak+ekSo2Clu27DD2eue5c");param.put("outTradeSn", "60938d0cf36577cd35e5a5e8");param.put("couponType", 1);param.put("payTime", System.currentTimeMillis());request("http://localhost:8091/api/v1/hello", param, ECC_SHA_SIGN_VERSION, "");}private JSONObject request(String url, Map<String, Object> requestBody, String signVersion, String encryptVersion) {String timestamp = String.valueOf(System.currentTimeMillis());String nonce = JiDigitUtil.createNonce(16);if (AES_ENCRYPT_VERSION.equals(encryptVersion)) {String bodyDataStr = JiDigitUtil.getBodyDataStr(requestBody);String encryptedData = JiDigitUtil.encryptAes(bodyDataStr, APP_CIPHER_SECRET, nonce);TreeMap<String, Object> treeMap = new TreeMap<>();treeMap.put(JiDigitUtil.ENCRYPT_VERSION_FIELD, AES_ENCRYPT_VERSION);treeMap.put(JiDigitUtil.CIPHER_TEXT_FIELD, encryptedData);requestBody = treeMap;}
//        authString,格式为:ji-HmacSHA256-v1/{appKey}/{timestamp}/{nonce}String authStringPrefix = String.format("%s/%s/%s/%s", signVersion, APP_KEY, timestamp, nonce);String signature;if (signVersion.equals(HMAC_SHA_SIGN_VERSION)) {signature = JiDigitUtil.hmacSha256(APP_SECRET, authStringPrefix, requestBody);} else if (signVersion.equals(RSA_SHA_SIGN_VERSION)) {signature = JiDigitUtil.signRsa(authStringPrefix, requestBody, PRIVATE_KEY);} else if (signVersion.equals(ECC_SHA_SIGN_VERSION)) {signature = JiDigitUtil.signEcdsa(authStringPrefix, requestBody, ECC_RIVATE_KEY);} else {throw new RuntimeException("签名类型不支持");}HttpHeaders requestHeaders = new HttpHeaders();requestHeaders.setContentType(MediaType.APPLICATION_JSON);requestHeaders.set(JiDigitUtil.AUTH_STRING_PREFIX, authStringPrefix);requestHeaders.set(JiDigitUtil.AUTHORIZATION, signature);HttpEntity requestEntity = new HttpEntity<>(requestBody, requestHeaders);System.out.println("请求入参:" + JSON.toJSONString(requestBody));System.out.println("requestHeaders:" + JSON.toJSONString(requestHeaders));RestTemplate restTemplate = new RestTemplate();ResponseEntity<JSONObject> response = restTemplate.exchange(url, HttpMethod.POST, requestEntity, JSONObject.class);final JSONObject body = response.getBody();System.out.println("请求返回:" + body);return body;}}

总结

以上就是对密码学的接口以及开放接口验签和加密的实现,这边写这边博客其一是对密码学做个总结方便加深印象。其二是将开放接口实现公开,可以让大家看看是否有安全漏洞。

这篇关于密码学总结,实现开放接口验签和加密的文章就介绍到这儿,希望我们推荐的文章对编程师们有所帮助!



http://www.chinasem.cn/article/614563

相关文章

使用Java解析JSON数据并提取特定字段的实现步骤(以提取mailNo为例)

《使用Java解析JSON数据并提取特定字段的实现步骤(以提取mailNo为例)》在现代软件开发中,处理JSON数据是一项非常常见的任务,无论是从API接口获取数据,还是将数据存储为JSON格式,解析... 目录1. 背景介绍1.1 jsON简介1.2 实际案例2. 准备工作2.1 环境搭建2.1.1 添加

Java实现任务管理器性能网络监控数据的方法详解

《Java实现任务管理器性能网络监控数据的方法详解》在现代操作系统中,任务管理器是一个非常重要的工具,用于监控和管理计算机的运行状态,包括CPU使用率、内存占用等,对于开发者和系统管理员来说,了解这些... 目录引言一、背景知识二、准备工作1. Maven依赖2. Gradle依赖三、代码实现四、代码详解五

java如何分布式锁实现和选型

《java如何分布式锁实现和选型》文章介绍了分布式锁的重要性以及在分布式系统中常见的问题和需求,它详细阐述了如何使用分布式锁来确保数据的一致性和系统的高可用性,文章还提供了基于数据库、Redis和Zo... 目录引言:分布式锁的重要性与分布式系统中的常见问题和需求分布式锁的重要性分布式系统中常见的问题和需求

SpringBoot基于MyBatis-Plus实现Lambda Query查询的示例代码

《SpringBoot基于MyBatis-Plus实现LambdaQuery查询的示例代码》MyBatis-Plus是MyBatis的增强工具,简化了数据库操作,并提高了开发效率,它提供了多种查询方... 目录引言基础环境配置依赖配置(Maven)application.yml 配置表结构设计demo_st

python使用watchdog实现文件资源监控

《python使用watchdog实现文件资源监控》watchdog支持跨平台文件资源监控,可以检测指定文件夹下文件及文件夹变动,下面我们来看看Python如何使用watchdog实现文件资源监控吧... python文件监控库watchdogs简介随着Python在各种应用领域中的广泛使用,其生态环境也

el-select下拉选择缓存的实现

《el-select下拉选择缓存的实现》本文主要介绍了在使用el-select实现下拉选择缓存时遇到的问题及解决方案,文中通过示例代码介绍的非常详细,对大家的学习或者工作具有一定的参考学习价值,需要的... 目录项目场景:问题描述解决方案:项目场景:从左侧列表中选取字段填入右侧下拉多选框,用户可以对右侧

Java中的密码加密方式

《Java中的密码加密方式》文章介绍了Java中使用MD5算法对密码进行加密的方法,以及如何通过加盐和多重加密来提高密码的安全性,MD5是一种不可逆的哈希算法,适合用于存储密码,因为其输出的摘要长度固... 目录Java的密码加密方式密码加密一般的应用方式是总结Java的密码加密方式密码加密【这里采用的

Python pyinstaller实现图形化打包工具

《Pythonpyinstaller实现图形化打包工具》:本文主要介绍一个使用PythonPYQT5制作的关于pyinstaller打包工具,代替传统的cmd黑窗口模式打包页面,实现更快捷方便的... 目录1.简介2.运行效果3.相关源码1.简介一个使用python PYQT5制作的关于pyinstall

使用Python实现大文件切片上传及断点续传的方法

《使用Python实现大文件切片上传及断点续传的方法》本文介绍了使用Python实现大文件切片上传及断点续传的方法,包括功能模块划分(获取上传文件接口状态、临时文件夹状态信息、切片上传、切片合并)、整... 目录概要整体架构流程技术细节获取上传文件状态接口获取临时文件夹状态信息接口切片上传功能文件合并功能小

python实现自动登录12306自动抢票功能

《python实现自动登录12306自动抢票功能》随着互联网技术的发展,越来越多的人选择通过网络平台购票,特别是在中国,12306作为官方火车票预订平台,承担了巨大的访问量,对于热门线路或者节假日出行... 目录一、遇到的问题?二、改进三、进阶–展望总结一、遇到的问题?1.url-正确的表头:就是首先ur