暴露在公网服务 API 通常需要做防刷机制,防止恶意请求,维护系统的稳定。
API 接口防刷前后端配合使用。后端实现主要有 拦截器 方式 和 使用 AOP  对控制层(Controller)进行切面编程的方式。
 
API 防刷 API 防刷策略通常有两种,分别是在前端和后端实现。
前端实现:通过短信验证码,图片验证码,拖图片滑块,识别不同图片,图片翻转摆正等。例如短信验证码接口防刷等。
前端验证达到次数,增加二次验证。例如短信验证码输入错误达到次数,增加图片验证码二次验证。
这类更多对授权访问或注册类的接口防刷。如登录、注册接口等。
 
后端实现:识别访问来源及在限定时间内的访问次数,超过阀值则归为恶意用户给出提示拒绝访问。
 
 
识别用户来源会获取用户的IP 地址,要注意同一公网 IP 下的多个合法用户。IP 地址存在请求头中,容易被窃取篡改,建议服务器使用 HTTPS 连接。
拦截器方式 关于 拦截器(Interceptor) 可参考 SpringMVC之HandlerInterceptor拦截器 ,Spring Boot 2实践系列(二十七):Listener, Filter, Interceptor 。
请求限制注解 定义请求限制注解,作用的 Controller 类或里面的方法上
使用注解的方式,可以灵活地对不同的接口设置各自的限制条件。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 @Target(ElementType.METHOD,ElementType.TYPE) @Retention(RetentionPolicy.RUNTIME) public  @interface  RequestLimit {         int  count ()  default  Integer.MAX_VALUE;          long  second ()  default  60 ; } 
 
拦截器  方式  和 AOP  方式都需要用到此注解。
限制访问拦截器 注意 :可以有多个拦截器组成拦截器链,可以在拦截器上使用 @Order 注解指定执行顺序,值越大,执行越靠后。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 @Component @Order(Integer.MAX_VALUE - 100) public  class  RequestLimitInterceptor  extends  HandlerInterceptorAdapter  {    Logger  logger  =  LogManager.getLogger(RequestLimitInterceptor.class);     @Autowired      private  RedisTemplate<String, Object> redisTemplate;     @Override      public  boolean  preHandle (HttpServletRequest request, HttpServletResponse response, Object handler)              throws  Exception {         logger.info("请求限制拦截器............." );                  if  (handler instanceof  HandlerMethod) {             HandlerMethod  handlerMethod  =  (HandlerMethod) handler;             Method  method  =  handlerMethod.getMethod();                          RequestLimit  methodReqLimit  =  handlerMethod.getMethodAnnotation(RequestLimit.class);             RequestLimit  classReqLimit  =  method.getDeclaringClass().getAnnotation(RequestLimit.class);                          RequestLimit  requestLimit  =  (null  != methodReqLimit ? methodReqLimit : classReqLimit);             if  (null  == requestLimit) {                                  return  true ;             }             int  second  =  requestLimit.second();             int  maxCount  =  requestLimit.count();             boolean  needLogin  =  requestLimit.needLogin();             String  key  =  request.getRequestURI();                          if  (needLogin) {                 HttpSession  session  =  request.getSession();                 Account  account  =  (Account) session.getAttribute("auth_user" );                 if  (null  == account) {                                          return  false ;                 } else  {                     key = key + "_"  + account.getId();                 }             }                          key = key + "_"  + IPUtil.getIpFromRequest(request);             key = DigestUtils.md5DigestAsHex(key.getBytes(Charset.defaultCharset()));             Integer  count  =  (Integer) redisTemplate.opsForValue().get(key);             if  (null  == count) {                                  redisTemplate.opsForValue().setIfAbsent(key, 1 , second, TimeUnit.SECONDS);             } else  if  (count < maxCount) {                                  redisTemplate.opsForValue().increment(key);             } else  {                 logger.info("接口请求次数超限制:{}" , key);                 response.setCharacterEncoding("UTF-8" );                 response.setContentType("application/json;charset=UTF-8" );                 ServletOutputStream  output  =  response.getOutputStream();                 output.write("{\"msg\":\"请求次数超出限制\"}" .getBytes("UTF-8" ));                 output.flush();                 output.close();                 return  false ;             }         }         return  true ;     } } 
 
