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