Java http加签、验签实现方案详解
作者:小先生ノ
为什么要加密验签? 防止报文明文传输
数据在网络传输过程中,容易被抓包。如果使用的是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); } }
到此这篇关于Java http加签、验签实现方案的文章就介绍到这了,更多相关Java http加签、验签内容请搜索脚本之家以前的文章或继续浏览下面的相关文章希望大家以后多多支持脚本之家!