配置拦截器生效 将拦截器添加到 WebMvcConfigurer 使其生效。
1 2 3 4 5 6 7 8 9 10 11 @Configuration public  class  WebConfig  implements  WebMvcConfigurer  {    @Override      public  void  addInterceptors (InterceptorRegistry registry)  {         registry.addInterceptor(new  LoginInterceptor ())                 .addPathPatterns("/**" )                 .excludePathPatterns("/login" );         registry.addInterceptor(new  RequestLimitInterceptor ());     } } 
 
使用请求拦截器 在 Controller 类或方法上使用请求限制注解。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 @RequestLimit(count = 20, second = 60, needLogin = false) @RestController @RequestMapping("/user") public  class  UserController  {    @RequestLimit(needLogin = true, count = 10, second = 1)      @RequestMapping("/getUser/{id}")      public  ResultBean<User> getUser (@PathVariable  Integer id) {         User  user  =  new  User ("张飞" , 1 , "深圳" , "13822223333" , LocalDate.now());         user.setId(id);         return  ResultHelper.success(user);     }     @RequestMapping("/getUser")      public  ResultBean<User> getUser (User user) {         user.setUsername("刘备" ).setSex(1 ).setAddress("湖北" ).setPhone("13020002000" ).setBirthday(LocalDate.now());         return  ResultHelper.success(user);     } } 
 
AOP 切面方式 AOP 注解方式的切入点的表达式有多种写法,可以指定包或类路径,指定注解路径,指定注解参数。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 @Pointcut("within(@org.springframework.web.bind.annotation.RestController *) && @annotation(requestLimit)") public  void  pointcut (RequestLimit requestLimit)  {} @Pointcut("@annotation(requestLimit)") public  void  pointcut (RequestLimit requestLimit)  {} @Pointcut("execution(* com.springboot.demo.controller.*.*(..))") public  void  pointcut ()  {} 
 
请求限制注解 定义请求限制注解,同上面拦截器中的请求限制注解一致。
AOP 切面方式一 定义 AOP ,在执行请求前处理,在类和方法上都有效,方法优先于类(方法粒度更细)。
下面的方式只对请求限制注解作用在 Controller 类和方法上都有效。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 @Aspect @Component public  class  RequestLimitAspectC  {    private  static  final  Logger  logger  =  LogManager.getLogger(RequestLimitAspectC.class);     @Autowired      private  RedisTemplate<String, Object> redisTemplate;          @Pointcut("within(@com.springboot.demo.common.annotation.RequestLimit *)")      public  void  pointcut ()  {     }          @Before("pointcut()")      public  void  around (JoinPoint joinPoint)  throws  IOException, NoSuchMethodException {                  Object  target  =  joinPoint.getTarget();                  Signature  signature  =  joinPoint.getSignature();                  RequestAttributes  requestAttributes  =  RequestContextHolder.getRequestAttributes();                  HttpServletRequest  request  =  (HttpServletRequest) requestAttributes.resolveReference(RequestAttributes.REFERENCE_REQUEST);                  HttpServletResponse  response  =  ((ServletRequestAttributes) requestAttributes).getResponse();                  String  sessionId  =  requestAttributes.getSessionId();                  String  ip  =  IPUtil.getIpFromRequest(request);         MethodSignature  methodSignature  =  (MethodSignature) signature;         Method  method  =  methodSignature.getMethod();         RequestLimit  methodReqLimit  =  method.getAnnotation(RequestLimit.class);         RequestLimit  classReqLimit  =  target.getClass().getAnnotation(RequestLimit.class);         RequestLimit  requestLimit  =  (null  != methodReqLimit ? methodReqLimit : classReqLimit);         int  maxCount  =  requestLimit.count();         int  second  =  requestLimit.second();         String  key  = sessionId + ":"  + request.getRequestURI() + ":"  + ip;         if  (requestLimit.needLogin()) {             Account  account  =  (Account) request.getSession().getAttribute("auth_user" );             if  (null  == account) {                 throw  new  RuntimeException ("访问必须先登录" );             }             key = key + ":"  + account.getId().toString();         }         key = "RequestLimit:"  + DigestUtils.md5DigestAsHex(key.getBytes(Charset.defaultCharset()));         Integer  count  =  (Integer) redisTemplate.opsForValue().get(key);         if  (null  == count) {                          redisTemplate.opsForValue().setIfAbsent(key, 1 , second, TimeUnit.SECONDS);         } else  if  (count < maxCount) {                          redisTemplate.opsForValue().set(key, count + 1 );         } else  {             logger.info("接口请求次数超限制:{}" , key);             response.setCharacterEncoding("UTF-8" );             response.setContentType("application/json;charset=UTF-8" );             ServletOutputStream  output  =  response.getOutputStream();             output.write("{\"msg\":\"请求次数超出限制\"}" .getBytes("UTF-8" ));             output.flush();             output.close();         }     } } 
 
