//如果是對(duì)稱加密,則是調(diào)用方和被調(diào)用方都知道的私鑰;如果是
//非對(duì)稱加密,調(diào)用方這里是被調(diào)用方生成的公鑰
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 {
// 將參數(shù)按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;

// 對(duì)stringSignTemp進(jìn)行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 {
// 模擬一個(gè)支付請求
PaymentRequest request = new PaymentRequest("txn-001", 100.0, "nonce-123");

// 模擬請求參數(shù)集合
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);

// 模擬服務(wù)器端驗(yàn)證簽名
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

示例代碼中嚴(yán)格來說是一個(gè)demo,大家還需根據(jù)實(shí)際情況進(jìn)行對(duì)應(yīng)關(guān)鍵屬性的傳遞設(shè)計(jì)。

通過代碼可以發(fā)現(xiàn),簽名的方式是只能驗(yàn)證數(shù)據(jù)有沒有被修改,但是,防不了重放攻擊。

我們繼續(xù)往下進(jìn)行。

重放攻擊

??????什么是重放攻擊

API重放攻擊(Replay Attacks)又稱為重播攻擊。就是把你的請求原封不動(dòng)地再發(fā)送一次,兩次…n次,一般正常的請求都會(huì)通過驗(yàn)證進(jìn)入到正常邏輯中,如果這個(gè)正常邏輯是插入數(shù)據(jù)庫操作,那么一旦插入數(shù)據(jù)庫的語句寫的不好,就有可能出現(xiàn)多條重復(fù)的數(shù)據(jù)。一旦是比較慢的查詢操作,就可能導(dǎo)致數(shù)據(jù)庫堵住等情況,如果是付款接口,或者購買接口就會(huì)造成損失。因此需要采用防重放的機(jī)制來做請求驗(yàn)證,下面介紹如何對(duì)接口做防重放攻擊。

帶時(shí)間戳的簽名算法

請求端:timestamp由請求方生成,代表請求被發(fā)送的時(shí)間(需雙方共用一套時(shí)間計(jì)數(shù)系統(tǒng))隨請求參數(shù)一并發(fā)出,并將 timestamp作為一個(gè)參數(shù)加入 sign 加密計(jì)算。
服務(wù)端:平臺(tái)服務(wù)器接到請求后對(duì)比當(dāng)前時(shí)間戳,設(shè)定不超過30s 即認(rèn)為該請求正常,否則認(rèn)為超時(shí)拒絕服務(wù)

但是這樣還是有缺陷的,若攻擊者如果在30s之內(nèi)進(jìn)行重放攻擊那就沒辦法了,因?yàn)?0s之內(nèi)的請求都認(rèn)為是合法請求,那將這30s設(shè)置的小一些,那多小算小了?太小的話,如果網(wǎng)絡(luò)擁擠,會(huì)將正常請求也拒絕掉的 !因此將時(shí)間改小這不是一個(gè)解決問題的根本辦法。所以更進(jìn)一步地,可以為sign 加上一個(gè)隨機(jī)碼(稱之為鹽值)這里我們定義為 nonce。

帶nonce的簽名算法

請求方:nonce 是由請求方生成的隨機(jī)數(shù)(在規(guī)定的時(shí)間內(nèi)保證有充足的隨機(jī)數(shù)產(chǎn)生,即在60s 內(nèi)產(chǎn)生的隨機(jī)數(shù)重復(fù)的概率為0)也作為參數(shù)之一加入 sign 簽名。?服務(wù)端:服務(wù)器接受到請求先判定 nonce 是否被請求過(一般會(huì)放到redis中),如果發(fā)現(xiàn) nonce 參數(shù)在規(guī)定時(shí)間是全新的則正常返回結(jié)果,反之,則判定是重放攻擊拒絕服務(wù)。

這里注意對(duì)于處理過的請求,將其nonce存放到redis的時(shí)候設(shè)置過期時(shí)間,一定要配置過期時(shí)間。否則占用Redis空間會(huì)越來越大。

