使用验证码的根本目的是为了保证执行操作的主体是人,而不是计算机,拦截不合法的操作。因此要尽量寻找只能由人来完成而计算机无法替代的工作,比如识别图片上带各种干扰线的文字,但随着技术的发展,这种验证很容易就被破解,任何一项技术的诞生与发展,都存在着双面效应,而且两方面也存在着相互制衡的关系,为了防止验证码被破解,开发者不得不研究出越来越高级的验证码,下文是我曾为老平台运营系统做过的验证码管理系统的一点总结,还有许多不足的地方,欢迎指教。
采用验证逐次升级的方式进行验证,首先是滑动验证,当滑动三次依然没有验证通过,就会切换成选字验证,选字验证三次依然没有验证通过,就会升级为短信验证。
从图片服务器随机取出图片,作为图形验证码背景图;
从图片素材随机选取像素点,切割图片生成底图,滑块图;
核心code:
该类生成的对象会存储到redis服务器中,验证码切割的信息,以及用户是否滑动正确的信息;
@Data
class ImageAttributes implements Serializable {
private static final long serialVersionUID = -1L;
/**
* 滑块滑到正确位置的左下角坐标
*/
private String x;
private String y;
/**
* 图片唯一标识
*/
private String qniqueId;
/**
* 判断滑动的位置是否正确
*/
private boolean passOrNot;
}
2.生成底图,顶图具体实现;
public static void pictureTempl( int x, int y,int rnum,String path, ByteArrayOutputStream byteArrayOutputStream) throws Exception {
String pictureTemplatePath = path+"mould.png";
String dealPicturePath = path+ rnum + ".jpg";
// 文件类型
String TemplateFiletype = pictureTemplatePath.substring(pictureTemplatePath.lastIndexOf(".") + 1);
// 模板图
BufferedImage imageTemplate = ImageIO.read(new File(pictureTemplatePath));
int width = imageTemplate.getWidth();//60
int height = imageTemplate.getHeight();//60
/*
* //源文件宽度 int oriWidth = oriImage.getWidth(); //源文件高度 int oriHeight
* =oriImage.getHeight();
*/
// 源文件宽度
int oriWidth = 328;
// 源文件高度
int oriHeight = 166;
// 最终图像
BufferedImage newImage = new BufferedImage(width, height, imageTemplate.getType());
Graphics2D graphics = newImage.createGraphics();
graphics.setBackground(Color.white);
int bold = 5;
// 源文件流
File Orifile = new File(dealPicturePath);
InputStream oriis = new FileInputStream(Orifile);
String oriFiletype = dealPicturePath.substring(dealPicturePath.lastIndexOf(".") + 1);
// 获取感兴趣的目标区域
BufferedImage targetImageNoDeal = getTargetArea(x, y, width, height, oriis, oriFiletype);
// 根据模板图片抠图
newImage = DealCutPictureByTemplate(targetImageNoDeal, imageTemplate, newImage);
// 设置“抗锯齿”的属性
graphics.setRenderingHint(RenderingHints.KEY_ANTIALIASING, RenderingHints.VALUE_ANTIALIAS_ON);
graphics.setStroke(new BasicStroke(bold, BasicStroke.CAP_BUTT, BasicStroke.JOIN_BEVEL));
graphics.drawImage(newImage, 0, 0, null);
graphics.dispose();
ImageIO.write(newImage, TemplateFiletype, byteArrayOutputStream);
}
生成UUID,该UUID用来映射顶图,底图,以及切入规则;
将切好的图片存入指定的文件夹,并将与之相关联的对象存储到redis服务器中,一张验证码生成完毕。
后台服务器是一个集群,定时任务如果不控制的话,每台服务器都会执行定时任务,导致出现一些BUG,所以这里根据ip设定指定一台服务器作为生产者生产验证码。
在redis服务器中维护了一个验证码信息存储池,该存储池的上限为10000,每天会启动定时任务将该验证码信息池打满。
具体流程图如下:
架构图如下:
定时生成1万张图片,更新时判断主机IP,防止多个地方都在跑这个定时,产生冲突,只让特定的主机生成,
用UUID生成图片的名字,一个是滑块一个是底图,滑块和底图名字一样,用jpg和png区分;
每一个图片用一个对象去维护,对象里有三个属性,UUID,以及切图的时候的x,y坐标点,对象放入集合,集合放入redis;
前端请求时生成一个UUID,用于区分是张三请求的图片,还是李四,或者是张三1点请求的还是2点请求的,区分请求的唯一性;
随机从2步骤里面拿一个对象,获得图片名字和x,y坐标点,拿完之后封装一个请求的键值对,key:3步骤的UUID value:(x,y,图片名字),然后放入缓存,设置失效时间2S;
去1步骤生成的1万张图片里拿到这2个图片,写给前端;
根据3步骤生成的UUID,去获得获得x,y并且按比例缩放与前端给的x,y做验证true or false;
步骤6的结果默认flase,防止提交的时候绕过前端,做二次验证,根据三步骤生成的UUID,去拿6步骤的true,false,再次验证。
下图是具体验证逻辑:
生成验证码图片的服务器固定,如果该服务器宕机,那么无法提供生成验证码的服务。
redis中维护的验证码信息存储值得阈值为10000,这意味着每次在执行定时任务的时候要将10000*2张图片存储到图片服务器中。造成没有必要的磁盘容量和IO消耗开销。
请求唯一标识缺少加密方案。
针对缺陷1可以用编写分布式锁进行处理,这样每台服务器都有机会执行定时任务,而不是只有一台服务器执行定时任务,使用分布式锁后,即使有服务器宕机,依然有其他服务器可以指定定时任务。
缩小验证码信息池容量。引入生产者消费者,消费者定期查看验证码信息容量池的验证码数量,如果数量不足,唤醒生产者生产验证码。
示例代码:https://github.com/xiao-ren-wu/validate-pic