AOP 切面方式二 定义 AOP,在执行请求前处理,只在方法上有效。
下面的方式只对方法 上的注解有效,如果请求限制注解作用在类上则无效。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 @Aspect @Component public  class  RequestLimitAspect  {    private  static  final  Logger  logger  =  LogManager.getLogger(RequestLimitAspect.class);     @Autowired      private  RedisTemplate<String, Object> redisTemplate;          @Pointcut("within(@org.springframework.web.bind.annotation.RestController *) && @annotation(requestLimit)")      public  void  pointcut (RequestLimit requestLimit)  {     }          @Before("pointcut(requestLimit)")      public  void  around (JoinPoint joinPoint, RequestLimit requestLimit)  throws  IOException {                  RequestAttributes  requestAttributes  =  RequestContextHolder.getRequestAttributes();                  HttpServletRequest  request  =  ((ServletRequestAttributes) requestAttributes).getRequest();                  HttpServletResponse  response  =  ((ServletRequestAttributes) requestAttributes).getResponse();         int  maxCount  =  requestLimit.count();         int  second  =  requestLimit.second();         Account  account  =  null ;         String  ip  =  IPUtil.getIpFromRequest(request);         HttpSession  session  =  request.getSession();         String  key  =  session.getId() + ":"  + request.getRequestURI() + ":"  + ip;         if  (requestLimit.needLogin()) {             account = (Account) session.getAttribute("auth_user" );             if  (null  == account) {                 throw  new  RuntimeException ("访问必须先登录" );             }             key = key + ":"  + account.getId().toString();         }         key = "RequestLimit:"  + DigestUtils.md5DigestAsHex(key.getBytes(Charset.defaultCharset()));         Integer  count  =  (Integer) redisTemplate.opsForValue().get(key);         if  (null  == count) {                          redisTemplate.opsForValue().setIfAbsent(key, 1 , second, TimeUnit.SECONDS);         } else  if  (count < maxCount) {                          redisTemplate.opsForValue().set(key, count + 1 );         } else  {             logger.info("接口请求次数超限制:{}" , key);             response.setCharacterEncoding("UTF-8" );             response.setContentType("application/json;charset=UTF-8" );             ServletOutputStream  output  =  response.getOutputStream();             output.write("{\"msg\":\"请求次数超出限制\"}" .getBytes("UTF-8" ));             output.flush();             output.close();         }     } } 
 
