一:关于限制同一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); @Override public 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的基本操作。