java中aop實現接口訪問頻率限制
引言
項目開發(fā)中我們有時會用到一些第三方付費的接口,這些接口的每次調用都會產生一些費用,有時會有別有用心之人惡意調用我們的接口,造成經濟損失;或者有時需要對一些執(zhí)行時間比較長的的接口進行頻率限制,這里我就簡單演示一下我的解決思路;
主要使用spring的aop特性實現功能;
代碼實現
首先需要一個注解,找個注解可以理解為一個坐標,標記該注解的接口都將進行訪問頻率限制;
package com.yang.prevent;
import java.lang.annotation.*;
/**
* 接口防刷注解
*/
@Documented
@Target({ElementType.METHOD})
@Retention(RetentionPolicy.RUNTIME)
public @interface Prevent {
/**
* 限制的時間值(秒)默認60s
*/
long value() default 60;
/**
* 限制規(guī)定時間內訪問次數,默認只能訪問一次
*/
long times() default 1;
/**
* 提示
*/
String message() default "";
/**
* 策略
*/
PreventStrategy strategy() default PreventStrategy.DEFAULT;
}
value就是限制周期,times是在一個周期內訪問次數,message是訪問頻率過多時的提示信息,strategy就是一個限制策略,是自定義的,如下:
package com.yang.prevent;
/**
* 防刷策略枚舉
*/
public enum PreventStrategy {
/**
* 默認(60s內不允許再次請求)
*/
DEFAULT
}
下面就是aop攔截的具體代碼:
package com.yang.prevent;
import com.yang.common.StatusCode;
import com.yang.constant.redis.RedisKey;
import com.yang.exception.BusinessException;
import com.yang.utils.IpUtils;
import org.aspectj.lang.JoinPoint;
import org.aspectj.lang.annotation.Aspect;
import org.aspectj.lang.annotation.Before;
import org.aspectj.lang.annotation.Pointcut;
import org.aspectj.lang.reflect.MethodSignature;
import org.springframework.data.redis.core.RedisTemplate;
import org.springframework.stereotype.Component;
import org.springframework.util.StringUtils;
import org.springframework.web.context.request.RequestAttributes;
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.nio.charset.StandardCharsets;
import java.util.Base64;
import java.util.Objects;
import java.util.concurrent.TimeUnit;
/**
* 防刷切面實現類
*/
@Aspect
@Component
public class PreventAop {
@Resource
private RedisTemplate<String, Long> redisTemplate;
/**
* 切入點
*/
@Pointcut("@annotation(com.yang.prevent.Prevent)")
public void pointcut() {}
/**
* 處理前
*/
@Before("pointcut()")
public void joinPoint(JoinPoint joinPoint) throws Exception {
// 獲取調用者ip
RequestAttributes requestAttributes = RequestContextHolder.currentRequestAttributes();
HttpServletRequest httpServletRequest = ((ServletRequestAttributes) requestAttributes).getRequest();
String userIP = IpUtils.getUserIP(httpServletRequest);
// 獲取調用接口方法名
MethodSignature methodSignature = (MethodSignature) joinPoint.getSignature();
Method method = joinPoint.getTarget().getClass().getMethod(
methodSignature.getName(),
methodSignature.getParameterTypes()); // 獲取該接口方法
String methodFullName = method.getDeclaringClass().getName() + method.getName(); // 獲取到方法名
Prevent preventAnnotation = method.getAnnotation(Prevent.class); // 獲取該接口上的prevent注解(為了使用該注解內的參數)
// 執(zhí)行對應策略
entrance(preventAnnotation, userIP, methodFullName);
}
/**
* 通過prevent注冊判斷執(zhí)行策略
* @param prevent 該接口的prevent注解對象
* @param userIP 訪問該接口的用戶ip
* @param methodFullName 該接口方法名
*/
private void entrance(Prevent prevent, String userIP, String methodFullName) throws Exception {
PreventStrategy strategy = prevent.strategy(); // 獲取校驗策略
if (Objects.requireNonNull(strategy) == PreventStrategy.DEFAULT) { // 默認就是default策略,執(zhí)行default策略方法
defaultHandle(userIP, prevent, methodFullName);
} else {
throw new BusinessException(StatusCode.FORBIDDEN, "無效的策略");
}
}
/**
* Default測試執(zhí)行方法
* @param userIP 訪問該接口的用戶ip
* @param prevent 該接口的prevent注解對象
* @param methodFullName 該接口方法名
*/
private void defaultHandle(String userIP, Prevent prevent, String methodFullName) throws Exception {
String base64StrIP = toBase64String(userIP); // 加密用戶ip(避免ip存在一些特殊字符作為redis的key不合法)
long expire = prevent.value(); // 獲取訪問限制時間
long times = prevent.times(); // 獲取訪問限制次數
// 限制特定時間內訪問特定次數
long count = redisTemplate.opsForValue().increment(
RedisKey.PREVENT_METHOD_NAME + base64StrIP + ":" + methodFullName, 1); // 訪問次數+1
if (count == 1) { // 如果訪問次數為1,則重置訪問限制時間(即redis超時時間)
redisTemplate.expire(
RedisKey.PREVENT_METHOD_NAME + base64StrIP + ":" + methodFullName,
expire,
TimeUnit.SECONDS);
}
if (count > times) { // 如果訪問次數超出訪問限制次數,則禁止訪問
// 如果有限制信息則使用限制信息,沒有則使用默認限制信息
String errorMessage =
!StringUtils.isEmpty(prevent.message()) ? prevent.message() : expire + "秒內不允許重復請求";
throw new BusinessException(StatusCode.FORBIDDEN, errorMessage);
}
}
/**
* 對象轉換為base64字符串
* @param obj 對象值
* @return base64字符串
*/
private String toBase64String(String obj) throws Exception {
if (StringUtils.isEmpty(obj)) {
return null;
}
Base64.Encoder encoder = Base64.getEncoder();
byte[] bytes = obj.getBytes(StandardCharsets.UTF_8);
return encoder.encodeToString(bytes);
}
}
注釋寫的很清楚了,這里我簡單說一下關鍵方法defaultHandle:
1,首先加密ip,原因就是避免ip存在一些特殊字符作為redis的key不合法,該ip是組成redis主鍵的一部分,redis主鍵格式為:polar:prevent:加密ip:方法名
這樣就能區(qū)分不同ip,同一ip下區(qū)分不同方法的訪問頻率;
2,expire和times都是從@Prevent注解中獲取的參數,默認是60s內最多訪問1次,可以自定義;
3,然后接口訪問次數+1(該設備ip下),如果該接口訪問次數為1,則說明這是這個ip第一次訪問該接口,或者是該接口的頻率限制已經解除,即該接口訪問次數+1前redis中沒有該ip對應接口的限制記錄,所以需要重新設置對應超時時間,表示新的一輪頻率限制開始;如果訪問次數超過最大次數,則禁止訪問該接口,直到該輪頻率限制結束,redis緩存的記錄超時消失,才可以再次訪問該接口;
這個方法理解了其他就不難了,其他方法就是給這個方法傳參或者作為校驗或數據處理的;
下面測試一下,分別標記兩個接口,一個使用@Prevent默認參數,一個使用自定義參數:

