因OA系统需要对exchange的邮箱服务器做单点登录,开发商(微软中国代理商)提供了一套.Net开发的单点登录方案。平时公司为了节省成本,这种单点登录的工作一般都是我们自己开发完成,而我这边OA系统(另一家开发商提供的)是Java开发的,虽然加密这类的标准是一样的,可是没有参考代码,只能盲敲调试,且OA系统还得在jsp中写Java代码,实在是有点折磨(全程nodepad++写代码,编译错误只能在上传到服务器运行时才能发现)。
以下是开发商提供的.Net加密代码:
- using System;
- using System.Security.Cryptography;
-
- namespace SignWithRSASHA256
- {
- public partial class TestForm : Form
- {
- public TestForm()
- {
- InitializeComponent();
- }
-
- private void signTest(object sender, EventArgs e)
- {
- // 123是私钥文件的密码
- X509Certificate2 cert = new X509Certificate2(@"cert\\ifcloud.pfx", "123");
- byte[] srcDataBuf = System.Text.Encoding.UTF8.GetBytes("待加密的字符串");
- RSACryptoServiceProvider RSA = (RSACryptoServiceProvider)cert.PrivateKey;
- byte[] signedDataBuf = RSA.SignData(srcDataBuf, HashAlgorithmName.SHA256, RSASignaturePadding.Pkcs1);
-
- // base64SrcData
- Console.WriteLine(HttpUtility.UrlEncode(Convert.ToBase64String(srcDataBuf)));
- // signedData
- Console.WriteLine(HttpUtility.UrlEncode(Convert.ToBase64String(signedDataBuf)));
- }
- }
- }
我要做的事情就是用Java给它实现一下。
看着这段代码不多,感觉用Java写应该也不难。但主要是对加密这块了解得太少,而且也怕1.6的jdk会有不支持的情况。
抱着学习的心态,我先去微软官方的文档查了下这段代码里面涉及到的几个类和它们的方法。
X509Certificate2 类 (System.Security.Cryptography.X509Certificates) | Microsoft Docs
X509Certificate2.PrivateKey 属性 (System.Security.Cryptography.X509Certificates) | Microsoft Docs
RSACryptoServiceProvider 类 (System.Security.Cryptography) | Microsoft Docs
RSA.SignData 方法 (System.Security.Cryptography) | Microsoft Docs
RSASignaturePadding.Pkcs1 属性 (System.Security.Cryptography) | Microsoft Docs
看完之后,模模糊糊,X509Certificate2的构造可以获取PrivateKey(私钥),最后一个SignData是进行加密签名,细节还是一无所知。
第一个想法是看下java有没有相同的类名,想着同为高级语言,类名总有相似之处吧?结果还真就没有。。。
然后就开始了艰辛的百度过程。分步走,第一步先实现获取私钥的功能,第二步再加密。
查到了这篇博客
java中PFX与CER的使用(上)_楓楓楓楓楓的博客-CSDN博客_java pfx
可以说是写得非常好的一篇博文了,代码和原理都介绍得很通透,可能是觉得这篇博客就是标准答案了,然后我就发现它里面有个rsaByPrivateKey方法,要么不做,要么做全套。于是我入坑了。贴出入坑代码:
- String OWA_data = "待加密字符串";
- PrivateKey privateKey = null;
- FileInputStream fileInputStream = null;
- try {
- fileInputStream = new FileInputStream("/usr/mailcert/ifcloud.pfx");
- byte[] pfxBytes = new byte[fileInputStream.available()];
- fileInputStream.read(pfxBytes);
- KeyStore ks = KeyStore.getInstance("PKCS12");
- char[] charPriKeyPass = "123".toCharArray();
- ks.load(new ByteArrayInputStream(pfxBytes), charPriKeyPass);
- Enumeration
aliasEnum = ks.aliases(); - String keyAlias = "";
- if (aliasEnum.hasMoreElements()) {
- keyAlias = aliasEnum.nextElement();
- }
- privateKey = (PrivateKey) ks.getKey(keyAlias, charPriKeyPass);
- } catch (Exception e) {
- e.printStackTrace();
- } finally {
- try {
- if (fileInputStream != null) {
- fileInputStream.close();
- }
- } catch (IOException e) {
- e.printStackTrace();
- }
- }
- out.println("######" + privateKey);
- String sign_base64 = "";
- try {
- Cipher cipher = Cipher.getInstance("RSA/ECB/PKCS1Padding");
- cipher.init(Cipher.ENCRYPT_MODE, privateKey);
- byte[] srcData = OWA_data.getBytes("UTF-8");
- int blockSize = cipher.getOutputSize(srcData.length)-11;
- byte[] encryptData = null;
- for (int i = 0; i < srcData.length; i += blockSize) {
- byte[] subarray = new byte[0];
- if(srcData != null) {
- int endIndexExclusive = i + blockSize;
- if (endIndexExclusive > srcData.length) {
- endIndexExclusive = srcData.length;
- }
- int newSize = endIndexExclusive - i;
- if (newSize <= 0) {
- continue;
- }
- subarray = new byte[newSize];
- System.arraycopy(srcData, i, subarray, 0, newSize);
- }
- byte[] doFinal = cipher.doFinal(subarray);
- if(encryptData == null) {
- encryptData = new byte[doFinal.length];
- System.arraycopy(doFinal, 0, encryptData, 0, doFinal.length);
- } else {
- encryptData = new byte[encryptData.length + doFinal.length];
- System.arraycopy(doFinal, 0, encryptData, encryptData.length, doFinal.length);
- }
- }
- sign_base64 = new String(Base64.encode(encryptData));
- out.println(sign_base64);
- } catch (Exception e) {
- e.printStackTrace();
- out.println(e);
- }
因为是jsp里面写的(供应商的系统框架太老,且不提供源码),只能改改jsp,所以调试只能靠打印(out.println)。
一运行,加密结果果然不出意外地对不上。
然后我发现,我没有使用SHA256,于是我就改了下最后几行代码:
- String result = new SHA256(new String(encryptData)).getHash();
- sign_base64 = new String(Base64.encode(result.getBytes("UTF-8")));
还是不对,想想也自然,完全没摸明白,想碰运气把代码写出来,不是那么简单。
但其实此时我已经通过读取PFX文件把私钥获取了,只是因为加密结果对不上,无法确认获取的私钥是否也是正确的(事后证明是正确的
继续找资料,然后查到了这个博客:SHA256withRSA (RSA2) 简单介绍_极客on之路的博客-CSDN博客_sha256withrsa
里面有一句:
SHA1WithRSA:用SHA算法进行签名,用RSA算法进行加密。同理 SHA256withRSA (RSA2) 使用SHA256签名。
我”顿悟“了,原来我加密和签名的顺序搞反了!!!(事后来看,这句话给我造成了太多误解
再次改完代码,一运行,哎嘿,还是错的。。。
此时我已经怀疑是不是.Net的代码有问题?(程序员通病,如果调试一直有问题,那可能是别人的代码有问题)
因为连续的错误,加上不明白原理,且每次改的是jsp,自然而然会产生一种程序健壮性太差的感觉,此时也极容易否定前面所有写出的代码。
后来想想,主要是微软的这段文档,写得太简略了:

使用指定的哈希算法和填充模式计算指定字节的哈希值,并对生产的哈希值进行签名。(这谁翻译的?绕口不说,还说了等于没说
直到我看到这篇博客:常见签名算法之SHA256withRSA_马拉萨的春天的博客-CSDN博客_sha256withrsa签名算法
- public static final String SIGNATURE_ALGORITHM = "SHA256withRSA";
- public static final String ENCODE_ALGORITHM = "SHA-256";
- public static final String PLAIN_TEXT = "test string";
-
- /**
- * 签名
- * @param privateKey 私钥
- * @param plainText 明文
- * @return
- */
- public static byte[] sign(PrivateKey privateKey, String plainText) {
- MessageDigest messageDigest;
- byte[] signed = null;
- try {
- messageDigest = MessageDigest.getInstance(ENCODE_ALGORITHM);
- messageDigest.update(plainText.getBytes());
- byte[] outputDigest_sign = messageDigest.digest();
- System.out.println("SHA-256编码后-----》"
- + org.apache.commons.codec.binary.Base64.encodeBase64String(outputDigest_sign));
- Signature Sign = Signature.getInstance(SIGNATURE_ALGORITHM);
- Sign.initSign(privateKey);
- Sign.update(outputDigest_sign);
- signed = Sign.sign();
- System.out.println("SHA256withRSA签名后-----》"
- + org.apache.commons.codec.binary.Base64.encodeBase64String(signed));
- } catch (NoSuchAlgorithmException | InvalidKeyException | SignatureException e) {
- e.printStackTrace();
- }
- return signed;
- }
这段代码,终于解决了我所有问题。
jdk文档:
MessageDigest 类为应用程序提供信息摘要算法的功能,如 MD5 或 SHA 算法。信息摘要是安全的单向哈希函数,它接收任意大小的数据,并输出固定长度的哈希值。
Signature 类用来为应用程序提供数字签名算法功能。数字签名用于确保数字数据的验证和完整性。
在所有算法当中,数字签名可以是 NIST 标准的 DSA,它使用 DSA 和 SHA-1。可以将使用 SHA-1 消息摘要算法的 DSA 算法指定为 SHA1withDSA。如果使用 RSA,对消息摘要算法则会有多种选择,因此,可以将签名算法指定为 MD2withRSA、MD5withRSA 或 SHA1withRSA。因为没有默认的算法名称,所以必须为其指定名称。
Signature 对象可用来生成和验证数字签名。
只怪之前没看到,修改我自己的代码如下:
- String OWA_data = "待加密字符串";
- PrivateKey privateKey = null;
- FileInputStream fileInputStream = null;
- try {
- fileInputStream = new FileInputStream("/usr/mailcert/ifcloud.pfx");
- byte[] pfxBytes = new byte[fileInputStream.available()];
- fileInputStream.read(pfxBytes);
- KeyStore ks = KeyStore.getInstance("PKCS12");
- char[] charPriKeyPass = "123".toCharArray();
- ks.load(new ByteArrayInputStream(pfxBytes), charPriKeyPass);
- Enumeration
aliasEnum = ks.aliases(); - String keyAlias = "";
- if (aliasEnum.hasMoreElements()) {
- keyAlias = aliasEnum.nextElement();
- }
- privateKey = (PrivateKey) ks.getKey(keyAlias, charPriKeyPass);
- } catch (Exception e) {
- e.printStackTrace();
- } finally {
- try {
- if (fileInputStream != null) {
- fileInputStream.close();
- }
- } catch (IOException e) {
- e.printStackTrace();
- }
- }
- String sign_base64 = "";
- try {
- byte[] OWA_data_Buf = OWA_data.getBytes("UTF-8");
- Signature Sign = Signature.getInstance("SHA256withRSA");
- Sign.initSign(privateKey);
- Sign.update(OWA_data_Buf);
- sign_base64 = new String(Base64.encode(Sign.sign()));
- out.println(sign_base64);
- } catch (Exception e) {
- e.printStackTrace();
- }
perfect!!!