文章目录
个别向外裸露的接口,都须要加上一个拜访限度,以避免有人歹意刷流量或者爆破,拜访限度的做法有很多种,从管制粒度上来看能够分为:全局拜访限度和接口拜访限度,本文讲的是接口拜访的限度。
本章解说的次要内容在我的项目中的地位: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
@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. 实现办法
步骤:
- 获取注解参数
- 获取以后申请的 ip
- 生成 key
- 获取 redis 中该 key 的拜访次数
-
判断次数是否超过范畴
- 若超出范围,则回绝拜访,返回提醒,并将 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
@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 {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
这里的全限定名须要改为本人的类路径名。
四、测试
- 把刚刚写的那个模块用 maven 进行本地打包
- 而后在其余服务中引入该模块为依赖,对须要进行拜访限度的办法应用。
- 运行我的项目
-
拜访该接口进行测试
- 刚开始失常
* 屡次拜访之后被回绝
* 查看 redis 数据,发现合乎我设定的条件
对文章中内容感兴趣的小伙伴能够搜寻微信公众号:敲代码的老贾,支付相应材料