SpringSecurity下,使用Redis實現驗證碼驗證,用戶錯誤登陸次數限制,鎖定/釋放用戶

寫在前面

本篇涉及兩個場景

  • 驗證碼驗證邏輯
  • 錯誤登錄控制(鎖定/釋放用戶)

本篇只是對這兩種場景的一種實現,可供參考,還有別的實現方式,可自行學習探索、使用

一、接口設計

1.1、驗證碼接口

在這裏插入圖片描述

1.2、登陸接口

在這裏插入圖片描述

二、驗證碼驗證邏輯

2.1、驗證碼生成,幾種生成方式可供參考,參考鏈接

參考中都是寫到文件,實際使用時,是請求驗證碼生成接口,接口響應寫到輸出流到頁面

/**
 * 生成驗證碼
 */
@RestController
public class CaptchaImageController {

    @Autowired
    private StringRedisTemplate stringRedisTemplate;

    @GetMapping("/code/image")
    public ResultBean createCode(String captchaId, HttpServletRequest request, HttpServletResponse response) throws IOException {

        if (StringUtil.isNullStr(captchaId)) {
            return ResultBean.error(CodeEnum.CUSTON_ERROR, "缺少參數");
        }

        // 設置大小,以及位數
        SpecCaptcha specCaptcha = new SpecCaptcha(129, 48, 4);
        // 設置字體
        specCaptcha.setFont(new Font("Times New Roman", Font.ITALIC, 34));
        // 設置類型
        specCaptcha.setCharType(Captcha.TYPE_NUM_AND_UPPER);

        stringRedisTemplate.opsForValue().set(
                RedisKeyGen.getCaptcha(captchaId),
                specCaptcha.text(),
                60,
                TimeUnit.SECONDS);
        specCaptcha.out(response.getOutputStream());
        return null;
    }
    
}

2.2、驗證碼文本,臨時存儲,基於Redis,有效期 1 分鐘

參考生成代碼中,存儲

 stringRedisTemplate.opsForValue().set(
                RedisKeyGen.getCaptcha(captchaId),
                specCaptcha.text(),
                60,
                TimeUnit.SECONDS);

除了Redis臨時存儲之外,還有以下方式作爲存儲

  • 使用關係型數據庫表作爲臨時存儲,校驗功能
  • 使用 Session 臨時存儲,校驗時從 Request 中獲取已生成的驗證碼文本與登錄接口傳參驗證碼文本比較

2.3、初次登陸不需要驗證碼

只是當前業務場景

2.4、驗證碼錯誤不計入錯誤登錄次數

驗證碼可無限刷新登錄,驗證碼錯誤不計入錯誤登錄控制 / 鎖定

三、錯誤登錄控制(5次)(鎖定/釋放用戶)

3.1、使用 Redis 臨時存儲錯誤次數(10分鐘內,記錄連續錯誤次數,最多五次)

3.2、10分鐘內,連續登陸錯誤(用戶名/密碼錯誤)5次後,鎖定用戶 4 小時

3.3、鎖定用戶,Redis 臨時存儲 鎖定用戶記錄 4 小時,4h 後自動釋放,可重新登陸

四、詳細代碼如下

@PostMapping("/login")
    public ResultBean login(
            HttpServletRequest request,
            HttpServletResponse response,
            String username,
            String password,
            String captchaId,
            String captchaCode) {
        // 驗證用戶是否被鎖定
        String lockUser = stringRedisTemplate.opsForValue().get(RedisKeyGen.getLockUser(username));
        if (!StringUtil.isNullStr(lockUser)) {
            return ResultBean.error(CodeEnum.USER_LOCKED);
        }
        //在去redis獲取登錄次數的一個key,有效期10分鐘,如果沒獲取這個key,但是驗證碼不爲空的時候,
        // 直接返回提示,驗證碼已過期,請刷新瀏覽器,
        String loginErrTimes = stringRedisTemplate.opsForValue().get(RedisKeyGen.getLoginErr(username));
        if (StringUtil.isNullStr(loginErrTimes) && !StringUtil.isNullStr(captchaCode)) {
            return ResultBean.error(CodeEnum.CAPTCHA_EXPIRED_ERROR);
        }

        Integer loginErrorTime = 0;
        if (!StringUtil.isNullStr(loginErrTimes)) {
            loginErrorTime = Integer.valueOf(loginErrTimes);
        }

        // 如果驗證嗎爲空(緩存刷新,首次登陸),那不需判斷驗證嗎,否則如果有,必須判斷驗證嗎是否正確
        if (!StringUtil.isNullStr(captchaCode)) {
            String code = stringRedisTemplate.opsForValue().get(RedisKeyGen.getCaptcha(captchaId));
            if (StringUtil.isNullStr(code)) {
                return ResultBean.error(CodeEnum.CAPTCHA_EXPIRED_ERROR);
            }
            if (!captchaCode.equalsIgnoreCase(code)) { // 忽略大小寫
                return ResultBean.error(CodeEnum.CAPTCHA_ERROR);
            }
        }

        // 10分鐘內,不可連續用戶/密碼錯誤 5 次
        Authentication authentication = null;
        try {
            UsernamePasswordAuthenticationToken upToken = new UsernamePasswordAuthenticationToken(username, password);
            authentication = authenticationManager.authenticate(upToken);
            SecurityContextHolder.getContext().setAuthentication(authentication);
            String token = jwtTokenProvider.generateToken(username);
            Cookie cookie = new Cookie(HEADER, token);
            cookie.setHttpOnly(true);
            cookie.setPath("/");
            //設置過期時間4小時
            cookie.setMaxAge(4 * 60 * 1000);
            cookie.setSecure(request.isSecure());
            cookie.setDomain(request.getServerName().toLowerCase());
            response.addCookie(cookie);
        } catch (AuthenticationException e) {
            int i = loginErrorTime + 1;
            stringRedisTemplate.opsForValue().set(RedisKeyGen.getLoginErr(username), String.valueOf(i), 10, TimeUnit.MINUTES);
            if (i == 4) {
                return ResultBean.error(CodeEnum.ERROR_FOUR);
            }
            if (i >= 5) {
                stringRedisTemplate.opsForValue().set(RedisKeyGen.getLockUser(username), "1", 4, TimeUnit.HOURS);
                return ResultBean.error(CodeEnum.USER_LOCKED);
            }
//            stringRedisTemplate.delete(Lists.newArrayList(RedisKeyGen.getUserInfo(username), RedisKeyGen.getUserResource(username)));
            return ResultBean.error(CodeEnum.PSAA_ERROR);
        }
        // 登陸成功,刪除緩存的鎖定用戶和錯誤登陸次數
        stringRedisTemplate.delete(Lists.newArrayList(RedisKeyGen.getLockUser(username), RedisKeyGen.getLoginErr(username)));
        return ResultBean.ok(getLoginVO(username));
    }
發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章