思路

  • 通过Redis进行限制。
  1. 用户在访问接口的时候,获取用户的ip:$request -> ip();
  2. 把ip作为key,访问次数作为对应的值,每次访问接口累加访问次数即可。
    • key的有效期是1分钟必须是第一次写入的时候 指定1分钟的有效期
  3. 下一次访问的时候,判断是否超过100次,如果超过的,吧ip加入黑名单中半个小时。
    • 用的数据类型是有序集合
    • 用户再访问的时候,判断他的ip是否在黑名单中,如果在黑名单,就不允许访问
    • 需要注意,我们只是吧这个ip放入黑名单半个小时。如果超过半个小时 需要从黑名单移除这个ip

存在的问题

  • 一分钟统计ip访问次数,可能会存在的问题?

    • 可能在有效期内,不超过访问的次数限制,但是接近这个值。
    • 比如说我们在12:00:01的时候,访问1次,在12:00:59访问了98次,这样的话是没有到100次的,在12:00统计的数据,在12:01之前就过期了。
    • 继续在12:01:01在去访问99次,也没有达到限制的100次。这样的话,在12:00:59到12:01:01这三秒钟超过了100次。这样的话 限制就不存在意义了。
  • 如何解决按分钟统计出现的问题?

    • 分开统计访问次数:

      1. 把一分钟分为6段,每段分别统计访问次数
      2. 每次再算访问次数的时候,统计出当前的时间端的访问次数
      3. 在向前去前5段时间的访问次数,把这6段时间加起来
      4. 判断是否超过我们的限制,如果超过加入黑名单
    • 令牌桶

      1. 在redis中设置一个集合,集合中存在10个元素
      2. 客户端每次调用接口的时候,从集合中取出一个值
      3. 如果能够正常去到值,说明正常,允许访问
      4. 如果没有取到,说明限制令牌桶是空的,则不允许访问
      5. 每隔10s中给集合中添加10个元素 【 不是每次都写10个元素,而是把集合补够10个数字即可 】

其他思路

  1. 网关控制流量洪峰,对在一个时间段内出现流量异常,可以拒绝请求。
  2. ip请求个数限制。对请求来源的ip请求个数做限制。
  3. http请求头信息校验;(例如hostUser-AgentReferer)。
  4. 对用户唯一身份uid进行限制和校验。例如基本的长度,组合方式,甚至有效性进行判断。或者uid具有一定的时效性。
  5. 前后端协议采用二进制方式进行交互或者协议采用签名机制。
  6. 人机验证,验证码,短信验证码,滑动图片形式,12306形式。

实现方案

  • 首先是写一个注解类
  • 拦截器中实现
  • 注册到springboot中
  • 在Controller中加入注解

注解类

  • 代码如下

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    17
    18
    19
    20
    21
    22
    package tk.fulsun.annotation;

    import java.lang.annotation.ElementType;
    import java.lang.annotation.Retention;
    import java.lang.annotation.RetentionPolicy;
    import java.lang.annotation.Target;

    /**
    * @author fulsun
    * @description: 访问限制注解类
    * @date 6/9/2021 2:25 PM
    */
    @Retention(RetentionPolicy.RUNTIME)
    @Target(ElementType.METHOD)
    public @interface AccessLimit {
    int seconds();

    int maxCount();

    boolean needLogin() default true;
    }

