目的

让用户访问某个接口时,限制一定时间只能访问N次

下面介绍后端的限制方法:

原理

服务器通过redis记录请求的次数,如果次数超过限制就不给访问。

设置redis 的key的时效性,过期自动销毁

自定义注解

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
/**
* 请求限制的自定义注解
*
* @Target 注解可修饰的对象范围,ElementType.METHOD 作用于方法,ElementType.TYPE 作用于类
* (ElementType)取值有:
*     1.CONSTRUCTOR:用于描述构造器
*     2.FIELD:用于描述域
*     3.LOCAL_VARIABLE:用于描述局部变量
*     4.METHOD:用于描述方法
*     5.PACKAGE:用于描述包
*     6.PARAMETER:用于描述参数
*     7.TYPE:用于描述类、接口(包括注解类型) 或enum声明
* @Retention定义了该Annotation被保留的时间长短:某些Annotation仅出现在源代码中,而被编译器丢弃;
* 而另一些却被编译在class文件中;编译在class文件中的Annotation可能会被虚拟机忽略,
* 而另一些在class被装载时将被读取(请注意并不影响class的执行,因为Annotation与class在使用上是被分离的)。
* 使用这个meta-Annotation可以对 Annotation的“生命周期”限制。
* (RetentionPoicy)取值有:
*     1.SOURCE:在源文件中有效(即源文件保留)
*     2.CLASS:在class文件中有效(即class保留)
*     3.RUNTIME:在运行时有效(即运行时保留)
*
* @Inherited
* 元注解是一个标记注解,@Inherited阐述了某个被标注的类型是被继承的。
* 如果一个使用了@Inherited修饰的annotation类型被用于一个class,则这个annotation将被用于该class的子类。
*/
@Documented
@Inherited
@Retention(RetentionPolicy.RUNTIME)
@Target({ElementType.METHOD,ElementType.TYPE})
public @interface AccessLimit {

// 失效时间 单位(秒)
int seconds();

// 最大请求次数
int maxCount();

}

配置拦截器

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
public class LimitInterceptor implements HandlerInterceptor {

private Logger logger = LoggerFactory.getLogger(LimitInterceptor.class);

@Autowired
private RedisCache redisCache;

@Override
public boolean preHandle(HttpServletRequest request, HttpServletResponse response, Object handler) throws Exception {
//判断请求是否属于方法的请求
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(); // 最大请求次数
String key = request.getServletPath()+request.getSession().getId();

Integer count = 0;
//从redis中获取用户访问的次数
if (redisCache.getCacheObject(key) != null) {
count = (Integer) redisCache.getCacheObject(key);
}

if (count == 0) {
//第一次访问
redisCache.setCacheObject(key, 1);
redisCache.expire(key, seconds, TimeUnit.SECONDS); //置缓存失效时间(单位:秒)
logger.info("{}第一次访问",key);
} else if (count < maxCount) {
//加1
redisCache.incr(key, 1);
} else {
//超出访问次数
logger.error("超出访问次数");
render(response, "请求次数过于频繁!");
return false;
}
}
return true;
}

/**
* 封装返回值
*
* @param response
* @param msg
* @throws Exception
*/
private void render(HttpServletResponse response, String msg) throws Exception {
response.setContentType("application/json;charset=utf-8");
PrintWriter out = response.getWriter();
out.print(new ObjectMapper().writeValueAsString(RespBean.error(msg)));
out.flush();
out.close();
}


}

注册拦截器

1
2
3
4
5
6
7
8
9
10
11
12
13
@Configuration
public class WebConfig implements WebMvcConfigurer {
//注意使用 bean 注解添加
@Bean
public LimitInterceptor limitHandlerInterceptor(){
return new LimitInterceptor();
}

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

在Controller的接口加上注解

1
2
3
4
5
6
7
8
9
@PostMapping("/")
//表示用户想重复访问该接口必须间隔6s及以上
@AccessLimit(seconds = 6,maxCount = 1)
public RespBean addEmp(@RequestBody Employee employee) {
if (employeeService.addEmp(employee)) {
return RespBean.ok("添加成功!");
}
return RespBean.error("添加失败!");
}