文章目录

个别向外裸露的接口,都须要加上一个拜访限度,以避免有人歹意刷流量或者爆破,拜访限度的做法有很多种,从管制粒度上来看能够分为:全局拜访限度和接口拜访限度,本文讲的是接口拜访的限度。

本章解说的次要内容在我的项目中的地位: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 ,如下图:

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

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 注解。

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@Slf4jpublic 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设置为注解上的值

残缺实现代码如下:

package cn.sticki.common.web.anno;import cn.sticki.common.result.RestResult;import cn.sticki.common.web.utils.RequestUtils;import cn.sticki.common.web.utils.ResponseUtils;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.0 * @date 2022/7/31 20:24 */@Aspect@Component@Slf4jpublic 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 {         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;        if (nowCount >= limit.count()) {             // 5\. 超出限度,回绝拜访            assert attribute.getResponse() != null;            log.info("拜访频繁被回绝拜访,ip:{},method:{}", ip, signature.getName());            ResponseUtils.objectToJson(attribute.getResponse(), RestResult.fail("拜访频繁"));            if (nowCount == limit.count()) {                 // 5.2 重置Redis工夫为设定的期待值                log.debug("重置redis值为{},期待{}", nowCount + 1, limit.waits());                redisTemplate.opsForValue().set(key, nowCount + 1, limit.waits(), TimeUnit.SECONDS);            }            return null;        }        if (count == null) {             // 重置计数器            log.debug("重置计数器");            redisTemplate.opsForValue().set(key, 1, limit.time(), TimeUnit.SECONDS);        } else {             // 计数器 +1,不重置TTL            redisTemplate.opsForValue().increment(key);        }        log.debug("办法放行");        return pjp.proceed();    }}

5. 开启spring主动拆卸

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

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

文件内容如下:

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

这里的全限定名须要改为本人的类路径名。

四、测试

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

  1. 而后在其余服务中引入该模块为依赖,对须要进行拜访限度的办法应用。

  1. 运行我的项目
  2. 拜访该接口进行测试

    • 刚开始失常
*   屡次拜访之后被回绝

*   查看redis数据,发现合乎我设定的条件

对文章中内容感兴趣的小伙伴能够搜寻微信公众号:敲代码的老贾,支付相应材料