拦截器中实现

  • 代码如下:

    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
    86
    87
    88
    89
    90
    91
    92
    93
    package tk.fulsun.config;

    import com.alibaba.fastjson.JSON;
    import java.io.OutputStream;
    import java.util.HashMap;
    import java.util.Map;
    import java.util.concurrent.Semaphore;
    import javax.servlet.http.HttpServletRequest;
    import javax.servlet.http.HttpServletResponse;
    import org.springframework.beans.factory.annotation.Autowired;
    import org.springframework.data.redis.core.RedisTemplate;
    import org.springframework.stereotype.Component;
    import org.springframework.web.method.HandlerMethod;
    import org.springframework.web.servlet.handler.HandlerInterceptorAdapter;
    import tk.fulsun.annotation.AccessLimit;
    import tk.fulsun.common.CodeMsg;
    import tk.fulsun.utils.RedisUtil;

    /**
    * @author fulsun
    * @description: 访问的拦截器
    * @date 6/9/2021 2:28 PM
    */
    @Component
    public class ApiAccessInterceptor extends HandlerInterceptorAdapter {
    @Autowired private RedisTemplate redisTemplate;
    @Autowired private RedisUtil redisUtil;
    private final Semaphore permit = new Semaphore(10, true);

    @Override
    public boolean preHandle(HttpServletRequest request, HttpServletResponse response, Object handler)
    throws Exception {
    redisUtil.setRedisTemplate(redisTemplate);
    // 判断请求是否属于方法的请求
    if (handler instanceof HandlerMethod) {
    HandlerMethod hm = (HandlerMethod) handler;
    // 获取方法中的注解,看是否有该注解
    AccessLimit accessLimit = hm.getMethodAnnotation(AccessLimit.class);
    if (accessLimit == null) {
    return true;
    }
    int seconds = accessLimit.seconds();
    int maxCount = accessLimit.maxCount();
    boolean login = accessLimit.needLogin();
    String key = request.getRequestURI();
    // 如果需要登录
    if (login) {
    // 获取登录的session进行判断
    // 这里假设用户id是1,项目中是动态获取的userId
    key += "" + "1";
    }

    // 从redis中获取用户访问的次数
    try {
    // 控制并发数量为10
    permit.acquire();
    Integer count = (Integer) redisUtil.get(key);
    System.out.println("当前次数 " + count);
    if (count == null) {
    // 第一次访问
    redisUtil.set(key, 1, seconds);
    } else if (count < maxCount) {
    // +1
    redisUtil.incr(key, 1);
    } else {
    // 超出访问次数, 这里的CodeMsg是一个返回参数
    render(response, CodeMsg.ACCESS_LIMIT_REACHED);
    return false;
    }
    } catch (InterruptedException e) {
    e.printStackTrace();
    } finally {
    permit.release();
    }
    }
    return true;
    }

    private void render(HttpServletResponse response, CodeMsg cm) throws Exception {
    response.setContentType("application/json;charset=UTF-8");
    response.setStatus(cm.getCode());
    OutputStream out = response.getOutputStream();
    Map result = new HashMap<>(3);
    result.put("status", "1");
    result.put("message", "");
    result.put("error", cm.getMessage());
    String str = JSON.toJSONString(result);
    out.write(str.getBytes("UTF-8"));
    out.flush();
    out.close();
    }
    }

注册到springboot中

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
package tk.fulsun.config;

import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.context.annotation.Configuration;
import org.springframework.web.servlet.config.annotation.InterceptorRegistry;
import org.springframework.web.servlet.config.annotation.WebMvcConfigurer;

/**
* @author fulsun
* @description: 自定义web的配置
* @date 6/9/2021 3:17 PM
*/
@Configuration
public class WebConfig implements WebMvcConfigurer {
@Autowired private ApiAccessInterceptor apiAccessInterceptor;

@Override
public void addInterceptors(InterceptorRegistry registry) {
registry.addInterceptor(apiAccessInterceptor);
}
}

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
package tk.fulsun.controller;

import org.springframework.web.bind.annotation.GetMapping;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.RestController;
import tk.fulsun.annotation.AccessLimit;

/**
* @author fulsun
* @description: API接口
* @date 6/9/2021 3:19 PM
*/
@RestController
public class FangshuaController {
@GetMapping("/h1")
public String hello() {
return "hello world";
}

@AccessLimit(seconds = 5, maxCount = 10, needLogin = true)
@RequestMapping("/h2")
public String hello2() {
return "请求成功";
}
}

测试结果

  • 使用jmeter,测试同时启动1000的并发在1秒访问 /h2 接口,得到的测试结果如下