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