IP防刷,也就是在短时间内有大量相同ip的请求,可能是恶意的,也可能是超出业务范围的。总之,我们需要杜绝短时间内大量请求的问题,怎么处理?

  其实这个问题,真的是太常见和太简单了,但是真正来做的时候,可能就不一定很简单了哦。

  我这里给一个解决方案,以供参考!

主要思路或者需要考虑的问题为:

  1. 因为现在的服务器环境几乎都是分布式环境,所以,用本地计数的方式肯定是不行了,所以我们需要一个第三方的工具来辅助计数;

  2. 可以选用数据库、缓存中间件、zk等组件来解决分布式计数问题;

  3. 使用自增计数,尽量保持原子性,避免误差;

  4. 统计周期为从当前倒推 interval 时间,还是直接以某个开始时间计数;

  5. 在何处进行拦截? 每个方法开始前? 还是请求入口处?

实现代码示例如下:

  1. import org.aspectj.lang.annotation.Aspect;
  2. import org.aspectj.lang.annotation.Before;
  3. import org.springframework.web.context.request.RequestAttributes;
  4. import org.springframework.web.context.request.RequestContextHolder;
  5. import org.springframework.web.context.request.ServletRequestAttributes;
  6. import redis.clients.jedis.Jedis;
  7.  
  8. import javax.annotation.Resource;
  9. import javax.servlet.http.HttpServletRequest;
  10.  
  11. /**
  12. * IP 防刷工具类, 10分钟内只最多允许1000次用户操作
  13. */
  14. @Aspect
  15. public class IpFlushFirewall {
  16.  
  17. @Resource
  18. private Jedis redisTemplate;
  19.  
  20. /**
  21. * 最大ip限制次数
  22. */
  23. private static int maxLimitIpHit = 1000;
  24.  
  25. /**
  26. * 检查时效,单位:秒
  27. */
  28. private static int checkLimitIpHitInterval = 600;
  29.  
  30. // 自测试有效性
  31. public static void main(String[] args) {
  32. IpFlushFirewall ipTest = new IpFlushFirewall();
  33. // 测试时直接使用new Jedis(), 正式运行时使用 redis-data 组件配置即可
  34. ipTest.redisTemplate = new Jedis("127.0.0.1", 6379);
  35. for (int i = 0; i < 10; i++) {
  36. System.out.println("new action: +" + i);
  37. ipTest.testLoginAction(new Object());
  38. System.out.println("action: +" + i + ", passed...");
  39. }
  40. }
  41.  
  42. // 测试访问的方法
  43. public Object testLoginAction(Object req) {
  44. // ip防刷
  45. String reqIp = "127.0.0.1";
  46. checkIpLimit(reqIp);
  47. // 用户信息校验
  48. System.out.println("login success...");
  49. // 返回用户信息
  50. return null;
  51. }
  52.  
  53. // 检测限制入口
  54. public void checkIpLimit(String ip) {
  55. if(isIpLimited(ip)) {
  56. throw new RuntimeException("操作频繁,请稍后再试!");
  57. }
  58. }
  59.  
  60. // ip 防刷 / 使用切面进行拦截
  61. @Before(value = "execution(public * com.*.*.*(..))")
  62. public void checkIpLimit() {
  63. RequestAttributes ra = RequestContextHolder.getRequestAttributes();
  64. ServletRequestAttributes sra = (ServletRequestAttributes) ra;
  65. HttpServletRequest request = sra.getRequest();
  66. String ip = getIp(request);
  67. if(isIpLimited(ip)) {
  68. throw new RuntimeException("操作频繁,请稍后再试!");
  69. }
  70. }
  71.  
  72. public static String getIp(HttpServletRequest request) {
  73. String ip = request.getHeader("x-forwarded-for");
  74. if (ip == null || ip.length() == 0 || "unknown".equalsIgnoreCase(ip)) {
  75. ip = request.getHeader("Proxy-Client-IP");
  76. }
  77. if (ip == null || ip.length() == 0 || "unknown".equalsIgnoreCase(ip)) {
  78. ip = request.getHeader("WL-Proxy-Client-IP");
  79. }
  80. if (ip == null || ip.length() == 0 || "unknown".equalsIgnoreCase(ip)) {
  81. ip = request.getRemoteAddr();
  82. }
  83. // 多级代理问题
  84. if(ip.contains(",")) {
  85. ip = ip.substring(0, ip.indexOf(',')).trim();
  86. }
  87. return ip;
  88. }
  89.  
  90. /**
  91. * 判断ip是否受限制, 非核心场景,对于非原子的更新计数问题不大,否则考虑使用分布式锁调用更新
  92. */
  93. private boolean isIpLimited(String reqIp) {
  94. String ipHitCache = getIpHitCacheKey(reqIp);
  95. // 先取旧数据作为本次判断,再记录本次访问
  96. String hitsStr = redisTemplate.get(ipHitCache);
  97. recordNewIpRequest(reqIp);
  98. // 新周期内,首次访问
  99. if(hitsStr == null) {
  100. return false;
  101. }
  102. // 之前有命中
  103. // 总数未超限,直接通过
  104. if(!isOverMaxLimit(Integer.valueOf(hitsStr) + 1)) {
  105. return false;
  106. }
  107. // 当前访问后超过限制后,再判断周期内的数据
  108. Long retainIpHits = countEffectiveIntervalIpHit(reqIp);
  109. redisTemplate.set(ipHitCache, retainIpHits + "");
  110. // 将有效计数更新回计数器,删除无效计数后,在限制范围内,则不限制操作
  111. if(!isOverMaxLimit(retainIpHits.intValue())) {
  112. return false;
  113. }
  114. return true;
  115. }
  116.  
  117. // 是否超过最大限制
  118. private boolean isOverMaxLimit(Integer nowCount) {
  119. return nowCount > maxLimitIpHit;
  120. }
  121.  
  122. // 每次访问必须记录
  123. private void recordNewIpRequest(String reqIp) {
  124. if(redisTemplate.exists(getIpHitCacheKey(reqIp))) {
  125. // 自增访问量
  126. redisTemplate.incr(getIpHitCacheKey(reqIp));
  127. }
  128. else {
  129. redisTemplate.set(getIpHitCacheKey(reqIp), "1");
  130. }
  131. redisTemplate.expire(getIpHitCacheKey(reqIp), checkLimitIpHitInterval);
  132. Long nowTime = System.currentTimeMillis() / 1000;
  133. // 使用 sorted set 保存记录时间,方便删除, zset 元素尽可能保持唯一,否则会导致统计有效时数据变少问题
  134. redisTemplate.zadd(getIpHitStartTimeCacheKey(reqIp), nowTime , reqIp + "-" + System.nanoTime() + Math.random());
  135. redisTemplate.expire(getIpHitStartTimeCacheKey(reqIp), checkLimitIpHitInterval);
  136. }
  137.  
  138. /**
  139. * 统计计数周期内有效的的访问次数(删除无效统计)
  140. *
  141. * @param reqIp 请求ip
  142. * @return 有效计数
  143. */
  144. private Long countEffectiveIntervalIpHit(String reqIp) {
  145. // 删除统计周期外的计数
  146. Long nowTime = System.currentTimeMillis() / 1000;
  147. redisTemplate.zremrangeByScore(getIpHitStartTimeCacheKey(reqIp), nowTime - checkLimitIpHitInterval, nowTime);
  148. return redisTemplate.zcard(getIpHitStartTimeCacheKey(reqIp));
  149. }
  150.  
  151. // ip 访问计数器缓存key
  152. private String getIpHitCacheKey(String reqIp) {
  153. return "secure.ip.limit." + reqIp;
  154. }
  155.  
  156. // ip 访问开始时间缓存key
  157. private String getIpHitStartTimeCacheKey(String reqIp) {
  158. return "secure.ip.limit." + reqIp + ".starttime";
  159. }
  160.  
  161. }

  如上解决思路为:

    1. 使用 redis 做计数器工具,做到数据统一的同时,redis 的高性能特性也保证了整个应用性能;

    2. 使用 redis 的 incr 做自增,使用一个 zset 来保存记录开始时间,做双重保险;

    3. 在计数超过限制后,再做开始有效性的检测,保证准确的同时,避免了每次都手动检查有时间有效性的动作;