结合防爬虫防刷 拦截器实现 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 @Slf4j public  class  IPBlockInterceptor  implements  HandlerInterceptor  {         private  static  final  long  TIME  =  10 ;     private  static  final  long  CNT  =  50 ;     private  Object  lock  =  new  Object ();          private  static  final  String  USERAGENT  =  "User-Agent" ;     private  static  final  String  CRAWLER  =  "crawler" ;     @Autowired      private  RedisHelper<Integer> redisHelper;     @Override      public  boolean  preHandle (HttpServletRequest request, HttpServletResponse response, Object handler)  throws  Exception {         synchronized  (lock) {                          boolean  checkAgent  =  checkAgent(request);                          boolean  checkIP  =  checkIP(request, response);             return  checkAgent && checkIP;         }     }     private  boolean  checkAgent (HttpServletRequest request)  {         String  header  =  request.getHeader(USERAGENT);         if  (StringUtils.isEmpty(header)) {             return  false ;         }         if  (header.contains(CRAWLER)) {             log.error("请求头有问题,拦截 ==> User-Agent = {}" , header);             return  false ;         }         return  true ;     }     private  boolean  checkIP (HttpServletRequest request, HttpServletResponse response)  throws  Exception {         String  ip  =  IPUtils.getClientIp(request);         String  url  =  request.getRequestURL().toString();         String  param  =  getAllParam(request);         boolean  isExist  =  redisHelper.isExist(ip);         if  (isExist) {                          int  cnt  =  redisHelper.incr(ip);             if  (cnt > IPBlockInterceptor.CNT) {                 OscResult<String> result = new  OscResult <>();                 response.setCharacterEncoding("UTF-8" );                 response.setHeader("content-type" , "application/json;charset=UTF-8" );                 result = result.fail(OscResultEnum.LIMIT_EXCEPTION);                 response.getWriter().print(JSON.toJSONString(result));                 log.error("ip = {}, 请求过快,被限制" , ip);                                  redisHelper.set(ip, --cnt);                 return  false ;             }             log.info("ip = {}, {}s之内第{}次请求{},参数为{},通过" , ip, TIME, cnt, url, param);         } else  {                          redisHelper.setEx(ip, IPBlockInterceptor.TIME, 1 );             log.info("ip = {}, {}s之内第1次请求{},参数为{},通过" , ip, TIME, url, param);         }         return  true ;     }     private  String getAllParam (HttpServletRequest request)  {         Map<String, String[]> map = request.getParameterMap();         StringBuilder  sb  =  new  StringBuilder ("[" );         map.forEach((x, y) -> {             String  s  =  StringUtils.join(y, "," );             sb.append(x + " = "  + s + ";" );         });         sb.append("]" );         return  sb.toString();     }     @Override      public  void  postHandle (HttpServletRequest request, HttpServletResponse response, Object handler, @Nullable  ModelAndView modelAndView)  throws  Exception {     }     @Override      public  void  afterCompletion (HttpServletRequest request, HttpServletResponse response, Object handler, @Nullable  Exception ex)  throws  Exception {     } } 
 
访问统计 使用 Shell 脚本实现访问统计
1 2 3 4 5 6 7 8 9 10 11 # !/bin/bash  #  复制日志到当前目录 cp /home/tomcat/apache-tomcat-8.5.23/workspace/osc/osc.log /home/shell/java/osc.log  #  将日志中的ip点号如: 120.74.147.123 换为 120:74:147:123 sed -i "s/\./:/g" osc.log #  筛选出只包含ip的行,并且只打印ip出来 awk '/limit/ {print $11}' osc.log > temp.txt #  根据ip的所有位数进行排序 并且统计次数 最后输出前50行 cat temp.txt | sort -t ':' -k1n -k2n -k3n -k4n | uniq -c | sort -nr | head -n 50 > result.txt #  删除无关紧要文件 rm -rf temp.txt osc.log 
 
摘自 如何识别恶意请求,进行反爬虫操作? 
其它参考 
Spring(二):Spring AOP 理解与应用  
Spring Boot 项目 API 防刷