技术背景
单纯的摘要算法实现简单,签名、验签速度快,但容易出现数据被拦截并同时替换内容和摘要的风险。对称加密通过对摘要进行加密保证摘要的安全,但需要事先交换密钥,如果密钥被获取,仍然有数据被拦截并替换的风险。非对称加密由于只交换公钥,私钥一直存于本地,保证了数据在传输过程中替换和更改会被接收端感知(私钥签名后,由于拦截方无法获得私钥,因此替换的数据无法被公钥验签成功),缺点是加密和解密比较耗时。这里对这三种方式分别进行了实现。
代码实现
MD5摘要算法实现签名、验签
为了保证数据被拦截替换时能被感知,应当对数据进行签名。具体的,可通过先对数据排序从而保证数据的唯一性,例如使用sortedMap实现排序,并将排序后的数据序列化为字符串(例如:使用=
连接key和value,使用&
连接不同的键值对),接着使用摘要算法获得字符串数据的摘要(即签名),最后将数据和摘要一并发送给接收方,接收方通过同样的操作获得数据的摘要并与接收到的摘要进行对比以判断数据是否被拦截替换。
-
编写MD5摘要工具类
/** * 使用MD5获取字符串摘要 */ public static String md5(String message) { try { // 进行md5编码 MessageDigest md = MessageDigest.getInstance("MD5"); byte[] digest = md.digest(message.getBytes(StandardCharsets.UTF_8))); // 将字节数组转换为16进制字符串 StringBuilder sb = new StringBuilder(); for (byte b : digest) { sb.append(String.format("%02x", b)); } return sb.toString(); } catch (NoSuchAlgorithmException | UnsupportedEncodingException e) { throw new RuntimeException(e); } }
-
使用MD5签名
/** * 使用MD5签名 */ public static String signMd5(SortedMap<String, ?> params) { // 将map转成url字符串形式 StringBuilder sb = new StringBuilder(); for (String key : params.keySet()) { sb.append(key).append("=").append(params.get(key).toString()).append("&"); } return md5(sb.toString()).toUpperCase(); }
-
使用MD5验签
/** * 使用MD5验签 */ public static Boolean validateSignMd5(SortedMap<String, ?> params, String sign) { return signMd5(params).equalsIgnoreCase(sign); }
AES对称加密实现签名、验签
MD5签名、验签虽然可以验证摘要和数据的一致性,但不能保证数据来源的可靠性。例如,拦截方替换数据的同时,使用相同的摘要算法生成摘要,导致接收方误判数据没有被替换。基于上述分析,可采用加密算法对摘要进行加密。具体的,发送方生成密钥并共享密钥给接收方,在每次对传输数据进行签名时,对摘要进行加密,最后将加密后的摘要和数据一并发送给接收方。接收方得到数据和加密的摘要后,首先使用相同的摘要算法对数据进行签名获得摘要,再拿密钥对接收到的摘要进行解密并与之比对。此时,即使数据被拦截,但由于拦截方不知道密钥,因此无法会替换的数据的摘要进行加密,从而保证数据和摘要的一致性。而常用的对称加密算法包括AES、DES等,其中由于DES的长度较短,已经不适用于当今数据加密安全性的要求,因此选用AES的实现方式。
加密不是指对明文进行加密传输,而是对摘要进行加密,防止明文和摘要在传输的过程中被同时替换。
-
生成随机的AES密钥
/** * @return Base64加密后的AES密钥 */ public static String generateAesKey() { SecureRandom secureRandom = new SecureRandom(); try { KeyGenerator keyGenerator = KeyGenerator.getInstance("AES"); keyGenerator.init(256, secureRandom); byte[] key = keyGenerator.generateKey().getEncoded(); return Base64.getEncoder().encodeToString(key); } catch (NoSuchAlgorithmException e) { throw new RuntimeException(e); } }
-
编写AES加密工具类
/** * AES加密 * @param text 明文 * @param aesKey 密钥 * @return 密文 */ public static String encryptAes(String text, String aesKey) { try { // 创建AES加密算法实例 Cipher cipher = Cipher.getInstance("AES/ECB/PKCS5Padding"); byte[] decode = Base64.getDecoder().decode(aesKey); SecretKeySpec keySpec = new SecretKeySpec(decode, "AES"); // 加密模式 cipher.init(Cipher.ENCRYPT_MODE, keySpec); // 生成密文并使用Base64编码 byte[] encrypted = cipher.doFinal(text.getBytes()); return Base64.getEncoder().encodeToString(encrypted); } catch (NoSuchAlgorithmException | NoSuchPaddingException | InvalidKeyException | IllegalBlockSizeException | BadPaddingException e) { throw new RuntimeException(e); } }
-
使用AES签名
/** * 使用md5和aes算法对params(sorted)签名 * @param aesKey 密钥 * @return */ public static String signAes(SortedMap<String, ?> params, String aesKey) { // 通过md5生成摘要 String abs = signMd5(params); return encryptAes(abs, aesKey); }
-
使用AES验签
/** * 对params验证 * @param params 明文 * @param sign 签名 * @param aesKey 密钥 * @return */ public static Boolean validateSignAes(SortedMap<String, ?> params, String sign, String aesKey) { return signAes(params, aesKey).equals(sign); }
RSA非对称加密实现签名、验签
对称加密中发送方和接收方使用同一把密钥,若密钥在共享的过程中被获取,则依旧会导致数据被拦截替换,因此,可将加密算法优化为非对称加密。具体的,发送方生成一对密钥,分为公钥和私钥,私钥保存在本地,公钥可共享,私钥加密后只有公钥可以解密,公钥加密后只有私钥可以解密。在这种场景下,私钥负责签名,公钥负责验签,由于私钥保存本地始终不共享,因此拦截方无法在替换数据的同时生成正确的签名,即接收方可以感知到数据是否被替换。
-
生成一对密钥并保存
/** * 生成一对密钥,以map形式返回,key为密钥名称(publicKey, privateKey), * value为对应base64编码的密钥 */ public static Map<String, String> generateRSAKeys() { try { KeyPairGenerator keyPairGenerator = KeyPairGenerator.getInstance("RSA"); // 密钥长度,常用为2048 keyPairGenerator.initialize(2048); // 生成密钥对 KeyPair keyPair = keyPairGenerator.generateKeyPair(); PublicKey publicKey = keyPair.getPublic(); PrivateKey privateKey = keyPair.getPrivate(); String publicKeyStr = Base64.getEncoder().encodeToString(publicKey.getEncoded()); String privateKeyStr = Base64.getEncoder().encodeToString(privateKey.getEncoded()); Map<String, String> keyMap = new HashMap<>(); keyMap.put("publicKey", publicKeyStr); keyMap.put("privateKey", privateKeyStr); return keyMap; } catch (NoSuchAlgorithmException e) { throw new RuntimeException(e); } } /** * 获取私钥 * * @param privateKey 字符串形式私钥 */ public static PrivateKey getPrivateKey(String privateKey) throws Exception { byte[] keyBytes = Base64.getDecoder().decode(privateKey.getBytes(StandardCharsets.UTF_8)); PKCS8EncodedKeySpec keySpec = new PKCS8EncodedKeySpec(keyBytes); KeyFactory keyFactory = KeyFactory.getInstance("RSA"); return keyFactory.generatePrivate(keySpec); } /** * 获取公钥 * * @param publicKey 字符串形式公钥 */ public static PublicKey getPublicKey(String publicKey) throws Exception { byte[] keyBytes = Base64.getDecoder().decode(publicKey.getBytes(StandardCharsets.UTF_8)); X509EncodedKeySpec encodedKeySpec = new X509EncodedKeySpec(keyBytes); KeyFactory keyFactory = KeyFactory.getInstance("RSA"); return keyFactory.generatePublic(encodedKeySpec); }
-
使用sha256摘要算法和RSA进行私钥签名
/** * 私钥签名,使用sha256摘要算法和RSA加密签名 * @param privateKey 私钥 */ public static String signSha256(SortedMap<String, ?> params, String privateKey) { // 预处理得到需要签名的字符串 StringBuilder sb = new StringBuilder(); for (String key : params.keySet()) { sb.append(key).append("=").append(params.get(key).toString()).append("&"); } String source = sb.toString().toUpperCase(); // 签名 try { // 创建key工厂 PrivateKey priKey = getPrivateKey(privateKey); // 使用指定算法的密钥工厂 Signature signature = Signature.getInstance("SHA256WithRSA"); signature.initSign(priKey); signature.update(source.getBytes()); byte[] sign = signature.sign(); // 采用base64算法进行转码,避免出现中文乱码 return Base64.getEncoder().encodeToString(sign); } catch (Exception e){ throw new RuntimeException(); } }
-
使用sha256摘要算法和RSA进行公钥验签
/** * 公钥验签,使用sha256摘要算法和RSA进行验签 * @param sign 签名 * @param publicKey 公钥 */ public static Boolean validateSignSha256(SortedMap<String, ?> params, String sign, String publicKey) { try { PublicKey pubKey = getPublicKey(publicKey); Signature verifySignature = Signature.getInstance("SHA256WithRSA"); verifySignature.initVerify(pubKey); // 明文url字符串 StringBuilder sb = new StringBuilder(); for (String key : params.keySet()) { sb.append(key).append("=").append(params.get(key).toString()).append("&"); } String source = sb.toString().toUpperCase(); verifySignature.update(source.getBytes()); return verifySignature.verify(Base64.getDecoder().decode(sign)); } catch (Exception e) { throw new RuntimeException(e); } }