教你如何实现接口防刷
前言
我们在浏览网站后台的时候,假如我们频繁请求,那么网站会提示 “请勿重复提交” 的字样,那么这个功能究竟有什么用呢,又是如何实现的呢?
其实这就是接口防刷的一种处理方式,通过在一定时间内限制同一用户对同一个接口的请求次数,其目的是为了防止恶意访问导致服务器和数据库的压力增大,也可以防止用户重复提交。
思路分析
接口防刷有很多种实现思路,例如:拦截器/AOP+Redis、拦截器/AOP+本地缓存、前端限制等等很多种实现思路,在这里我们来讲一下 拦截器+Redis 的实现方式。
其原理就是 在接口请求前由拦截器拦截下来,然后去 redis 中查询是否已经存在请求了,如果不存在则将请求缓存,若已经存在则返回异常。具体可以参考下图
![未命名文件.png](data:image/gif;base64,R0lGODlhAQABAIAAAAAAAP///yH5BAEAAAAALAAAAAABAAEAAAIBRAA7)
具体实现
注:以下代码中的 AjaxResult
为统一返回对象,这里就不贴出代码了,大家可以根据自己的业务场景来编写。
编写 RedisUtils
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 94 95 96 97 98 99 100 101 102 103 104 105 106 107 108 109 110 111 112 113 114 115 116 117 118 119 120 121 122 123 124 125 126 127 128 129 130 131 132 133 134 135 136 137 138 139 140 141 142 143 144 145 146 147 148 149 150 151 152 153 154 155 156 157 158 159 160 161 162 163 164
| import com.apply.core.exception.MyRedidsException; import org.springframework.beans.factory.annotation.Autowired; import org.springframework.data.redis.core.RedisTemplate; import org.springframework.stereotype.Component; import org.springframework.util.CollectionUtils;
import java.util.Collections; import java.util.List; import java.util.Map; import java.util.Set; import java.util.concurrent.TimeUnit;
@Component public class RedisUtils {
@Autowired private RedisTemplate<String, Object> redisTemplate;
public boolean expire(String key, long time) { try { if (time > 0) { redisTemplate.expire(key, time, TimeUnit.SECONDS); } return true; } catch (Exception e) { e.printStackTrace(); return false; } }
public long getExpire(String key) { return redisTemplate.getExpire(key, TimeUnit.SECONDS); }
public boolean hasKey(String key) { try { return redisTemplate.hasKey(key); } catch (Exception e) { e.printStackTrace(); return false; } }
@SuppressWarnings("unchecked") public void del(String... key) { if (key != null && key.length > 0) { if (key.length == 1) { redisTemplate.delete(key[0]); } else { redisTemplate.delete(CollectionUtils.arrayToList(key)); } } }
public Object get(String key) { return key == null ? null : redisTemplate.opsForValue().get(key); }
public boolean set(String key, Object value) { try { redisTemplate.opsForValue().set(key, value); return true; } catch (Exception e) { e.printStackTrace(); return false; } }
public boolean set(String key, Object value, long time) { try { if (time > 0) { redisTemplate.opsForValue().set(key, value, time, TimeUnit.SECONDS); } else { set(key, value); } return true; } catch (Exception e) { e.printStackTrace(); return false; } }
public long incr(String key, long delta) { if (delta < 0) { throw new MyRedidsException("递增因子必须大于0"); } return redisTemplate.opsForValue().increment(key, delta); }
public long decr(String key, long delta) { if (delta < 0) { throw new MyRedidsException("递减因子必须大于0"); } return redisTemplate.opsForValue().increment(key, -delta); } }
|
定义Interceptor
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
| import com.alibaba.fastjson.JSON; import com.apply.common.utils.redis.RedisUtils; import com.apply.common.validator.annotation.AccessLimit; import com.apply.core.http.AjaxResult; import org.springframework.beans.factory.annotation.Autowired; import org.springframework.stereotype.Component; import org.springframework.web.method.HandlerMethod; import org.springframework.web.servlet.handler.HandlerInterceptorAdapter;
import javax.servlet.ServletOutputStream; import javax.servlet.http.HttpServletRequest; import javax.servlet.http.HttpServletResponse; import java.io.IOException; import java.util.Objects;
@Component public class RepeatRequestIntercept extends HandlerInterceptorAdapter {
@Autowired private RedisUtils redisUtils;
private final int seconds = 1;
private final int max = 1;
public boolean preHandle(HttpServletRequest request, HttpServletResponse response, Object handler) throws Exception { if (handler instanceof HandlerMethod) { String key = request.getRemoteAddr() + "-" + request.getMethod() + "-" + request.getRequestURL(); Object requestCountObj = redisUtils.get(key); if (Objects.isNull(requestCountObj)) { redisUtils.set(key, 1, seconds); } else { response.setContentType("application/json;charset=utf-8"); ServletOutputStream os = response.getOutputStream(); AjaxResult<Void> result = AjaxResult.error(100, "请求已提交,请勿重复请求"); String jsonString = JSON.toJSONString(result); os.write(jsonString.getBytes()); os.flush(); os.close(); return false; } } return 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
| import com.apply.common.validator.intercept.RepeatRequestIntercept; import com.apply.core.base.entity.Constants; import org.springframework.beans.factory.annotation.Autowired; import org.springframework.context.annotation.Configuration; import org.springframework.web.servlet.config.annotation.CorsRegistry; import org.springframework.web.servlet.config.annotation.InterceptorRegistry; import org.springframework.web.servlet.config.annotation.ResourceHandlerRegistry; import org.springframework.web.servlet.config.annotation.WebMvcConfigurer;
@Configuration public class WebConfig implements WebMvcConfigurer {
@Autowired private RepeatRequestIntercept repeatRequestIntercept;
@Override public void addInterceptors(InterceptorRegistry registry) { registry.addInterceptor(repeatRequestIntercept); } }
|
我们再来编写一个接口用于测试
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19
| import com.apply.common.validator.annotation.AccessLimit; import org.springframework.web.bind.annotation.GetMapping; import org.springframework.web.bind.annotation.RequestMapping; import org.springframework.web.bind.annotation.RestController;
@RestController public class TestController {
@GetMapping("/test") public String test(){ return "SUCCESS"; }
}
|
最后我们来看一下结果是否符合我们的预期:
1秒内的第一次请求:
![image.png](data:image/gif;base64,R0lGODlhAQABAIAAAAAAAP///yH5BAEAAAAALAAAAAABAAEAAAIBRAA7)
1秒内的第二次请求:
![image.png](data:image/gif;base64,R0lGODlhAQABAIAAAAAAAP///yH5BAEAAAAALAAAAAABAAEAAAIBRAA7)
确实已经达到了我们的预期,但是如果我们对特定接口进行拦截,或对不同接口的限定拦截时间和次数不同的话,这种实现方式无法满足我们的需求,所以我们要提出改进。
改进
我们可以去写一个自定义的注解,并将 seconds
和 max
设置为该注解的属性,再在拦截器中判断请求的方法是否包含该注解,如果包含则执行拦截方法,如果不包含则直接返回。
自定义注解 RequestLimit
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
| import java.lang.annotation.ElementType; import java.lang.annotation.Retention; import java.lang.annotation.RetentionPolicy; import java.lang.annotation.Target;
@Retention(RetentionPolicy.RUNTIME) @Target(ElementType.METHOD) public @interface RequestLimit {
int seconds() default 1;
int max() default 1;
}
|
改进 RepeatRequestIntercept
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
|
@Component public class RepeatRequestIntercept extends HandlerInterceptorAdapter {
@Autowired private RedisUtils redisUtils;
@Override public boolean preHandle(HttpServletRequest request, HttpServletResponse response, Object handler) throws Exception { if (handler instanceof HandlerMethod) { HandlerMethod hm = (HandlerMethod) handler; RequestLimit anno = hm.getMethodAnnotation(RequestLimit.class); if (Objects.isNull(anno)) { return true; } int seconds = anno.seconds(); int max = anno.max(); String key = request.getRemoteAddr() + "-" + request.getMethod() + "-" + request.getRequestURL(); Object requestCountObj = redisUtils.get(key); if (Objects.isNull(requestCountObj)) { redisUtils.set(key, 1, seconds); } else { int requestCount = Integer.parseInt(requestCountObj.toString()); if (requestCount < max) { redisUtils.incr(key, 1); } else { refuse(response); return false; } } } return true; }
private void refuse(HttpServletResponse response) throws IOException { response.setContentType("application/json;charset=utf-8"); ServletOutputStream os = response.getOutputStream(); AjaxResult<Void> result = AjaxResult.error(100, "请求已提交,请勿重复请求"); String jsonString = JSON.toJSONString(result); os.write(jsonString.getBytes()); os.flush(); os.close(); }
}
|
这样我们就可以实现我们的需求了。