一皆疹、API重放攻擊
我們在設(shè)計接口的時候,最怕一個接口被用戶截取用于重放攻擊抵知。重放攻擊是什么呢墙基?就是把你的請求原封不動地再發(fā)送一次,兩次...n次刷喜,重放攻擊是二次請求残制,黑客通過抓包獲取到了請求的HTTP報文,然后黑客自己編寫了一個類似的HTTP請求掖疮,發(fā)送給服務(wù)器初茶。也就是說服務(wù)器處理了兩個請求,先處理了正常的HTTP請求浊闪,然后又處理了黑客發(fā)送的篡改過的HTTP請求恼布。
如果這個正常邏輯是插入數(shù)據(jù)庫操作,那么一旦插入數(shù)據(jù)庫的語句寫的不好搁宾,就有可能出現(xiàn)多條重復(fù)的數(shù)據(jù)折汞。一旦是比較慢的查詢操作,就可能導(dǎo)致數(shù)據(jù)庫堵住等情況盖腿。
1.1 重放攻擊的概念:
重放攻擊是計算機(jī)世界黑客常用的攻擊方式之一爽待,所謂重放攻擊就是攻擊者發(fā)送一個目的主機(jī)已接收過的包,來達(dá)到欺騙系統(tǒng)的目的翩腐,主要用于身份認(rèn)證過程鸟款。
二、重放攻擊的防御方案
2.1 基于timestamp方案
每次HTTP請求茂卦,都需要加上timestamp參數(shù)何什,然后把timestamp和其他參數(shù)一起進(jìn)行數(shù)字簽名。因為一次正常的HTTP請求等龙,從發(fā)出到達(dá)服務(wù)器一般都不會超過60s处渣,所以服務(wù)器收到HTTP請求之后伶贰,首先判斷時間戳參數(shù)與當(dāng)前時間相比較,是否超過了60s霍比,如果超過了則認(rèn)為是非法的請求幕袱。
假如黑客通過抓包得到了我們的請求url:
http://www.reibang.com?uid=3535353535353535&time=1543991604448&sign=eaba21f90e635c22d2d775731ec03a92
其中
long uid = 3535353535353535L;
String token = "fewgjiwghwoi3ji4oiwjo34ir4erojwk";
long time = new Date().getTime();//1543991604448
String sign = MD5Utils.MD5Encode("uid=" + uid + "&time=" + time + token,"utf8");
public class MD5Utils {
private static final String hexDigIts[] = {"0","1","2","3","4","5","6","7","8","9","a","b","c","d","e","f"};
/**
* MD5加密
* @param origin 字符
* @param charsetname 編碼
* @return
*/
public static String MD5Encode(String origin, String charsetname){
String resultString = null;
try{
resultString = new String(origin);
MessageDigest md = MessageDigest.getInstance("MD5");
if(null == charsetname || "".equals(charsetname)){
resultString = byteArrayToHexString(md.digest(resultString.getBytes()));
}else{
resultString = byteArrayToHexString(md.digest(resultString.getBytes(charsetname)));
}
}catch (Exception e){
}
return resultString;
}
public static String byteArrayToHexString(byte b[]){
StringBuffer resultSb = new StringBuffer();
for(int i = 0; i < b.length; i++){
resultSb.append(byteToHexString(b[i]));
}
return resultSb.toString();
}
public static String byteToHexString(byte b){
int n = b;
if(n < 0){
n += 256;
}
int d1 = n / 16;
int d2 = n % 16;
return hexDigIts[d1] + hexDigIts[d2];
}
}
一般情況下暴备,黑客從抓包重放請求耗時遠(yuǎn)遠(yuǎn)超過了60s悠瞬,所以此時請求中的time參數(shù)已經(jīng)失效了。
如果黑客修改time參數(shù)為當(dāng)前的時間戳涯捻,則sign參數(shù)對應(yīng)的數(shù)字簽名就會失效浅妆,因為黑客不知道token值,沒有辦法生成新的數(shù)字簽名障癌。
但這種方式的漏洞也是顯而易見的凌外,如果在60s之內(nèi)進(jìn)行重放攻擊,那就沒辦法了涛浙,所以這種方式不能保證請求僅一次有效康辑。
2.2 基于nonce方案
nonce是僅一次有效的隨機(jī)字符串,要求每次請求時轿亮,該參數(shù)要保證不同疮薇,所以該參數(shù)一般與時間戳有關(guān),我們這里為了方便起見我注,直接使用時間戳作為種子按咒,隨機(jī)生成16位的字符串,作為nonce參數(shù)但骨。
我們將每次請求的nonce參數(shù)存儲到一個redis中励七。 每次處理HTTP請求時,首先判斷該請求的nonce參數(shù)是否在redis中奔缠,如果存在則認(rèn)為是非法請求掠抬。
假如黑客通過抓包得到了我們的請求url:
http://www.reibang.com?uid=3535353535353535&nonce=RLLUammMSInlrNWb&sign=d2f7406dfdeea3561f753d9e0d1dc320
long uid = 3535353535353535L;
String token = "fewgjiwghwoi3ji4oiwjo34ir4erojwk";
long time = new Date().getTime();//1543993280840
String nonce = RandomUtils.getRandomChar(time);
String sign = MD5Utils.MD5Encode("uid=" + uid + "&nonce=" + nonce + token,"utf8");
public class RandomUtils {
public static String getRandomChar(long time){
Random random = new Random(time);
StringBuffer sb = new StringBuffer();
for(int i = 0; i < 16; i++){
char c = (char)(random.nextLong() % 26 + 97);
sb.append(c);
}
return sb.toString();
}
}
nonce參數(shù)在首次請求時,已經(jīng)被存儲到了服務(wù)器上的redis中校哎,再次發(fā)送請求會被識別并拒絕两波。
nonce參數(shù)作為數(shù)字簽名的一部分,是無法篡改的贬蛙,因為黑客不清楚token雨女,所以不能生成新的sign。
這種方式也有很大的問題阳准,那就是存儲nonce的redis會越來越大氛堕,驗證nonce是否存在redis中的耗時會越來越長。我們不能讓nonce集合無限大野蝇,所以需要定期清理該“集合”讼稚,但是一旦該集合被清理括儒,我們就無法驗證被清理了的nonce參數(shù)了。也就是說锐想,假設(shè)該集合平均1天清理一次的話帮寻,我們抓取到的該url,雖然當(dāng)時無法進(jìn)行重放攻擊赠摇,但是我們還是可以每隔一天進(jìn)行一次重放攻擊的固逗。而且存儲24小時內(nèi),所有請求的“nonce”參數(shù)藕帜,也是一筆不小的開銷烫罩。
2.2 基于timestamp+nonce方案
我們常用的防止重放的機(jī)制是使用timestamp和nonce來做的重放機(jī)制。
每個請求帶的時間戳不能和當(dāng)前時間超過一定規(guī)定的時間(60s)洽故。這樣請求即使被截取了贝攒,你也只能在60s內(nèi)進(jìn)行重放攻擊,過期失效时甚。
但是攻擊者還有60s的時間攻擊隘弊。所以我們就需要加上一個nonce隨機(jī)數(shù),防止60s內(nèi)出現(xiàn)重復(fù)請求荒适。
timstamp參數(shù)對于超過60s的請求梨熙,都認(rèn)為非法請求;
redis存儲60s內(nèi)的nonce參數(shù)的集合吻贿,60s內(nèi)重復(fù)則認(rèn)為是非法請求串结。
long uid = 3535353535353535L;
String token = "fewgjiwghwoi3ji4oiwjo34ir4erojwk";
long time = new Date().getTime();//1543993979284
String nonce = RandomUtils.getRandomChar(time);
String sign = MD5Utils.MD5Encode("uid=" + uid + "&time" + time +"&nonce=" + nonce + token,"utf8");
三、服務(wù)端實現(xiàn)流程
服務(wù)端第一次在接收到這個nonce的時候做下面行為:
1 去redis中查找是否有key為nonce:{nonce}的string
2 如果沒有舅列,則創(chuàng)建這個key肌割,把這個key失效的時間和驗證time失效的時間一致,比如是60s帐要。
3 如果有把敞,說明這個key在60s內(nèi)已經(jīng)被使用了,那么這個請求就可以判斷為重放請求榨惠。
3.1 示例
那么比如奋早,下面這個請求:
time,nonce赠橙,sign都是為了簽名和防重放使用耽装。
time是發(fā)送接口的時間,nonce是隨機(jī)串期揪,sign是對uid掉奄,time,nonce。簽名的方法可以是md5({秘要}key1=val1&key2=val2&key3=val3...)
服務(wù)端接到這個請求:
1 先驗證sign簽名是否合理凤薛,證明請求參數(shù)沒有被中途篡改
2 再驗證time是否過期姓建,證明請求是在最近60s被發(fā)出的
3 最后驗證nonce是否已經(jīng)有了诞仓,證明這個請求不是60s內(nèi)的重放請求