API接口的安全性主要是為了保證數(shù)據(jù)不會(huì)被篡改和重復(fù)調(diào)用,實(shí)現(xiàn)方案主要圍繞Token、時(shí)間戳和Sign三個(gè)機(jī)制展開設(shè)計(jì)。
1. Token授權(quán)機(jī)制
用戶使用用戶名密碼登錄后服務(wù)器給客戶端返回一個(gè)Token(必須要保證唯一,可以結(jié)合UUID和本地設(shè)備標(biāo)示),并將Token-UserId以鍵值對(duì)的形式存放在緩存服務(wù)器中(我們是使用Redis),并要設(shè)置失效時(shí)間。服務(wù)端接收到請(qǐng)求后進(jìn)行Token驗(yàn)證,如果Token不存在,說明請(qǐng)求無效。Token是客戶端訪問服務(wù)端的憑證。
# uuid 是手機(jī)設(shè)備的唯一標(biāo)示
String token = UUID.randomUUID().toString() + "_" + uuid;
2. 時(shí)間戳超時(shí)機(jī)制
用戶每次請(qǐng)求都帶上當(dāng)前時(shí)間的時(shí)間戳timestamp,服務(wù)端接收到timestamp后跟當(dāng)前時(shí)間進(jìn)行比對(duì),如果時(shí)間差大于一定時(shí)間(比如30秒),則認(rèn)為該請(qǐng)求失效。時(shí)間戳超時(shí)機(jī)制是防御重復(fù)調(diào)用和爬取數(shù)據(jù)的有效手段。
當(dāng)然這里需要注意的地方是保證客戶端和服務(wù)端的“當(dāng)前時(shí)間”是一致的,我們采取的對(duì)齊方式是客戶端第一次連接服務(wù)端時(shí)請(qǐng)求一個(gè)接口獲取服務(wù)端的當(dāng)前時(shí)間A1,再和客戶端的當(dāng)前時(shí)間B1做一個(gè)差異化計(jì)算(A1-B1=AB),得出差異值A(chǔ)B,客戶端再后面的請(qǐng)求中都是傳B1+AB給到服務(wù)端。
// timeStamp是客戶端從Header傳過來的值
Long timeStamp = RequestHeaderContext.getInstance().getTimeStamp();
boolean checkTime = checkTime(timeStamp, 30 * 1000);
if (!checkTime) {
return responseErrorAPISecurity(response);
}
// checkTime方法
public static boolean checkTime(Long time, Integer variable){
Long currentTimeMillis = System.currentTimeMillis();
Long addTime = currentTimeMillis + variable;
Long subTime = currentTimeMillis - variable;
if (addTime > time && time > subTime){
return true;
}
return false;
}
3. API簽名機(jī)制
將“請(qǐng)求的API參數(shù)”+“時(shí)間戳”+“鹽”進(jìn)行MD5算法加密,加密后的數(shù)據(jù)就是本次請(qǐng)求的簽名signature,服務(wù)端接收到請(qǐng)求后以同樣的算法得到簽名,并跟當(dāng)前的簽名進(jìn)行比對(duì),如果不一樣,說明參數(shù)被更改過,直接返回錯(cuò)誤標(biāo)識(shí)。簽名機(jī)制保證了數(shù)據(jù)不會(huì)被篡改。
// 請(qǐng)求的API參數(shù),如果是再body,則MD5;如果是param,則原字符串
StringBuffer urlSign = new StringBuffer();
if ("POST".equals(request.getMethod()) || "PUT".equals(request.getMethod())) {
String bodyStr = RequestReaderUtil.ReadAsChars(request);
String bodySign = "";
if (!StringUtils.isEmpty(bodyStr)){
bodySign = DigestUtils.md5DigestAsHex((bodyStr).getBytes());
}
urlSign = new StringBuffer(bodySign);
} else if ("GET".equals(request.getMethod()) || "DELETE".equals(request.getMethod())) {
String params = request.getQueryString();
if (params == null){
params = "";
}
urlSign = new StringBuffer(params);
}
// “請(qǐng)求的API參數(shù)”+“時(shí)間戳”+“鹽”進(jìn)行MD5算法加密
String sign = DigestUtils.md5DigestAsHex(urlSign.append(timeStamp).append(salt).toString().getBytes());
// signature是客戶端從Header傳過來的值
if (signature.equals(sign)) {
return true;
} else {
return false;
}
4. 注意事項(xiàng)
1、因?yàn)橛脩舻卿浀腡oken是和設(shè)備唯一標(biāo)示綁定的,所以一個(gè)用戶有可能會(huì)有多個(gè)有效的Token,那么當(dāng)用戶在修改登錄密碼時(shí)需要把所有的Token刪除,我的做法是在Redis保存了一個(gè)value是List(值是該用戶所有的有效的Token)的Key,當(dāng)修改密碼時(shí)會(huì)把該Key下的所有Token干掉。
2、客戶端每次請(qǐng)求,在Header里面有timeStamp的值,簽名中也是用這個(gè)timeStamp組合簽名的,要確保這兩個(gè)值是一致的。因?yàn)槲覀冊趯?shí)際開發(fā)中,發(fā)現(xiàn)客戶端的同事在加密時(shí)通過函數(shù)獲取了當(dāng)前時(shí)間A,在請(qǐng)求時(shí)也通過函數(shù)獲取了當(dāng)前時(shí)間B,有時(shí)候這兩個(gè)當(dāng)前時(shí)間會(huì)差幾毫秒,導(dǎo)致簽名校驗(yàn)失敗。
/**
* 登錄后由服務(wù)端生成并返回
*/
private String token;
/**
* 安全校驗(yàn)字段(接口參數(shù)+時(shí)間戳+加鹽:取MD5生成)
*/
private String signature;
/**
* 設(shè)備唯一標(biāo)識(shí)
*/
private String udid;
/**
* 時(shí)間戳,13位,比如:1532942172000
*/
private Long timeStamp;
5. 安全保障總結(jié)
在以上機(jī)制下,
如果有人劫持了請(qǐng)求,并對(duì)請(qǐng)求中的參數(shù)進(jìn)行了修改,簽名就無法通過;
如果有人使用已經(jīng)劫持的URL進(jìn)行DOS攻擊和爬取數(shù)據(jù),那么他也只能最多使用30s;
如果簽名算法都泄露了怎么辦?可能性很小,因?yàn)檫@里的“鹽”值只有我們的CTO知道。