前后端分離驗證碼之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的前后端分離項目


免責聲明!

本站轉載的文章為個人學習借鑒使用,本站對版權不負任何法律責任。如果侵犯了您的隱私權益,請聯系本站郵箱yoyou2525@163.com刪除。



 
粵ICP備18138465號   © 2018-2025 CODEPRJ.COM