網站首頁 編程語言 正文
系列說明
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
,如下圖:
然后我們先寫注解的內容:
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設置為注解上的值
完整實現代碼如下(內容干凈無毒,可以放心CV,僅需將返回值進行修改):
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數據,發現符合我設定的條件
總結
原文鏈接:https://juejin.cn/post/7153499705261948942
相關推薦
- 2022-06-22 android實現注冊登錄程序_Android
- 2022-11-05 pytest官方文檔解讀之安裝和使用插件的方法_python
- 2022-08-15 Redis緩存三大異常的處理方案梳理總結_Redis
- 2022-01-19 wangeditor富文本編輯器拓展菜單——格式刷
- 2023-03-25 Rust你不認識的所有權_Rust語言
- 2022-12-09 C++筆記-設置cout輸出數據的寬度和填充方式_C 語言
- 2023-07-04 Spring中@Transactional注解事務傳播行為propagation參數說明
- 2022-06-07 Python?Numpy庫的超詳細教程_python
- 最近更新
-
- window11 系統安裝 yarn
- 超詳細win安裝深度學習環境2025年最新版(
- Linux 中運行的top命令 怎么退出?
- MySQL 中decimal 的用法? 存儲小
- get 、set 、toString 方法的使
- @Resource和 @Autowired注解
- Java基礎操作-- 運算符,流程控制 Flo
- 1. Int 和Integer 的區別,Jav
- spring @retryable不生效的一種
- Spring Security之認證信息的處理
- Spring Security之認證過濾器
- Spring Security概述快速入門
- Spring Security之配置體系
- 【SpringBoot】SpringCache
- Spring Security之基于方法配置權
- redisson分布式鎖中waittime的設
- maven:解決release錯誤:Artif
- restTemplate使用總結
- Spring Security之安全異常處理
- MybatisPlus優雅實現加密?
- Spring ioc容器與Bean的生命周期。
- 【探索SpringCloud】服務發現-Nac
- Spring Security之基于HttpR
- Redis 底層數據結構-簡單動態字符串(SD
- arthas操作spring被代理目標對象命令
- Spring中的單例模式應用詳解
- 聊聊消息隊列,發送消息的4種方式
- bootspring第三方資源配置管理
- GIT同步修改后的遠程分支