本文特色: 针对验证码的生成作了不少算法优化和调整,支持一个典型的验证码生成和验证流程,利用缓存服务器解决聚群部署架构中数据同步的问题。
基本需求:
- 登陆页面显示一个随机验证码图片、有背景干扰
- 用户输入验证码大于3位以后,开始实时验证正确性,若是正确则在输入框后面提示(好比“√”)
- 登陆时,后台检查验证码是否正确
- 支持服务器集群部署的架构
交互过程和设计思路:
- 前端打开login页面时,向后端请求一个验证码图片;login页面点击验证码图片时,从新请求一个新图片
- 后端处理验证码图片请求:
- 随机生成一个字符串
- 随机生成一个UUID做为cookie的值,放进HttpServletResponse中送给前端
- 把cookie的值做为key、随机字符串做为value存进Redis中,设置过时时间(好比2分钟)
- 根据字符串生成一个图片:
- 随机打印一些小字母或数字做为图片的背景
- 把字符串拆成独立的字母,每一个字母在必定范围内随机上下左右偏移,并旋转一个角度
- 经过调整偏移量和角度防止字符位置超出图片范围
- 把图片经过HttpServletResponse发送给前端
- 前端监控用户的输入,当验证码输入框中的文本长度大于3的时候,经过Ajax发送给后端校验
- 后端处理校验请求:
- 经过HttpServletRequest获取以前设置的cookie的值,把它做为key去Redis中获取value
- 比较Redis中获取的value和前端发送来的字符串,若是匹配,则一样以cookie的值做为key在Redis中写入验证经过的标识(好比"1",同时设置过时时间,好比2分钟),不然写入或清空Redis中以前写入的标识
- 返回成功或失败的结果给前端
- 前端根据校验结果设置输入框后面的提示标识(好比“√”)
- 前端提交登陆表单,此时不须要提交用户输入的验证码,由于后端已经根据cookie记录验证码是否正确
- 后端处理用户的登陆表单:从cookie中取出值后,去Redis查找是否经过的标识(4.2中设置的),若是找不到成功的标识,则提示用户“验证码错误或已通过期”
- 前端判断登陆是否成功,若是失败则从新向后端请求一个验证码图片、自动刷新验证码
效果图:
单独的验证码大图:
代码实现:
- static char[] chars = { 'A', 'B', 'C', 'D', 'E', 'F', 'G', 'H', 'I', 'J',
- 'K', 'L', 'M', 'N', 'O', 'P', 'Q', 'R', 'S', 'T', 'U', 'V', 'W',
- 'X', 'Y', 'Z', '0', '1', '2', '3', '4', '5', '6', '7', '8', '9' };
- public static String genCaptcha(int count) {
- StringBuilder captcha = new StringBuilder();
-
- for(int i=0; i<count; i++){
- char c = chars[ThreadLocalRandom.current().nextInt(chars.length)];
- captcha.append(c);
- }
- return captcha.toString();
- }
-
- public static BufferedImage genCaptchaImg(String captcha){
- ThreadLocalRandom r = ThreadLocalRandom.current();
-
- int count = captcha.length();
- int fontSize = 80;
- int fontMargin = fontSize/4;
- int width = (fontSize+fontMargin)*count+fontMargin;
- int height = (int) (fontSize*1.2);
- int avgWidth = width/count;
- int maxDegree = 26;
-
-
- Color bkColor = Color.WHITE;
-
- Color[] catchaColor = {Color.MAGENTA, Color.BLACK, Color.BLUE, Color.CYAN, Color.GREEN, Color.ORANGE, Color.PINK};
-
- BufferedImage image = new BufferedImage(width, height, BufferedImage.TYPE_INT_RGB);
- Graphics2D g = image.createGraphics();
-
-
- g.setColor(bkColor);
- g.fillRect(0, 0, width, height);
-
-
- g.setColor(Color.BLACK);
- g.drawRect(0, 0, width-1, height-1);
-
-
- int dSize = fontSize/3;
- Font font = new Font("Fixedsys", Font.PLAIN, dSize);
- g.setFont(font);
- int dNumber = width*height/dSize/dSize;
- for(int i=0; i<dNumber; i++){
- char d_code = chars[r.nextInt(chars.length)];
- g.setColor(new Color(r.nextInt(255),r.nextInt(255),r.nextInt(255)));
- g.drawString(String.valueOf(d_code), r.nextInt(width), r.nextInt(height));
- }
-
-
-
-
- font = new Font(Font.MONOSPACED, Font.ITALIC|Font.BOLD, fontSize);
-
- g.setFont(font);
-
- for(int i=0; i<count; i++){
- char c = captcha.charAt(i);
- g.setColor(catchaColor[r.nextInt(catchaColor.length)]);
-
-
- int degree = r.nextInt(-maxDegree, maxDegree+1);
-
-
- double offsetFactor = 1-(Math.abs(degree)/(maxDegree+1.0));
-
- g.rotate(degree * Math.PI / 180);
- int x = (int) (fontMargin + r.nextInt(avgWidth-fontSize)*offsetFactor);
- int y = (int) (fontSize + r.nextInt(height-fontSize)*offsetFactor);
-
- g.drawString(String.valueOf(c), x, y);
-
- g.rotate(-degree * Math.PI / 180);
- g.translate(avgWidth, 0);
-
-
-
-
- }
-
- g.dispose();
-
- return image;
- }
- public static final String CAPTCHA = "captcha:";
- public static final String CAPTCHA_CODE = "code";
- public static final String CAPTCHA_CHECKED = "checked";
- public static final int CAPTCHA_EXPIRED = 2;
- @RestController
- @RequestMapping("/servlet/auth")
- public class AuthController {
- @RequestMapping(value = "/captcha")
- public void captcha(HttpServletRequest request, HttpServletResponse response){
- try {
-
- BufferedImage image = authService.genCaptcha(response);
-
- response.setContentType("image/jpeg");
-
-
- ServletOutputStream outStream = response.getOutputStream();
- ImageIO.write(image, "jpeg", outStream);
- outStream.close();
- } catch (Exception ex) {
- logger.error(ex.getMessage(), ex);
- }
- }
-
-
- @RequestMapping(value = "/verifyCaptcha", method = RequestMethod.POST)
- public JsonResult verifyCaptcha(HttpServletRequest request, @RequestBody Map<String, String> map) {
- try {
- return authService.verifyCaptcha(request, map);
- } catch (Exception ex) {
- logger.error(ex.getMessage(), ex);
- return new JsonResult(ReturnCode.EXCEPTION, "检查验证码失败!", null);
- }
- }
-
-
- @RequestMapping(value = "/webLogin", method = RequestMethod.POST)
- public JsonResult webLogin(HttpServletRequest request, HttpServletResponse response, @RequestBody Map<String, String> map) {
- try {
-
- String captchaCookie = WebUtils.getCookieByName(request, "YsbCaptcha");
- map.put("captchaCookie", captchaCookie);
-
- return authService.webLogin(map, response, true);
- } catch (Exception ex) {
- logger.error(ex.getMessage(), ex);
- return new JsonResult(ReturnCode.EXCEPTION, "用户登陆失败!", null);
- }
- }
- }
- @Override
- public JsonResult webLogin(Map<String, String> map, HttpServletResponse response, boolean captchaRequired) {
-
-
- if(captchaRequired){
- String cookieId = map.get("captchaCookie");
- String storedCaptcha = redisOperator.hget(RedisConstants.CAPTCHA+cookieId, RedisConstants.CAPTCHA_CHECKED);
- if(!"1".equals(storedCaptcha)){
- return new JsonResult(ReturnCode.ERROR, "验证码错误或已通过期!", null);
- }
- }
-
-
- String userAccount = map.get("userAccount");
- String pw = map.get("password");
-
- if(StringUtils.isEmpty(userAccount) || StringUtils.isEmpty(pw)){
- return new JsonResult(ReturnCode.PARAMSERROR, "用户名或密码不能为空!", null);
- }
-
- TabUser user = checkUserExist(userAccount, 1);
-
- if(user==null || !user.getPassword().equals(EncryptUtils.MD5Str(pw + user.getLoginSalt()))){
- return new JsonResult(ReturnCode.ERROR, "用户名或密码错误!", null);
- }
-
-
-
-
-
-
-
-
-
-
- return new JsonResult(ReturnCode.SUCCESS, "登陆成功", null);
- }
-
-
- @Override
- public BufferedImage genCaptcha(HttpServletResponse response) {
-
-
- String captcha = WebUtils.genCaptcha(5);
-
-
- String cookieId = UUID.randomUUID().toString().replace("-", "").toUpperCase();
- WebUtils.addCookie(response, "YsbCaptcha", cookieId, RedisConstants.CAPTCHA_EXPIRED);
-
-
- redisOperator.hset(RedisConstants.CAPTCHA+cookieId, RedisConstants.CAPTCHA_CODE, captcha, RedisConstants.CAPTCHA_EXPIRED*60);
-
-
- BufferedImage image = WebUtils.genCaptchaImg(captcha);
- return image;
- }
-
- @Override
- public JsonResult verifyCaptcha(HttpServletRequest request, Map<String, String> map) {
-
- String receivedCaptcha = map.get("captcha");
-
- if(!StringUtils.isEmpty(receivedCaptcha)){
- String cookieId = WebUtils.getCookieByName(request, "YsbCaptcha");
- String storedCaptcha = redisOperator.hget(RedisConstants.CAPTCHA+cookieId, RedisConstants.CAPTCHA_CODE);
- if(receivedCaptcha.toUpperCase().equals(storedCaptcha)){
- redisOperator.hset(RedisConstants.CAPTCHA+cookieId, RedisConstants.CAPTCHA_CHECKED, "1", RedisConstants.CAPTCHA_EXPIRED*60);
- return new JsonResult(ReturnCode.SUCCESS, "有效的验证码。", true);
- }else{
- redisOperator.hset(RedisConstants.CAPTCHA+cookieId, RedisConstants.CAPTCHA_CHECKED, "0", RedisConstants.CAPTCHA_EXPIRED*60);
- }
- }
-
- return new JsonResult(ReturnCode.SUCCESS, "无效的验证码。", false);
- }