調用getToleById接口,意思是60s內只能調用該接口1次:
第一次調用成功,redis鍵值為1


第二次失敗,需要等60s

redis鍵值變成了2

getRoleList是自定義參數,意思是20s內最多只能訪問該接口5次:
未超出頻率限制


超出頻率限制


總體流程就是這樣了,aop理解好了不難,也比較實用,可以在自己項目中使用;
到此這篇關于java中aop實現接口訪問頻率限制的文章就介紹到這了,更多相關java aop接口訪問頻率限制內容請搜索腳本之家以前的文章或繼續(xù)瀏覽下面的相關文章希望大家以后多多支持腳本之家!
相關文章
Intellij無法創(chuàng)建java文件解決方案
這篇文章主要介紹了Intellij無法創(chuàng)建java文件解決方案,文中通過示例代碼介紹的非常詳細,對大家的學習或者工作具有一定的參考學習價值,需要的朋友可以參考下2020-10-10
解決spring cloud服務啟動之后回到命令行會自動掛掉問題
這篇文章主要介紹了解決spring cloud服務啟動之后回到命令行會自動掛掉問題,具有很好的參考價值,希望對大家有所幫助。一起跟隨小編過來看看吧2020-09-09
詳解JAVA抓取網頁的圖片,JAVA利用正則表達式抓取網站圖片
這篇文章主要介紹了詳解JAVA抓取網頁的圖片,JAVA利用正則表達式抓取網站圖片,非常具有實用價值,需要的朋友可以參考下。2016-12-12
Java中構造函數,set/get方法和toString方法使用及注意說明
這篇文章主要介紹了Java中構造函數,set/get方法和toString方法的使用及注意說明,具有很好的參考價值,希望對大家有所幫助。如有錯誤或未考慮完全的地方,望不吝賜教2023-01-01
MyBatis-Plus中公共字段的統(tǒng)一處理的實現
在開發(fā)中經常遇到多個實體類有共同的屬性字段,這些字段屬于公共字段,本文主要介紹了MyBatis-Plus中公共字段的統(tǒng)一處理的實現,具有一定的參考價值,感興趣的可以了解一下2023-08-08

