为什么要加密验签? 防止报文明文传输
数据在网络传输过程中,容易被抓包。如果使用的是HTTP协议的请求/响应(Request OR Response),它是明文传输的,都是可以被截获、篡改、重放(重发)的。所以需要进行数据的加密验签,所以需要考虑以下几点。
- 防伪装攻击(案例:在公共网络环境中,第三方 有意或恶意 的调用我们的接口)
- 防篡改攻击(案例:在公共网络环境中,请求头/查询字符串/内容 在传输过程被修改)
- 防重放攻击(案例:在公共网络环境中,请求被截获,稍后被重放或多次重放)
- 防数据信息泄漏(案例:截获用户登录请求,截获到账号、密码等)
实现方式
常见的方式,就是对关键字段加密。比如查询订单接口,就可以对订单号进行加密。一般常用的加密算法对称加密算法(如:AES),或者哈希算法处理(如:MD5)
对称加密:加密和解密使用相同秘钥的加密算法
采用单钥密码系统的加密方法,同一个密钥可以同时用作信息的加密和解密,这种加密方法称为对称加密,也称为单密钥加密。
非对称加密:非对称加密算法需要两个密钥(公开密钥和私有密钥)。公钥和私钥是成对存在的,如果用公钥对数据加密,只有对应的私钥才能解密。 (非对称加密是更安全的做法,加密是算法RSA或SM2)
非对称加密算法需要两个密钥来进行加密和解密,这两个密钥是公开密钥(public key,简称公钥)和私有密钥(private key,简称私钥)。
加签验签:使用Hash算法(如 MD5或者SHA-256)把原始请求参数生成报文摘要,然后用私钥对这个摘要进行加密,得到报文对应的sign
加签:用Hash函数把原始报文生成报文摘要,然后用私钥对这个摘要进行加密,就得到这个报文对应的数字签名。通常来说呢,请求方会把「数字签名和报文原文」一并发送给接收方。
验签:接收方拿到原始报文和数字签名后,用「同一个Hash函数」从报文中生成摘要A。另外,用对方提供的公钥对数字签名进行解密,得到摘要B,对比A和B是否相同,就可以得知报文有没有被篡改过。
客户端操作
请求参数:
字段 | 类型 | 必传 | 说明 |
---|---|---|---|
sign | String | 是 | 接口签名,用户接口验证 |
app_id | String | 是 | 开放平台的APP_ID,例如:1234 |
date_time | String | 是 | 当前时间戳 |
key | String | 是 | 开发平台的APP_KEY,例如:XA12#Da |
name | String | 是 | 业务参数 |
age | String | 是 | 业务参数 |
业务参数消息体数据格式:Content-Type 指定为 application/json
1.将请求参数中除sign外的多个键值对,根据键按照字典序排序,并按照"key1=value1&key2=value2…"的格式拼成一个字符串
String sortStr=" age=11&app_id=1234&date_time=1656926899731&name=xxx"
2.将key拼接在第一步中排序后的字符串后面得到待签名字符串
String sortStr ="age=11&app_id=1234&date_time=1656926899731&name=xxxkey=XA12#Da"
3.使用md5算法加密待加密字符串并转为大写即为sign
String sign ="57A132B7585F77B1948812275BE945B8"
4.将sign添加到请求参数中
https://www.baidu.com/test/get?age=11&app_id=1234&date_time=1656926899731&name=xxx&sign=57A132B7585F77B1948812275BE945B8
需要注意以下重要规则:
◆ 请求参数中有中文时,中文需要经过url编码,但计算签名时不需要;
◆ 请求参数的值为空则不参与签名;
◆ 参数名区分大小写;
◆ sign参数不参与签名;
服务端操作
1.接收到请求参数,转JSON格式
2.验签
2.1拿出用户签名
2.2根据APP_ID 拿去数据库中的KEY,使用该KEY进行重签参数
2.3如果重签结果和用户签名一致则通过,否则返回签名错误
2.4校验参数中的时间戳,如果时间戳 超过当前时间5分钟则签名失效
3.如果c、d都通过则正常请求业务
package com.chinaunicom.utils;
import cn.hutool.crypto.SecureUtil;
import com.alibaba.fastjson.JSONObject;
import lombok.extern.slf4j.Slf4j;
import org.apache.commons.lang3.StringUtils;
import java.util.*;
/**
* @author yming wang
* @date 2024/3/4 13:48
* @desc
*/
@Slf4j
public class SignUtil {
/**
* sign有效期
*/
private static final int TIMES = 111 * 60 * 1000;
public static boolean check(JSONObject params, String appKey, String sign) {
try {
//公钥验签
sign = RsaUtils.decryptByPublicKey(sign, appKey);
if (!sign.equals(getSign(params, appKey))) {
log.info("签名内容正确");
return false;
}
Long expireTime = params.getLong("timestamp");
Long currTime = System.currentTimeMillis();
if ((currTime - expireTime) < 0 || (currTime - expireTime) > TIMES) {
log.info("签名时间已过期");
return false;
}
log.info("验签成功");
return true;
} catch (Exception e) {
log.error("验签发生异常:", e);
}
return false;
}
/**
* @param params
* @return java.lang.String
* @params:
* @author yming wang
* @date 2024/3/4 14:44
* @desc 加签算法: 原始报文 ---hash算法---> 消息摘要 ---RSA私钥加密---> 数字签名
* 验签算法:数字签名 ---RSA公钥解密--> 消息摘要 ---> 根据参数重新摘要 ---> 对比摘要喜喜
*/
public static String getSign(JSONObject params, String appKey) {
//将参数进行升序
String sortParams = sortParams(params, appKey);
//将参数进行hash生成消息摘要
String sign = SecureUtil.md5(sortParams);
return sign;
}
/**
* @param params
* @param appKey
* @return java.lang.String
* @params:
* @author yming wang
* @date 2024/3/4 15:25
* @desc 将参数进行升序
*/
public static String sortParams(JSONObject params, String appKey) {
List<Map.Entry<String, Object>> entries = new ArrayList<>(params.entrySet());
Collections.sort(entries, Comparator.comparing(Map.Entry::getKey));
StringBuffer str = new StringBuffer();
for (Map.Entry<String, Object> entry : entries) {
Object value = entry.getValue();
if (value != null && StringUtils.isNotBlank(value.toString())) {
str.append(entry.getKey());
str.append("=");
str.append(value);
str.append("&");
}
}
//md5加上盐值避免根绝request body参数生成sign
str.append("appKey");
str.append("=");
str.append(appKey);
return str.toString();
}
public static void main(String[] args) throws Exception {
String privateKey = "privateKey ";
String publicKey = "publicKey ";
JSONObject data = new JSONObject();
data.put("appId", "10002");
data.put("username", "用户名");
data.put("account", "用户账号");
String pwd = RsaUtils.encryptByPrivateKey("用户密码", privateKey);
log.info("encryptPwd:{}", pwd);
data.put("password", pwd);
long timestamp = System.currentTimeMillis();
data.put("timestamp", timestamp);
//消息摘要
String sign = getSign(data, publicKey);
log.info("timestamp:{}", timestamp);
log.info("isTrue:{}", sign.equals(getSign(data, publicKey)));
log.info("消息摘要:{}", sign);
//生成数字证书
sign = RsaUtils.encryptByPrivateKey(sign, privateKey);
log.info("生成数字证书:{}", sign);
log.info("打印请求参数:{}", data);
log.info("验签:{}", check(data, publicKey, sign));
}
}
package com.chinaunicom.utils;
import lombok.extern.slf4j.Slf4j;
import javax.crypto.Cipher;
import java.security.*;
import java.security.spec.PKCS8EncodedKeySpec;
import java.security.spec.X509EncodedKeySpec;
import java.util.Base64;
import java.util.HashMap;
import java.util.Map;
/**
* @program: CSDN
* @description: yming wang
* @author: wyming
* @create: 2021-06-08 09:30:14
**/
@Slf4j
public class RsaUtils {
/**
* 签名算法名称
*/
private static final String RSA_KEY_ALGORITHM = "RSA";
/**
* 标准签名算法名称
*/
private static final String RSA_SIGNATURE_ALGORITHM = "SHA1withRSA";
private static final String RSA2_SIGNATURE_ALGORITHM = "SHA256withRSA";
/**
* RSA密钥长度,默认密钥长度是1024,密钥长度必须是64的倍数,在512到65536位之间,不管是RSA还是RSA2长度推荐使用2048
*/
private static final int KEY_SIZE = 2048;
/**
* 生成密钥对
*
* @return 返回包含公私钥的map
*/
public static Map<String, String> generateKey() {
KeyPairGenerator keygen;
try {
keygen = KeyPairGenerator.getInstance(RSA_KEY_ALGORITHM);
} catch (NoSuchAlgorithmException e) {
throw new RuntimeException("RSA初始化密钥出现错误,算法异常");
}
SecureRandom secrand = new SecureRandom();
//初始化随机产生器
secrand.setSeed("Alian".getBytes());
//初始化密钥生成器
keygen.initialize(KEY_SIZE, secrand);
KeyPair keyPair = keygen.genKeyPair();
//获取公钥并转成base64编码
byte[] pub_key = keyPair.getPublic().getEncoded();
String publicKeyStr = Base64.getEncoder().encodeToString(pub_key);
//获取私钥并转成base64编码
byte[] pri_key = keyPair.getPrivate().getEncoded();
String privateKeyStr = Base64.getEncoder().encodeToString(pri_key);
//创建一个Map返回结果
Map<String, String> keyPairMap = new HashMap<>();
keyPairMap.put("publicKeyStr", publicKeyStr);
keyPairMap.put("privateKeyStr", privateKeyStr);
return keyPairMap;
}
/**
* 公钥加密(用于数据加密)
*
* @param data 加密前的字符串
* @param publicKeyStr base64编码后的公钥
* @return base64编码后的字符串
* @throws Exception
*/
public static String encryptByPublicKey(String data, String publicKeyStr) throws Exception {
//Java原生base64解码
byte[] pubKey = Base64.getDecoder().decode(publicKeyStr);
//创建X509编码密钥规范
X509EncodedKeySpec x509KeySpec = new X509EncodedKeySpec(pubKey);
//返回转换指定算法的KeyFactory对象
KeyFactory keyFactory = KeyFactory.getInstance(RSA_KEY_ALGORITHM);
//根据X509编码密钥规范产生公钥对象
PublicKey publicKey = keyFactory.generatePublic(x509KeySpec);
//根据转换的名称获取密码对象Cipher(转换的名称:算法/工作模式/填充模式)
Cipher cipher = Cipher.getInstance(keyFactory.getAlgorithm());
//用公钥初始化此Cipher对象(加密模式)
cipher.init(Cipher.ENCRYPT_MODE, publicKey);
//对数据加密
byte[] encrypt = cipher.doFinal(data.getBytes());
//返回base64编码后的字符串
return Base64.getEncoder().encodeToString(encrypt);
}
/**
* 私钥解密(用于数据解密)
*
* @param data 解密前的字符串
* @param privateKeyStr 私钥
* @return 解密后的字符串
* @throws Exception
*/
public static String decryptByPrivateKey(String data, String privateKeyStr) throws Exception {
//Java原生base64解码
byte[] priKey = Base64.getDecoder().decode(privateKeyStr);
//创建PKCS8编码密钥规范
PKCS8EncodedKeySpec pkcs8KeySpec = new PKCS8EncodedKeySpec(priKey);
//返回转换指定算法的KeyFactory对象
KeyFactory keyFactory = KeyFactory.getInstance(RSA_KEY_ALGORITHM);
//根据PKCS8编码密钥规范产生私钥对象
PrivateKey privateKey = keyFactory.generatePrivate(pkcs8KeySpec);
//根据转换的名称获取密码对象Cipher(转换的名称:算法/工作模式/填充模式)
Cipher cipher = Cipher.getInstance(keyFactory.getAlgorithm());
//用私钥初始化此Cipher对象(解密模式)
cipher.init(Cipher.DECRYPT_MODE, privateKey);
//对数据解密
byte[] decrypt = cipher.doFinal(Base64.getDecoder().decode(data));
//返回字符串
return new String(decrypt);
}
/**
* 私钥加密(用于数据签名)
*
* @param data 加密前的字符串
* @param privateKeyStr base64编码后的私钥
* @return base64编码后后的字符串
* @throws Exception
*/
public static String encryptByPrivateKey(String data, String privateKeyStr) throws Exception {
//Java原生base64解码
byte[] priKey = Base64.getDecoder().decode(privateKeyStr);
//创建PKCS8编码密钥规范
PKCS8EncodedKeySpec pkcs8KeySpec = new PKCS8EncodedKeySpec(priKey);
//返回转换指定算法的KeyFactory对象
KeyFactory keyFactory = KeyFactory.getInstance(RSA_KEY_ALGORITHM);
//根据PKCS8编码密钥规范产生私钥对象
PrivateKey privateKey = keyFactory.generatePrivate(pkcs8KeySpec);
//根据转换的名称获取密码对象Cipher(转换的名称:算法/工作模式/填充模式)
Cipher cipher = Cipher.getInstance(keyFactory.getAlgorithm());
//用私钥初始化此Cipher对象(加密模式)
cipher.init(Cipher.ENCRYPT_MODE, privateKey);
//对数据加密
byte[] encrypt = cipher.doFinal(data.getBytes());
//返回base64编码后的字符串
return Base64.getEncoder().encodeToString(encrypt);
}
/**
* 公钥解密(用于数据验签)
*
* @param data 解密前的字符串
* @param publicKeyStr base64编码后的公钥
* @return 解密后的字符串
* @throws Exception
*/
public static String decryptByPublicKey(String data, String publicKeyStr) throws Exception {
//Java原生base64解码
byte[] pubKey = Base64.getDecoder().decode(publicKeyStr);
//创建X509编码密钥规范
X509EncodedKeySpec x509KeySpec = new X509EncodedKeySpec(pubKey);
//返回转换指定算法的KeyFactory对象
KeyFactory keyFactory = KeyFactory.getInstance(RSA_KEY_ALGORITHM);
//根据X509编码密钥规范产生公钥对象
PublicKey publicKey = keyFactory.generatePublic(x509KeySpec);
//根据转换的名称获取密码对象Cipher(转换的名称:算法/工作模式/填充模式)
Cipher cipher = Cipher.getInstance(keyFactory.getAlgorithm());
//用公钥初始化此Cipher对象(解密模式)
cipher.init(Cipher.DECRYPT_MODE, publicKey);
//对数据解密
byte[] decrypt = cipher.doFinal(Base64.getDecoder().decode(data));
//返回字符串
return new String(decrypt);
}
/**
* RSA签名
*
* @param data 待签名数据
* @param priKey 私钥
* @param signType RSA或RSA2
* @return 签名
* @throws Exception
*/
public static String sign(byte[] data, byte[] priKey, String signType) throws Exception {
//创建PKCS8编码密钥规范
PKCS8EncodedKeySpec pkcs8KeySpec = new PKCS8EncodedKeySpec(priKey);
//返回转换指定算法的KeyFactory对象
KeyFactory keyFactory = KeyFactory.getInstance(RSA_KEY_ALGORITHM);
//根据PKCS8编码密钥规范产生私钥对象
PrivateKey privateKey = keyFactory.generatePrivate(pkcs8KeySpec);
//标准签名算法名称(RSA还是RSA2)
String algorithm = RSA_KEY_ALGORITHM.equals(signType) ? RSA_SIGNATURE_ALGORITHM : RSA2_SIGNATURE_ALGORITHM;
//用指定算法产生签名对象Signature
Signature signature = Signature.getInstance(algorithm);
//用私钥初始化签名对象Signature
signature.initSign(privateKey);
//将待签名的数据传送给签名对象(须在初始化之后)
signature.update(data);
//返回签名结果字节数组
byte[] sign = signature.sign();
//返回Base64编码后的字符串
return Base64.getEncoder().encodeToString(sign);
}
/**
* RSA校验数字签名
*
* @param data 待校验数据
* @param sign 数字签名
* @param pubKey 公钥
* @param signType RSA或RSA2
* @return boolean 校验成功返回true,失败返回false
*/
public static boolean verify(byte[] data, byte[] sign, byte[] pubKey, String signType) throws Exception {
//返回转换指定算法的KeyFactory对象
KeyFactory keyFactory = KeyFactory.getInstance(RSA_KEY_ALGORITHM);
//创建X509编码密钥规范
X509EncodedKeySpec x509KeySpec = new X509EncodedKeySpec(pubKey);
//根据X509编码密钥规范产生公钥对象
PublicKey publicKey = keyFactory.generatePublic(x509KeySpec);
//标准签名算法名称(RSA还是RSA2)
String algorithm = RSA_KEY_ALGORITHM.equals(signType) ? RSA_SIGNATURE_ALGORITHM : RSA2_SIGNATURE_ALGORITHM;
//用指定算法产生签名对象Signature
Signature signature = Signature.getInstance(algorithm);
//用公钥初始化签名对象,用于验证签名
signature.initVerify(publicKey);
//更新签名内容
signature.update(data);
//得到验证结果
return signature.verify(sign);
}
public static void demo() throws Exception {
Map<String, String> stringStringMap = generateKey();
String publicKeyStr = stringStringMap.get("publicKeyStr");
String privateKeyStr = stringStringMap.get("privateKeyStr");
System.out.println("-----------------生成的公钥和私钥------------------------------");
System.out.println("获取到的公钥:" + publicKeyStr);
System.out.println("获取到的私钥:" + privateKeyStr);
// 待加密数据
String data = "tranSeq=1920542585&amount=100&payType=wechat";
// 公钥加密
System.out.println("---------公钥--------加密和解密------------------------------");
System.out.println("待加密的数据:" + data);
String encrypt = RsaUtils.encryptByPublicKey(data, publicKeyStr);
System.out.println("加密后数据:" + encrypt);
// 私钥解密
String decrypt = RsaUtils.decryptByPrivateKey(encrypt, privateKeyStr);
System.out.println("解密后数据:" + decrypt);
// 私钥加密
System.out.println("----------私钥-------加密和解密------------------------------");
System.out.println("待加密的数据:" + data);
encrypt = RsaUtils.encryptByPrivateKey(data, privateKeyStr);
System.out.println("加密后数据:" + encrypt);
// 私钥解密
decrypt = RsaUtils.decryptByPublicKey(encrypt, publicKeyStr);
System.out.println("解密后数据:" + decrypt);
}
}