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、驗證碼接口
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));
}