Java http加簽、驗簽實現(xiàn)方案詳解
為什么要加密驗簽? 防止報文明文傳輸
數(shù)據(jù)在網(wǎng)絡傳輸過程中,容易被抓包。如果使用的是HTTP協(xié)議的請求/響應(Request OR Response),它是明文傳輸?shù)?,都是可以被截獲、篡改、重放(重發(fā))的。所以需要進行數(shù)據(jù)的加密驗簽,所以需要考慮以下幾點。
- 防偽裝攻擊(案例:在公共網(wǎng)絡環(huán)境中,第三方 有意或惡意 的調用我們的接口)
- 防篡改攻擊(案例:在公共網(wǎng)絡環(huán)境中,請求頭/查詢字符串/內容 在傳輸過程被修改)
- 防重放攻擊(案例:在公共網(wǎng)絡環(huán)境中,請求被截獲,稍后被重放或多次重放)
- 防數(shù)據(jù)信息泄漏(案例:截獲用戶登錄請求,截獲到賬號、密碼等)
實現(xiàn)方式
常見的方式,就是對關鍵字段加密。比如查詢訂單接口,就可以對訂單號進行加密。一般常用的加密算法對稱加密算法(如:AES),或者哈希算法處理(如:MD5)
對稱加密:加密和解密使用相同秘鑰的加密算法
采用單鑰密碼系統(tǒng)的加密方法,同一個密鑰可以同時用作信息的加密和解密,這種加密方法稱為對稱加密,也稱為單密鑰加密。
非對稱加密:非對稱加密算法需要兩個密鑰(公開密鑰和私有密鑰)。公鑰和私鑰是成對存在的,如果用公鑰對數(shù)據(jù)加密,只有對應的私鑰才能解密。 (非對稱加密是更安全的做法,加密是算法RSA或SM2)
非對稱加密算法需要兩個密鑰來進行加密和解密,這兩個密鑰是公開密鑰(public key,簡稱公鑰)和私有密鑰(private key,簡稱私鑰)。
加簽驗簽:使用Hash算法(如 MD5或者SHA-256)把原始請求參數(shù)生成報文摘要,然后用私鑰對這個摘要進行加密,得到報文對應的sign
加簽:用Hash函數(shù)把原始報文生成報文摘要,然后用私鑰對這個摘要進行加密,就得到這個報文對應的數(shù)字簽名。通常來說呢,請求方會把「數(shù)字簽名和報文原文」一并發(fā)送給接收方。

驗簽:接收方拿到原始報文和數(shù)字簽名后,用「同一個Hash函數(shù)」從報文中生成摘要A。另外,用對方提供的公鑰對數(shù)字簽名進行解密,得到摘要B,對比A和B是否相同,就可以得知報文有沒有被篡改過。