4. 正常的统计周期超时,借助redis自动淘汰机制清理,无需手动管理;

    5. 使用切面的方式进行请求拦截,避免业务代码入侵;

一个简单IP防刷工具类, x秒内最多允许y次单ip操作的更多相关文章

  1. 实现一个简单的http请求工具类

    OC自带的http请求用起来不直观,asihttprequest库又太大了,依赖也多,下面实现一个简单的http请求工具类 四个文件源码大致如下,还有优化空间 MYHttpRequest.h(类定义, ...

  2. 一个简单的Java文件工具类

    package com.xyworkroom.ntko.util; import java.io.File; import java.io.FileInputStream; import java.i ...

  3. 基于Dapper二次封装了一个易用的ORM工具类:SqlDapperUtil

    基于Dapper二次封装了一个易用的ORM工具类:SqlDapperUtil,把日常能用到的各种CRUD都进行了简化封装,让普通程序员只需关注业务即可,因为非常简单,故直接贴源代码,大家若需使用可以直 ...

  4. [Winform]一个简单的账户管理工具

    最近一直觉得注册的账户越来越多,帐号密码神马的容易弄混.自己就折腾了一个简单的账户管理工具,其实实现也挺简单,将每个账户的密码及相关密码提示信息,经aes算法加密之后保存到数据库,当前登录用户可以查询 ...

  5. Qt5.9一个简单的多线程实例(类QThread)(第一种方法)

    Qt开启多线程,主要用到类QThread.有两种方法,第一种用一个类继承QThread,然后重新改写虚函数run().当要开启新线程时,只需要实例该类,然后调用函数start(),就可以开启一条多线程 ...

  6. 超简单的okhttp封装工具类(上)

      版权声明:转载请注明出处:http://blog.csdn.net/piaomiao8179 https://blog.csdn.net/piaomiao8179/article/details/ ...

  7. 一个好的Java时间工具类DateTime

    此类的灵感来源于C# 虽然网上有什么date4j,但是jar太纠结了,先给出源码,可以继承到自己的util包中,作为一个资深程序员,我相信都有不少好的util工具类,我也希望经过此次分享,能带动技术大 ...

  8. java高并发系列 - 第15天:JUC中的Semaphore,最简单的限流工具类,必备技能

    这是java高并发系列第15篇文章 Semaphore(信号量)为多线程协作提供了更为强大的控制方法,前面的文章中我们学了synchronized和重入锁ReentrantLock,这2种锁一次都只能 ...

  9. C#-用Winform制作一个简单的密码管理工具

    为什么要做? 首先是为了练习一下c#. 想必大家都有过记不起某个平台的账号密码的经历,那种感受着实令人抓狂.那这么多账号密码根本记不住!我之前用python写过一个超级简单(连账号信息都写在代码里那种 ...

随机推荐

  1. windows使用pyecharts报错 No module named 'pyecharts_snapshot

    下载此文件后,使用命令 pip install pyecharts_snapshot-0.1.8-py2.py3-none-any.whl 安装完成即可 链接地址:https://pypi.org/p ...

  2. .net基础学java系列(三)徘徊反思

    .net基础学java系列(三)徘徊反思 上一篇文章:.net基础学java系列(二)IDE 之 插件 这两天晚上看完了IDEA的教学视频:https://edu.51cto.com/course/1 ...

  3. [转]Example Design - Using the AXI DMA in polled mode to transfer data to memory

    Description Attached to this Answer Record is an Example Design for using the AXI DMA in polled mode ...

  4. hbase 问题整理

    阅读本文可以带着下面问题:1.HBase遇到问题,可以从几方面解决问题?2.HBase个别请求为什么很慢?你认为是什么原因?3.客户端读写请求为什么大量出错?该从哪方面来分析?4.大量服务端excep ...

  5. 使用smb映射到本地时 访问权限,请联系管理员错误

    1 这个原因是违反了 SELinux安全策略导致的 2 解决办法  关闭SELinux 先使用getenforce ,如果是Enforcing 就使用setenforce 0 关闭

  6. c语言第一次作业--分支 顺序结构

    1.1思维导图 1.2.1本周学习体会以及代码量学习体会 1.2.2学习体会 因为在假期时只看了小部分的学习视频,也没有刷题量,导致了在开始就感觉到差同学的进程很多.刚开始觉得老师讲课很快,在恶补了很 ...

  7. Tomcat 配置文件server.xml详解

    前言 Tomcat隶属于Apache基金会,是开源的轻量级Web应用服务器,使用非常广泛.server.xml是Tomcat中最重要的配置文件,server.xml的每一个元素都对应了Tomcat中的 ...

  8. python3安装lxmlpipinstall安装失败解决办法

    最近在学习python爬虫技术,lxml模块拥有很强大的获取元素功能,但是安装时总超时报错,如下解决办法 选择好python版本→注意pip版本→下载对应lxml.whl→键入对应的字符串→bingo ...

  9. C# 动态调用WebService 3

    using Microsoft.CSharp; using System; using System.CodeDom; using System.CodeDom.Compiler; using Sys ...

  10. [OC] UIcollectionView 与 UIcollectionViewCell 的使用

    UICollectionView    @interface ViewController ()<UICollectionViewDelegate,UICollectionViewDataSou ...