图形验证码设计实现

验证码不是一个功能性的需求,他并不能带来业务的提高,也不能带来任何价值。验证码只是为了解决机器问题才诞生的。在设计和验证码演化的过程当中,必须同时考虑安全性和体验。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;
}复制代码
相关文章
相关标签/搜索