問(wèn)題來(lái)源:
近日發(fā)現(xiàn)某些用戶對(duì)一些商品進(jìn)行多次下單,而且比較頻繁,而且下單時(shí)間都在同一秒內(nèi),懷疑產(chǎn)生重復(fù)請(qǐng)求
問(wèn)題描述:
用戶在商品頁(yè)面,多次點(diǎn)擊下單按鈕,后臺(tái)怎么知道是用戶對(duì)一個(gè)商品進(jìn)行多次下單,還是人工誤操作或者客戶端異常進(jìn)行重復(fù)請(qǐng)求下單了呢?
解決方案:
- 進(jìn)入商品頁(yè)面,就是下單頁(yè)面的時(shí)候,產(chǎn)生一個(gè)不重復(fù)的隨機(jī)數(shù)。
- 下單的時(shí)候把這個(gè)隨機(jī)數(shù)帶上
- 下單校驗(yàn)的時(shí)候,利用Redis緩存鎖,先鎖這個(gè)隨機(jī)數(shù),再做業(yè)務(wù)處理,做完再釋放。
問(wèn)題升級(jí):
如果客戶端重復(fù)的下單請(qǐng)求過(guò)多,或者后臺(tái)處理過(guò)快,就會(huì)產(chǎn)生所謂的重放攻擊。
如,客戶端請(qǐng)求模擬
請(qǐng)求A 20:18:01 xxx/order?storeid=1&randomNum=abcd123 用戶下單
請(qǐng)求A 獲得鎖成功,開始處理
請(qǐng)求B 20:18:01 xxx/order?storeid=1&randomNum=abcd123 用戶下單
請(qǐng)求B 獲得鎖失敗
請(qǐng)求C 20:18:01 xxx/order?storeid=1&randomNum=abcd123 用戶下單
請(qǐng)求C 獲得鎖失敗
請(qǐng)求A 下單成功,釋放鎖
請(qǐng)求D 20:18:01 xxx/order?storeid=1&randomNum=abcd123 用戶下單
請(qǐng)求D 獲得鎖成功,開始處理
請(qǐng)求D 下單成功,釋放鎖
針對(duì)同一個(gè)隨機(jī)數(shù)randomNum=abcd123居然下單成功2次,這就是重復(fù)攻擊帶來(lái)的危害
問(wèn)題升級(jí),解決方案:
- 下單校驗(yàn)的時(shí)候,利用Redis緩存鎖,先鎖這個(gè)隨機(jī)數(shù),鎖成功之后,判斷隨機(jī)數(shù)是否曾經(jīng)處理過(guò),如果沒(méi)有就把隨機(jī)數(shù)加入緩存設(shè)置時(shí)長(zhǎng)為X,再做業(yè)務(wù)處理,做完再釋放。
關(guān)鍵代碼
/**
* 獲得鎖
* @param lockId
* @return
*/
public boolean getLock(String lockId) {
try {
String KEY_LOCK_ID_="LOCK_ID_";
String KEY_LOCK_HIS_ID_="LOCK_HIS_ID_";
Boolean success = redisTemplate.opsForValue().setIfAbsent(KEY_LOCK_ID_+lockId, "lock");
//解決重放攻擊
if(success != null && success){
if(hasKey(KEY_LOCK_HIS_ID_+lockId)){
success = false;
logger.error("【REDIS操作】【獲得鎖失敗】【已存在歷史鎖碼】【懷疑重放攻擊:"+lockId+"】");
}else{
success = true;
set(KEY_LOCK_HIS_ID_+lockId,lockId,60*60*24);
}
}
return success;
} catch (Exception e) {
logger.error("【REDIS操作】【獲得鎖錯(cuò)誤】",e);
return false;
}
}
/**
* 釋放鎖
* @param lockId
*/
public void releaseLock(String lockId) {
try {
String KEY_LOCK_ID_="LOCK_ID_";
redisTemplate.delete(KEY_LOCK_ID_ + lockId);
} catch (Exception e) {
logger.error("【REDIS操作】【釋放鎖錯(cuò)誤】",e);
}
}