圖形驗證碼設計實現

hatch發表於2018-04-04

驗證碼不是一個功能性的需求,他並不能帶來業務的提升,也不能帶來任何價值。驗證碼只是為了解決機器問題才誕生的。在設計和驗證碼演化的過程中,必須同時考慮安全性和體驗。

設計要點

  • 圖片驗證一次性
  • 超時未驗證失效
  • 支援4位驗證碼,字元以英文和數字組成
  • 支援簡單干擾

儲存選擇

首先我想到了快取。目前比較流行的快取服務是redis。操作速度是傳統關係型資料庫查詢的幾十甚至上百倍,效能上面沒問題。我們知道,驗證碼是有時效的,可以利用他的快取時效性

介面設計

[GET]/captchas 獲取圖片驗證碼 { "signature":"xx", //驗證碼簽名 "payload":"xxxxx" //圖片的base64資料 }

實現

  • 調介面生成一個驗證碼signature,和驗證碼圖片資料

    其中驗證碼signature=base64(timestamp:random:sign(timestamp+random+code+secretKey))

    其中secretKey是服務端私鑰,在任何場景都不應該流露出去。一旦客戶端得知這個secret,就可以自己生成驗證碼憑證了。

  • 驗證時需帶上驗證碼signature和驗證碼newcode

    --拿到signature中timestamp,根據設定的驗證碼有效期判斷驗證碼是否過期

    --判斷sign(timestamp+random+code+secretKey)和sign(timestamp+random+newcode+secretKey)是否相等,不相等,可能是簽名被篡改了或者驗證碼輸入錯誤

    --判斷該簽名是否已在黑名單中,如果已在黑命名單說明已經被驗證過了

    --驗證通過,加入黑名單。即將該signature存入redis,有效期設定為5分鐘(注意此有效期要大於驗證碼的有效期,避免多次驗證)

小結

  • 驗證碼signature生成時參照jwt的思路實現的
  • 生成驗證碼signature,和驗證碼圖片資料時是根據演算法實時生成,只有驗證通過之後才加入黑名單儲存在redis中。相比較於獲取驗證碼時就放入redis儲存並設定有效期而言,可以很好的防止使用者暴力獲取驗證碼而不驗證,大大避免了多餘的儲存。

附錄

生成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;
}複製程式碼

相關文章