持续创造,加快生长!这是我参加「日新方案 10 月更文应战」的第13天,点击检查活动概况

系列阐明

本系列文章根据我的开源微服务项目【学校博客】进行剖析和解说,一切源码均可在GitHub库房上找到。 系列文章地址请见我的 学校博客专栏。

  • GitHub地址:github.com/stick-i/scb…

目前项目还有很大改善和完善的空间,欢迎各位有意愿的同学参加项目奉献(特别前端),一同学习一同前进。

项目的技术栈首要是:
后端 Java + SpringBoot + SpringCloud + Nacos + Getaway + Fegin + MybatisPlus + MySQL + Redis + ES + RabbitMQ + Minio + 七牛云OSS + Jenkins + Docker
前端 Vue + ElementUI + Axios(说实话前端我不太清楚)


前言

一般向外露出的接口,都需求加上一个拜访约束,以避免有人歹意刷流量或者爆破,拜访约束的做法有很多种,从操控粒度上来看能够分为:全局拜访约束和接口拜访约束,本文讲的是接口拜访的约束。

本章解说的首要内容在项目中的方位:scblogs / common / common-web / src / main / java / cn / sticki / common / web / anno /

我的写法是根据 AOP + 自界说注解 + Redis,而且封装在一个独自的模块 common-web 下,需求运用的模块只需引进该包,而且给需求约束的办法增加注解即可,很便利,且松耦合。

唯一的缺点是该办法只支持在办法上增加注解,不支持给类增加,假如想给一个类的一切办法增加上约束,则有必要给该类的一切办法都加上该注解才行。 假如有同学想把这个缺点完善一下,欢迎到文章顶部的git链接中拜访并参加咱们的项目。


完成过程

一、引进依靠

完成这个功用咱们首要需求 Redis 和 AOP的依靠,redis咱们用spring的,然后aop运用org.aspectj下的aspectjweaver,首要便是下面这两个

        <dependency>
                <groupId>org.springframework.boot</groupId>
                <artifactId>spring-boot-starter-data-redis</artifactId>
        </dependency>
        <dependency>
                <groupId>org.aspectj</groupId>
                <artifactId>aspectjweaver</artifactId>
        </dependency>

PS:我的项目文件中引进的是我自己的 common-redis 模块,里边包含了 spring redis的依靠。

二、写注解

新建一个包,命名为anno,然后在包下新建注解,命名为RequestLimit,再新建一个类,命名为RequestLimitAspect,如下图:

基于 AOP + Redis + 自定义注解 实现细粒度的接口IP访问限制 | 开源微服务项目

然后咱们先写注解的内容:

package cn.sticki.common.web.anno;
import org.springframework.core.Ordered;
import org.springframework.core.annotation.Order;
import java.lang.annotation.*;
/**
 * Request 恳求约束阻拦
 *
 * @author 阿杆
 * @version 1.0
 * @date 2022/7/31 20:19
 */
@Retention(RetentionPolicy.RUNTIME)
@Target(ElementType.METHOD)
@Documented
@Order(Ordered.HIGHEST_PRECEDENCE)
public @interface RequestLimit {
	/**
	 * 答应拜访的次数,默认值120
	 */
	int count() default 120;
	/**
	 * 距离的时间段,单位秒,默认值60
	 */
	int time() default 60;
	/**
	 * 拜访达到约束后需求等候的国际,单位秒,默认值120
	 */
	int waits() default 120;
}

阐明:

  • 这里咱们设置@Target(ElementType.METHOD),意思是这个注解只能运用在办法上。
  • 设置@Order(Ordered.HIGHEST_PRECEDENCE),是为了让这个注解的的优先级升高,也便是先判别拜访约束,再做其他的事情。
  • 然后注解内的参数,是用于不同接口下设置不同的约束的,运用者能够根据接口的需求,进行设置。

三、写逻辑(注解盘绕)

咱们现在根据RequestLimit注解写盘绕运转的逻辑,也便是开始写 RequestLimitAspect 的内容了,下面都是在这个类中进行操作的。

1. 增加注解

给刚刚新建的 RequestLimitAspect类上运用 @Aspect ,由于等会咱们还要把这个类主动注入到Spring傍边,所以还得给它加上 @Component 注解。