客戶端操作
請求參數(shù):
| 字段 | 類型 | 必傳 | 說明 |
|---|---|---|---|
| sign | String | 是 | 接口簽名,用戶接口驗證 |
| app_id | String | 是 | 開放平臺的APP_ID,例如:1234 |
| date_time | String | 是 | 當前時間戳 |
| key | String | 是 | 開發(fā)平臺的APP_KEY,例如:XA12#Da |
| name | String | 是 | 業(yè)務參數(shù) |
| age | String | 是 | 業(yè)務參數(shù) |
業(yè)務參數(shù)消息體數(shù)據(jù)格式:Content-Type 指定為 application/json
1.將請求參數(shù)中除sign外的多個鍵值對,根據(jù)鍵按照字典序排序,并按照"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添加到請求參數(shù)中
https://www.baidu.com/test/get?age=11&app_id=1234&date_time=1656926899731&name=xxx&sign=57A132B7585F77B1948812275BE945B8
需要注意以下重要規(guī)則:
◆ 請求參數(shù)中有中文時,中文需要經(jīng)過url編碼,但計算簽名時不需要;
◆ 請求參數(shù)的值為空則不參與簽名;
◆ 參數(shù)名區(qū)分大小寫;
◆ sign參數(shù)不參與簽名;
服務端操作
1.接收到請求參數(shù),轉JSON格式
2.驗簽
2.1拿出用戶簽名
2.2根據(jù)APP_ID 拿去數(shù)據(jù)庫中的KEY,使用該KEY進行重簽參數(shù)
2.3如果重簽結果和用戶簽名一致則通過,否則返回簽名錯誤
2.4校驗參數(shù)中的時間戳,如果時間戳 超過當前時間5分鐘則簽名失效
3.如果c、d都通過則正常請求業(yè)務
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("驗簽發(fā)生異常:", e);
}
return false;
}
/**
* @param params
* @return java.lang.String
* @params:
* @author yming wang
* @date 2024/3/4 14:44
* @desc 加簽算法: 原始報文 ---hash算法---> 消息摘要 ---RSA私鑰加密---> 數(shù)字簽名
* 驗簽算法:數(shù)字簽名 ---RSA公鑰解密--> 消息摘要 ---> 根據(jù)參數(shù)重新摘要 ---> 對比摘要喜喜
*/
public static String getSign(JSONObject params, String appKey) {
//將參數(shù)進行升序
String sortParams = sortParams(params, appKey);
//將參數(shù)進行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 將參數(shù)進行升序
*/
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參數(shù)生成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);
//生成數(shù)字證書
sign = RsaUtils.encryptByPrivateKey(sign, privateKey);
log.info("生成數(shù)字證書:{}", sign);
log.info("打印請求參數(shù):{}", 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的倍數(shù),在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初始化密鑰出現(xiàn)錯誤,算法異常");
}
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);
//創(chuàng)建一個Map返回結果
Map<String, String> keyPairMap = new HashMap<>();
keyPairMap.put("publicKeyStr", publicKeyStr);
keyPairMap.put("privateKeyStr", privateKeyStr);
return keyPairMap;
}
/**
* 公鑰加密(用于數(shù)據(jù)加密)
*
* @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);
//創(chuàng)建X509編碼密鑰規(guī)范
X509EncodedKeySpec x509KeySpec = new X509EncodedKeySpec(pubKey);
//返回轉換指定算法的KeyFactory對象
KeyFactory keyFactory = KeyFactory.getInstance(RSA_KEY_ALGORITHM);
//根據(jù)X509編碼密鑰規(guī)范產生公鑰對象
PublicKey publicKey = keyFactory.generatePublic(x509KeySpec);
//根據(jù)轉換的名稱獲取密碼對象Cipher(轉換的名稱:算法/工作模式/填充模式)
Cipher cipher = Cipher.getInstance(keyFactory.getAlgorithm());
//用公鑰初始化此Cipher對象(加密模式)
cipher.init(Cipher.ENCRYPT_MODE, publicKey);
//對數(shù)據(jù)加密
byte[] encrypt = cipher.doFinal(data.getBytes());
//返回base64編碼后的字符串
return Base64.getEncoder().encodeToString(encrypt);
}
/**
* 私鑰解密(用于數(shù)據(jù)解密)
*
* @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);
//創(chuàng)建PKCS8編碼密鑰規(guī)范
PKCS8EncodedKeySpec pkcs8KeySpec = new PKCS8EncodedKeySpec(priKey);
//返回轉換指定算法的KeyFactory對象
KeyFactory keyFactory = KeyFactory.getInstance(RSA_KEY_ALGORITHM);
//根據(jù)PKCS8編碼密鑰規(guī)范產生私鑰對象
PrivateKey privateKey = keyFactory.generatePrivate(pkcs8KeySpec);
//根據(jù)轉換的名稱獲取密碼對象Cipher(轉換的名稱:算法/工作模式/填充模式)
Cipher cipher = Cipher.getInstance(keyFactory.getAlgorithm());
//用私鑰初始化此Cipher對象(解密模式)
cipher.init(Cipher.DECRYPT_MODE, privateKey);
//對數(shù)據(jù)解密
byte[] decrypt = cipher.doFinal(Base64.getDecoder().decode(data));
//返回字符串
return new String(decrypt);
}
/**
* 私鑰加密(用于數(shù)據(jù)簽名)
*
* @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);
//創(chuàng)建PKCS8編碼密鑰規(guī)范
PKCS8EncodedKeySpec pkcs8KeySpec = new PKCS8EncodedKeySpec(priKey);
//返回轉換指定算法的KeyFactory對象
KeyFactory keyFactory = KeyFactory.getInstance(RSA_KEY_ALGORITHM);
//根據(jù)PKCS8編碼密鑰規(guī)范產生私鑰對象
PrivateKey privateKey = keyFactory.generatePrivate(pkcs8KeySpec);
//根據(jù)轉換的名稱獲取密碼對象Cipher(轉換的名稱:算法/工作模式/填充模式)
Cipher cipher = Cipher.getInstance(keyFactory.getAlgorithm());
//用私鑰初始化此Cipher對象(加密模式)
cipher.init(Cipher.ENCRYPT_MODE, privateKey);
//對數(shù)據(jù)加密
byte[] encrypt = cipher.doFinal(data.getBytes());
//返回base64編碼后的字符串
return Base64.getEncoder().encodeToString(encrypt);
}
/**
* 公鑰解密(用于數(shù)據(jù)驗簽)
*
* @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);
//創(chuàng)建X509編碼密鑰規(guī)范
X509EncodedKeySpec x509KeySpec = new X509EncodedKeySpec(pubKey);
//返回轉換指定算法的KeyFactory對象
KeyFactory keyFactory = KeyFactory.getInstance(RSA_KEY_ALGORITHM);
//根據(jù)X509編碼密鑰規(guī)范產生公鑰對象
PublicKey publicKey = keyFactory.generatePublic(x509KeySpec);
//根據(jù)轉換的名稱獲取密碼對象Cipher(轉換的名稱:算法/工作模式/填充模式)
Cipher cipher = Cipher.getInstance(keyFactory.getAlgorithm());
//用公鑰初始化此Cipher對象(解密模式)
cipher.init(Cipher.DECRYPT_MODE, publicKey);
//對數(shù)據(jù)解密
byte[] decrypt = cipher.doFinal(Base64.getDecoder().decode(data));
//返回字符串
return new String(decrypt);
}
/**
* RSA簽名
*
* @param data 待簽名數(shù)據(jù)
* @param priKey 私鑰
* @param signType RSA或RSA2
* @return 簽名
* @throws Exception
*/
public static String sign(byte[] data, byte[] priKey, String signType) throws Exception {
//創(chuàng)建PKCS8編碼密鑰規(guī)范
PKCS8EncodedKeySpec pkcs8KeySpec = new PKCS8EncodedKeySpec(priKey);
//返回轉換指定算法的KeyFactory對象
KeyFactory keyFactory = KeyFactory.getInstance(RSA_KEY_ALGORITHM);
//根據(jù)PKCS8編碼密鑰規(guī)范產生私鑰對象
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);
//將待簽名的數(shù)據(jù)傳送給簽名對象(須在初始化之后)
signature.update(data);
//返回簽名結果字節(jié)數(shù)組
byte[] sign = signature.sign();
//返回Base64編碼后的字符串
return Base64.getEncoder().encodeToString(sign);
}
/**
* RSA校驗數(shù)字簽名
*
* @param data 待校驗數(shù)據(jù)
* @param sign 數(shù)字簽名
* @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);
//創(chuàng)建X509編碼密鑰規(guī)范
X509EncodedKeySpec x509KeySpec = new X509EncodedKeySpec(pubKey);
//根據(jù)X509編碼密鑰規(guī)范產生公鑰對象
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);
// 待加密數(shù)據(jù)
String data = "tranSeq=1920542585&amount=100&payType=wechat";
// 公鑰加密
System.out.println("---------公鑰--------加密和解密------------------------------");
System.out.println("待加密的數(shù)據(jù):" + data);
String encrypt = RsaUtils.encryptByPublicKey(data, publicKeyStr);
System.out.println("加密后數(shù)據(jù):" + encrypt);
// 私鑰解密
String decrypt = RsaUtils.decryptByPrivateKey(encrypt, privateKeyStr);
System.out.println("解密后數(shù)據(jù):" + decrypt);
// 私鑰加密
System.out.println("----------私鑰-------加密和解密------------------------------");
System.out.println("待加密的數(shù)據(jù):" + data);
encrypt = RsaUtils.encryptByPrivateKey(data, privateKeyStr);
System.out.println("加密后數(shù)據(jù):" + encrypt);
// 私鑰解密
decrypt = RsaUtils.decryptByPublicKey(encrypt, publicKeyStr);
System.out.println("解密后數(shù)據(jù):" + decrypt);
}
}到此這篇關于Java http加簽、驗簽實現(xiàn)方案的文章就介紹到這了,更多相關Java http加簽、驗簽內容請搜索腳本之家以前的文章或繼續(xù)瀏覽下面的相關文章希望大家以后多多支持腳本之家!
相關文章
VSCode新手教程之配置Java環(huán)境的詳細教程
這篇文章主要給大家介紹了關于VSCode新手教程之配置Java環(huán)境的詳細教程,工欲善其事必先利其器,想要工作順利我們先搭建好JAVA的開發(fā)環(huán)境,需要的朋友可以參考下2023-10-10

