- 实现思路
- 在拦截器Interceptor中拦截请求
- 通过地址+请求uri作为调用者访问接口的区分在Redis中进行计数达到限流目的
- 简单实现
- 定义参数
- #接口防刷配置,时间单位都是秒. 如果second秒内访问次数达到times,就禁用lockTime秒
- access:
- limit:
- second: 10 #一段时间内
- times: 3 #最大访问次数
- lockTime: 5 #禁用时长
复制代码 - 代码实现
- 定义拦截器:实现HandlerInterceptor接口,重写preHandle()方法
- @Slf4j
- @Component
- public class AccessLimintInterceptor implements HandlerInterceptor {
-
- @Resource
- private RedisTemplate redisTemplate;
- //锁住时的key前缀
- private static final String LOCK_PREFIX = "LOCK";
- //统计次数的key前缀
- private static final String COUNT_PREFIX = "COUNT";
- //访问周期
- @Value("${access.limit.second}")
- private long second;
- //访问周期内最大访问次数
- @Value("${access.limit.times}")
- private int times;
- //禁用时长
- @Value("${access.limit.lockTime}")
- private long lockTime;
-
- @Override
- public boolean preHandle(HttpServletRequest request, HttpServletResponse response, Object handler) throws Exception {
- return true;
- }
复制代码 - 注册拦截器:配置类实现WebMvcConfigurer接口,重写addInterceptors()方法
- @Configuration
- public class WebConfig implements WebMvcConfigurer {
- @Resource
- private AccessLimintInterceptor accessLimintInterceptor;
- //在这个方法中注册拦截器
- @Override
- public void addInterceptors(InterceptorRegistry registry) {
- //注册拦截器
- InterceptorRegistration interceptorRegistration = registry.addInterceptor(accessLimintInterceptor);
- //配置要拦截的路径。优化为实现自定义注解,那就拦截所有路径,在拦截器中判断是否使用了注解,没使用就放行
- // interceptorRegistration.addPathPatterns("/search/**");
- interceptorRegistration.addPathPatterns("/**");
- WebMvcConfigurer.super.addInterceptors(registry);
- }
- }
复制代码 - 自定义异常,方便错误提示。
- /*
- * @Description TODO (自定义访问限制异常,防刷)
- * 创建人: 程长新
- * 创建时间:2023/11/12 8:46
- **/
- public class AccessLimitException extends RuntimeException{
- public AccessLimitException() {
- }
- public AccessLimitException(Throwable e) {
- super(e.getMessage(),e);
- }
- public AccessLimitException(String message) {
- super(message);
- }
- }
复制代码 添加全局异常捕捉- /*
- * @Description TODO (全局异常处理)
- * 创建人: 程长新
- * 创建时间:2023/11/7 9:54
- **/
- @RestControllerAdvice
- public class AdviceController {
- @ExceptionHandler(Exception.class)
- public String exceptionHandler(HttpServletRequest request,
- HttpServletResponse response,
- Exception e){
- return e.getMessage();
- }
- @ExceptionHandler(AccessLimitException.class)
- public String exceptionHandler(AccessLimitException e){
- return "访问次数过多,请稍候再试";
- }
- }
复制代码 - 处理逻辑
- /** 不使用自定义注解时的逻辑
- *获取锁key
- * 1 锁key为空,未被禁用,进入处理逻辑
- * 获取计数key
- * 1)计数key为空,说明首次访问,设置计数key为1,放行
- * 2)计数key不为空,判断是否达到最大访问次数
- * (1)达到:返回错误提示
- * (2)未达到:计数值+1
- * 2 锁key不为空,已被禁用,直接返回提示
- */
- public boolean preHandle(HttpServletRequest request, HttpServletResponse response, Object handler) throws Exception {
- log.info("进入拦截器");
- //获取访问的url和访问者ip
- String requestURI = request.getRequestURI();
- String remoteAddr = request.getRemoteAddr();
- String lockKey = LOCK_PREFIX + requestURI + remoteAddr;
- Object o = redisTemplate.opsForValue().get(lockKey);
- if (Objects.isNull(o)){
- //还未被禁用
- //查看当前访问次数
- String countKey = COUNT_PREFIX + requestURI + remoteAddr;
- Integer count = (Integer)redisTemplate.opsForValue().get(countKey);
- if (Objects.isNull(count)){
- //首次访问
- log.info("{}用户首次访问接口{}",remoteAddr,requestURI);
- redisTemplate.opsForValue().set(countKey, 1, second, TimeUnit.SECONDS);
- log.info("访问次数写入redis");
- }else {
- log.info("{}用户第{}次访问接口{}", remoteAddr, count + 1, requestURI);
- //此用户在设置的一段时间内已经访问过该接口
- //判断次数+1是否超过最大限制
- if (count++ >= times){
- //超过最大限制,禁用该用户对此接口的访问
- log.info("{}用户访问接口{}已达到最大限制,禁用",remoteAddr,requestURI);
- redisTemplate.opsForValue().set(lockKey, 1, lockTime, TimeUnit.SECONDS);
- //返回提示
- // throw new RuntimeException("服务器繁忙,请稍候再试");
- throw new AccessLimitException();
- }else {
- //访问次数+1
- ValueOperations valueOperations = redisTemplate.opsForValue();
- valueOperations.set(countKey, count, second, TimeUnit.SECONDS);
- }
- }
- }else {
- //已被禁用,返回提示
- throw new AccessLimitException();
- }
- return true;
- }
复制代码 - 目前存在的问题
此时已经简单实现了限流功能,但是上边配置拦截路径直接写了/**,是为了方便测试,但是如果正常开发应该不会写全部,应该单个配置,那么就要为每个接口添加配置,比较繁琐。并且现在对所有接口的限制都是一样的规则,时间都是一样的,如果想要有不同的时间规则,那么就需要设置多个过滤器,明显是不合适的,所以需要优化。
- 优化一:自定义注解+反射
- 定义注解
- /*
- * @Description TODO (自定义接口防刷注解)
- * 创建人: 程长新
- * 创建时间:2023/11/12 9:03
- **/
- @Target({ElementType.METHOD})//注解可以作用在方法上
- @Retention(RetentionPolicy.RUNTIME)
- @Documented
- public @interface AccessLimit {
- /**
- * 时间周期
- */
- long second() default 5L;
- /**
- * 最大访问次数
- */
- int times() default 3;
- /**
- * 禁用时长
- */
- long lockTime() default 3L;
- }
复制代码 - 将注解标注写需要限流的方法上
- @AccessLimit(second = 10L, times = 5, lockTime = 2L)
- @GetMapping("/search")
- public String search(){
- return "进来了";
- }
复制代码 - 修改处理逻辑
主要修改:通过反射获取到方法注解,判断是否需要进行限流,如果需要就获取注解中的参数进行处理- @Override
- public boolean preHandle(HttpServletRequest request, HttpServletResponse response, Object handler) throws Exception {
- log.info("进入拦截器");
- //判断拦截的是否为接口方法
- if (handler instanceof HandlerMethod){
- log.info("开始处理");
- //转化为目标方法对象
- HandlerMethod targetMethod = (HandlerMethod) handler;
- //获取对象的AccessLimit注解
- AccessLimit accessLimit = targetMethod.getMethodAnnotation(AccessLimit.class);
- //如果获取到注解再进行处理,否则直接放行
- if(Objects.nonNull(accessLimit)){
- //防刷处理逻辑
- //获取访问的接口的访问者IP
- String remoteAddr = request.getRemoteAddr();
- String requestURI = request.getRequestURI();
- //拼接锁key和计数key
- String lockKey = LOCK_PREFIX + requestURI + remoteAddr;
- String countKey = COUNT_PREFIX + requestURI + remoteAddr;
- //从redis中获取锁值
- Object o = redisTemplate.opsForValue().get(lockKey);
- if (Objects.nonNull(o)){
- log.info("用户{},访问{}接口,被禁用",remoteAddr,requestURI);
- //获取锁值不为空说明已经禁用,直接返回
- throw new AccessLimitException();
- }else {
- //未被禁用
- //获取注解中设置的x,y,z时间值
- long second1 = accessLimit.second();
- int times1 = accessLimit.times();
- long lockTime1 = accessLimit.lockTime();
- //获取访问次数
- Integer o1 = (Integer) redisTemplate.opsForValue().get(countKey);
- if (Objects.isNull(o1)){
- log.info("用户{},访问{}接口,首次访问",remoteAddr,requestURI);
- //首次访问,保存访问次数为1
- redisTemplate.opsForValue().set(countKey,1,second1,TimeUnit.SECONDS);
- }else {
- //判断访问次数
- if (o1 == times1){
- log.info("用户{},访问{}接口,达到次数限制被禁用",remoteAddr,requestURI);
- //已经达到限制,禁用,返回
- redisTemplate.opsForValue().set(lockKey,1,lockTime1,TimeUnit.SECONDS);
- //删除计数key,已经禁用,这个也就没必要了
- redisTemplate.delete(countKey);
- throw new AccessLimitException();
- }else {
- log.info("用户{},访问{}接口,现在第{}次访问",remoteAddr,requestURI,(o1 + 1));
- //次数加1
- redisTemplate.opsForValue().set(countKey,++o1,second1,TimeUnit.SECONDS);
- }
- }
- }
- }
- }
- return true;
- }
复制代码 - 目前存在的问题
对需要进行限流的每个方法得挨个添加注解,那么如果一个controller中的所以接口都需要限流处理的话,每个接口挨个添加注解的做法属实不怎么样。应该做到如果在一个controller上添加了注解,那么这个controller中的所以接口都进行限流,如果某个接口上也添加了注解,那么就采用就近原则使用接口上注解的参数。仍然需要优化
- 优化二:注解作用于类上
- 添加注解作用范围
- @Target({ElementType.METHOD, ElementType.TYPE})//添加ElementType.TYPE范围
- @Retention(RetentionPolicy.RUNTIME)
- @Documented
- public @interface AccessLimit {
- /**
- * 时间周期
- */
- long second() default 5L;
- /**
- * 最大访问次数
- */
- int times() default 3;
- /**
- * 禁用时长
- */
- long lockTime() default 3L;
- }
复制代码 - 修改处理逻辑
- /**自定义注解可以作用在类上之后的逻辑
- * 1 获取类上的注解
- * 2 获取方法上的注解
- * 3 判断类是是否有注解
- * 1)类上没有
- * 判断方法上是否存在注解
- * 不存在:说明该接口不需要防刷,放行就可以
- * 存在:获取注解中的值,进行处理
- * 2)类上存在注解
- * 判断方法上是否存在注解
- * 不存在:说明该方法使用类上的统一配置
- * 存在:采用就近原则,使用方法上注解的值进行处理
- */
- @Override
- public boolean preHandle(HttpServletRequest request, HttpServletResponse response, Object handler) throws Exception {
- //判断拦截的是否为接口方法
- if (handler instanceof HandlerMethod){
- //转化为目标方法
- HandlerMethod targetMethod = (HandlerMethod) handler;
- //获取目标类上的注解
- //不可以直接使用targetMethod.getClass(),这样获取到的是HandlerMethod,不是真正想要的controller类
- // Class<? extends HandlerMethod> aClass = targetMethod.getClass();
- Class<?> targetClass = targetMethod.getMethod().getDeclaringClass();
- AccessLimit classAccessLimit = targetClass.getAnnotation(AccessLimit.class);
- //获取目标方法上的注解,
- AccessLimit methodAccessLimit = targetMethod.getMethodAnnotation(AccessLimit.class);
- //类名#方法名[参数个数]
- String shortLogMessage = targetMethod.getShortLogMessage();
- long second = 0L;//一段时间内
- int times = 0;//最大访问次数
- long lockTime = 0L;//禁用时长
- if (Objects.nonNull(classAccessLimit)){
- //类上存在注解
- if (Objects.nonNull(methodAccessLimit)){
- //方法上存在注解,就近原则,使用方法上注解的参数
- second = methodAccessLimit.second();
- times = methodAccessLimit.times();
- lockTime = methodAccessLimit.lockTime();
- }else {
- second = classAccessLimit.second();
- times = classAccessLimit.times();
- lockTime = classAccessLimit.lockTime();
- }
- //只传uri的话,如果请求中含有路径参数,那么请求同一个接口但传递不同参数也会记录为不同的key,就会导致防刷失效,所以将uri改为类名+方法名
- if(isLimit(second, times, lockTime, request.getRemoteAddr(), shortLogMessage)){
- throw new AccessLimitException();
- }
- }else {
- //类上不存在注解
- //判断方法上是否存在
- if (Objects.nonNull(methodAccessLimit)){
- //方法上存在注解
- second = methodAccessLimit.second();
- times = methodAccessLimit.times();
- lockTime = methodAccessLimit.lockTime();
- if(isLimit(second, times, lockTime, request.getRemoteAddr(), shortLogMessage)){
- throw new AccessLimitException();
- }
- }
- //方法上不存在,不用分支了,直接到最后return true
- }
- }
- return true;
- }
- /**
- * 判断该ip访问此uri是否已经被限制
- * @param second
- * @param times
- * @param lockTime
- * @param ip
- * @param uri 请求的接口名:类名#方法名[参数个数]
- * @return true:禁用 false:未禁用
- */
- public boolean isLimit(long second, int times, long lockTime, String ip, String uri){
- String lockKey = LOCK_PREFIX + ip + uri;
- String countKey = COUNT_PREFIX + ip + uri;
- Object o = redisTemplate.opsForValue().get(lockKey);
- if (Objects.nonNull(o)){
- log.info("用户{},访问{}接口,被禁用",ip,uri);
- //获取锁值不为空说明已经禁用,直接返回
- return true;
- }else {
- //未被禁用
- //获取访问次数
- Integer o1 = (Integer) redisTemplate.opsForValue().get(countKey);
- if (Objects.isNull(o1)){
- log.info("用户{},访问{}接口,首次访问",ip,uri);
- //首次访问,保存访问次数为1
- redisTemplate.opsForValue().set(countKey,1,second,TimeUnit.SECONDS);
- }else {
- //判断访问次数
- if (o1 == times){
- log.info("用户{},访问{}接口,达到次数限制被禁用",ip,uri);
- //已经达到限制,禁用,返回
- redisTemplate.opsForValue().set(lockKey,1,lockTime,TimeUnit.SECONDS);
- //删除计数key,已经禁用,这个也就没必要了
- redisTemplate.delete(countKey);
- return true;
- }else {
- log.info("用户{},访问{}接口,现在第{}次访问",ip,uri,(o1 + 1));
- //次数加1
- // redisTemplate.opsForValue().set(countKey,++o1,second,TimeUnit.SECONDS);
- Long increment = redisTemplate.opsForValue().increment(countKey);
- }
- }
- }
- return false;
- }
复制代码 - 到此限流方案完善
免责声明:如果侵犯了您的权益,请联系站长,我们会及时删除侵权内容,谢谢合作! |