基于 AOP + Redis + 自定义注解 实现细粒度的接口IP访问限制 | 开源微服务项目

2. 注入 RedisTemplate

由于咱们是要把拜访次数记录在redis中的(分布式嘛),所以咱们必定得有 redis 的东西类。

那么问题来了,咱们这是个东西模块,本身并不会被启动,也没有启动类,更没有什么配置文件,那这种情况下,咱们该怎么取得redis呢?

答案是:找引进咱们的的模块要 RedisTemplate。由于这些Bean都是被spring管控的,包含RedisTemplate,也包含咱们现在写的RequestLimitAspect ,它们将来都是在spring容器内的,所以咱们直接在代码里找spring进行注入就能够了。将来引进咱们的模块中假如有RedisTemplate可用,那咱们自然就能够拿到。

所以这步很简单,直接注入即可,但是不要忘了界说一个key前缀,等会用来拼接到redis的key上。

	@Resource
	private RedisTemplate<String, Integer> redisTemplate;
	private static final String IPLIMIT_KEY = "ipLimit:";

3. 界说办法

在类中界说一个before办法,并在办法上运用@Around() 注解,Around内填入之前新建的 RequestLimit 的全路径名,做到这一步,代码就会像我这样:

package cn.sticki.common.web.anno;
import lombok.extern.slf4j.Slf4j;
import org.aspectj.lang.ProceedingJoinPoint;
import org.aspectj.lang.annotation.Around;
import org.aspectj.lang.annotation.Aspect;
import org.springframework.data.redis.core.RedisTemplate;
import org.springframework.stereotype.Component;
import javax.annotation.Resource;
/**
 * @author 阿杆
 * @version 1.0
 * @date 2022/7/31 20:24
 */
@Aspect
@Component
@Slf4j
public class RequestLimitAspect {
	@Resource
	private RedisTemplate<String, Integer> redisTemplate;
	private static final String IPLIMIT_KEY = "ipLimit:";
	/**
	 * 阻拦有 {@link RequestLimit}注解的办法
	 */
	@Around("@annotation(cn.sticki.common.web.anno.RequestLimit)")
	public Object before(ProceedingJoinPoint pjp) throws Throwable {
		return pjp.proceed();
	}
}

4. 完成办法

过程:

  1. 获取注解参数
  2. 获取当时恳求的ip和恳求的办法
  3. 生成key
  4. 获取redis中该key的拜访次数
  5. 判别次数是否超过规模
    • 若超出规模,则拒绝拜访,回来提示,并将TTL重置为注解上的等候时间
    • 若没有超过规模,则答应拜访,并将拜访次数+1
    • 若查询不到该key,则往redis中进行增加,将值设置为1,将TTL设置为注解上的值

完好完成代码如下(内容洁净无毒,能够定心CV,仅需将回来值进行修改):

package cn.sticki.common.web.anno;
import cn.sticki.common.web.exception.FrequentVisitsException;
import cn.sticki.common.web.utils.RequestUtils;
import lombok.extern.slf4j.Slf4j;
import org.aspectj.lang.ProceedingJoinPoint;
import org.aspectj.lang.annotation.Around;
import org.aspectj.lang.annotation.Aspect;
import org.aspectj.lang.reflect.MethodSignature;
import org.springframework.data.redis.core.RedisTemplate;
import org.springframework.stereotype.Component;
import org.springframework.web.context.request.RequestContextHolder;
import org.springframework.web.context.request.ServletRequestAttributes;
import javax.annotation.Resource;
import javax.servlet.http.HttpServletRequest;
import java.lang.reflect.Method;
import java.util.concurrent.TimeUnit;
/**
 * @author 阿杆
 * @version 1.1
 * @date 2022/10/30 18:10
 */
