web登录:随机验证码的设计和实现

本文特点: 针对验证码的生成做了很多算法优化和调整,支持一个典型的验证码生成和验证流程,利用缓存服务器解决聚群部署架构中数据同步的问题。
 
基本需求:
  • 登录页面显示一个随机验证码图片、有背景干扰
  • 用户输入验证码大于3位之后,开始实时验证正确性,如果正确则在输入框后面提示(比如“√”)
  • 登录时,后台检查验证码是否正确
  • 支持服务器集群部署的架构
 
(绝对原创,转载请注明转自Clement-Xu的博客: http://blog.csdn.net/clementad/article/details/48788361
 
交互过程和设计思路:
  1. 前端打开login页面时,向后端请求一个验证码图片;login页面点击验证码图片时,重新请求一个新图片
  2. 后端处理验证码图片请求:
    1. 随机生成一个字符串
    2. 随机生成一个UUID作为cookie的值,放进HttpServletResponse中送给前端
    3. 把cookie的值作为key、随机字符串作为value存进Redis中,设置过期时间(比如2分钟)
    4. 根据字符串生成一个图片:
      1. 随机打印一些小字母或数字作为图片的背景
      2. 把字符串拆成独立的字母,每个字母在一定范围内随机上下左右偏移,并旋转一个角度
      3. 通过调整偏移量和角度防止字符位置超出图片范围
    5. 把图片通过HttpServletResponse发送给前端
  3. 前端监控用户的输入,当验证码输入框中的文本长度大于3的时候,通过Ajax发送给后端校验
  4. 后端处理校验请求:
    1. 通过HttpServletRequest获取之前设置的cookie的值,把它作为key去Redis中获取value
    2. 比较Redis中获取的value和前端发送来的字符串,如果匹配,则同样以cookie的值作为key在Redis中写入验证通过的标识(比如"1",同时设置过期时间,比如2分钟),否则写入或清空Redis中之前写入的标识
    3. 返回成功或失败的结果给前端
  5. 前端根据校验结果设置输入框后面的提示标识(比如“√”)
  6. 前端提交登录表单,此时不需要提交用户输入的验证码,因为后端已经根据cookie记录验证码是否正确
  7. 后端处理用户的登录表单:从cookie中取出值后,去Redis查找是否通过的标识(4.2中设置的),如果找不到成功的标识,则提示用户“验证码错误或已经过期”
  8. 前端判断登录是否成功,如果失败则重新向后端请求一个验证码图片、自动刷新验证码
 
效果图:
单独的验证码大图:
 
代码实现:
  • 前端页面
[html]  view plain  copy
 
  1. <html>  
  2. <head>  
  3.     <meta charset="UTF-8">  
  4.     <title>登录</title>  
  5.       
  6.     <script src="./resources/js/jquery-1.11.3.js" charset="UTF-8" type="text/javascript"></script>  
  7.     <script src="./resources/js/sys.js" charset="UTF-8" type="text/javascript"></script>  
  8.     <script src="./resources/js/md5.js" charset="UTF-8" type="text/javascript"></script>  
  9.       
  10.     <link href="./resources/css/common.css" type="text/css" rel="stylesheet" />  
  11.       
  12.     <style>  
  13.         .captcha_img{cursor:pointer;height:40px; width:173px;}  
  14.     </style>  
  15.       
  16. </head>  
  17. <body>  
  18.     <form name="loginForm">  
  19.         <div>  
  20.             <div>请登录:</div>  
  21.             <div>  
  22.                 <input type="text" maxlength="20" name="userAccount" placeholder="用户名" />  
  23.             </div>  
  24.             <div>  
  25.                 <input type="password" name="password" placeholder="密码" />  
  26.             </div>  
  27.             <div>  
  28.                 <input type="text" id="captcha" placeholder="验证码"/>  
  29.                 <span id="captchaChecked" style="display:none;color:green;font-weight:bold">√</span>  
  30.             </div>  
  31.             <div>  
  32.                 <img class="captcha_img" id="captchaImg" alt="点击刷新验证码">  
  33.             </div>  
  34.             <div>  
  35.                 <button type="button" id="submit">登录</button>  
  36.             </div>  
  37.         </div>  
  38.     </form>  
  39.       
  40.     <script>  
  41.         var captchaChecked = false;  
  42.           
  43.         $(function() {  
  44.             refreshCaptcha();  
  45.               
  46.             $("#captcha").on("keyup", checkCaptchaInput);  
  47.             $("#captchaImg").on("click", refreshCaptcha);  
  48.             $("#submit").on("click", goLogin);  
  49.         });  
  50.   
  51.         function checkCaptchaInput(){  
  52.             var captchaText =$(this).val()   
  53.             if(captchaText.length <=3 ){ //验证码一般大于三位  
  54.                 $("#captchaChecked").hide();  
  55.                 return;  
  56.             }  
  57.               
  58.             ajaxRequest("/servlet/auth/verifyCaptcha", {captcha : captchaText},  
  59.                 function callback(result) {  
  60.                     if(result.code == "40001"){  
  61.                         if(result.data==true){  
  62.                             $("#captchaChecked").show();  
  63.                             captchaChecked = true;  
  64.                         }else{  
  65.                             $("#captchaChecked").hide();  
  66.                             captchaChecked = false;  
  67.                         }  
  68.                     }else{  
  69.                         alert(result.message);  
  70.                     }  
  71.                 });  
  72.                   
  73.                 if(event.keyCode==13){  
  74.                     goLogin();  
  75.                 }  
  76.         }  
  77.   
  78.         function goLogin() {  
  79.             if(!captchaChecked){  
  80.                 alert("请输入正确的验证码!");  
  81.                 return;  
  82.             }  
  83.           
  84.             var params = $("form").serializeObject();  
  85.             params.password = md5(params.password);  
  86.             ajaxRequest("/servlet/auth/webLogin", params,  
  87.                 function callback(result) {  
  88.                     if(result.code == "40001"){  
  89.                         alert("登录成功");  
  90.                         history.go(-1);  
  91.                     }  
  92.                 },  
  93.                 function errorCallback(){ //发生错误,刷新验证码  
  94.                     refreshCaptcha();  
  95.                 });  
  96.         }  
  97.   
  98.         function refreshCaptcha() {  
  99.             //重载验证码  
  100.             $('#captchaImg').attr('src', getApiRoot()+'/servlet/auth/captcha?' + Math.random());  
  101.         }  
  102.     </script>  
  103. </body>  
  104. </html>  
  • 生成随机验证码和图片的工具类
[java]  view plain  copy
 
  1. static char[] chars = { 'A', 'B', 'C', 'D', 'E', 'F', 'G', 'H', 'I', 'J',   
  2.            'K', 'L', 'M', 'N', 'O', 'P', 'Q', 'R', 'S', 'T', 'U', 'V', 'W',   
  3.            'X', 'Y', 'Z', '0', '1', '2', '3', '4', '5', '6', '7', '8', '9' };  
[java]  view plain  copy
 
  1. /** 
  2.  * 生成一个位数为count的随机验证码 
  3.  * @param count 
  4.  * @return 
  5.  */  
  6. public static String genCaptcha(int count) {  
  7.     StringBuilder captcha = new StringBuilder();  
  8.       
  9.     for(int i=0; i<count; i++){  
  10.         char c = chars[ThreadLocalRandom.current().nextInt(chars.length)];//随机选取一个字母或数字  
  11.         captcha.append(c);  
  12.     }  
  13.     return captcha.toString();  
  14. }  
  15.   
  16. /** 
  17.  * 为一个验证码生成一个图片 
  18.  *  
  19.  * 特性: 
  20.  * - 颜色随机 
  21.  * - 上下位置随机 
  22.  * - 左右位置随机,但字符之间不会重叠 
  23.  * - 左右随机旋转一个角度 
  24.  * - 避免字符出界 
  25.  * - 随机颜色的小字符做背景干扰 
  26.  * - 根据字符大小自动调整图片大小、自动计算干扰字符的个数 
  27.  *  
  28.  * @author XuJijun 
  29.  *   
  30.  * @param captcha 
  31.  * @return 
  32.  */  
  33. public static BufferedImage genCaptchaImg(String captcha){  
  34.     ThreadLocalRandom r = ThreadLocalRandom.current();  
  35.   
  36.     int count = captcha.length();  
  37.     int fontSize = 80; //code的字体大小  
  38.     int fontMargin = fontSize/4; //字符间隔  
  39.     int width = (fontSize+fontMargin)*count+fontMargin; //图片长度  
  40.     int height = (int) (fontSize*1.2); //图片高度,根据字体大小自动调整;调整这个系数可以调整字体占图片的比例  
  41.     int avgWidth = width/count; //字符平均占位宽度  
  42.     int maxDegree = 26; //最大旋转度数  
  43.       
  44.     //背景颜色  
  45.     Color bkColor = Color.WHITE;  
  46.     //验证码的颜色  
  47.     Color[] catchaColor = {Color.MAGENTA, Color.BLACK, Color.BLUE, Color.CYAN, Color.GREEN, Color.ORANGE, Color.PINK};  
  48.       
  49.     BufferedImage image = new BufferedImage(width, height, BufferedImage.TYPE_INT_RGB);  
  50.     Graphics2D g = image.createGraphics();  
  51.       
  52.     //填充底色为灰白  
  53.     g.setColor(bkColor);  
  54.     g.fillRect(0, 0, width, height);  
  55.       
  56.     //画边框  
  57.     g.setColor(Color.BLACK);  
  58.     g.drawRect(0, 0, width-1, height-1);  
  59.       
  60.     //画干扰字母、数字  
  61.     int dSize = fontSize/3; //调整分母大小以调整干扰字符大小  
  62.     Font font = new Font("Fixedsys", Font.PLAIN, dSize);  
  63.     g.setFont(font);  
  64.     int dNumber = width*height/dSize/dSize;//根据面积计算干扰字母的个数  
  65.     for(int i=0; i<dNumber; i++){  
  66.         char d_code = chars[r.nextInt(chars.length)];  
  67.         g.setColor(new Color(r.nextInt(255),r.nextInt(255),r.nextInt(255)));  
  68.         g.drawString(String.valueOf(d_code), r.nextInt(width), r.nextInt(height));  
  69.     }  
  70.       
  71.     //开始画验证码:  
  72.       
  73.     // 创建字体     
  74.     font = new Font(Font.MONOSPACED, Font.ITALIC|Font.BOLD, fontSize);  
  75.     // 设置字体       
  76.     g.setFont(font);  
  77.   
  78.     for(int i=0; i<count; i++){  
  79.         char c = captcha.charAt(i);  
  80.         g.setColor(catchaColor[r.nextInt(catchaColor.length)]);//随机选取一种颜色  
  81.           
  82.         //随机旋转一个角度[-maxDegre, maxDegree]  
  83.         int degree = r.nextInt(-maxDegree, maxDegree+1);  
  84.           
  85.         //偏移系数,和旋转角度成反比,以避免字符在图片中越出边框  
  86.         double offsetFactor = 1-(Math.abs(degree)/(maxDegree+1.0));//加上1,避免出现结果为0  
  87.           
  88.         g.rotate(degree * Math.PI / 180); //旋转一个角度  
  89.         int x = (int) (fontMargin + r.nextInt(avgWidth-fontSize)*offsetFactor); //横向偏移的距离  
  90.         int y = (int) (fontSize + r.nextInt(height-fontSize)*offsetFactor); //上下偏移的距离  
  91.           
  92.         g.drawString(String.valueOf(c), x, y); //x,y是字符的左下角,偏离原点的距离!!!  
  93.           
  94.         g.rotate(-degree * Math.PI / 180); //画完一个字符之后,旋转回原来的角度  
  95.         g.translate(avgWidth, 0);//移动到下一个画画的原点  
  96.         //System.out.println(c+": x="+x+" y="+y+" degree="+degree+" offset="+offsetFactor);  
  97.           
  98.         //X、Y坐标在合适的范围内随机,不旋转:  
  99.         //g.drawString(String.valueOf(c), width/count*i+r.nextInt(width/count-fontSize), fontSize+r.nextInt(height-fontSize));  
  100.     }  
  101.       
  102.     g.dispose();  
  103.       
  104.     return image;  
  105. }  


  • 后端常量定义
[java]  view plain  copy
 
  1. //web登录相关:  
  2. /** 验证码,Hash类型, 后面跟着cookie Id */  
  3. public static final String CAPTCHA = "captcha:";  
  4. /** 验证码,field,验证码内容*/  
  5. public static final String CAPTCHA_CODE = "code";  
  6. /** 验证码,field,验证码是否已经验证过 */  
  7. public static final String CAPTCHA_CHECKED = "checked";  
  8. /** 验证码失效时间,分钟 */  
  9. public static final int CAPTCHA_EXPIRED = 2;  
  • 后端Controller代码
[java]  view plain  copy
 
  1. @RestController  
  2. @RequestMapping("/servlet/auth")  
  3. public class AuthController {  
  4.     @RequestMapping(value = "/captcha")  
  5.     public void captcha(HttpServletRequest request, HttpServletResponse response){  
  6.         try {  
  7.             //把校验码转为图像  
  8.             BufferedImage image = authService.genCaptcha(response);  
  9.               
  10.             response.setContentType("image/jpeg");  
  11.               
  12.             //输出图像  
  13.             ServletOutputStream outStream = response.getOutputStream();  
  14.             ImageIO.write(image, "jpeg", outStream);  
  15.             outStream.close();  
  16.         } catch (Exception ex) {  
  17.             logger.error(ex.getMessage(), ex);  
  18.         }  
  19.     }  
  20.   
  21.     /** 
  22.      * 检查验证码是否正确 
  23.      * @param request 
  24.      * @param map 
  25.      * @return 
  26.      */  
  27.     @RequestMapping(value = "/verifyCaptcha", method = RequestMethod.POST)  
  28.     public JsonResult verifyCaptcha(HttpServletRequest request, @RequestBody Map<String, String> map) {  
  29.         try {  
  30.             return authService.verifyCaptcha(request, map);  
  31.         } catch (Exception ex) {  
  32.             logger.error(ex.getMessage(), ex);  
  33.             return new JsonResult(ReturnCode.EXCEPTION, "检查验证码失败!", null);  
  34.         }  
  35.     }  
  36.       
  37.     /** 
  38.      * 用户登录(web) 
  39.      *  
  40.      * @param map(userName, password) 
  41.      * @return 
  42.      */  
  43.     @RequestMapping(value = "/webLogin", method = RequestMethod.POST)  
  44.     public JsonResult webLogin(HttpServletRequest request, HttpServletResponse response, @RequestBody Map<String, String> map) {  
  45.         try {  
  46.             //获取验证码的cookie id  
  47.             String captchaCookie = WebUtils.getCookieByName(request, "YsbCaptcha");  
  48.             map.put("captchaCookie", captchaCookie);  
  49.               
  50.             return authService.webLogin(map, response, true);  
  51.         } catch (Exception ex) {  
  52.             logger.error(ex.getMessage(), ex);  
  53.             return new JsonResult(ReturnCode.EXCEPTION, "用户登录失败!", null);  
  54.         }  
  55.     }  
  56. }  
  • 后端Service代码
[java]  view plain  copy
 
  1. /** 
  2.  * 用户web登录,response用于写入token到cookie中 
  3.  */  
  4. @Override  
  5. public JsonResult webLogin(Map<String, String> map,   HttpServletResponse response, boolean captchaRequired) {  
  6.       
  7.     //检查验证码是否已经被验证过  
  8.     if(captchaRequired){  
  9.         String cookieId = map.get("captchaCookie");  
  10.         String storedCaptcha = redisOperator.hget(RedisConstants.CAPTCHA+cookieId, RedisConstants.CAPTCHA_CHECKED);  
  11.         if(!"1".equals(storedCaptcha)){  
  12.             return new JsonResult(ReturnCode.ERROR, "验证码错误或已经过期!", null);  
  13.         }  
  14.     }  
  15.       
  16.     // 验证用户名密码  
  17.     String userAccount = map.get("userAccount");  
  18.     String pw = map.get("password");  
  19.       
  20.     if(StringUtils.isEmpty(userAccount) || StringUtils.isEmpty(pw)){  
  21.         return new JsonResult(ReturnCode.PARAMSERROR, "用户名或密码不能为空!", null);  
  22.     }  
  23.       
  24.     TabUser user = checkUserExist(userAccount, 1);  
  25.       
  26.     if(user==null || !user.getPassword().equals(EncryptUtils.MD5Str(pw + user.getLoginSalt()))){//前端送过来的是md5编码后的密码,小写  
  27.         return new JsonResult(ReturnCode.ERROR, "用户名或密码错误!", null);  
  28.     }  
  29.       
  30.       
  31.     // TODO 生成token,  
  32.       
  33.     // TODO token存进redis,过期时间为24小时(不可能有人连续24小时不关浏览器的吧?)  
  34.       
  35.     // TODO token放进response的cookie中,maxAge=0表示关闭浏览器后cookie就失效  
  36.       
  37.     // TODO 记录登录日志  
  38.       
  39.     return new JsonResult(ReturnCode.SUCCESS, "登录成功", null);  
  40. }  
  41.   
  42.   
  43. /** 
  44.  * 生成一个验证码并保存到redis中 
  45.  */  
  46. @Override  
  47. public BufferedImage genCaptcha(HttpServletResponse response) {  
  48.   
  49.     //生成一个校验码  
  50.     String captcha = WebUtils.genCaptcha(5);  
  51.       
  52.     //生成一个cookie ID,并塞进response里面  
  53.     String cookieId = UUID.randomUUID().toString().replace("-", "").toUpperCase();  
  54.     WebUtils.addCookie(response, "YsbCaptcha", cookieId, RedisConstants.CAPTCHA_EXPIRED);  
  55.       
  56.     //把校验码、是否已经通过校验(缺省不设置为0)保存到redis中,以cookie ID 为key  
  57.     redisOperator.hset(RedisConstants.CAPTCHA+cookieId, RedisConstants.CAPTCHA_CODE, captcha, RedisConstants.CAPTCHA_EXPIRED*60);  
  58.       
  59.     //把校验码转为图像  
  60.     BufferedImage image = WebUtils.genCaptchaImg(captcha);  
  61.     return image;  
  62. }  
  63.   
  64. /** 
  65.  * 检查验证码是否正确,并把结果写入redis中 
  66.  */  
  67. @Override  
  68. public JsonResult verifyCaptcha(HttpServletRequest request, Map<String, String> map) {  
  69.       
  70.     String receivedCaptcha = map.get("captcha");   
  71.       
  72.     if(!StringUtils.isEmpty(receivedCaptcha)){  
  73.         String cookieId = WebUtils.getCookieByName(request, "YsbCaptcha");  
  74.         String storedCaptcha = redisOperator.hget(RedisConstants.CAPTCHA+cookieId, RedisConstants.CAPTCHA_CODE);  
  75.         if(receivedCaptcha.toUpperCase().equals(storedCaptcha)){  
  76.             redisOperator.hset(RedisConstants.CAPTCHA+cookieId, RedisConstants.CAPTCHA_CHECKED, "1", RedisConstants.CAPTCHA_EXPIRED*60);  
  77.             return new JsonResult(ReturnCode.SUCCESS, "有效的验证码。", true);  
  78.         }else{  
  79.             redisOperator.hset(RedisConstants.CAPTCHA+cookieId, RedisConstants.CAPTCHA_CHECKED, "0", RedisConstants.CAPTCHA_EXPIRED*60);  
  80.         }  
  81.     }  
  82.       
  83.     return new JsonResult(ReturnCode.SUCCESS, "无效的验证码。", false);  
  84. }  

转载于:https://www.cnblogs.com/ice-blog/p/7407090.html

  • 1
    点赞
  • 1
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值