接口簽名總結(jié)

上面所有內(nèi)容可以概括為接口簽名,接口簽名是目前主流的方案。核心處理流程為:

接口簽名總結(jié)起來就是通過一些簽名規(guī)則對(duì)參數(shù)進(jìn)行簽名,然后把簽名的信息放入請求頭部,服務(wù)端收到客戶端請求之后,同樣的只需要按照已定的規(guī)則生產(chǎn)對(duì)應(yīng)的簽名串與客戶端的簽名信息進(jìn)行對(duì)比,如果一致,就進(jìn)入業(yè)務(wù)處理流程;如果不通過,就提示簽名驗(yàn)證失敗。

在接口簽名方案中,主要有四個(gè)核心參數(shù):

1、appid表示應(yīng)用ID(可以視情況添加),接口請求數(shù)據(jù)記性簽名加密,不同的對(duì)接項(xiàng)目分配不同的appid,保證數(shù)據(jù)安全。

2、timestamp 表示時(shí)間戳,當(dāng)請求的時(shí)間戳與服務(wù)器中的時(shí)間戳,差值在5分鐘之內(nèi),屬于有效請求,不在此范圍內(nèi),屬于無效請求

3、nonce 表示隨機(jī)數(shù),用于防止重復(fù)提交驗(yàn)證

4、signature 表示簽名字段,用于判斷接口請求是否有效。

我再補(bǔ)充一個(gè)大家會(huì)首先想到但存在較大問題的方案,就是token方案。

token方案

從上圖,我們可以很清晰的看到,token 方案的實(shí)現(xiàn)主要有以下幾個(gè)步驟:

在實(shí)際使用過程中,當(dāng)用戶登錄成功之后,生成的token存放在redis中時(shí)是有時(shí)效的,一般設(shè)置為2個(gè)小時(shí),過了2個(gè)小時(shí)之后會(huì)自動(dòng)失效,這個(gè)時(shí)候我們就需要重新登錄,然后再次獲取有效token。

token方案,是目前業(yè)務(wù)類型的項(xiàng)目當(dāng)中使用最廣的方案,而且實(shí)用性非常高,可以很有效的防止黑客們進(jìn)行抓包、爬取數(shù)據(jù)。

但是 token 方案也有一些缺點(diǎn)!最明顯的就是與第三方公司進(jìn)行接口對(duì)接的時(shí)候,當(dāng)你的接口請求量非常大,這個(gè)時(shí)候 token 突然失效了,會(huì)有大量的接口請求失敗。

正常流程是當(dāng)token失效時(shí),會(huì)調(diào)用刷新token接口,刷新完成之后,在token失效與重新刷新token這個(gè)時(shí)間間隔期間,就會(huì)出現(xiàn)大量的請求失敗的日志,因此在實(shí)際API對(duì)接過程中,我不推薦大家采用 token方案。

本文章轉(zhuǎn)載微信公眾號(hào)@java架構(gòu)師進(jìn)階之路

上一篇:

PyJWT:輕松搞定Token認(rèn)證,讓你的API更安全!

下一篇:

API安全:內(nèi)部審計(jì)師快速參考指南
#你可能也喜歡這些API文章!

我們有何不同?

API服務(wù)商零注冊

多API并行試用

數(shù)據(jù)驅(qū)動(dòng)選型,提升決策效率

查看全部API→
??

熱門場景實(shí)測,選對(duì)API

#AI文本生成大模型API

對(duì)比大模型API的內(nèi)容創(chuàng)意新穎性、情感共鳴力、商業(yè)轉(zhuǎn)化潛力

25個(gè)渠道
一鍵對(duì)比試用API 限時(shí)免費(fèi)

#AI深度推理大模型API

對(duì)比大模型API的邏輯推理準(zhǔn)確性、分析深度、可視化建議合理性

10個(gè)渠道
一鍵對(duì)比試用API 限時(shí)免費(fèi)