前言
与第三方对接最麻烦的是语言不同,因语言不同内置实现相关标准加密算法还是略微有所差异,对接单点登录场景再寻常不过,由于时间紧迫且对接方使用Java,所以留给我对接开发和联调的时间本就不多,于是乎,在熬夜发版后,继而开始提前研究对接方所提供的加密方式大致处理
方案一(C#实现)
数据对接加密算法采用RSA SHA1 1024位、同时呢,在Java中对于1024或其他位数,对密文有长度限制,所以利用了分段加密,密文长度为117,解密长度为128,如此通用处理方式,网上肯定是可以搜索到的,截取加密部分片段,如下:
public static byte[] encryptByPublicKey(byte[] data, String publicKey) throws Exception { byte[] keyBytes = Base64.getDecoder().decode(publicKey); X509EncodedKeySpec x509KeySpec = new X509EncodedKeySpec(keyBytes); KeyFactory keyFactory = KeyFactory.getInstance(KEY_ALGORITHM); Key publicK = keyFactory.generatePublic(x509KeySpec); Cipher cipher = Cipher.getInstance(keyFactory.getAlgorithm()); cipher.init(Cipher.ENCRYPT_MODE, publicK); int inputLen = data.length; ByteArrayOutputStream out = new ByteArrayOutputStream(); int offSet = 0; byte[] cache; int i = 0; while (inputLen - offSet > 0) { if (inputLen - offSet > 117) { cache = cipher.doFinal(data, offSet, 117); } else { cache = cipher.doFinal(data, offSet, inputLen - offSet); } out.write(cache, 0, cache.length); i++; offSet = i * 117; } byte[] encryptedData = out.toByteArray(); out.close(); return encryptedData; }
当然对于密钥在从证书中导出来肯定是二进制的,那么各个对接方最后转换为字符串方式有多种,比如base64、再比如转换为16进制等等,大同小异,这里就不展开了
了解完Java完整代码实现,我们需要对相关业务参数利用对接方所提供的公钥进行加密从而形成签名,以此请求时,将我们本地生成的公钥传递过去,在响应后进行验签,然后通过私钥解密
好了,讲到这里,我们假设已经本地生成证书,然后我们导出RSA公钥和私钥,伪代码如下:
var certificate = new X509Certificate2("pfx_path", "password", X509KeyStorageFlags.Exportable); var rsa = certificate.GetRSAPrivateKey(); var privateKey = rsa.ExportRSAPrivateKey(); var publicKey = rsa.ExportRSAPublicKey();
那么我们如何利用对接方公钥进行加密呢?接下来在.NET Core中实现就需要解决上述加密算法出现的两个问题
1. Java中可以通过RSA密钥(公钥或私钥)字符串转换为RSA密钥类,.NET Core提供了?
2. 获取到RSA密钥类,调用对应实例doFinal方法进行分段加密,那么是否可以通过.NET Core中的RSA加密方法,也分段加密,结果是否一致?对接方通过公钥字符串加载RSA 公钥,代码如下:
public static RSAPublicKey loadPublicKeyByStr(String publicKeyStr) { byte[] buffer = Util.hexToByte(publicKeyStr); KeyFactory keyFactory = KeyFactory.getInstance("RSA"); X509EncodedKeySpec keySpec = new X509EncodedKeySpec(buffer); return (RSAPublicKey) keyFactory.generatePublic(keySpec); }
然后稍微查看了下加载类的解释
所以我依图索骥,于是乎,大致有了如下模样
var servicePublicKey = "123456"; var rsa = RSA.Create(); rsa.ImportSubjectPublicKeyInfo(Encoding.UTF8.GetBytes(servicePublicKey), out _);
好了,目前通过导入公钥主题信息貌似拿到了RSA,接下来则是分段加密,上述方法中cipher.doFinal,应该是指定算法实例的加密处理,那我们首先截取对应数据的分段,然后调用rsa的Encrypt方法,那加密填充模式是否一致,不得而知,默认如下提供为PKCS1?
var servicePublicKey = "123456"; var plainTextData = Encoding.UTF8.GetBytes("jeffcky"); var rsa = RSA.Create(); rsa.ImportSubjectPublicKeyInfo(Encoding.UTF8.GetBytes(servicePublicKey), out _); var outputStream = new MemoryStream(); var inputLen = plainTextData.Length; int offSet = 0; int i = 0; while (inputLen - offSet > 0) { Span<byte> bytes = plainTextData; byte[] encryptData; if (inputLen - offSet > 117) { Span<byte> slicedBytes = bytes.Slice(start: offSet, length: 117); encryptData = rsa.Encrypt(slicedBytes.ToArray(), RSAEncryptionPadding.Pkcs1); } else { Span<byte> slicedBytes = bytes.Slice(start: offSet, length: inputLen - offSet); encryptData = rsa.Encrypt(slicedBytes.ToArray(), RSAEncryptionPadding.Pkcs1); } outputStream.Write(encryptData, 0, encryptData.Length); i++; offSet = i * 117; }
一顿不知其结果的操作后,经调用对接方接口,响应验签失败,反复改了几版后,依然失败
方案二(IKVM)
不再过多深究其细节实现差异,采用稳妥方式借用IKVM直接在C#中复制对接方Java代码应该可以搞定(https://github.com/ikvm-revived/ikvm)
根据经验来看,我只用到加密,应该只需要用到IKVM.OpenJDK.Core和IKVM.OpenJDK.Security两个库,于是我们在nuget上下载.NET Framework实现版本,我使用的是.NET Framework 4.7.2,想想.NET Framework 4.6+可以适配.NET Standard,那是否也可以经过编译后,通过.NET Core添加程序集引用呢?
在.NET Framework 4.7.2引入上述核心库后,在控制台测试验证加密、验签一点问题都么有,好了,接下来则是将其编译,在.NET Core 3.1中添加程序集进行调用,加载程序集方法时直接抛出大致如下异常
FileNotFoundException: Could not load file or assembly 'System.Configuration.ConfigurationManager, Version=4.0.3.0, Culture=neutral, PublicKeyToken=cc7b13ffcd2ddd51'. The system cannot find the file specified.
继续添加对应版本程序集,版本过低,在nuget上根本找不到,涉及到.NET Core基础设施,看是否在github runtimes仓库能否找到对应issue
解决了上述程序集问题,接下来再次运行,又来其他异常,.NET Core版本IO加载文件该方法已被剔除
坑一波接着一波,看来使用.NET Core引用.NET Framework程序集此路行不通,而且这仅仅只是在windows下测试,发布到linux上出现的问题无法预知,我再次翻了下,居然发现了.NET Core分支,惊喜不惊喜,查看分支就可以知道已有老外在处理迁移到.NET Core版本,只是还处于未发布状态
fork原.NET Framework版本迁移至适配.NET Core链接(https://github.com/ams-ts-ikvm/ikvm-bin/tree/net_core_compat/bin),如果只是用到相关加密,只需引用上述链接bin目录下,如下库即可
最后,还需要添加上述在.NET Framework中测试中出现异常的包以及版本(System.Configuration.ConfigurationManager),如下:
经过上述一番折腾过程,终于对接上,耗时一天多,真尼玛是不容易,搞完一个字,累的一p
总结
使用还未发布适配.NET Core的IKVM,在仅有时间内,快速对接上了Java加密,如若后续再遇到类似比较耗时耗力的加密方式,还不如使用IKVM,将更多时间花在处理业务逻辑上才是正道