1. 概述
接口防刷,顾名思义:就是想让某个接口某个人在某段时间内只能请求N次。
在项目中比较常见的问题也有,比如Web端表单重复提交,可以用token来解决。
具体接口设计可以查看之前的博客【接口设计看这一篇就够了】
2. 原理
在请求的时候,服务器通过redis记录请求的次数,如果次数超过限制就不给访问
3. 代码实现
3.1 引入依赖
1 2 3 4 5 6 7 8 9
| <dependency> <groupId>org.springframework.boot</groupId> <artifactId>spring-boot-starter-web</artifactId> </dependency> <dependency> <groupId>org.springframework.data</groupId> <artifactId>spring-data-redis</artifactId> <scope>compile</scope> </dependency>
|
3.2 添加配置
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17
| spring: application: name: request-limit-demo redis: password: xiaoyuge database: 0 port: 6379 host: localhost lettuce: pool: max-idle: 10 min-idle: 0 max-active: 10 max-wait: -1ms timeout: 10000ms server: port: 8080
|
3.3 自定义注解
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
|
@Documented @Inherited @Target({ElementType.METHOD,ElementType.TYPE}) @Retention(RetentionPolicy.RUNTIME) public @interface RequestLimit { int second() default 1; int maxCount() default 1; }
|
3.4 定义拦截器
定义一个拦截器,请求之前,对请求次数校验
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
| import com.alibaba.fastjson.JSONObject; import lombok.extern.slf4j.Slf4j; 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 top.lrshuai.limit.annotation.RequestLimit; import top.lrshuai.limit.common.ApiResultEnum; import top.lrshuai.limit.common.Result; import javax.servlet.http.HttpServletRequest; import javax.servlet.http.HttpServletResponse; import java.io.IOException; import java.io.PrintWriter; import java.lang.reflect.Method; import java.util.concurrent.TimeUnit;
@Slf4j @Component public class RequestLimitIntercept extends HandlerInterceptorAdapter {
@Autowired private RedisTemplate<String, Object> redisTemplate;
@Override public boolean preHandle(HttpServletRequest request, HttpServletResponse response, Object handler) throws Exception {
if (handler.getClass().isAssignableFrom(HandlerMethod.class)) { HandlerMethod handlerMethod = (HandlerMethod) handler; Method method = handlerMethod.getMethod(); RequestLimit methodAnnotation = method.getAnnotation(RequestLimit.class); RequestLimit classAnnotation = method.getDeclaringClass().getAnnotation(RequestLimit.class); RequestLimit requestLimit = methodAnnotation != null ? methodAnnotation : classAnnotation; if (requestLimit != null) { if (isLimit(request, requestLimit)) { responseOut(response, ApiResultEnum.REQUST_LIMIT); return false; } } } return super.preHandle(request, response, handler); }
public boolean isLimit(HttpServletRequest request, RequestLimit requestLimit) { String limitKey = request.getServletPath() + request.getSession().getId(); Integer redisCount = (Integer) redisTemplate.opsForValue().get(limitKey); if (redisCount == null) { redisTemplate.opsForValue().set(limitKey, 1, requestLimit.second(), TimeUnit.SECONDS); } else { if (redisCount >= requestLimit.maxCount()) { return true; } redisTemplate.opsForValue().increment(limitKey); } return false; }
private void responseOut(HttpServletResponse response, ApiResultEnum result) throws IOException { response.setCharacterEncoding("UTF-8"); response.setContentType("application/json; charset=utf-8"); PrintWriter out = null; String json = result.toString(); out = response.getWriter(); out.append(json); } }
|
3.5 注册拦截器
1 2 3 4 5 6 7 8 9 10 11 12 13
| @Slf4j @Component public class WebMvcConfig implements WebMvcConfigurer {
@Autowired private RequestLimitIntercept requestLimitIntercept;
@Override public void addInterceptors(InterceptorRegistry registry) { log.info("添加拦截"); registry.addInterceptor(requestLimitIntercept); } }
|
3.6 Redis配置
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
| @Configuration @EnableCaching public class RedisConfig extends CachingConfigurerSupport { @Bean @SuppressWarnings(value = {"unchecked", "rawtypes"}) public RedisTemplate<Object, Object> redisTemplate(RedisConnectionFactory connectionFactory) { RedisTemplate<Object, Object> template = new RedisTemplate<>(); template.setConnectionFactory(connectionFactory);
FastJson2JsonRedisSerializer serializer = new FastJson2JsonRedisSerializer(Object.class);
template.setKeySerializer(new StringRedisSerializer()); template.setValueSerializer(serializer);
template.setHashKeySerializer(new StringRedisSerializer()); template.setHashValueSerializer(serializer);
template.afterPropertiesSet(); return template; }
@Bean public DefaultRedisScript<Long> limitScript() { DefaultRedisScript<Long> redisScript = new DefaultRedisScript<>(); redisScript.setScriptText(limitScriptText()); redisScript.setResultType(Long.class); return redisScript; }
private String limitScriptText() { return "local key = KEYS[1]\n" + "local count = tonumber(ARGV[1])\n" + "local time = tonumber(ARGV[2])\n" + "local current = redis.call('get', key);\n" + "if current and tonumber(current) > count then\n" + " return tonumber(current);\n" + "end\n" + "current = redis.call('incr', key)\n" + "if tonumber(current) == 1 then\n" + " redis.call('expire', key, time)\n" + "end\n" + "return tonumber(current);"; } }
|
3.7 Controller层测试
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20
| @RestController @RequestMapping @RequestLimit(maxCount = 5, second = 5) public class TestController {
@GetMapping("/test1") @RequestLimit public String test1() { return "test1"; }
@GetMapping("/test2") public String test2() { return "test2"; } }
|
参考博客SpringBoot API 接口防刷