验证码的作用?为什么会有验证码这么烦人的东西?直接手动输入用 户名和密码进行登录不就好了?像12306上的验证码,可以说是国内站点识 别难度第一NB的验证码了。。。
验证码可以有效防止这种问题对某一个特定注册用户用特定程序暴力破解方式进行不断的登陆尝试,实际上是用它是现在很多网站通行的方式。
对于学过web安全的同学们来说,想必应该有所了解,在表单中,每个请求其实就是客户端向服务端发送数据包的过程,而安全界有各种好用的抓包工具,比如Burp Suite ,一旦我通过这款软件对你的登录表单进行数据包拦截,那么它自带的暴力破解密码的工具就发挥了作用,通过指定的字典集以及指定好你的用户名和密码这两个变量,burp可以模拟无数次的尝试登录请求,通过这样的跑字典来破解出你的用户名和密码,但是如果多了一层验证码的校验,那么难度则是上升了一层,每次请求验证码都会刷新一次,每次的结果都是不同的。这就是验证码的作用,为了就是防止不断地尝试破解密码而加上的一层盾。
意义:不少网站为了防止用户利用机器人自动注册、登录、灌水,都采用了验证码技术。所谓验证码,就是将一串随机产生的数字或符号,生成一幅图片,图片里加上一些干扰象素(防止OCR),由用户肉眼识别其中的验证码信息,输入表单提交网站验证,验证成功后才能使用某项功能。(这是来自百度的一段话).
这次公司培训让我体会到了,验证码写了虽然是写了,但是还会存在绕过的现象, 下面直接先上代码实现一下功能,后面在继续说一下安全性的验证码应该如何去写。
采用的是Springmvc 的框架,所以在前端访问的时候通过Servlet进行后台请求。而验证码的实现最安全的做法就是写在后台去实现,同时不要把验证码的结果直接暴露在url地址上,否则等于没有做。
前端jsp页面,验证码请求地址/VerifyCode:
<!DOCTYPE html> <%@ page language="java" pageEncoding="UTF-8"%> <%@ page contentType="text/html; charset=UTF-8"%> <html> <head> <script type="text/javascript"> /** * 刷新验证码 */ function reloadCode(){ var time = new Date().getTime(); document.getElementById("imgCode").src="<%=request.getContextPath() %>/VerifyCode?d=" + time; } function checkLoginNoNull() { if ($.trim($("#verifycode").val()) == "") { alert("验证码不能为空!请重新输入!"); return false; } } </script> </head> <body> <img src="<%=request.getContextPath()%>/VerifyCode" id="imgCode" alt="看不清楚?请点击刷新验证码" onclick="javascript:reloadCode();" class="yzm-img" /> </body> </html>
后台实现逻辑:
利用了java的graphics 画布填充到前端,输出是img的格式,把验证码的属性放在了Session当中。后续做校验从session中那
@RequestMapping("/VerifyCode") public void VerifyCode(HttpServletRequest request, HttpServletResponse response) { // 创建一个宽100,高50,且不带透明色的image对象 100 50 BufferedImage bi = new BufferedImage(100, 50, BufferedImage.TYPE_INT_RGB); Graphics g = bi.getGraphics(); //RGB色彩 Color c = new Color(200, 150, 255); // 框中的背景色 g.setColor(c); // 颜色填充像素 g.fillRect(0, 0, 100, 50); // 定义验证码字符数组 char[] ch = "ABCDEFGHIJKLMNPQRSTUVWXYZ0123456798".toCharArray(); Random r = new Random(); int len = ch.length; int index; StringBuffer sb = new StringBuffer(); // 取出四个数字 for (int i = 0; i < 4; i++) { // 循环四次随机取长度定义为索引 index = r.nextInt(len); g.setColor(new Color(r.nextInt(88), r.nextInt(188), r.nextInt(255))); Font font = new Font("Times New Roman", Font.ITALIC, 18); g.setFont(font); g.drawString(ch[index] + "", (i * 18) + 10, 30); sb.append(ch[index]); } //放入Session中 request.getSession().setAttribute("piccode", sb.toString()); try { ImageIO.write(bi, "JPG", response.getOutputStream()); } catch (IOException e) { e.printStackTrace(); } }
后台登录校验:
校验登录的时候,切记不要先验证验证码!否则也是和没做验证码一样,如果先验证验证码的话,相当于就直接通过了你的这道锁,那么岂不是和没做达到了一个结果,通过了你的验证,后面又可以进行暴力破解了。
我这里因为省略了用户名和密码的写法,所以应该讲它们同时进行判断,类似下面:
/* * userCode为用户名,md16为前端传过来的密码,password则是数据库中的密码 */ //Session中存好的验证码数值 String piccode = (String) request.getSession().getAttribute("piccode"); //前端用户输入的验证码值 String verifycode = request.getParameter("verifycode"); // 验证码判断 if (userCode != null && !userCode.equals("") && verifycode.equals(piccode)) { if(md16.equals(password)){ .........//此处省略,这里做一些登录成功后的操作 } }
你以为这样就完了?并不是的,上面的写法是我第一版的写法,虽然是通过后端校验, 虽然是完成了功能,但是它的作用并没有起到什么,因为通过burp测试发现这样的写法验证码 并不会每次自动刷新。。
上面说的是什么意思呢?大家可以想下,我此处的写法,一个是通过手动点击触发js会刷新验证码,还有一种就是重新访问后台这个/VerifyCode,触发这个的条件是重新访问这个jsp页面,也就是说数据包拦截后,我重新无数次的表单,验证码我只需要输入正确的就可以进行绕过了,因为每次提交表单发送数据包,这个验证码属于正确后是不会变化的!
所以为了防止这种安全漏洞的出现,我们可以这么做:
把验证码对应Session的属性每次销毁即可,代码如下:
//下面这个代码应该写在什么位置? //这个代码正确的位置是放在如果用户名不对或者密码不对或者验证码不对后执行的操作 session.removeAttribute("piccode");
以上就是一套完整的验证码体系。