圖形驗證碼設計實現

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

設計要點

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

存儲選擇

首先我想到了緩存。目前比較流行的緩存服務是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生成時參照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;
}複製代碼
相關文章
相關標籤/搜索