驗證碼不是一個功能性的需求,他並不能帶來業務的提高,也不能帶來任何價值。驗證碼只是爲了解決機器問題才誕生的。在設計和驗證碼演化的過程當中,必須同時考慮安全性和體驗。redis
首先我想到了緩存。目前比較流行的緩存服務是redis。操做速度是傳統關係型數據庫查詢的幾十甚至上百倍,性能上面沒問題。咱們知道,驗證碼是有時效的,能夠利用他的緩存時效性算法
[GET]/captchas 獲取圖片驗證碼 { "signature":"xx", //驗證碼簽名 "payload":"xxxxx" //圖片的base64數據 }
數據庫
調接口生成一個驗證碼signature,和驗證碼圖片數據緩存
其中驗證碼signature=base64(timestamp:random:sign(timestamp+random+code+secretKey))安全
其中secretKey是服務端私鑰,在任何場景都不該該流露出去。一旦客戶端得知這個secret,就能夠本身生成驗證碼憑證了。app
驗證時需帶上驗證碼signature和驗證碼newcodedom
--拿到signature中timestamp,根據設置的驗證碼有效期判斷驗證碼是否過時性能
--判斷sign(timestamp+random+code+secretKey)和sign(timestamp+random+newcode+secretKey)是否相等,不相等,多是簽名被篡改了或者驗證碼輸入錯誤ui
--判斷該簽名是否已在黑名單中,若是已在黑命名單說明已經被驗證過了spa
--驗證經過,加入黑名單。即將該signature存入redis,有效期設置爲5分鐘(注意此有效期要大於驗證碼的有效期,避免屢次驗證)
生成signature
/**
* 生成簽名
* @param time 時間戳,單位毫秒
* @param random 隨機數
* @param secretKey 服務端私鑰
* @return
* @throws Exception
*/
public static String signature(long time, String random, String secretKey) throws Exception {
String signature = String.format("%s:%s:%s", time, random, getSign(time, random, secretKey));
return Base64Utils.encodeStr(signature.getBytes());
}
public static String getSign(long time, String random, String secretKey) throws Exception{
StringBuilder sign = new StringBuilder();
sign.append(time);
sign.append("\n");
sign.append(random);
sign.append("\n");
sign.append(secretKey);
sign.append("\n");
return EncryptUtil.encryptSHA256(sign.toString());
}
複製代碼
驗證signature
/**
* 驗證簽名
* @param signature 待驗證簽名
* @param random 隨機數
* @param secretKey 服務端私鑰
* @param expire 過時時間,單位毫秒
* @return
*/
public static Boolean validateSign(String signature, String random, String secretKey, int expire) {
String sign = Base64Utils.decodeStr(signature);
String[] signs = sign.split(":");
if (signs.length < 3) {
return false;
}
long curTimestamp = System.currentTimeMillis();
long signTimestamp = Long.valueOf(signs[0]);
if ((curTimestamp - signTimestamp) > expire) {
return false;
}
if (!random.equals(signs[1])) {
return false;
}
String newSign = null;
try {
newSign = getSign(Long.valueOf(signs[0]), signs[1], secretKey);
} catch (Exception e) {
// TODO:記錄日誌
return false;
}
if (!signs[2].equals(newSign)) {
return false;
}
return true;
}複製代碼