如何在项目中应用数字签名技术
来源:互联网 发布:sql 删除表 编辑:程序博客网 时间:2024/05/01 23:52
最近公司项目中提出,内部公共WEB API接口访问需要提高安全性,最终选用token和数字签名技术来实现。那么就来记录一下其中数字签名部分的实现过程。
首先,来了解一下什么是数字签名。数字签名技术是将摘要信息用发送者的私钥加密,与原文一起传送给接收者。接收者只有用发送者的公钥才能解密被加密的摘要信息,然后用HASH函数对收到的原文产生一个摘要信息,与解密的摘要信息对比。如果相同,则说明收到的信息是完整的,在传输过程中没有被修改,否则说明信息被修改过,因此数字签名能够验证信息的完整性。数字签名是个加密的过程,数字签名验证是个解密的过程。
知道了数字签名的意义和作用,我们就可以来模拟进行数字签名实验一下了。其实数字签名技术已经很成熟,如果你也是使用java进行项目开发,那么JDK里就已经帮我们实现了数字签名和验签的方法,我们可以简便的通过调用这些方法来实现数据签名的功能,接下来我们就来实际操作一下。
首先我们确定一下,选用JDK自带的SHA1withRSA算法来进行签名和验签,由客户端(PC客户端,android客户端,IOS客户端)对发起的http请求参数进行摘要签名,服务端收到后验签,进行单向的数据签名验证,下面是我自己写的一个签名应用的例子
/** * Project Name: UDPTest * File Name: MySignature.java * Package Name: signature * Date: 2016年11月23日下午3:58:05 * Copyright (c) 2016, hadlinks All Rights Reserved. * */package signature;import java.io.FileInputStream;import java.io.FileOutputStream;import java.security.Key;import java.security.KeyFactory;import java.security.KeyPair;import java.security.KeyPairGenerator;import java.security.KeyStore;import java.security.PrivateKey;import java.security.PublicKey;import java.security.SecureRandom;import java.security.Signature;import java.security.cert.Certificate;import java.security.spec.KeySpec;import java.security.spec.PKCS8EncodedKeySpec;import java.security.spec.X509EncodedKeySpec;import java.util.Arrays;import java.util.Enumeration;import java.util.HashMap;import java.util.Map;import java.util.Base64;/** * ClassName: MySignature * Function: RSA数字签名,数字签名遵循“私钥签名,公钥验签”原则。 * date: 2016年11月23日 下午3:58:05 * * @author songw (songw@hadlinks.com) * @version * @since JDK 1.8 */public class MySignature { /** * 数字签名算法。JDK只提供了MD2withRSA, MD5withRSA, SHA1withRSA,其他的算法需要第三方包才能支持 */ public static final String SIGNATURE_ALGORITHM = "SHA1withRSA"; public static final String ALGORITHM = "RSA"; public static final int KEYSIZE = 2048; public static final String PUBLIC_KEY = "publicKey"; public static final String PRIVATE_KEY = "privateKey"; public static final String CHARSET = "UTF-8"; public static PublicKey publicKey = null; public static PrivateKey privateKey = null; public static void main(String[] args) throws Exception{ /** * 从生成好的JKS中提取公私钥,并转化出IOS开发可用的PKCS12标准的秘钥仓库 */ try { KeyStore keyStore = KeyStore.getInstance("JKS"); // JKS证书仓库的访问密码 char[] storePassword = "storePassword".toCharArray(); keyStore.load(new FileInputStream( "C:\\keypair\\keypair.jks"), storePassword); // JKS仓库中证书/私钥的访问密码 char[] keyPassword = "keyPassword".toCharArray(); // 私钥在JKS中对应的别名 String privateKeyAlias = "test"; Key key = keyStore.getKey(privateKeyAlias, keyPassword); KeyPair keyPair = null; if (key instanceof PrivateKey) { // 证书在JKS中对应的别名 String certAlias = "test"; Certificate cert = keyStore.getCertificate(certAlias); PublicKey publicKey = cert.getPublicKey(); keyPair = new KeyPair(publicKey, (PrivateKey) key); } publicKey = keyPair.getPublic(); privateKey = keyPair.getPrivate(); System.out.println("生成的公钥:" + toHexString(publicKey.getEncoded())); PublicKey a = strToPublicKey(toHexString(publicKey.getEncoded())); System.out.println("转化后的公钥:" + toHexString(a.getEncoded())); System.out.println("生成的私钥:" + toHexString(privateKey.getEncoded())); PrivateKey b = strToPrivateKey(toHexString(privateKey.getEncoded())); System.out.println("转化后的私钥:" + toHexString(b.getEncoded())); // 将JKS转化成PKCS12标准的秘钥仓库 JSKToPKCS12("C:\\keypair\\keypair.jks", "storePassword", "C:\\keypair\\keypair.p12", "newStorePasswordForP12"); System.out.println("转化P12文件成功!"); } catch (Exception ex) { ex.printStackTrace(); return; } /** * 假设现在客户端签名后向服务器端发送消息 * 用RSA的私钥进行签名 */ String[] params = {"username=username", "password=password", "token=token"}; //组成待签名的明文内容 String plainText = sortWithASCIIAsc(params); System.out.println("待签名的明文:"+plainText); byte[] signature = sign(privateKey, plainText.getBytes(CHARSET)); System.out.println("生成的签名:"+toHexString(signature)); //将签名得到的byte[]进行base64编码,获得sign字符串,作为get/post请求中的sign参数传给服务器端 String sign = base64Encode(signature); System.out.println("base64编码后的签名:"+sign); /** * 假设现在服务器端收到了客户端的消息 * 先进行base64解码,得到客户端的signature * 将客户端的signature内容用公钥进行验签操作 */ byte[] clientSignature = base64Decoce(sign); System.out.println("base64解码后的签名:"+toHexString(clientSignature)); /** * 假设服务器拦截所有来自客户端的请求 * 获取到了url参数内容 * 实际操作过程中需要重新组装、排序明文内容 * 这里为了方便直接使用处理好的明文 */ byte[] decodedText = plainText.getBytes(CHARSET); System.out.println("验签结果" + verify(publicKey, clientSignature, decodedText)); } /** * 签名,三步走 * 1. 实例化,传入算法 * 2. 初始化,传入私钥 * 3. 签名 * @param key * @param plainText * @return */ public static byte[] sign(PrivateKey privateKey, byte[] plainText) throws Exception{ //实例化 Signature signature = Signature.getInstance(SIGNATURE_ALGORITHM); //初始化,传入私钥 signature.initSign(privateKey); //更新 signature.update(plainText); //签名 return signature.sign(); } /** * 验签,三步走 * 1. 实例化,传入算法 * 2. 初始化,传入公钥 * 3. 验签 * @param publicKey * @param signatureVerify * @param plainText * @return */ public static boolean verify(PublicKey publicKey, byte[] signatureVerify, byte[] plainText ) throws Exception{ //实例化 Signature signature = Signature.getInstance(SIGNATURE_ALGORITHM); //初始化 signature.initVerify(publicKey); //更新 signature.update(plainText); //验签 return signature.verify(signatureVerify); } /** * 生成RSA密钥对 * @throws Exception */ private static Map<String, Object> generateKeyPair() throws Exception { Map<String, Object> result = new HashMap<String, Object>(); //RSA算法要求有一个可信任的随机数源 SecureRandom secureRandom = new SecureRandom(); //为RSA算法创建一个KeyPairGenerator对象 KeyPairGenerator keyPairGenerator = KeyPairGenerator.getInstance(ALGORITHM); //利用上面的随机数据源初始化这个KeyPairGenerator对象 keyPairGenerator.initialize(KEYSIZE, secureRandom); //生成密匙对 KeyPair keyPair = keyPairGenerator.generateKeyPair(); //得到公钥 PublicKey publicKey = keyPair.getPublic(); //得到私钥 PrivateKey privateKey = keyPair.getPrivate(); //填充返回结果 result.put("privateKey", privateKey); //填充返回结果 result.put("publicKey", publicKey); return result; } /** * 利用jdk8自带的Base64工具类进行base64编码 * @param param * @return */ public static String base64Encode(byte[] param){ return Base64.getEncoder().encodeToString(param); } /** * 利用jdk8自带的Base64工具类进行base64解码 * @param param * @return */ public static byte[] base64Decoce(String param){ return Base64.getDecoder().decode(param); } /** * 将传入的String数组按照ASCII进行升序排列 * @param param */ public static String sortWithASCIIAsc(String[] params){ //String本身已经实现CompareTo,所以直接使用sort方法进行排序 Arrays.sort(params); String result = ""; //将参数组装成明文 for(String param : params){ result += param + "&"; } return result.substring(0, result.length() - 1); } /** * byte数组转字符串的方法 * @param bytes * @return */ public static String toHexString(byte[] bytes){ return toHexString(bytes, ""); } public static String toHexString(byte[] bytes, String split){ if(isEmpty(bytes)){ return null; } StringBuilder sb = new StringBuilder(); for(byte b : bytes){ sb.append(toHexString(b)).append(split); } return sb.toString(); } public static boolean isEmpty(byte[] bytes){ return bytes==null || bytes.length==0; } public static String toHexString(byte b){ String hex = null; int i = (int)b; String s = Integer.toHexString(i); if(i >= 0){ int len = s.length(); if(len < 2){ s = "0" + s; } hex = s; } else{ hex = s.substring(6); } return hex.toUpperCase(); } /** * 还原公钥 * @param key * @return * @throws Exception */ public static PublicKey strToPublicKey(String key) throws Exception{ byte[] keyBytes = parseHexStringToArray(key); KeySpec keySpec = new X509EncodedKeySpec(keyBytes); KeyFactory factory = KeyFactory.getInstance("RSA"); return factory.generatePublic(keySpec); } /** * * @param key * @return * @throws Exception */ public static PrivateKey strToPrivateKey(String key) throws Exception{ byte[] keyBytes = parseHexStringToArray(key); PKCS8EncodedKeySpec keySpec = new PKCS8EncodedKeySpec(keyBytes); KeyFactory factory = KeyFactory.getInstance("RSA"); return factory.generatePrivate(keySpec); } public static byte[] parseHexStringToArray(String s){ if(isEmpty(s)){ return null; } int len = s.length(); if(len % 2 !=0){ return null; } int size = len / 2; byte[] data = new byte[size]; for(int i=0; i<size; i++){ String sub = s.substring(i*2, i*2+2); data[i] = parseHexString(sub); } return data; } /** * Check if a string is empty * @param s * @return */ public static boolean isEmpty(String s){ return s == null || s.trim().length() == 0; } public static byte parseHexString(String s){ int i = Integer.parseInt(s, 16); return (byte)i; } /** * 从JKS格式转换为PKCS12格式 * @param srcFile String JKS格式证书库 * @param srcPasswd String JKS格式证书库密码 * @param destFile String PKCS12格式证书库 * @param destPasswd String PKCS12格式证书库密码 */ public static void JSKToPKCS12(String srcFile, String srcPasswd, String destFile, String destPasswd){ try { KeyStore inputKeyStore = KeyStore.getInstance("JKS"); FileInputStream fis = new FileInputStream(srcFile); char[] srcPwd = null, destPwd = null; if ((srcPasswd == null) || srcPasswd.trim().equals("")) { srcPwd = null; } else { srcPwd = srcPasswd.toCharArray(); } if ((destPasswd == null) || destPasswd.trim().equals("")) { destPwd = null; } else { destPwd = destPasswd.toCharArray(); } inputKeyStore.load(fis, srcPwd); fis.close(); KeyStore outputKeyStore = KeyStore.getInstance("PKCS12"); Enumeration<String> enums = inputKeyStore.aliases(); while (enums.hasMoreElements()) { String keyAlias = (String) enums.nextElement(); System.out.println("alias=[" + keyAlias + "]"); outputKeyStore.load(null, destPwd ); if (inputKeyStore.isKeyEntry(keyAlias)) { Key key = inputKeyStore.getKey(keyAlias, srcPwd); Certificate[] certChain = inputKeyStore.getCertificateChain(keyAlias); outputKeyStore.setKeyEntry(keyAlias, key, destPwd, certChain); } String fName = destFile.substring(0, destFile.indexOf(".p12")); fName += "_" + keyAlias + ".p12"; FileOutputStream out = new FileOutputStream(fName); outputKeyStore.store(out, destPwd); out.close(); outputKeyStore.deleteEntry(keyAlias); } } catch (Exception e) { e.printStackTrace(); } }}我们再来看一下签名过程中的主要步骤:
步骤一:生成RSA公私钥对。因为选用了SHA1withRSA算法,所以必然是需要一对RSA公私钥的,这里原本我们可以借助java.security包下的工具类,直接在程序中生成RSA密钥对。参考代码:
public static final String ALGORITHM = "RSA"; public static final int KEYSIZE = 2048; public static PublicKey publicKey = null; public static PrivateKey privateKey = null;/** * 生成RSA密钥对 * @throws Exception */ private static Map<String, Object> generateKeyPair() throws Exception { Map<String, Object> result = new HashMap<String, Object>(); //RSA算法要求有一个可信任的随机数源 SecureRandom secureRandom = new SecureRandom(); //为RSA算法创建一个KeyPairGenerator对象 KeyPairGenerator keyPairGenerator = KeyPairGenerator.getInstance(ALGORITHM); //利用上面的随机数据源初始化这个KeyPairGenerator对象 keyPairGenerator.initialize(KEYSIZE, secureRandom); //生成密匙对 KeyPair keyPair = keyPairGenerator.generateKeyPair(); //得到公钥 PublicKey publicKey = keyPair.getPublic(); //得到私钥 PrivateKey privateKey = keyPair.getPrivate(); //填充返回结果 result.put("privateKey", privateKey); //填充返回结果 result.put("publicKey", publicKey); return result; }
但是由于实际项目开发中存在IOS客户端,Apple是不支持直接使用字符串进行加密解密的,那么私钥的传递就成了问题。我们在程序中生成了公私钥之后,还需要一种可用的方式将私钥给到客户端。鉴于IOS开发使用的加解密工具是Openssl,所以只能借助PKCS12标准的秘钥库让IOS程序来读取私钥。这里我们借助JDK的另一个keytool工具,生成一个带有RSA密钥对的JKS标准的keystore,这样我们只需要将JKS文件转化成p12文件,就能提供给IOS开发者一个可用的RSA私钥了。
生成keystore很简单,直接在命令行里执行
keytool -genkey -alias test -keysize 2048 -validity 3650 -keyalg RSA -dname "CN=localhost" -keypass keyPassword -storepass storePassword -keystore keypair.jks
这样就能在当前目录下获得一个JKS文件,里面包含了一对RSA密钥对
步骤二:分发密钥对。我们从JKS文件中提取出我们需要使用的公私钥,顺便将JKS转化成P12标准的秘钥库。提取到公私钥之后通过toHexString(publicKey.getEncoded())方法可以将公钥/私钥内容转化成16进制的字符串以便传递,同样的可以通过方法strToPublicKey(toHexString(publicKey.getEncoded()))重新将字符串还原成一个公钥/私钥。在本实例中,客户端使用私钥进行签名,服务器端使用公钥来验证签名。
/** * 假设现在客户端签名后向服务器端发送消息 * 用RSA的私钥进行签名 */ String[] params = {"username=username", "password=password", "token=token"}; //组成待签名的明文内容 String plainText = sortWithASCIIAsc(params); System.out.println("待签名的明文:"+plainText); byte[] signature = sign(privateKey, plainText.getBytes(CHARSET)); System.out.println("生成的签名:"+toHexString(signature)); //将签名得到的byte[]进行base64编码,获得sign字符串,作为get/post请求中的sign参数传给服务器端 String sign = base64Encode(signature); System.out.println("base64编码后的签名:"+sign);
步骤三:生成签名字符串。有了私钥之后,我们就可以用私钥来生成签名了,假设我们客户端请求时参数为username,password和token,那么签名之前我们先要约定好使用一种排序方法,对这些参数进行排序,否则,如果是post请求,服务器端收到请求后就无法知道客户端传递的参数的顺序,那么就有可能导致验证签名的过程失败。换句话说,签名其实是对一系列有序内容的加密(加密后的内容可以看成是包含了所有参数内容的一份摘要,所以也叫摘要签名),验签就是对这部分有序的内容进行再次加密和比对,如果签名后的内容与客户端发送的摘要内容的一致,则验签通过,否则认为请求内容在传递过程中被篡改或发送方身份存疑。排序的具体方法可以由客户端和服务端自行约定,只需要保持一致即可,为了方便,直接采用ASCII码升序进行排列,对应方法sortWithASCIIAsc(params)。排序完成后,调用Signature工具类里面的sign()方法进行签名,这里要注意,签名后的字符串可能很长,为了便于传输,我们再对签名后的字符串进行base64编码,这样,签名字符串就获取成功了
/** * 假设现在服务器端收到了客户端的消息 * 先进行base64解码,得到客户端的signature * 将客户端的signature内容用公钥进行验签操作 */ byte[] clientSignature = base64Decoce(sign); System.out.println("base64解码后的签名:"+toHexString(clientSignature)); /** * 假设服务器拦截所有来自客户端的请求 * 获取到了url参数内容 * 实际操作过程中需要重新组装、排序明文内容 * 这里为了方便直接使用处理好的明文 */ byte[] decodedText = plainText.getBytes(CHARSET); System.out.println("验签结果" + verify(publicKey, clientSignature, decodedText));步骤四:验签。签名成功后我们还要验证一下,私钥签名出来的内容确实能用公钥来验签,所以我们按照签名的规则,模拟服务器收到http请求后,先对签名字符串进行base64解码,还原成原始签名字符串。再重新对收到的参数进行排序,然后调用Signature工具类里的verify()方法来验证签名的有效性。这样我们整个数字签名的过程就结束了。
- 如何在项目中应用数字签名技术
- 数字签名怎样应用在存储系统中
- 如何在pdf中设置数字签名
- 基于java的数字签名技术在电子政务中的应用
- 加密、解密、数字签名等技术在支付行业的应用
- android开发中如何获得应用的数字签名
- 在制造业环境中如何应用仓储管理技术
- 如何在实际项目开发中使用LinQ技术
- 如何在Androdi Studio中获取SHA1数字签名和MD5
- 如何在iOS中使用SM2进行数字签名及校验
- 数字签名在REST中的应用
- Microsoft.Net中数字签名技术
- Micorsoft.Net中数字签名技术
- Microsoft.Net中数字签名技术
- Microsoft.Net中数字签名技术
- Microsoft.Net中数字签名技术
- Microsoft.Net中数字签名技术
- CA证书与数字签名技术及应用
- 【数据结构与算法】实现四则运算
- STM32L1学习笔记03 了解STM32CubeL1
- RabbitMQ 发送消息 确认消息 是否 发送成功
- 调用 android 系统拍照结合 android-crop 裁剪图片
- APNS 学习总结(四)
- 如何在项目中应用数字签名技术
- iOS crash报告问题
- 腾讯第一“伪娘” 他是最会化妆的IT男
- 设计模式(1)观察者模式简单理解
- 关键帧动画的停止
- 数据结构课程设计——ytu学生成绩管理系统
- 列举几个 JavaScript 中常用的全局函数,并描述其作用
- 适配时,请注意单位:px、dpi
- Android studio和eclipse快捷键------Android sdk