天天看點

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

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

  • ​​寫在前面​​
  • ​​一、接口設計​​
  • ​​1.1、驗證碼接口​​
  • ​​1.2、登陸接口​​
  • ​​二、驗證碼驗證邏輯​​
  • ​​2.1、驗證碼生成,幾種生成方式可供參考,[參考連結](javascript:void(0))​​
  • ​​2.2、驗證碼文本,臨時存儲,基于Redis,有效期 1 分鐘​​
  • ​​2.3、初次登陸不需要驗證碼​​
  • ​​2.4、驗證碼錯誤不計入錯誤登入次數​​
  • ​​三、錯誤登入控制(5次)(鎖定/釋放使用者)​​
  • ​​3.1、使用 Redis 臨時存儲錯誤次數(10分鐘内,記錄連續錯誤次數,最多五次)​​
  • ​​3.2、10分鐘内,連續登陸錯誤(使用者名/密碼錯誤)5次後,鎖定使用者 4 小時​​
  • ​​3.3、鎖定使用者,Redis 臨時存儲 鎖定使用者記錄 4 小時,4h 後自動釋放,可重新登陸​​
  • ​​四、詳細代碼如下​​

寫在前面

本篇涉及兩個場景

  • 驗證碼驗證邏輯
  • 錯誤登入控制(鎖定/釋放使用者)

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

一、接口設計

1.1、驗證碼接口

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

1.2、登陸接口

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

二、驗證碼驗證邏輯

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));
    }