zoukankan      html  css  js  c++  java
  • 前后端分离验证码之cookie+redis方案

        前后端分离后,由于没有了session,导致验证码内容存储在session已经不可能了,因此考虑存储在redis。本文将介绍一种基于cookie + redis方案的验证码

    一、方案的提出

    (1)验证码存放位置

        没了session,则存储在redis,why?
        因为redis 具有key自动过期,所以用来存放验证码最为合适

    (2)验证码的生成及校验

    • 方案一
      • 网上比较常见的方案,公司妇幼系统也在用的方案
      • 生成阶段:每次调用验证码接口,生成一个唯一key,value为验证码的值,然后存放redis;返回前端时,不再是一张图片,而是json,内含唯一Key + 验证码图片base64
      • 校验阶段:请求后台校验时,需要带上这个key + 验证码的value
      • 优点:实现代码简单
      • 缺点:如果用户恶意的频繁大量调用验证码接口,由于旧的验证码存储在redis的key没有立即删除,redis中验证码key会堆积(虽然有过期,但也顶不住大量并发生成);需要设置额外的限流拦截
    • 方案二
      • 参考开源项目 https://captcha.anji-plus.com/#/doc
      • 生成阶段:依赖前端vue组件,生成一个浏览器全局的clientUid,传给后端去生成验证码;后端redis存储key为clientUid,value为验证码值,返回json,内含clientUid+图片base64
      • 校验阶段:请求后台时,需要带上这个clientUid+ 验证码的value
      • 优点:完美解决了方案一带来的用户恶意刷新验证码导致redis中验证码堆积的问题
      • 不足:比较依赖前端生成的clientUid,如果能够不依赖这个前端的clientUid就更好了
    • 方案三
      • 参考方案二思路,但是用了cookie技术
      • 生成阶段:客户端唯一标识clientUid不需要需要前端,而是以cookie的方式写入到浏览器,如果浏览器已经有该cookie,则以浏览器cookie有值为准。这样这个客户端唯一标识就不依赖前端了;后端redis存储key为clientUid,value为验证码值;返回前端时可以直接返回图片,response加一个addCookie的操作
      • 校验阶段:从cookie中读取到客户端唯一标识,然后去redis中取验证码对应的值进行内容比对即可
      • 优点:完善了方案二的不足,同时如果从session的项目改造成token时,该方案的前端改动最少
      • 不足:依赖cookie,不适合无cookie场景

        本次恰好参与了一个从session改成token的前后端分离项目,验证码直接用的方案三

    二、实现代码

    (1)验证码的生成

        Controller层
        /**
         * 获取用户登录图形验证码
         *
         * @param request
         * @param response
         */
        @ApiOperation(value = "获取图形验证码")
        @GetMapping("verifyCode")
        public void verifyCode(@ApiIgnore HttpServletRequest request, @ApiIgnore HttpServletResponse response) {
            // 从cookie中获取验证码对应的唯一key
            String verifyKey = Optional.ofNullable(WebUtils.getCookie(request,
                    WebSecurityConfiguration.USER_LOGIN_VERIFY_CODE_COOKIE_NAME))
                    .map(Cookie::getValue).orElse(null);
            if (StringUtils.isBlank(verifyKey)) {
                verifyKey = UUID.randomUUID().toString().replace("-", "");
            }
            // 这里每个请求都add新cookie,如果不每次add,则有可能会导致 cookie的path发生变化
            response.addCookie(CookieHelper.generateCookie(WebSecurityConfiguration.USER_LOGIN_VERIFY_CODE_COOKIE_NAME,
                    verifyKey ,request));
            // 生成随机字串,用来做验证码
            String verifyCode = VerifyCodeUtils.generateVerifyCode(4);
            try {
                // 生成图片
                int width = 100;
                int height = 40;
                response.setHeader("Pragma", "No-cache");
                response.setHeader("Cache-Control", "no-cache");
                response.setDateHeader("Expires", 0);
                response.setContentType("image/jpeg");
                VerifyCodeUtils.outputImage(width, height, response.getOutputStream(), verifyCode);
            } catch (IOException e) {
                log.error("生成用户登录图形验证码出错:", e);
                throw new SPIException(BasicEcode.FAILED);
            }
            // 存入会话redis, 2分钟内有效
            redisService.set(RedisConstant.VERIFY_CODE_KEY_PREFIX + verifyKey, verifyCode, 120, TimeUnit.SECONDS);
        }
        cookie工具类
    /**
     * CookieHelper
     *
     * @author ZENG.XIAO.YAN
     * @version 1.0
     * @Date 2021-07-14
     */
    public class CookieHelper {
        private CookieHelper() {
        }
    
        public static Cookie generateCookie(String name, String value, HttpServletRequest request) {
            Cookie cookie = new Cookie(name, value);
            // 这个path的写法参考SpringBoot源码写的
            cookie.setPath(getRequestContext(request));
            cookie.setSecure(false);
            cookie.setHttpOnly(true);
            // 设置为-1时,关闭浏览器自动失效,设置为0马上失效
            cookie.setMaxAge(-1);
            return cookie;
        }
    
        private static String getRequestContext(HttpServletRequest request) {
            String contextPath = request.getContextPath();
            return contextPath.length() > 0 ? contextPath : "/";
        }
    
    }

    (2)检验相关代码

        直接上代码
            // 校验图形验证码
            // 1.从cookie中取出验证码的key
            Cookie cookie = WebUtils.getCookie(request, WebSecurityConfiguration.USER_LOGIN_VERIFY_CODE_COOKIE_NAME);
            String verifyKey = Optional.ofNullable(cookie).map(Cookie::getValue).orElse(null);
            if (StringUtils.isBlank(verifyKey)) {
                // 没有key,直接提示过期
                CustomResponse.error(request, response, PlatformExceptionCode.VERIFY_CODE_EXPIRED);
                return null;
            }
            // 2.从redis中拿到对应key的数据
            String redisKey = RedisConstant.VERIFY_CODE_KEY_PREFIX + verifyKey;
            String redisVerifyCode = (String) redisService.get(redisKey);
            if (StringUtils.isBlank(redisVerifyCode)) {
                CustomResponse.error(request, response, PlatformExceptionCode.VERIFY_CODE_EXPIRED);
                return null;
            }
            // 3.比较值
            if (!redisVerifyCode.equalsIgnoreCase(authenticationBean.getVerifyCode())) {
                CustomResponse.error(request, response, PlatformExceptionCode.VERIFY_CODE_ERROR);
                return null;
            }
            // 图形验证码校验成功后,直接从会话中移除
            redisService.delete(redisKey);

    三、小结

        cookie +redis的方式的验证码特别适合那种从session改造成token的前后端分离项目

    作者:zeng1994
    出处:http://www.cnblogs.com/zeng1994/
    本文版权归作者和博客园共有,欢迎转载!但未经作者同意必须保留此段声明,且在文章页面明显位置给出原文链接!

  • 相关阅读:
    GCD介绍(二): 多核心的性能
    GCD介绍(一): 基本概念和Dispatch Queue
    iOS 中如何监测某段代码运行的时间
    DSOframer 无法正常加载的解决方案
    Hexo 官方主题 landscape-plus 优化
    在 Parallels Desktop 中,全屏模式使用 Win7,唤醒时黑屏
    VS2015 企业版不支持 JavaScript 语法高亮、智能提醒
    解决 Boot Camp 虚拟机升级到 Windows 10 后 Parallels Desktop 不能识别的问题
    利用SkyDrive Pro 迅速批量下载SharePoint Server 上已上传的文件
    SharePoint Server 2013 让上传文件更精彩
  • 原文地址:https://www.cnblogs.com/zeng1994/p/816d1ef7f38f6b9060e8a2f5e9e9599f.html
Copyright © 2011-2022 走看看