//如果是對稱加密,則是調用方和被調用方都知道的私鑰;如果是
//非對稱加密,調用方這里是被調用方生成的公鑰
private static final String SECRET_KEY = "your_secret_key";
// 模擬支付請求類
static class PaymentRequest {
private String transactionId;
private double amount;
private String nonce;
public PaymentRequest(String transactionId, double amount, String nonce) {
this.transactionId = transactionId;
this.amount = amount;
this.nonce = nonce;
}
public String getTransactionId() {
return transactionId;
}
public double getAmount() {
return amount;
}
public String getNonce() {
return nonce;
}
}
public static String generateSign(Map<String, String> params, String secret) throws NoSuchAlgorithmException {
// 將參數按ASCII碼從小到大排序
Map<String, String> sortedParams = new TreeMap<>(params);
StringBuilder stringA = new StringBuilder();
// 拼接成字符串stringA
for (Map.Entry<String, String> entry : sortedParams.entrySet()) {
if (entry.getValue() != null && !entry.getValue().isEmpty()) {
stringA.append(entry.getKey()).append(entry.getValue());
}
}
// 在stringA最后拼接上secret密鑰得到stringSignTemp字符串
String stringSignTemp = stringA.toString() + secret;
// 對stringSignTemp進行MD5加密得到signValue
return md5(stringSignTemp);
}
private static String md5(String input) throws NoSuchAlgorithmException {
MessageDigest md = MessageDigest.getInstance("MD5");
byte[] messageDigest = md.digest(input.getBytes());
StringBuilder sb = new StringBuilder();
for (byte b : messageDigest) {
sb.append(String.format("%02x", b));
}
return sb.toString();
}
public static void main(String[] args) {
try {
// 模擬一個支付請求
PaymentRequest request = new PaymentRequest("txn-001", 100.0, "nonce-123");
// 模擬請求參數集合
Map<String, String> params = new TreeMap<>();
params.put("transactionId", request.getTransactionId());
params.put("amount", String.valueOf(request.getAmount()));
params.put("nonce", request.getNonce());
// 生成簽名
String sign = generateSign(params, SECRET_KEY);
System.out.println("Generated Sign: " + sign);
// 模擬服務器端驗證簽名
boolean isValid = verifySign(params, sign, SECRET_KEY);
System.out.println("Is Sign Valid: " + isValid);
} catch (NoSuchAlgorithmException e) {
e.printStackTrace();
}
}
public static boolean verifySign(Map<String, String> params, String providedSign, String secret) throws NoSuchAlgorithmException {
// 生成簽名
String generatedSign = generateSign(params, secret);
// 比較生成的簽名和提供的簽名
return generatedSign.equals(providedSign);
}
}
//輸出:
Generated Sign: 1f7cc39bcb0eb7e293c29d49906d69d6
Is Sign Valid: true
示例代碼中嚴格來說是一個demo,大家還需根據實際情況進行對應關鍵屬性的傳遞設計。
通過代碼可以發現,簽名的方式是只能驗證數據有沒有被修改,但是,防不了重放攻擊。
我們繼續往下進行。
重放攻擊
??????什么是重放攻擊
API重放攻擊(Replay Attacks)又稱為重播攻擊。就是把你的請求原封不動地再發送一次,兩次…n次,一般正常的請求都會通過驗證進入到正常邏輯中,如果這個正常邏輯是插入數據庫操作,那么一旦插入數據庫的語句寫的不好,就有可能出現多條重復的數據。一旦是比較慢的查詢操作,就可能導致數據庫堵住等情況,如果是付款接口,或者購買接口就會造成損失。因此需要采用防重放的機制來做請求驗證,下面介紹如何對接口做防重放攻擊。
帶時間戳的簽名算法
請求端:timestamp由請求方生成,代表請求被發送的時間(需雙方共用一套時間計數系統)隨請求參數一并發出,并將 timestamp作為一個參數加入 sign 加密計算。
服務端:平臺服務器接到請求后對比當前時間戳,設定不超過30s 即認為該請求正常,否則認為超時拒絕服務
但是這樣還是有缺陷的,若攻擊者如果在30s之內進行重放攻擊那就沒辦法了,因為30s之內的請求都認為是合法請求,那將這30s設置的小一些,那多小算小了?太小的話,如果網絡擁擠,會將正常請求也拒絕掉的 !因此將時間改小這不是一個解決問題的根本辦法。所以更進一步地,可以為sign 加上一個隨機碼(稱之為鹽值)這里我們定義為 nonce。
帶nonce的簽名算法
請求方:nonce 是由請求方生成的隨機數(在規定的時間內保證有充足的隨機數產生,即在60s 內產生的隨機數重復的概率為0)也作為參數之一加入 sign 簽名。?服務端:服務器接受到請求先判定 nonce 是否被請求過(一般會放到redis中),如果發現 nonce 參數在規定時間是全新的則正常返回結果,反之,則判定是重放攻擊拒絕服務。
這里注意對于處理過的請求,將其nonce存放到redis的時候設置過期時間,一定要配置過期時間。否則占用Redis空間會越來越大。
接口簽名總結
上面所有內容可以概括為接口簽名,接口簽名是目前主流的方案。核心處理流程為:
接口簽名總結起來就是通過一些簽名規則對參數進行簽名,然后把簽名的信息放入請求頭部,服務端收到客戶端請求之后,同樣的只需要按照已定的規則生產對應的簽名串與客戶端的簽名信息進行對比,如果一致,就進入業務處理流程;如果不通過,就提示簽名驗證失敗。
在接口簽名方案中,主要有四個核心參數:
1、appid表示應用ID(可以視情況添加),接口請求數據記性簽名加密,不同的對接項目分配不同的appid,保證數據安全。
2、timestamp 表示時間戳,當請求的時間戳與服務器中的時間戳,差值在5分鐘之內,屬于有效請求,不在此范圍內,屬于無效請求
3、nonce 表示隨機數,用于防止重復提交驗證
4、signature 表示簽名字段,用于判斷接口請求是否有效。
我再補充一個大家會首先想到但存在較大問題的方案,就是token方案。
token方案
從上圖,我們可以很清晰的看到,token 方案的實現主要有以下幾個步驟:
- 1、用戶登錄成功之后,服務端會給用戶生成一個唯一有效的憑證,這個有效值被稱為token
- 2、當用戶每次請求其他的業務接口時,需要在請求頭部帶上token
- 3、服務端接受到客戶端業務接口請求時,會驗證token的合法性,如果不合法會提示給客戶端;如果合法,才會進入業務處理流程。
在實際使用過程中,當用戶登錄成功之后,生成的token存放在redis中時是有時效的,一般設置為2個小時,過了2個小時之后會自動失效,這個時候我們就需要重新登錄,然后再次獲取有效token。
token方案,是目前業務類型的項目當中使用最廣的方案,而且實用性非常高,可以很有效的防止黑客們進行抓包、爬取數據。
但是 token 方案也有一些缺點!最明顯的就是與第三方公司進行接口對接的時候,當你的接口請求量非常大,這個時候 token 突然失效了,會有大量的接口請求失敗。
正常流程是當token失效時,會調用刷新token接口,刷新完成之后,在token失效與重新刷新token這個時間間隔期間,就會出現大量的請求失敗的日志,因此在實際API對接過程中,我不推薦大家采用 token方案。
本文章轉載微信公眾號@java架構師進階之路
我們有何不同?
API服務商零注冊
多API并行試用
數據驅動選型,提升決策效率
查看全部API→