防止重放機制
一、API重放攻擊
我們在設計介面的時候,最怕一個介面被使用者擷取用於重放攻擊。重放攻擊是什麼呢?就是把你的請求原封不動地再發送一次,兩次...n次,重放攻擊是二次請求,黑客通過抓包獲取到了請求的HTTP報文,然後黑客自己編寫了一個類似的HTTP請求,傳送給伺服器。也就是說伺服器處理了兩個請求,先處理了正常的HTTP請求,然後又處理了黑客傳送的篡改過的HTTP請求。
如果這個正常邏輯是插入資料庫操作,那麼一旦插入資料庫的語句寫的不好,就有可能出現多條重複的資料。一旦是比較慢的查詢操作,就可能導致資料庫堵住等情況。
1.1 重放攻擊的概念:
重放攻擊是計算機世界黑客常用的攻擊方式之一,所謂重放攻擊就是攻擊者傳送一個目的主機已接收過的包,來達到欺騙系統的目的,主要用於身份認證過程。
二、重放攻擊的防禦方案
2.1 基於timestamp方案
每次HTTP請求,都需要加上timestamp引數,然後把timestamp和其他引數一起進行數字簽名。因為一次正常的HTTP請求,從發出到達伺服器一般都不會超過60s,所以伺服器收到HTTP請求之後,首先判斷時間戳引數與當前時間相比較,是否超過了60s,如果超過了則認為是非法的請求。
假如黑客通過抓包得到了我們的請求url:
43991604448&sign=eaba21f90e635c22d2d775731ec03a92" target="_blank" rel="nofollow,noindex">http://www.jianshu.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]; } }
一般情況下,黑客從抓包重放請求耗時遠遠超過了60s,所以此時請求中的time引數已經失效了。
如果黑客修改time引數為當前的時間戳,則sign引數對應的數字簽名就會失效,因為黑客不知道token值,沒有辦法生成新的數字簽名。
但這種方式的漏洞也是顯而易見的,如果在60s之內進行重放攻擊,那就沒辦法了,所以這種方式不能保證請求僅一次有效。
2.2 基於nonce方案
nonce是僅一次有效的隨機字串,要求每次請求時,該引數要保證不同,所以該引數一般與時間戳有關,我們這裡為了方便起見,直接使用時間戳作為種子,隨機生成16位的字串,作為nonce引數。
我們將每次請求的nonce引數儲存到一個redis中。 每次處理HTTP請求時,首先判斷該請求的nonce引數是否在redis中,如果存在則認為是非法請求。
假如黑客通過抓包得到了我們的請求url:
http://www.jianshu.com?uid=3535353535353535&nonce=RLLUammMSInlrNWb&sign=d2f7406dfdeea3561f753d9e0d1dc320long 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引數在首次請求時,已經被儲存到了伺服器上的redis中,再次傳送請求會被識別並拒絕。
nonce引數作為數字簽名的一部分,是無法篡改的,因為黑客不清楚token,所以不能生成新的sign。
這種方式也有很大的問題,那就是儲存nonce的redis會越來越大,驗證nonce是否存在redis中的耗時會越來越長。我們不能讓nonce集合無限大,所以需要定期清理該“集合”,但是一旦該集合被清理,我們就無法驗證被清理了的nonce引數了。也就是說,假設該集合平均1天清理一次的話,我們抓取到的該url,雖然當時無法進行重放攻擊,但是我們還是可以每隔一天進行一次重放攻擊的。而且儲存24小時內,所有請求的“nonce”引數,也是一筆不小的開銷。
2.2 基於timestamp+nonce方案
我們常用的防止重放的機制是使用timestamp和nonce來做的重放機制。
每個請求帶的時間戳不能和當前時間超過一定規定的時間(60s)。這樣請求即使被截取了,你也只能在60s內進行重放攻擊,過期失效。
但是攻擊者還有60s的時間攻擊。所以我們就需要加上一個nonce隨機數,防止60s內出現重複請求。
timstamp引數對於超過60s的請求,都認為非法請求; redis儲存60s內的nonce引數的集合,60s內重複則認為是非法請求。
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");
三、服務端實現流程
服務端第一次在接收到這個nonce的時候做下面行為:
1 去redis中查詢是否有key為nonce:{nonce}的string
2 如果沒有,則建立這個key,把這個key失效的時間和驗證time失效的時間一致,比如是60s。
3 如果有,說明這個key在60s內已經被使用了,那麼這個請求就可以判斷為重放請求。
3.1 示例
那麼比如,下面這個請求:
time,nonce,sign都是為了簽名和防重放使用。
time是傳送介面的時間,nonce是隨機串,sign是對uid,time,nonce。簽名的方法可以是md5({祕要}key1=val1&key2=val2&key3=val3...)
服務端接到這個請求:
1 先驗證sign簽名是否合理,證明請求引數沒有被中途篡改
2 再驗證time是否過期,證明請求是在最近60s被髮出的
3 最後驗證nonce是否已經有了,證明這個請求不是60s內的重放請求