AOP?Redis自定義注解實現(xiàn)細粒度接口IP訪問限制
系列說明
GitHub地址:github.com/stick-i/scb…
目前項目還有很大改進和完善的空間,歡迎各位有意愿的同學參與項目貢獻(尤其前端),一起學習一起進步??。
項目的技術(shù)棧主要是:
后端 Java + SpringBoot + SpringCloud + Nacos + Getaway + Fegin + MybatisPlus + MySQL + Redis + ES + RabbitMQ + Minio + 七牛云OSS + Jenkins + Docker
前端 Vue + ElementUI + Axios(說實話前端我不太清楚??)
一般向外暴露的接口,都需要加上一個訪問限制,以防止有人惡意刷流量或者爆破,訪問限制的做法有很多種,從控制粒度上來看可以分為:全局訪問限制和接口訪問限制,本文講的是接口訪問的限制。
本章講解的主要內(nèi)容在項目中的位置:
scblogs / common / common-web / src / main / java / cn / sticki / common / web / anno /
我的寫法是基于 AOP + 自定義注解 + Redis,并且封裝在一個單獨的模塊 common-web
下,需要使用的模塊只需引入該包,并且給需要限制的方法添加注解即可,很方便,且松耦合??。
唯一的缺點是該方法只支持在方法上添加注解,不支持給類添加,如果想給一個類的所有方法添加上限制,則必須給該類的所有方法都加上該注解才行??。 如果有同學想把這個缺點完善一下,歡迎到文章頂部的git鏈接中訪問并加入我們的項目??。
實現(xiàn)步驟
一、引入依賴
實現(xiàn)這個功能我們主要需要 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
,如下圖:
然后我們先寫注解的內(nèi)容:
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 { /** * 允許訪問的次數(shù),默認值120 */ int count() default 120; /** * 間隔的時間段,單位秒,默認值60 */ int time() default 60; /** * 訪問達到限制后需要等待的世界,單位秒,默認值120 */ int waits() default 120; }
說明:
- 這里我們設(shè)置@Target(ElementType.METHOD),意思是這個注解只能使用在方法上。
- 設(shè)置@Order(Ordered.HIGHEST_PRECEDENCE),是為了讓這個注解的的優(yōu)先級升高,也就是先判斷訪問限制,再做其他的事情。
- 然后注解內(nèi)的參數(shù),是用于不同接口下設(shè)置不同的限制的,使用者可以根據(jù)接口的需求,進行設(shè)置。
三、寫邏輯(注解環(huán)繞)
我們現(xiàn)在基于RequestLimit
注解寫環(huán)繞運行的邏輯,也就是開始寫 RequestLimitAspect
的內(nèi)容了,下面都是在這個類中進行操作的。
1. 添加注解
給剛剛新建的 RequestLimitAspect
類上使用 @Aspect
,因為等會我們還要把這個類自動注入到Spring當中,所以還得給它加上 @Component
注解。
2. 注入 RedisTemplate
由于我們是要把訪問次數(shù)記錄在redis中的(分布式嘛),所以我們肯定得有 redis 的工具類。
那么問題來了,我們這是個工具模塊,本身并不會被啟動,也沒有啟動類,更沒有什么配置文件,那這種情況下,我們該如何獲得redis呢?
答案是:找引入我們的的模塊要 RedisTemplate。
因為這些Bean都是被spring管控的,包括RedisTemplate,也包括我們現(xiàn)在寫的RequestLimitAspect ,它們將來都是在spring容器內(nèi)的,所以我們直接在代碼里找spring進行注入就可以了。將來引入我們的模塊中如果有RedisTemplate可用,那我們自然就可以拿到。
所以這步很簡單,直接注入即可,但是不要忘了定義一個key前綴,等會用來拼接到redis的key上。
@Resource private RedisTemplate<String, Integer> redisTemplate; private static final String IPLIMIT_KEY = "ipLimit:";
3. 定義方法
在類中定義一個before
方法,并在方法上使用@Around()
注解,Around內(nèi)填入之前新建的 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. 實現(xiàn)方法
步驟:
- 獲取注解參數(shù)
- 獲取當前請求的ip
- 生成key
- 獲取redis中該key的訪問次數(shù)
- 判斷次數(shù)是否超過范圍
- 若超出范圍,則拒絕訪問,返回提示,并將TTL重置為注解上的等待時間
- 若沒有超過范圍,則允許訪問,并將訪問次數(shù)+1
- 若查詢不到該key,則往redis中進行添加,將值設(shè)置為1,將TTL設(shè)置為注解上的值
完整實現(xiàn)代碼如下(內(nèi)容干凈無毒,可以放心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 獲取注解參數(shù) 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中的數(shù)據(jù) 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時間為設(shè)定的等待值 log.debug("重置redis值為{},等待{}", nowCount + 1, limit.waits()); redisTemplate.opsForValue().set(key, nowCount + 1, limit.waits(), TimeUnit.SECONDS); } return null; } if (count == null) { // 重置計數(shù)器 log.debug("重置計數(shù)器"); redisTemplate.opsForValue().set(key, 1, limit.time(), TimeUnit.SECONDS); } else { // 計數(shù)器 +1,不重置TTL redisTemplate.opsForValue().increment(key); } log.debug("方法放行"); return pjp.proceed(); } }
5. 開啟spring自動裝配
spring會自動注入spring.factories
文件中的類,所以我們只需要編寫spring.factories
即可。
首先在resources下新建META-INF文件夾,然后在該文件夾下新建文件,命名為spring.factories
。
文件內(nèi)容如下:
org.springframework.boot.autoconfigure.EnableAutoConfiguration=\ cn.sticki.common.web.anno.RequestLimitAspect
這里的全限定名需要改為自己的類路徑名。
四、測試
- 把剛剛寫的那個模塊用maven進行本地打包
- 然后在其他服務(wù)中引入該模塊為依賴,對需要進行訪問限制的方法使用。
運行項目
訪問該接口進行測試
剛開始正常
多次訪問之后被拒絕
查看redis數(shù)據(jù),發(fā)現(xiàn)符合我設(shè)定的條件
總結(jié)
本文講解了如何在微服務(wù)中優(yōu)雅的實現(xiàn)一個公用的接口訪問限制工具,更多關(guān)于AOP Redis 接口IP訪問限制的資料請關(guān)注腳本之家其它相關(guān)文章!
相關(guān)文章
手把手教你用Redis 實現(xiàn)點贊功能并且與數(shù)據(jù)庫同步
本文主要介紹了Redis 實現(xiàn)點贊功能并且與數(shù)據(jù)庫同步,文中通過示例代碼介紹的非常詳細,對大家的學習或者工作具有一定的參考學習價值,需要的朋友們下面隨著小編來一起學習學習吧2022-05-05Redis中的數(shù)據(jù)結(jié)構(gòu)跳表詳解
跳表是一種基于并聯(lián)的鏈表結(jié)構(gòu),用于在有序元素序列中快速查找元素的數(shù)據(jù)結(jié)構(gòu),本文給大家介紹Redis中的數(shù)據(jù)結(jié)構(gòu)跳表,感興趣的朋友跟隨小編一起看看吧2024-06-06華為歐拉openEuler編譯安裝Redis的實現(xiàn)步驟
本文主要介紹了華為歐拉openEuler編譯安裝Redis的實現(xiàn)步驟,文中通過示例代碼介紹的非常詳細,對大家的學習或者工作具有一定的參考學習價值,需要的朋友們下面隨著小編來一起學習學習吧2023-01-01