前后端分离验证码之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工具类
public class CookieHelper {
    private CookieHelper() {
    }


    public static Cookie generateCookie(String name, String value, String path, HttpServletRequest request) {
        Cookie cookie = new Cookie(name, value);
        // 这个path的写法参考SpringBoot源码写的
        cookie.setPath(StringUtils.isBlank(path) ? getRequestContext(request) : path);
        cookie.setSecure(false);
        cookie.setHttpOnly(true);
        // 设置为-1时,关闭浏览器自动失效,设置为0马上失效
        cookie.setMaxAge(-1);
        return cookie;
    }

    public static void addCookie(String name, String value, HttpServletRequest request, HttpServletResponse response) {
        response.addCookie(generateCookie(name, value, null, request));
    }

    /**
     * 设置全局cookie,相同域名下不同项目都可以访问
     * @param name cookie名称
     * @param value cookie值
     * @param request
     * @param response
     */
    public static void addGlobalCookie(String name, String value, HttpServletRequest request, HttpServletResponse response){
        response.addCookie(generateCookie(name, value, "/", request));
    }

    /**
     * description: 删除cookie
     * @param name cookie名字
     * @param request
     * @param response
     * @return void
     * @author ZENG.XIAO.YAN
     * @time 2021-07-23 13:57
     */
    public static void deleteCookie(String name, HttpServletRequest request, HttpServletResponse response) {
        Cookie cookie = new Cookie(name, null);
        cookie.setPath(getRequestContext(request));
        cookie.setMaxAge(0);
        response.addCookie(cookie);
    }

    /**
     * 删除全局cookie
     * @param name
     * @param request
     * @param response
     */
    public static void deleteGlobalCookie(String name, HttpServletRequest request, HttpServletResponse response) {
        Cookie cookie = new Cookie(name, null);
        cookie.setPath("/");
        cookie.setMaxAge(0);
        response.addCookie(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的前后端分离项目
posted @ 2021-10-26 23:36  zeng1994  阅读(1983)  评论(0编辑  收藏  举报