@Aspect
@Component
@Slf4j
public class RequestLimitAspect {
   @Resource
   private RedisTemplate<String, Integer> redisTemplate;
   private static final String IPLIMIT_KEY = "ipLimit:";
   private static final int OVERTIME_VALUE = -90000;
   /**
    * 阻拦有 {@link RequestLimit}注解的办法
    */
   @Around("@annotation(cn.sticki.common.web.anno.RequestLimit)")
   public Object before(ProceedingJoinPoint pjp) throws Throwable {
      MethodSignature signature = (MethodSignature) pjp.getSignature();
      // 1. 获取被阻拦的办法和办法名
      Method method = signature.getMethod();
      String methodName = signature.getDeclaringTypeName() + "." + signature.getName();
      log.debug("阻拦办法{}", methodName);
      // 1.2 获取注解参数
      RequestLimit limit = method.getAnnotation(RequestLimit.class);
      // 2. 获取当时线程的恳求
      ServletRequestAttributes attribute = (ServletRequestAttributes) RequestContextHolder.getRequestAttributes();
      if (attribute == null) {
         log.warn(this.getClass().getName() + "只能用于web controller办法");
         return pjp.proceed();
      }
      HttpServletRequest request = attribute.getRequest();
      // 2.2 获取当时恳求的ip
      String ip = RequestUtils.getIpAddress(request);
      // 3. 生成key
      String key = IPLIMIT_KEY + methodName + ":" + ip;
      // 4. 获取Redis中的数据
      Integer count = redisTemplate.opsForValue().get(key);
      int nowCount = count == null ? 0 : count;
      // 5. 超出约束,重置ttl和value
      if (nowCount >= limit.count()) {
         // 5.2 重置Redis时间为设定的等候值
         log.info("拜访频繁被拒绝拜访,ip:{},method:{}", ip, signature.getName());
         redisTemplate.opsForValue().set(key, OVERTIME_VALUE, limit.waits(), TimeUnit.SECONDS);
         nowCount = OVERTIME_VALUE;
      }
      if (nowCount < 0) {
         // aop履行顺序在advice之前,故履行完当时aop办法后还会继续履行ResponseAdvice
         // 但我并不期望程序继续履行ResponseAdvice,所以抛出反常,回来值交给ExceptionAdvice处理
         throw new FrequentVisitsException();
      }
      Boolean isReset = false;
      if (count == null) {
         // 6. 重置计数器
         log.debug("重置计数器");
         isReset = redisTemplate.opsForValue().setIfAbsent(key, 1, limit.time(), TimeUnit.SECONDS);
      }
      // 判别是否重置成功,避免并发重置的情况
      if (count != null || !Boolean.TRUE.equals(isReset)) {
         // 计数器 +1,不重置TTL
         redisTemplate.opsForValue().increment(key);
      }
      log.debug("办法放行");
      return pjp.proceed();
   }
}

5. 开启spring主动安装

spring会主动注入spring.factories文件中的类,所以咱们只需求编写spring.factories即可。

首先在resources下新建META-INF文件夹,然后在该文件夹下新建文件,命名为spring.factories

基于 AOP + Redis + 自定义注解 实现细粒度的接口IP访问限制 | 开源微服务项目

文件内容如下:

org.springframework.boot.autoconfigure.EnableAutoConfiguration=\
  cn.sticki.common.web.anno.RequestLimitAspect

这里的全限定名需求改为自己的类路径名。

四、测试

  1. 把刚刚写的那个模块用maven进行本地打包

基于 AOP + Redis + 自定义注解 实现细粒度的接口IP访问限制 | 开源微服务项目

  1. 然后在其他服务中引进该模块为依靠,对需求进行拜访约束的办法运用。

基于 AOP + Redis + 自定义注解 实现细粒度的接口IP访问限制 | 开源微服务项目

  1. 运转项目

  2. 拜访该接口进行测试

    • 刚开始正常

      基于 AOP + Redis + 自定义注解 实现细粒度的接口IP访问限制 | 开源微服务项目

    • 多次拜访之后被拒绝

      基于 AOP + Redis + 自定义注解 实现细粒度的接口IP访问限制 | 开源微服务项目

    • 检查redis数据,发现符合我设定的条件

      基于 AOP + Redis + 自定义注解 实现细粒度的接口IP访问限制 | 开源微服务项目

总结

本文解说了怎么在微服务中优雅的完成一个共用的接口拜访约束东西。

假如你有兴趣学习微服务,欢迎订阅我的专栏和重视我。假如你想在项目中提高自己,欢迎来github参加我的项目,也欢迎私聊我。

假如本文中有任何不当或者过错的地方,欢迎我们批评指正。