關於限制同一個IP訪問頻率和限制使用者登入時候輸錯密碼次數限制(超過即限制)
阿新 • • 發佈:2018-12-20
一:關於限制同一IP的基本的思路
spring action請求頻率限制(不能限制靜態資源的請求)
限制同一ip在一定時間內, 對server請求的次數.
由ip第一次請求來做為時間點, 將時間,請求次數快取到redis.
1. 第一次請求(redis中無快取記錄), 初始化快取(時間=當前, 次數=1) .
2. 非第一次請求, 從redis中取出快取與當前時間相比.
2.1: 快取時間過期(小於當前-intervalInMS), 同 1.處理
2.2: 快取時間有效, 快取請求次數+1, 如果請求次數>=requestMaxCount, 請求過於頻繁, 不交於action處理.
二:示例程式碼,採用的是SpringMVC的過濾器
public class FrequencyInterceptor extends HandlerInterceptorAdapter { private long intervalInMS = 10000L;//請求時間段, 以ms為單位 private int requestMaxCount = 30;//時間段內, 請求次數上限, 其餘將不會處理 private Logger logger = LoggerFactory.getLogger(FrequencyInterceptor.class); @Overridepublic boolean preHandle(HttpServletRequest request, HttpServletResponse response, Object handler) throws Exception { //請求頻率限制 String ip = this.getIP(request); if(StringUtils.isEmpty(ip)){ logger.error("未獲取到目標ip"); return true; } //資料快取至redis中long currentTimeInMS = Calendar.getInstance().getTimeInMillis(); String frequencyDetail = RedisPoolsUtil.get(redisKey(ip)); if(StringUtils.isEmpty(frequencyDetail)){ //redis中未查詢到記錄 logger.info("ip: {}, 第一次請求", ip); //初始化資訊到redis resetRedisCache(ip, currentTimeInMS, 1); return true; } //不是第一次請求, 先比較時間 JSONObject obj = JSONObject.parseObject(frequencyDetail); long requestTime = obj.getLong("requestTime"); if(currentTimeInMS-requestTime >= this.intervalInMS){ //時間間隔較長, 重新記數 logger.info("ip: {}, 間隔過長, 重新計數", ip); resetRedisCache(ip, currentTimeInMS, 1); return true; } //有效計時時間內 int requestCount = obj.getIntValue("requestCount"); if(requestCount > this.requestMaxCount){ //請求過於頻繁 logger.info("{} 請求過於頻繁", ip); return false; } else{ //有效計數時間內, 計數+1 logger.debug("ip: {}, 請求計數: {}", ip, requestCount); resetRedisCache(ip, requestTime, requestCount + 1); } return true; } /* * @description: 獲取請求ip * @date: 2018/6/12 * @param: * @return: */ private String getIP(HttpServletRequest request) { String ip = request.getHeader("X-Forwarded-For"); if(StringUtils.isNotEmpty(ip) && !"unKnown".equalsIgnoreCase(ip)){ int index = ip.indexOf(","); if(index != -1){ return ip.substring(0,index); }else{ return ip; } } ip = request.getHeader("X-Real-IP"); if(StringUtils.isNotEmpty(ip) && !"unKnown".equalsIgnoreCase(ip)){ return ip; } return request.getRemoteAddr(); } /* * @description: 快取到redis中的key * @date: 2018/6/12 * @param: * @return: */ private String redisKey(String ip){ return "interceptor.frequency." + ip; } /* * @description: 重設redis快取 * @date: 2018/6/12 * @param: * @return: */ private void resetRedisCache(String ip, long currentTimeInMS, int count){ //初始化資訊到redis JSONObject obj = new JSONObject(); obj.put("requestTime", currentTimeInMS); obj.put("requestCount", count); RedisPoolsUtil.set(redisKey(ip), obj.toJSONString()); } }
使用者密碼在五分鐘內輸錯超過四次即鎖定使用者(不採用資料庫進行持久化計數)。
一:基本的思路是
1:同樣使用redis作為快取
2:redis中儲存時間戳資料和計數資料
3:同樣的對比時間戳的大小。
二:程式碼如下圖(該程式碼為使用者輸入的密碼與資料庫的密碼不同,即輸錯了密碼且在五分鐘內輸錯了四次,即鎖定使用者!!)
if (!encryptedPassword.equals(user.getPassword())) { // 使用者校驗不通過 int limitNumber = 1; //第一次輸錯密碼的記錄時間儲存在redis中 Long loginTime = Calendar.getInstance().getTimeInMillis(); log.info("使用者登入,帳號={},認證不通過", account); if (StringUtil.isEmpty(limitIs) || "null".equals(limitIs)) { jedis.set(account + "limit", limitNumber + "#" + loginTime); } else { //記錄密碼錯誤的次數 final long fiveMillis = 300000L; limitNumber = Integer.valueOf(limitIs.split("#")[0]); //第一次輸錯密碼的時間戳 Long loginTimes = Long.valueOf(limitIs.split("#")[1]); //當前輸入密碼的時間戳 Long nowTime = Calendar.getInstance().getTimeInMillis(); //五分鐘以內的時間輸錯的密碼計數器+1,更新redis的值 if (limitNumber < 4 && (nowTime - loginTimes) < fiveMillis) { limitNumber += 1; jedis.set(account + "limit", limitNumber + "#" + loginTimes); } //五分鐘以內輸錯四次鎖定使用者 if (limitNumber >= 4 && (nowTime - loginTimes) < fiveMillis) { //資料庫中鎖定使用者 int a = userService.lockUsers(account); if (a == 1) { log.info("使用者因5分鐘以內輸錯密碼次數超過四次被鎖定:{}", account); } } //超過五分鐘的時候,再次輸入密碼的時候將redis的值重置為1 if ((nowTime - loginTimes) > fiveMillis) { jedis.set(account + "limit", 1 + "#" + nowTime); limitNumber = 1; } } if (!StringUtil.isEmpty(limitIs) && !"null".equals(limitIs) && limitNumber >= 4) { rst.setResultCode(ReqResult.resultCode_login_error); rst.setReturnObject("因多次輸入密碼錯誤被鎖定,請聯絡管理員解鎖"); return ReqResultUtil.genResultResponse(rst); } else { rst.setResultCode(ReqResult.resultCode_login_error); rst.setReturnObject("密碼錯誤,您已輸錯" + limitNumber + "次,還有" + (4 - limitNumber) + "次機會"); return ReqResultUtil.genResultResponse(rst); } }
三:當用戶繼續登入且連續輸錯四次密碼被鎖定的時候(提示操作)
if (user.getStatus() == 0 && !StringUtil.isEmpty(limitIs) && !"null".equals(limitIs) && Integer.valueOf(limitIs.split("#")[0]) >= 4) { log.info("使用者登入,帳號={},使用者因多次輸入密碼已經禁用", account); // 帳號多次輸入錯誤密碼被禁用 rst.setResultCode(ReqResult.resultCode_user_forbid); rst.setReturnObject("使用者因多次輸入密碼錯誤已經禁用,請聯絡管理人員"); return ReqResultUtil.genResultResponse(rst); }
四:當用戶在輸錯密碼未超過四次的時候,如最後一次機會輸入的密碼是對的。此刻將redis的資料重置。(採用的String 型別的“null”)
rst.setResultCode(ReqResult.RESULT_CODE_SUCCESS); rst.setReturnObject("使用者驗證通過"); if (!StringUtil.isEmpty(limitIs) && !"null".equals(limitIs)) { jedis.set(account + "limit", "null"); }
以上便是兩種限制的基本思路,和實際專案的程式碼。涉及到Springmvc的過濾器的使用。redis的基本操作。