前言
密码学在计算机领域源远流长,应用广泛。当前每时每刻,每一个连接到互联网的终端,手机,电脑,iPad都会和互联网有无数次的数据交互,如果这些数据都是明文传输那将是难以想象的。为了保护用户隐私,防止重要数据被窃取,篡改,我们需要对我们的数据进行加密。
本节我就如何进行加密,做简单介绍。
1. 对称加密
1.1 简介
这一类算法是加密密钥和解密密钥是相同的,加密密钥是解密密钥的逆运算,也就是一种完全对称的行为,所以叫做对称加密算法。对称加密算法是一种初等的加密算法。从安全性来讲的话没有那么高
1.2 常用算法
下面案例统一以Apache Codec 提供的加密库和jdk本身类库为例,pom坐标如下:
<dependency>
<groupId>commons-codec</groupId>
<artifactId>commons-codec</artifactId>
<version>1.15</version>
</dependency>
1.2.1 DES
工具类
public class DESUtils {
//密钥算法
private static final String KEY_ALGORITHM = "DES";
//加密算法/工作模式/填充方式
private static final String DEFAULT_CIPHER_ALGORITHM = "DES/ECB/PKCS5Padding";
/**使用DES对字符串加密
* @param str utf8编码的字符串
* @param key 密钥(8字节)
* @return 加密结果
* @throws Exception
*/
public static byte[] desEncrypt(String str, String key) throws Exception {
if (str == null || key == null) return null;
Cipher cipher = Cipher.getInstance(DEFAULT_CIPHER_ALGORITHM);
cipher.init(Cipher.ENCRYPT_MODE, new SecretKeySpec(key.getBytes("utf-8"), KEY_ALGORITHM));
byte[] bytes = cipher.doFinal(str.getBytes("utf-8"));
return bytes;
}
/**使用DES对数据解密
* @param bytes utf8编码的二进制数据
* @param key 密钥(8字节)
* @return 解密结果
* @throws Exception
*/
public static String desDecrypt(byte[] bytes, String key) throws Exception {
if (bytes == null || key == null) return null;
Cipher cipher = Cipher.getInstance(DEFAULT_CIPHER_ALGORITHM);
cipher.init(Cipher.DECRYPT_MODE, new SecretKeySpec(key.getBytes("utf-8"), KEY_ALGORITHM));
bytes = cipher.doFinal(bytes);
return new String(bytes, "utf-8");
}
}
测试类
@Test
public void testEncrypt() throws Exception{
String data="我是测试数据,用来测试DES";
String key="2234234d";
byte[] bytes = DESUtils.desEncrypt(data, key);
System.out.println("加密后报文为:"+new String(bytes));
String s = DESUtils.desDecrypt(bytes, key);
System.out.println("解密后报文为:"+s);
}
//加密后报文为:]�~;ᠬ$F��/���
//9�'f˕:����N��O��b�A
//解密后报文为:我是测试数据,用来测试DES
1.2.2 3DES
工具类:
public class DESUtils {
/**使用DES对字符串加密
* @param str utf8编码的字符串
* @param key 密钥(24字节)
* @return 加密结果
* @throws Exception
*/
public static byte[] truibleDesEncrypt(String str, String key) throws Exception {
if (str == null || key == null) return null;
Cipher cipher = Cipher.getInstance("DESede/ECB/PKCS5Padding");
cipher.init(Cipher.ENCRYPT_MODE, new SecretKeySpec(key.getBytes("utf-8"), "DESede"));
byte[] bytes = cipher.doFinal(str.getBytes("utf-8"));
return bytes;
}
/**使用DES对数据解密
* @param bytes utf8编码的二进制数据
* @param key 密钥(24字节)
* @return 解密结果
* @throws Exception
*/
public static String tribleDesDecrypt(byte[] bytes, String key) throws Exception {
if (bytes == null || key == null) return null;
Cipher cipher = Cipher.getInstance("DESede/ECB/PKCS5Padding");
cipher.init(Cipher.DECRYPT_MODE, new SecretKeySpec(key.getBytes("utf-8"), "DESede"));
bytes = cipher.doFinal(bytes);
return new String(bytes, "utf-8");
}
}
测试类:
@Test
public void testTribleDES() throws Exception{
String data="我是测试数据,用来测试3DES";
String key="12345678abcdefghABCDEFGH";
byte[] bytes = DESUtils.truibleDesEncrypt(data, key);
System.out.println("加密后报文为:"+new String(bytes));
String s = DESUtils.tribleDesDecrypt(bytes, key);
System.out.println("解密后报文为:"+s);
}
//加密后报文为:
//���1�����u�{��B0\�T��
//��V�j�;�1c�
//解密后报文为:我是测试数据,用来测试3DES
1.2.3 AES(推荐)
工具类:
public class AESUtils {
//密钥算法
private static final String KEY_ALGORITHM = "AES";
//加密算法/工作模式/填充方式
private static final String DEFAULT_CIPHER_ALGORITHM = "AES/ECB/PKCS5Padding";
/**
* 加密
*
* @param toEncryptStr key
* @return
*/
public static String encrypt(String toEncryptStr, String key) {
byte[] data = toEncryptStr.getBytes();
//统一密钥为8的倍数,这里统一md5处理成32位,解密一样
byte[] secretKey = parseHexStr2Byte(md5(key));
try {
// 实例化
Cipher cipher = Cipher.getInstance(DEFAULT_CIPHER_ALGORITHM);
// 使用密钥初始化,设置为加密模式
cipher.init(Cipher.ENCRYPT_MODE, new SecretKeySpec(secretKey, KEY_ALGORITHM));
// 执行操作
return parseByte2HexStr(cipher.doFinal(data));
} catch (Exception e) {
return "";
}
}
//解密 带偏移量
public static String decodeWithIv(String strText, String key, String ivStr) {
byte[] StrdoFinal = null;
try {
Cipher cipher = Cipher.getInstance("AES/CFB/NoPadding");
SecretKeySpec keyByte = new SecretKeySpec(key.getBytes(), "AES");
IvParameterSpec iv = new IvParameterSpec(ivStr.getBytes());
byte[] contents = Base64.getDecoder().decode(strText);
cipher.init(Cipher.DECRYPT_MODE, keyByte, iv);
StrdoFinal = cipher.doFinal(contents);
} catch (Exception e) {
e.printStackTrace();
}
return new String(StrdoFinal);
}
/**
* 解密
*
* @param toDecryptStr key
* @return
*/
public static String decrypt(String toDecryptStr, String key) {
byte[] data = parseHexStr2Byte(toDecryptStr);
byte[] secretKey = parseHexStr2Byte(md5(key));
try {
// 实例化
Cipher cipher = Cipher.getInstance(DEFAULT_CIPHER_ALGORITHM);
// 使用密钥初始化,设置为解密模式
cipher.init(Cipher.DECRYPT_MODE, new SecretKeySpec(secretKey, KEY_ALGORITHM));
// 执行操作
return new String(cipher.doFinal(data), "utf-8");
} catch (Exception e) {
return "";
}
}
/**
* 将二进制转换成16进制
*
* @param buf
* @return
*/
public static String parseByte2HexStr(byte buf[]) {
StringBuilder sb = new StringBuilder();
for (int i = 0; i < buf.length; i++) {
String hex = Integer.toHexString(buf[i] & 0xFF);
if (hex.length() == 1) {
hex = '0' + hex;
}
sb.append(hex.toUpperCase());
}
return sb.toString();
}
/**
* 将16进制转换为二进制
*
* @param hexStr
* @return
*/
public static byte[] parseHexStr2Byte(String hexStr) {
if (hexStr.length() < 1) {
return new byte[0];
}
byte[] result = new byte[hexStr.length() / 2];
for (int i = 0; i < hexStr.length() / 2; i++) {
int high = Integer.parseInt(hexStr.substring(i * 2, i * 2 + 1), 16);
int low = Integer.parseInt(hexStr.substring(i * 2 + 1, i * 2 + 2), 16);
result[i] = (byte) (high * 16 + low);
}
return result;
}
/**
* MD5加密算法
* 说明:32位加密算法
*
* @param s 待加密的数据
* @return 加密结果,全小写的字符串(utf-8)
*/
public static String md5(String s) {
char hexDigits[] = {'0', '1', '2', '3', '4', '5', '6', '7', '8', '9',
'a', 'b', 'c', 'd', 'e', 'f'};
try {
byte[] btInput = s.getBytes("utf-8");
// 获得MD5摘要算法的 MessageDigest 对象
MessageDigest mdInst = MessageDigest.getInstance("MD5");
// 使用指定的字节更新摘要
mdInst.update(btInput);
// 获得密文
byte[] md = mdInst.digest();
// 把密文转换成十六进制的字符串形式
int j = md.length;
char str[] = new char[j * 2];
int k = 0;
for (int i = 0; i < j; i++) {
byte byte0 = md[i];
str[k++] = hexDigits[byte0 >>> 4 & 0xf];
str[k++] = hexDigits[byte0 & 0xf];
}
return new String(str);
} catch (Exception e) {
e.printStackTrace();
return null;
}
}
}
测试类:
public class TestAES {
@Test
public void testEncrypt() throws Exception{
String data="我是测试数据,用来测试AES";
String key="2234234363456d";
String encrypt = AESUtils.encrypt(data, key);
System.out.println("加密后报文为:"+encrypt);
String s = AESUtils.decrypt(encrypt, key);
System.out.println("解密后报文为:"+s);
}
//加密后报文为:52CE397C7042173E569C5E7FA3F657C32FB658D2DB2F9CE7E3EF5F1914DD1B29FFB95C128E5B10EA8DA4E494B669790F
//解密后报文为:我是测试数据,用来测试AES
}
1.3 算法优缺点
- DES对秘钥长度有限制,必须是八个字节
- 3DES是DES的过渡版本,会对数据加密三次更高安全一点,秘钥长度必须是24位,我在测试类用的测试秘钥可以看到秘钥位数。
- AES是更高级加密标准,加密速度更快,安全性更高,(也是现在很多公司非敏感信息报文加密的选择之一)
- 可以看到上面三个工具类加密解密过程实际都很类似,3DES只是替换了DES工具类中的加密算法,DES 改为DESede
- 工具类中的AES/ECB/PKCS5Padding,AES是加密算法,ECB是工作模式,PKCS5Padding是填充方式。因为AES是分组加密算法,也称块加密。每一组16字节。这样明文就会分成多块。当有一块不足16字节时就会进行填充。可不必关注具体细节,保证加密解密用的模式一样即可。
2. 非对称加密
2.1 简介
如下图所示,在非对称算法中,加密方用公钥加密数据,解密方用私钥解密数据,因为公钥和私钥是不同的秘钥,所以秘钥泄漏的风险会极大程度降低。 由于非对称加密的密钥生成麻烦,所以无法做到一次一密,而且其加密速度很慢,无法对大量数据加密。因此最常用的使用场景就是数字签名和密码传输,用作数字签名时使用私钥加签,公钥验;用作加密解密时,使用公钥加密,私钥解密。
2.2 常用算法
2.2.1 RSA(推荐)
工具类:
package com.wanlong.encrypt;
import org.apache.commons.codec.binary.Base64;
import javax.crypto.Cipher;
import java.io.ByteArrayOutputStream;
import java.security.*;
import java.security.interfaces.RSAPrivateKey;
import java.security.interfaces.RSAPublicKey;
import java.security.spec.PKCS8EncodedKeySpec;
import java.security.spec.X509EncodedKeySpec;
import java.util.HashMap;
import java.util.Map;
/**
* @author wanlong
* @version 1.0
* @description:
* @date 2023/4/23 14:58
*/
public class RSAUtils {
public static final String KEY_ALGORITHM = "RSA";
public static final String SIGNATURE_ALGORITHM = "MD5withRSA";
private static final String PUBLIC_KEY = "RSAPublicKey";
private static final String PRIVATE_KEY = "RSAPrivateKey";
private static final int MAX_ENCRYPT_BLOCK = 117;
private static final int MAX_DECRYPT_BLOCK = 256;
public RSAUtils() {
}
public static Map<String, Object> genKeyPair() throws Exception {
KeyPairGenerator keyPairGen = KeyPairGenerator.getInstance("RSA");
keyPairGen.initialize(2048);
KeyPair keyPair = keyPairGen.generateKeyPair();
RSAPublicKey publicKey = (RSAPublicKey)keyPair.getPublic();
RSAPrivateKey privateKey = (RSAPrivateKey)keyPair.getPrivate();
Map<String, Object> keyMap = new HashMap(2);
keyMap.put("RSAPublicKey", publicKey);
keyMap.put("RSAPrivateKey", privateKey);
return keyMap;
}
public static String sign(String content, String privateKey) throws Exception {
byte[] data = content.getBytes();
byte[] keyBytes = Base64.decodeBase64(privateKey);
PKCS8EncodedKeySpec pkcs8KeySpec = new PKCS8EncodedKeySpec(keyBytes);
KeyFactory keyFactory = KeyFactory.getInstance("RSA");
PrivateKey privateK = keyFactory.generatePrivate(pkcs8KeySpec);
Signature signature = Signature.getInstance("MD5withRSA");
signature.initSign(privateK);
signature.update(data);
return Base64.encodeBase64String(signature.sign());
}
public static boolean verify(String content, String publicKey, String sign) {
try {
byte[] data = content.getBytes();
byte[] keyBytes = Base64.decodeBase64(publicKey);
X509EncodedKeySpec keySpec = new X509EncodedKeySpec(keyBytes);
KeyFactory keyFactory = KeyFactory.getInstance("RSA");
PublicKey publicK = keyFactory.generatePublic(keySpec);
Signature signature = Signature.getInstance("MD5withRSA");
signature.initVerify(publicK);
signature.update(data);
return signature.verify(Base64.decodeBase64(sign));
} catch (Exception var9) {
var9.printStackTrace();
return false;
}
}
public static String decryptByPrivateKey(String data, String privateKey) throws Exception {
byte[] encryptedData = Base64.decodeBase64(data);
byte[] keyBytes = Base64.decodeBase64(privateKey);
PKCS8EncodedKeySpec pkcs8KeySpec = new PKCS8EncodedKeySpec(keyBytes);
KeyFactory keyFactory = KeyFactory.getInstance("RSA");
Key privateK = keyFactory.generatePrivate(pkcs8KeySpec);
Cipher cipher = Cipher.getInstance(keyFactory.getAlgorithm());
cipher.init(2, privateK);
int inputLen = encryptedData.length;
ByteArrayOutputStream out = new ByteArrayOutputStream();
int offSet = 0;
for(int i = 0; inputLen - offSet > 0; offSet = i * 256) {
byte[] cache;
if (inputLen - offSet > 256) {
cache = cipher.doFinal(encryptedData, offSet, 256);
} else {
cache = cipher.doFinal(encryptedData, offSet, inputLen - offSet);
}
out.write(cache, 0, cache.length);
++i;
}
byte[] decryptedData = out.toByteArray();
out.close();
return new String(decryptedData);
}
public static String decryptByPublicKey(String data, String publicKey) throws Exception {
byte[] encryptedData = Base64.decodeBase64(data);
byte[] keyBytes = Base64.decodeBase64(publicKey);
X509EncodedKeySpec x509KeySpec = new X509EncodedKeySpec(keyBytes);
KeyFactory keyFactory = KeyFactory.getInstance("RSA");
Key publicK = keyFactory.generatePublic(x509KeySpec);
Cipher cipher = Cipher.getInstance(keyFactory.getAlgorithm());
cipher.init(2, publicK);
int inputLen = encryptedData.length;
ByteArrayOutputStream out = new ByteArrayOutputStream();
int offSet = 0;
for(int i = 0; inputLen - offSet > 0; offSet = i * 256) {
byte[] cache;
if (inputLen - offSet > 256) {
cache = cipher.doFinal(encryptedData, offSet, 256);
} else {
cache = cipher.doFinal(encryptedData, offSet, inputLen - offSet);
}
out.write(cache, 0, cache.length);
++i;
}
byte[] decryptedData = out.toByteArray();
out.close();
return new String(decryptedData);
}
public static String encryptByPublicKey(String content, String publicKey) throws Exception {
byte[] data = content.getBytes();
byte[] keyBytes = Base64.decodeBase64(publicKey);
X509EncodedKeySpec x509KeySpec = new X509EncodedKeySpec(keyBytes);
KeyFactory keyFactory = KeyFactory.getInstance("RSA");
Key publicK = keyFactory.generatePublic(x509KeySpec);
Cipher cipher = Cipher.getInstance(keyFactory.getAlgorithm());
cipher.init(1, publicK);
int inputLen = data.length;
ByteArrayOutputStream out = new ByteArrayOutputStream();
int offSet = 0;
for(int i = 0; inputLen - offSet > 0; offSet = i * 117) {
byte[] cache;
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;
}
byte[] encryptedData = out.toByteArray();
out.close();
return Base64.encodeBase64String(encryptedData);
}
public static String encryptByPrivateKey(String content, String privateKey) throws Exception {
byte[] data = content.getBytes();
byte[] keyBytes = Base64.decodeBase64(privateKey);
PKCS8EncodedKeySpec pkcs8KeySpec = new PKCS8EncodedKeySpec(keyBytes);
KeyFactory keyFactory = KeyFactory.getInstance("RSA");
Key privateK = keyFactory.generatePrivate(pkcs8KeySpec);
Cipher cipher = Cipher.getInstance(keyFactory.getAlgorithm());
cipher.init(1, privateK);
int inputLen = data.length;
ByteArrayOutputStream out = new ByteArrayOutputStream();
int offSet = 0;
for(int i = 0; inputLen - offSet > 0; offSet = i * 117) {
byte[] cache;
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;
}
byte[] encryptedData = out.toByteArray();
out.close();
return Base64.encodeBase64String(encryptedData);
}
public static String getPrivateKey(Map<String, Object> keyMap) throws Exception {
Key key = (Key)keyMap.get("RSAPrivateKey");
return Base64.encodeBase64String(key.getEncoded());
}
public static String getPublicKey(Map<String, Object> keyMap) throws Exception {
Key key = (Key)keyMap.get("RSAPublicKey");
return Base64.encodeBase64String(key.getEncoded());
}
public static void main(String[] args) throws Exception {
Map<String, Object> keyMap = genKeyPair();
String publicKey = getPublicKey(keyMap);
String privateKey = getPrivateKey(keyMap);
System.out.println("publicKey=" + publicKey);
System.out.println("privateKey=" + privateKey);
String mingwen = "nihaoaaaaadsagdsagdagdaghah哈哈";
System.out.println("明文=" + mingwen);
System.out.println("明文长度=" + mingwen.length());
String sign = sign(mingwen, privateKey);
System.out.println("签名=" + sign);
System.out.println("验签结果=" + verify(mingwen, publicKey, sign));
String miwen = encryptByPublicKey(mingwen, publicKey);
System.out.println("公钥加密密文=" + miwen);
System.out.println("公钥加密密文长度=" + miwen.length());
System.out.println("私钥解密明文=" + decryptByPrivateKey(miwen, privateKey));
String mi = encryptByPrivateKey(mingwen, privateKey);
System.out.println("私钥加密密文=" + mi);
System.out.println("私钥加密密文长度=" + mi.length());
System.out.println("公钥解密明文=" + decryptByPublicKey(mi, publicKey));
}
}
测试:
@Test
public void testRSA() throws Exception{
RSAUtils.main(new String[2]);
}
代码运行结果:
publicKey=MIIBIjANBgkqhkiG9w0BAQEFAAOCAQ8AMIIBCgKCAQEA9A0Lc6M439G7sS5halq8M9uKWWrTpNRgXGoatCFTKQIVK6mjotBvAlAHNNFBs/9AAlLXFoiU/9nj6cCQDSC5PF/Q4G3UP+4KtebVXWfgn+AcW1wyOneb6tk1PpIA3260KCk+Kk67Aazk+EpyRH3OblE3vrUxo9jYljd9TSlfKtEnPYhuLgUIYXnZtjBwSlQhj84zpsN0qg2dewCa3vjHky0rt98j09KRItZOzFtWnCR9fDlPbFUd3LeotNCuEPi18EWjkvz8UYW1z1L1Ct7LePSlu5+kdERRExLxcuaTifqlyqTlZYB6RcHKb2TLG4edE/LPQDobzCfUxZfW4icDmQIDAQAB
privateKey=MIIEvgIBADANBgkqhkiG9w0BAQEFAASCBKgwggSkAgEAAoIBAQD0DQtzozjf0buxLmFqWrwz24pZatOk1GBcahq0IVMpAhUrqaOi0G8CUAc00UGz/0ACUtcWiJT/2ePpwJANILk8X9DgbdQ/7gq15tVdZ+Cf4BxbXDI6d5vq2TU+kgDfbrQoKT4qTrsBrOT4SnJEfc5uUTe+tTGj2NiWN31NKV8q0Sc9iG4uBQhhedm2MHBKVCGPzjOmw3SqDZ17AJre+MeTLSu33yPT0pEi1k7MW1acJH18OU9sVR3ct6i00K4Q+LXwRaOS/PxRhbXPUvUK3st49KW7n6R0RFETEvFy5pOJ+qXKpOVlgHpFwcpvZMsbh50T8s9AOhvMJ9TFl9biJwOZAgMBAAECggEAH1rdscmysO7dUnJHCccGjxMRv1M+RdsTVkw7ihEOAiLGXYm6AF0PJhjqFBseeeW9b479G2QI4KY55fpbh5RAuEUHLQ5vSpdmwOAbYZjK/z9n5UT/Hwvm+FFXyeYwPoSWmOJTnlPnKtvvvwaxj6MqNnow00usSQS/Lu2KJ7O9Cl78lL+ff1Ld/LHW1C4kG81RKChUrZA6StGi5YVHJOiJW51lzFWVmWa0slQRMftFx9dTne0+aReQHZCrKzoRLZY/0RGXWy7vreCsxffIUh4TldwMBnb1PFIun5H0ifGhQ6SMWSQd/8hGVVajgjE3duEHmMKHkfPGCgFFEHiOnNCKUQKBgQD8Hw0J5fGoyAelye8mSqgLEOOP2jKbDcn+BcrkVFx6szgNERTn0Ms8Tlu2D62mOF+rOzr/eTMfpA7XWKzmNraybSjMnNtwu0b6yiZSNM8s7O2YsjX1L1zdgqUN7HTKwiVq9DgqlTsoN7HU4aCgT/4f6q1zgfHw4LsN8snOreJ1ZwKBgQD3zjWy4IJeRmNhWHaMOE0y6K0y4fFLl8Q9Vws/x05lzSUdYfl75AHqoOQNLp4hJ2n9wQcuEddn/gRfKprUY7NRAHSC5twEFTNW+N/TyhHg1Ec02aYi8nd51bt52Dm+Z5SH4+jszOsEiXjjKiQxXAA7UB+GqKeDpQfdRPPCuF4e/wKBgQDGlhqqEkHgBPbpIn7JtYJZfQsRkvfLY/gXqy5d3Qke6K2ctTi3Q6HhtYLNi9nmlH7em//jnO6k3I+IFePSTYRoVE0Ppfm++PR2s+WtWMVbCKA5Zx3TK2RFYhNqPTbdQkFA1m6rM/JtwjjDipj/zSJIt4u58L/GUO1lljhZIGPAvwKBgGvqBUCMvLlJdeXinF2b9yKAaUyLUIVW5kLAMUnpBZ8Xo14UWI03Ug5XWnjTIFHX1DSJZxMM+f1zhXvcFekAMgrIY/TNjGrKwLzTTMXyba63Qvsk/VCivpz0EtyQ6TSd7vTur3TQGFi1dUuYAoLsSb2Kuu/tamXN7IrTiEP3/iUJAoGBANKCyFd/jeE4/DkN/GL3NLx4VTXC+lN0OzPA0pt4R4fPNkFsq7j6CWBMpB6NUu+XI7005JTa2vsOGI9alZmUxerEGvIpbvmplDgu/x1kjrR/RbTEVUB+aV2EIqG7u6rslbk8dbMjlTfvYqytLwC9Pk6E3oR/eYS8Dgg8Kpx3Z1BQ
明文=wedfefasdvasdva我打球问答区文昌
明文长度=23
签名=GoNvFHfF3yM1syLnmU9lbbp77jK8bvNlJi54WWPNMQTblOrjuWdiwdiZxwJoZFZNfo1lbmrv9M1fAkJKXBiUh1Nar9YehVytgif4rp2aqeiNmpNO7vT/ZIncDuIXdj2nqPgTbW2o1x8+XNWeKDuhJdrluD+bOWUce6JqI4ZR6LJw35NxckwTFTqNMGF2ta8btISY8/dMuNBlZZX7eCY/HXecgVCfosta5eQHvQY1iIjNh4i787wc7/xcvqIqKsR8XfBCV/g0UPj4FthEt098Fyi4NPXOtOcw5x9Fxz1RYRgU0I22eBsNDWysmOyKi9U2dSLAlPg2BDP7W1Pz0GVrPw==
验签结果=true
公钥加密密文=qnQBSypoQhFupfRWftCcwNTMEuwtmwqZva6uvKfg0Ioyh6ZKGdROtTXMBZ78d3yDMRXgOM5Cjn5/KNC+MiP7qMZQFM4txSqEcOT3qzaI6/qV5FD7FewOkTz585JHkjgxkwFZEUGiMgOTM1xzVfPHDmn34fvO2FkkbK7WscDzNeMOWMvh642L0MSxolWmyBZdAR0YmDhOi2nedN3ltay6dCm+Ozxnpj3BA9PHUQilhZbbjOFYuEf/ZcWkfK9Npd13fUmdfIkiF772AKV0QRCxj1I+bGWxSrspODyqm5vv5r6VMeM0zODT+WOHcx++Fxr7pORusQpAiQN4oCq/FGrv9g==
公钥加密密文长度=344
私钥解密明文=wedfefasdvasdva我打球问答区文昌
私钥加密密文=K5mZQo5f/CcFeXyQFHdfZrcow05ar+s8fpvo6YtkNsueLj5YOZBAdArNugNALs/xPBW9Q9Qj2dGqS3uDIEMEyLMNtl0PPQfymD41SiArFJXJut1z3Wu4BAiwht2XhMyyJfyqdjwLdj1etVKtw+7EYe9hkzSDMKMyNuIBwY9iOkBZf1VdQTSrfVSE/T8vwecYukkDlBZsb7EkveHMLA7Sz0fLhLJZwPQ6l1sY1MCX+wbYH5CdQHnIxbredtdj8gA0EXEe9wUdvOvpUONCbAzeZOqALFPiJh3LlrsD1L3KzNsp1vBmvIJI2xd0/1J7i0fv4JuvEiB+lCNkPPQGE+cXCA==
私钥加密密文长度=344
公钥解密明文=wedfefasdvasdva我打球问答区文昌
2.2.2 DSA
工具类:
public class DSAUtil {
private static final String ALGORITHM = "DSA";
private static final String DEFAULT_SIGNATURE_ALGORITHM = "SHA1withDSA";
private static final int DEFAULT_KEY_SIZE = 1024;
/**
* 生成密钥对
*/
public static InnerKey generateKey() throws NoSuchAlgorithmException {
return generateKey(DEFAULT_KEY_SIZE);
}
/**
* 生成密钥对
* @param keysize
* @return
* @throws NoSuchAlgorithmException
*/
public static InnerKey generateKey(int keysize) throws NoSuchAlgorithmException {
// 初始化密钥
KeyPairGenerator keyPairGenerator = KeyPairGenerator.getInstance(ALGORITHM);
keyPairGenerator.initialize(keysize);
KeyPair keyPair = keyPairGenerator.generateKeyPair();
return InnerKey.builder()
.publicKey(keyPair.getPublic().getEncoded())
.privateKey(keyPair.getPrivate().getEncoded())
.build();
}
public static byte[] sign(byte[] privateKey, byte[] data)
throws InvalidKeySpecException, NoSuchAlgorithmException, InvalidKeyException, SignatureException {
return sign(privateKey, data, DEFAULT_SIGNATURE_ALGORITHM);
}
/**
* 使用私钥进行签名
* @param privateKey 私钥
* @param data 数据
* @param signatureAlgorithm 签名算法
* @return
* @throws Exception
*/
public static byte[] sign(byte[] privateKey, byte[] data, String signatureAlgorithm)
throws NoSuchAlgorithmException, InvalidKeySpecException, InvalidKeyException, SignatureException {
KeyFactory keyFactory = KeyFactory.getInstance(ALGORITHM);
PKCS8EncodedKeySpec pkcs8EncodedKeySpec = new PKCS8EncodedKeySpec(privateKey);
PrivateKey privateKey2 = keyFactory.generatePrivate(pkcs8EncodedKeySpec);
Signature signature = Signature.getInstance(signatureAlgorithm);
signature.initSign(privateKey2);
signature.update(data);
byte[] bytes = signature.sign();
return bytes;
}
public static boolean verifySign(byte[] publicKey, byte[] data, byte[] sign)
throws InvalidKeySpecException, NoSuchAlgorithmException, InvalidKeyException, SignatureException {
return verifySign(publicKey, data, sign, DEFAULT_SIGNATURE_ALGORITHM);
}
/**
* 使用公钥验证签名
*
* @param publicKey 公钥
* @param data 数据
* @param sign 数据签名
* @param signatureAlgorithm 签名算法
* @return
* @throws Exception
*/
public static boolean verifySign(byte[] publicKey, byte[] data, byte[] sign, String signatureAlgorithm)
throws NoSuchAlgorithmException, InvalidKeySpecException, InvalidKeyException, SignatureException {
KeyFactory keyFactory = KeyFactory.getInstance(ALGORITHM);
X509EncodedKeySpec x509EncodedKeySpec = new X509EncodedKeySpec(publicKey);
PublicKey publicKey2 = keyFactory.generatePublic(x509EncodedKeySpec);
Signature signature = Signature.getInstance(signatureAlgorithm);
signature.initVerify(publicKey2);
signature.update(data);
boolean bool = signature.verify(sign);
return bool;
}
@Data
@NoArgsConstructor
@AllArgsConstructor
@Builder
public static class InnerKey {
private byte[] publicKey;
private byte[] privateKey;
}
}
@Test
public void testDSA() throws Exception{
DSAUtil.InnerKey innerKey = DSAUtil.generateKey();
String text = "我是测试数据数据,用来验证DSA";
System.out.println("公钥:" + Base64.encodeBase64String(innerKey.getPublicKey()));
System.out.println("私钥:" + Base64.encodeBase64String(innerKey.getPrivateKey()));
byte[] sign = DSAUtil.sign(innerKey.getPrivateKey(), text.getBytes());
System.out.println("原文:" + text);
System.out.println("数字签名:" + Base64.encodeBase64String(sign));
boolean bool = DSAUtil.verifySign(innerKey.getPublicKey(), text.getBytes(), sign);
System.out.println("验签结果:" + bool);
}
代码运行结果:
公钥:MIIBtzCCASwGByqGSM44BAEwggEfAoGBAP1/U4EddRIpUt9KnC7s5Of2EbdSPO9EAMMeP4C2USZpRV1AIlH7WT2NWPq/xfW6MPbLm1Vs14E7gB00b/JmYLdrmVClpJ+f6AR7ECLCT7up1/63xhv4O1fnxqimFQ8E+4P208UewwI1VBNaFpEy9nXzrith1yrv8iIDGZ3RSAHHAhUAl2BQjxUjC8yykrmCouuEC/BYHPUCgYEA9+GghdabPd7LvKtcNrhXuXmUr7v6OuqC+VdMCz0HgmdRWVeOutRZT+ZxBxCBgLRJFnEj6EwoFhO3zwkyjMim4TwWeotUfI0o4KOuHiuzpnWRbqN/C/ohNWLx+2J6ASQ7zKTxvqhRkImog9/hWuWfBpKLZl6Ae1UlZAFMO/7PSSoDgYQAAoGAQGnbiw4RQTV5Z+QrMtKWJ2XcfQIVnYtWpkraSwEO689M+Kjy/eegaa2gk+I6MK476TtiC9TH1LlhAcchxEar+NyeDMSEJ/44dIRBG2k+2T9dVKv/YZaFm7bDcwCY1lj0MIQ/Khk5UfG/XspjT6cZ63k3wOO9s3caCa6a8sBqdRY=
私钥:MIIBSwIBADCCASwGByqGSM44BAEwggEfAoGBAP1/U4EddRIpUt9KnC7s5Of2EbdSPO9EAMMeP4C2USZpRV1AIlH7WT2NWPq/xfW6MPbLm1Vs14E7gB00b/JmYLdrmVClpJ+f6AR7ECLCT7up1/63xhv4O1fnxqimFQ8E+4P208UewwI1VBNaFpEy9nXzrith1yrv8iIDGZ3RSAHHAhUAl2BQjxUjC8yykrmCouuEC/BYHPUCgYEA9+GghdabPd7LvKtcNrhXuXmUr7v6OuqC+VdMCz0HgmdRWVeOutRZT+ZxBxCBgLRJFnEj6EwoFhO3zwkyjMim4TwWeotUfI0o4KOuHiuzpnWRbqN/C/ohNWLx+2J6ASQ7zKTxvqhRkImog9/hWuWfBpKLZl6Ae1UlZAFMO/7PSSoEFgIULc0aRq94NyvcjcbxHCoGCylUPXk=
原文:我是测试数据数据,用来验证DSA
数字签名:MCwCFAlEkt+8er3L2mPwt/cf+81++Y6dAhQte9bmVDUAuFhcQLt3cqKXD6wRuA==
验签结果:true
2.3 算法优缺点
- RSA 在日常接口对接加密,验签中是常用方案,DSA用的比较少
- 对于大多数用例、行业和监管环境,RSA 和 DSA 非常相似,提供相同的加密强度,两者之间的差异相对较小。 这两种算法也同样兼容领先的互联网协议,包括 Nettle、OpenSSL、wolfCrypt、Crypto++ 和 cryptlib
3. 其他常用算法
3.1 MD5
3.1.1 简介
MD5算法是典型的消息摘要算法,其前身有MD2、MD3和MD4算法,它由MD4、MD3和MD2算法改进而来。不论是哪一种MD算法,它们都需 要获得一个随机长度的信息并产生一个128位的信息摘要。如果将这个128位的二进制摘要信息换算成十六进制,可以得到一个32位的字符串,故我们见到的大部分MD5算法的数字指纹都是32为十六进制的字符串。
3.1.2 使用
@Test
public void testMD5(){
String md5Hex = DigestUtils.md5Hex("这是一串密码");
System.out.println("加密报文为:"+md5Hex);
//加密报文为:122141b53a06a34818d5afa6d17151e5
}
3.1.3 使用场景
- 密码加密管理
- 信息摘要,验证报文完整性
3.2 Base64
3.2.1 简介
Base64是基于64个可打印字符来表示二进制数据的编解码方式。
正因为可编解码,所以它主要的作用不在于安全性,而在于让内容能在各个网关间无错的传输。
3.2.2 使用
@Test
public void testBase64(){
String base64String = Base64.encodeBase64String("我是一直小菜鸟".getBytes());
System.out.println("base64转码后字符串为"+base64String);
byte[] bytes = Base64.decodeBase64(base64String);
System.out.println("base64解密后的字符串为"+new String(bytes));
// base64转码后字符串为5oiR5piv5LiA55u05bCP6I+c6bif
// base64解密后的字符串为我是一直小菜鸟
}
3.2.3 使用场景
- 数据加解密之前的转码,转为字节码再加密,解密
- 数据交互过程中,先转码再传输,保证数据在不同系统之间无错传输
3.3 URLS
3.3.1 简介
在访问浏览器的时候,如果是get请求,很多参数信息会直接在请求地址里面,如果此时携带的参数值本身有些特殊字符,比如中文,=,& ,此时直接访问浏览器无法正确识别,需要先将特殊字符转码。
3.3.2 使用
@Test
public void testUrlCodec() throws Exception{
String encode = new URLCodec().encode("我是一个粉刷匠,粉刷本领强");
System.out.println("UrlEncode加密后的报文为:"+encode);
String decode = new URLCodec().decode(encode);
System.out.println("UrlCodeC解密后的报文为:"+decode);
// UrlEncode加密后的报文为:%E6%88%91%E6%98%AF%E4%B8%80%E4%B8%AA%E7%B2%89%E5%88%B7%E5%8C%A0%EF%BC%8C%E7%B2%89%E5%88%B7%E6%9C%AC%E9%A2%86%E5%BC%BA
// UrlCodeC解密后的报文为:我是一个粉刷匠,粉刷本领强
}
3.3.3 使用场景
- 当字符串数据以url的形式传递给web服务器时,字符串中如果含有空格,可能会造成信息获取失败(浏览器无法识别);此外还有一些特殊字符,如邮箱参数中的@,=符等,需要encode之后以符合url的规范。
- 一个接口,在传送的时候,我们要参数中包含一个url回调地址,也就是提供给对方进行回调的。那么如果是http 使用get请求的时候,不可以url+url ,以为回调地址中也可能包含参数连接符&等。这样就会混淆两个url。这个时候,就需要使用urlencode将回调地址编码。
- 总而言之,我们需要编码,使得浏览器不会把我们请求地址包含的数据里面的特殊字符等识别为地址的分隔符等
4. 注意事项
4.1 非对称加密与对称加密比较
4.1.1 对称加密
优点:算法公开、计算量小、加密速度快、加密效率高。
缺点:安全性差,只要有秘钥,数据没有安全空间。因为秘钥一样,秘钥泄漏风险大
4.1.2 非对称加密
优点:安全性相对较高,公钥和私钥分开保存
缺点:加密解密性能差,花费时间久
4.2 MD5 使用优化
4.2.1 md5加盐
- md5算法是不可逆的,即只可以计算摘要,不可以根据摘要反解密出来原始报文。但是因为很多用户维护密码都相对简单,比如123456,admin,或者生日等,加上单纯MD5算出来的值一直是固定的,那么如果有一个md5库,那可能会根据md5值反推测出用户真实的密码。
- 比如:123456 的MD5 是 ABC,而又有一个md5 库,里面维护了明文和对应的md5值。刚好维护了123456 对应的摘要为ABC,那么黑客就可以推测出用户真实密码为123456。进而模拟用户登录,盗窃用户数据和资产。
- 当用户首次提供密码时(通常是注册时),由系统自动往这个密码里撒一些“佐料”,然后再散列。生成的数据摘要和盐保存起来,以便于下次用户验证使用。而当用户登录时,系统为用户提供的代码撒上同样的“佐料”,然后散列,再比较散列值,已确定密码是否正确。
- 这里的“佐料”被称作“Salt值”,这个值是由系统随机生成的,并且只有系统知道。这样,即便两个用户使用了同一个密码,由于系统为它们生成的salt值不同,他们的散列值也是不同的。即便黑客可以通过自己的密码和自己生成的散列值来找具有特定密码的用户,但这个几率太小了(密码和salt值都得和黑客使用的一样才行)。
- 加盐可以在数据的前面或者后面,或者中间加盐,只要系统维护好加盐的规则,保证注册的时候的密码,登录用同样的密码可以登录即可。比如明文密码是123456,随机盐值是uyx677,拼接后字符串是uyx677123456,根据这个摘要得到dfdfd9923,在盐值不暴露情况下,黑客很难破解密码。
4.2.2 校验完整性
每个文件都可以用MD5验证程序算出一个固定的MD5值,是独一无二的。一般来说,开发方会在软件发布时预先算出文件的MD5值,如果文件被盗用,加了木马或者被篡改版权,那么它的MD5值也随之改变,也就是说我们对比文件当前的MD5值和它标准的MD5值来检验它是否正确和完整。
- 例如网盘中的秒传4G文件,可以使用用户需要上传的文件进行Md5运算,判断与服务器中是否存在该文件,如果存在只需添加文件索引,不存在再真正上传。
- 例如自动升级的客户端,判断下载的程序安装包是否完整,可以计算文件的MD5值,与服务器端计算的Md5值进行比对。
- 比如给合作方提供的更新jar包,可以计算一个md5值和jar包一起提供给对方,对方拿到jar部署之前,先校验jar的md5值和提供的一致,保证jar包是完整且版本没问题再部署
5. 成熟接口对接方案介绍
5.1 api设计
上面提到了,对称加密比较快,但是不安全,非对称加密比较慢,但是相对安全,还有为了保证数据完整性,我们可以加个md5验证签名,那么设计相对来说比较合理的接口对接,这三种加密我们都会涉及到。具体可以设计如下:
5.1.1 创建分配密钥
对称加密,我们选择算法AES
非对称加密,我们选择算法RSA
签名算法,我们可以选择md5 或者RSA
- 服务端分配给固定的渠道客户id ,标识是哪个渠道进来的客户 clientId
- 服务端为这个客户生成RSA密钥对,公钥提供给客户,serverPublicKey,serverPrikey
- 客户端生成自己的RSA密钥对,公钥提供给服务端,clientPublicKey,clinetPrikey
5.1.2 客户端加密过程:
- 假设业务参数如下: “clientId”:“201534”,“creditCode”:“dfoekwdldllrkhsfd”,orderNo: KD30ea5df4f
- 将业务参数维护为一个字符串对象paramJsonString
- 报文用clientPubKey AES对称加密为encrypt
- 加密后的报文计算MD5值为md5Hex
- md5Hex转大写,用客户端私钥clientPriKey 加签 ,算法RSA
- 组装参数调用服务方
@Test
public void testClientEncrypt() throws Exception {
//服务端分配的唯一标识
String clientId = "123445";
String creditCode = "923834342323";
String orderNo = "202304232019011212";
//设置时间戳
long timeStamp = System.currentTimeMillis();
//客户端公钥,会线下提供给服务方
String clientPubKey = "MIIBIjANBgkqhkiG9w0BAQEFAAOCAQ8AMIIBCgKCAQEAhAtKgUEJH9zjCiZ1YxbfwAiSVXqPhph6rqL3g8XUooaEhm7a6p8Kj4OvFZIj8++4Ma7fFN6uG9uqmHMeOgWbuqDKIIWiABN0+3AIwX6rveqJ94tR523CayvM2ha0YcRSoWFsgHDFT2nDw/dcZlzYPtXGdtlrOWLpdh9OVAp5u5jC6bowXzl+1va4rwg+BxSYML4kcee33X4TK8a6OVFuoE78z0NNfAtRAW+b6WvlrySZQUkYMUhKxXR4n+aGI+919eTxKQI6wltl5b1k++hlTRhUWq3aSOI3dw+bjVw3hIoWxDnIQVjhS+C4jZnmA3OYMXJmAFCTk+XJKXS/1jOJpwIDAQAB";
//客户端私钥,客户端自己保存,保证不外泄
String clientPriKey = "MIIEvQIBADANBgkqhkiG9w0BAQEFAASCBKcwggSjAgEAAoIBAQCEC0qBQQkf3OMKJnVjFt/ACJJVeo+GmHquoveDxdSihoSGbtrqnwqPg68VkiPz77gxrt8U3q4b26qYcx46BZu6oMoghaIAE3T7cAjBfqu96on3i1HnbcJrK8zaFrRhxFKhYWyAcMVPacPD91xmXNg+1cZ22Ws5Yul2H05UCnm7mMLpujBfOX7W9rivCD4HFJgwviRx57fdfhMrxro5UW6gTvzPQ018C1EBb5vpa+WvJJlBSRgxSErFdHif5oYj73X15PEpAjrCW2XlvWT76GVNGFRardpI4jd3D5uNXDeEihbEOchBWOFL4LiNmeYDc5gxcmYAUJOT5ckpdL/WM4mnAgMBAAECggEAEFkdhlc4+/rrGSydyADi/vGQvIiKEI38UnbW0jfCFcU1zzcWX4oFUrNpD/CiBuedFHmkIP9Y6xYNTwahrWlYWIjjvZcN8Zh3GVJLozk9pivNnRpgr5iFQ6OOn0nEV35pBJX9SwGrTCO6Gx8bD4lbNJxfEAPnJJm8ceV6WylY3QLju24cuNSZe+iVSsi12lasm0C9cuN+GXxAT/LIAEDHdhO2ve/b2Pmc8A7s3TKpaeDbEYPPQFr5AdOsQWKR3G1Xj5y9zfWYAxYrlKDRaVzn0ZQRMkr78Dc1I5wGBt7zuQ+NV2RcZqN+dwND3CUflr0M0VZ91IzEgA4rjqAYBRgakQKBgQDufi9FBGvYCD2q97J8Yoz3R8dJCNwD7XB4QtgHILu5AL1utWPxV+wEb6e1Np8metgiZ6eRsy0/RITv9Cpw7CXYs6dDO2E50YZ1SCxw48y7DcuUhJ8Gcw1teOW10jrgrgj5mx5lZXk+y+kwDBwJnIV5aPGTkII4L6ZEZdTgqT/OawKBgQCNvLH5VVXACCR59DopSMO72zqa7tpTrZjP2F2RvwUnjm5h0Lg+hUBd3pQxR5tBrgL8K6JidPLu1KYlIKJstRFcPBxRRi2oAp7Xab91IS9AicMqRp/DQ9fGbvOEFfbqrsYSBmkyLTa9/u8tJ4q97cGl7tPDQnsYCXztjKy5VznItQKBgFT7n+pvBeLIJPlcJDzQfTzMVgX/Yy7/F9myr/nyPxJQYjAl8MQaOV7nTpJzMUjr80JnPz3a5B+sUdaTYeA1NdM/STXa3e1VbR0zp6peciChbM6yZ1xL+RfuQaSFkjdnrIkNLkqhWw/2Z1SYrBD8tk8qBxM3Jj/RMuUiiDW5+aObAoGAcXQIlgdLo3IhGIl5ANDg9H8wcxIcKvz7I+wSQfbAfoBRNPTodgsdkcX3o1apHTLX6thWnC72wlvvPirYOOHbKpRnKltdcm3ejZl1CqHse+GKGk371kZ48rqfLCyUwBf0Ljt5exOcDQuCkgdj1FH7PwJj+Zk+hOgbWt7O53C9rT0CgYEA5D1Sj0CPaCUdp9wVnrm+KYyQdxiFk7EpYhIK43g/6o1Ka8Ow/G+1L2sttVJOjqfH4sRVxpYsTBva6/3uo+imPU+8SyLwOGD7m0yschLkPbUtYYver7QRO/dWmcl9OQU1euHjJnvLcQJ12dqWibxD5I8lAVNnhOYUycfNyTZUwjI=";
//业务数据对象封装
JSONObject param = new JSONObject();
param.put("clientId", clientId);
param.put("creditCode", creditCode);
param.put("orderNo", orderNo);
param.put("timeStamp", timeStamp);
//数据转字符串
String paramJsonString = param.toJSONString();
//使用客户端公钥对称加密 业务数据(因为业务数据可能比较大,所以选择对称加密,提高加解密效率)
String encrypt = AESUtils.encrypt(paramJsonString, clientPubKey);
//md5摘要加密数据
String md5Hex = DigestUtils.md5Hex(encrypt);
//对md5值转大写,加签
String sign = RSAUtils.sign(md5Hex.toUpperCase(), clientPriKey);
//组装请求参数
JSONObject reqMsg = new JSONObject();
reqMsg.put("clientId", clientId);
reqMsg.put("data", encrypt);
reqMsg.put("sign", sign);
reqMsg.put("timeStamp", timeStamp);
//调用参数
System.out.println(reqMsg.toJSONString());
//{"timeStamp":1682255617696,"clientId":"123445","data":"C69844465FA1D8CDF447B841B434D2777F255A608A09CE0C84767986BD95E3CCA2B778306AA561C79B0B2345F274371F38B5FD7BDBB12828084981BEA7AA6B49E64964D1AF936BD8742EA9F4E6A8C3AE82DD8DA9FCA9949DF64F14728641DF2A91835E37DDA8C6CFA86A832EC1761EDD","sign":"HWZcm/NVMuljAIrCBjcstq5n0uGXDM9oA6xTT6fWm7nfuDPQyFqPLz0F4uNeZdV/GGp1dMH8K+SwGUee3TNorpBRVRRF9g9HUOy6uC3ifpdVpO34/grtxWAfPnUOMXzKEBtn36p2gOz9Nex9NCqr1BmzsfeKV/h3W6k+DXoJIHx3m7hGT4uSa4zwaq0ncsXCnkDh7x6JS5VjC6POGzeoFH7QFex7m1F12yOuTLRy78IVHygCMOML/5Id6xF+r/dUXUcpolsnlRpbMHr4ScqLWoOmT+tODLq5HcY5jYLmADEocpJAik88S7Fvjwr8ksj+xXMTiRV7X5MAMVmRn34bpg=="}
}
5.1.3 服务端解密请求过程:
- 判断客户ID是否存在
- 请求报文验证签名
- 验签通过后解密参数
- 处理业务请求
@Test
public void testServerDecrypt() throws Exception {
//服务端分配的唯一标识
String clientId = "123445";
//客户端公钥,会线下提供给服务方
String clientPubKey = "MIIBIjANBgkqhkiG9w0BAQEFAAOCAQ8AMIIBCgKCAQEAhAtKgUEJH9zjCiZ1YxbfwAiSVXqPhph6rqL3g8XUooaEhm7a6p8Kj4OvFZIj8++4Ma7fFN6uG9uqmHMeOgWbuqDKIIWiABN0+3AIwX6rveqJ94tR523CayvM2ha0YcRSoWFsgHDFT2nDw/dcZlzYPtXGdtlrOWLpdh9OVAp5u5jC6bowXzl+1va4rwg+BxSYML4kcee33X4TK8a6OVFuoE78z0NNfAtRAW+b6WvlrySZQUkYMUhKxXR4n+aGI+919eTxKQI6wltl5b1k++hlTRhUWq3aSOI3dw+bjVw3hIoWxDnIQVjhS+C4jZnmA3OYMXJmAFCTk+XJKXS/1jOJpwIDAQAB";
//服务端接收到请求
JSONObject reqMsg = JSONObject.parseObject("{\"timeStamp\":1682255617696,\"clientId\":\"123445\",\"data\":\"C69844465FA1D8CDF447B841B434D2777F255A608A09CE0C84767986BD95E3CCA2B778306AA561C79B0B2345F274371F38B5FD7BDBB12828084981BEA7AA6B49E64964D1AF936BD8742EA9F4E6A8C3AE82DD8DA9FCA9949DF64F14728641DF2A91835E37DDA8C6CFA86A832EC1761EDD\",\"sign\":\"HWZcm/NVMuljAIrCBjcstq5n0uGXDM9oA6xTT6fWm7nfuDPQyFqPLz0F4uNeZdV/GGp1dMH8K+SwGUee3TNorpBRVRRF9g9HUOy6uC3ifpdVpO34/grtxWAfPnUOMXzKEBtn36p2gOz9Nex9NCqr1BmzsfeKV/h3W6k+DXoJIHx3m7hGT4uSa4zwaq0ncsXCnkDh7x6JS5VjC6POGzeoFH7QFex7m1F12yOuTLRy78IVHygCMOML/5Id6xF+r/dUXUcpolsnlRpbMHr4ScqLWoOmT+tODLq5HcY5jYLmADEocpJAik88S7Fvjwr8ksj+xXMTiRV7X5MAMVmRn34bpg==\"}");
//1.看clientId在系统中是否存在,如果不存在,报错,响应异常
//2.如果存在,验签,确认 数据完整性以及防止篡改
String data = reqMsg.getString("data");
String serverParamSign = reqMsg.getString("sign");
//3.报文验证签名
String md5 = DigestUtils.md5Hex(data).toUpperCase();
boolean verify = RSAUtils.verify(md5, clientPubKey, serverParamSign);
//如果签名不对,报错,响应异常
if (!verify) {
System.out.println("数据验签失败,非法请求,请确认");
return;
}
//如果验签通过,解密业务参数(客户端公钥线下已提供给服务端)
String decrypt = AESUtils.decrypt(data, clientPubKey);
System.out.println("收到的客户端请求参数为" + decrypt);
//---------服务端处理请求---------------
// 收到的客户端请求参数为{"timeStamp":1682255617696,"clientId":"123445","creditCode":"923834342323","orderNo":"202304232019011212"}
}
5.1.4 服务端加密返回过程:
- 服务端处理好请求后,返回报文加签
- 组装返回参数,返回
- 注意,这里如果数据重要的话,也可以先将报文先对称加密,客户端对称解密后使用
@Test
public void testServerEncrypt() throws Exception {
//服务端私钥
String serverPriKey = "MIIEvgIBADANBgkqhkiG9w0BAQEFAASCBKgwggSkAgEAAoIBAQCUxuPd/L+ivYUt22APgsUYWgcADVTVOYQtUxqku+5lwOE1TlcYRfP0EBvhkVlbv8KOnLroPRwgpQ4acTz7LObqWkHlmZbP3Z3n4sgZtBzfSb9n7jAqluvdA7HEkfC9MerInfYqxqDe2OLLoF/rxT62zhXiJenZk4+IV86WwhPHgTJ0B/UfYGFB7DK1ylNCUZF9sVkViQx8kwSEFx36XzjMW60sZSr3sRPtSkck5Ce4wZlJSTCVhngz2M1+xTRcSJ0zzIbfITyYiVmfpi7lFnBY27JgguxiVLusPrD6HNhnqX4ksD4vSkTF41AWljN4hRGvVcU0rvsiqzn6dSf+JkPjAgMBAAECggEATttw0ivyo17S5XB+5RnKQFMPnM50JaSb065V0/UsslBeznNcpKgHW1jiflgdRg1JIXAffZNOglsaM5fL29J7/sGiZgFXt4ve85b3uvAF0bB0PLE6ZIikMfyQUAGO2alIqddYMGohwJyy8X1q+jwNNrXbUYgJAGM/U+Q+wxfRhg+2uWD+qALHg0SqlSjtrevojlRVDfpCODq4hYb6x3DS2bcO90+jVbVVOr9ss8zYkZR+e6BjgIxDhFZcV6OMVlNjFhiJldOKRw3ocZcc2xNAsiEslDH3C5JEKINEP4iJOfH8lzdl+eYx60Y3lLYe1FmfmqOePOyzmfU9nUcHKT6vuQKBgQD5ctSpLc2kP3bmZ/xbBnzWgK6nUrExqM7txlvorw9dx4o9u2Xg1V1UG3i2Bt96VnIkeWKbkm+qiEnpqc72B4W3GvbyVN6oDFY7kYQVqmqDCBXlAI0SQDU+bsywSN8l7qfRMj9Eszhl+dpWbYF/ZAAPTxJxV5f4qHn1uahImKZr1wKBgQCYrzFdlEcEvtzBJrZMq0cvZdR/nZoaWaEEuHGLz5muxHO/RFFgIUaWaKu6SjesMcIxshlG7G+7jvb7QK9ggVIB3MJzcLN/WwF8hfPUGbEJL0z05yrlq5RlbEL8a17twhFclFDoYKGcC+gshsM2jJj3d4yr4CJNsdHgeK/kUySG1QKBgQDhBiWxOBB6SoYH82ZC9udG2W60onQWSCHjfT0L+l/yST770UFmQROf++g4zJps0e7F2HqLvN3fCHMkxnpclwyY0UpT8RuqnPZtZiexI7pR2clme8jG3gUp/OnJXPNwVIouvRU19Da8R1ge/0oCkUM8jCgm9s2xEspyULjuREZcoQKBgHE5IVtqtItxWdSl93O9Y6ljUwRbg8ZchsowWIs8JJP0PH0ulUW7B1RMRGR92vEHaSFfF91/QZANgVtqaWMgIwcchNoETid3/g8walLzxlOwTtohfq8X4JChir8ShC+9xRApMCJSXiWQstgqCyMtedWxQpDGQcnpvmA+PvkLKsANAoGBAOSRLf5QVGhaZ6KHp2424aSylcbjD/cyHx8Vcayf/5berXGhWrawonE3TmBOZMPlAfviHUDN2Pmj3uViik2k7A20pLQUsgengxwn9nlnxMyh7eRJa+ov7+xaWx5llfKWSMJ972XTuzZOVRg+IhtE8Gj6cOjn9A3yhhi7ZYw8iFa9";
//假设业务数据如下:
String businessData = "我是服务端返回业务数据";
JSONObject result = new JSONObject();
result.put("code", "000000");
result.put("msg", "请求处理成功");
JSONObject resultData = new JSONObject();
resultData.put("data", businessData);
result.put("data", resultData.toJSONString());
//返回报文加签,服务端私钥加签
String serverSign = RSAUtils.sign(DigestUtils.md5Hex(businessData).toUpperCase(), serverPriKey);
result.put("sign", serverSign);
System.out.println("服务端返回为" + result.toJSONString());
//{"msg":"请求处理成功","code":"000000","data":"{\"data\":\"我是服务端返回业务数据\"}","sign":"XEDkV4wx8abvBS41pFhnM1EukH3Nsy4KnFhDQwg2ezwvVvZVQnnS47lDGcOu6S9MuAnqQPYgQIg6GEtryfoYClSOOSkvddCLlLBrlmGNs6CL6hsmVDcA5drnw1YozZfLPi7lbVHb1hOG6MMcQyVHNgxVAzpHtU72gZAYDGYdqZfJ+VPf0IMS5eLUwMxSw+zgvMekMKJYobfQOD/KHr1gT/ui9PWzbqXXG26GkMWlxa+i+gIoVBPSe3R+UiwzY+T6ZOGyB14/195MVXwh1I8WcMzuu7XmsstNa9fCKz5MFnKcXo+vwvQ/tO7X/+IPN4IAw5ZdBT/nveGDbi3XbQh0lw=="}
}
5.1.5 客户端解密服务端返回过程
- 客户端拿到响应后,先判断返回码是否正常
- 返回码正常验证签名,防止数据传输过程被篡改,或者丢包
- 验证签名后,可以使用数据
- 注意,这里如果数据重要的话,也可以先将报文先对称加密,客户端对称解密后使用
@Test
public void testClientDecrypt() throws Exception {
//服务端公钥,线下提供给客户端
String serverPubKey = "MIIBIjANBgkqhkiG9w0BAQEFAAOCAQ8AMIIBCgKCAQEAlMbj3fy/or2FLdtgD4LFGFoHAA1U1TmELVMapLvuZcDhNU5XGEXz9BAb4ZFZW7/Cjpy66D0cIKUOGnE8+yzm6lpB5ZmWz92d5+LIGbQc30m/Z+4wKpbr3QOxxJHwvTHqyJ32Ksag3tjiy6Bf68U+ts4V4iXp2ZOPiFfOlsITx4EydAf1H2BhQewytcpTQlGRfbFZFYkMfJMEhBcd+l84zFutLGUq97ET7UpHJOQnuMGZSUkwlYZ4M9jNfsU0XEidM8yG3yE8mIlZn6Yu5RZwWNuyYILsYlS7rD6w+hzYZ6l+JLA+L0pExeNQFpYzeIURr1XFNK77Iqs5+nUn/iZD4wIDAQAB";
//--------------服务端返回,客户端收到返回验证报文可靠性---------
JSONObject result = JSONObject.parseObject("{\"msg\":\"请求处理成功\",\"code\":\"000000\",\"data\":\"{\\\"data\\\":\\\"我是服务端返回业务数据\\\"}\",\"sign\":\"XEDkV4wx8abvBS41pFhnM1EukH3Nsy4KnFhDQwg2ezwvVvZVQnnS47lDGcOu6S9MuAnqQPYgQIg6GEtryfoYClSOOSkvddCLlLBrlmGNs6CL6hsmVDcA5drnw1YozZfLPi7lbVHb1hOG6MMcQyVHNgxVAzpHtU72gZAYDGYdqZfJ+VPf0IMS5eLUwMxSw+zgvMekMKJYobfQOD/KHr1gT/ui9PWzbqXXG26GkMWlxa+i+gIoVBPSe3R+UiwzY+T6ZOGyB14/195MVXwh1I8WcMzuu7XmsstNa9fCKz5MFnKcXo+vwvQ/tO7X/+IPN4IAw5ZdBT/nveGDbi3XbQh0lw==\"}");
String businessData = result.getJSONObject("data").getString("data");
String serverSign = result.getString("sign");
//1.判断code是否正常,如果正常,验证签名,如果异常,直接报错
//2.验签
boolean verify = RSAUtils.verify(DigestUtils.md5Hex(businessData).toUpperCase(), serverPubKey, serverSign);
System.out.println("验签结果:" + verify);
//验签通过,可以使用数据
System.out.println("获取到的业务数据为:" + businessData);
}
5.2 接口设计其他考量
5.2.1 是否需要幂等
- 在某些重要的接口中,幂等是很重要的控制手段,不幂等,如果网络抖动,或者超时等,客户端可能会重复调用多次,如果是扣款接口等,会造成灾难性的影响
- 接口幂等现在因为服务都是集群部署,常用方式就是加分布式锁控制,锁的ID一般是请求ID,或者账单号码等,这个具体接口细节具体对待
5.2.2 是否同步改为异步
- http请求是无状态的,如果服务端处理请求真的需要很长的时间,不可能让客户端和服务端一直维护一个连接,等待服务端返回,这对客户端和服务端都是极大的压力。
- 如果服务处理时间超长的话,可以考虑接口异步处理,先返回客户端正常收到请求,等数据处理完再异步通知客户端处理结果
- 这种一般要和客户端提前约定好异步回调地址和参数格式
- 在这种异步回调交互中,还需要考虑重试机制和补偿机制,比如,回调失败怎么处理,是否需要定时任务补偿回调;多次回调失败怎么处理,次数需要有限制,不然可能出现定时任务积压异常回调请求,导致回调的客户端处理压力过大
5.2.3 接口返回码和异常统一封装管理
- 接口的返回码统一封装是一个老生常谈的话题,统一的好处不言而喻,我们可以通过返回码查询字典表,一眼看到具体是什么问题
- 与返回码有极大关联的是统一异常管理机制,该抛业务异常抛业务异常,该抛参数异常抛参数异常,最后在web层自定义异常处理器,统一处理应用层抛出的异常,并针对不同的异常,做不同返回码区分,总而言之,最终目的是通过返回码能定位到是哪个服务的什么异常,提高生产问题排查效率
5.2.4 加时间戳
- 加时间戳的主要目的是服务端可以看到这边请求的真实创建时间
- 是客户端调用接口时对应的当前时间戳,时间戳用于防止DoS攻击。当黑客劫持了请求的url去DoS攻击,每次调用接口时接口都会判断服务器当前系统时间和接口中传的的timestamp的差值,如果这个差值超过某个设置的时间(假如5分钟),那么这个请求将被拦截掉
- 如果在设置的超时时间范围内,是不能阻止DoS攻击的。timestamp机制只能减轻DoS攻击的时间,缩短攻击时间。如果黑客修改了时间戳的值可通过sign签名机制来处理。如上面的测试类一样,时间戳作为加密的一部分。
参考文档:
https://www.cnblogs.com/myseries/p/11581170.html