java中aop實(shí)現(xiàn)接口訪問(wèn)頻率限制
引言
項(xiàng)目開發(fā)中我們有時(shí)會(huì)用到一些第三方付費(fèi)的接口,這些接口的每次調(diào)用都會(huì)產(chǎn)生一些費(fèi)用,有時(shí)會(huì)有別有用心之人惡意調(diào)用我們的接口,造成經(jīng)濟(jì)損失;或者有時(shí)需要對(duì)一些執(zhí)行時(shí)間比較長(zhǎng)的的接口進(jìn)行頻率限制,這里我就簡(jiǎn)單演示一下我的解決思路;
主要使用spring的aop特性實(shí)現(xiàn)功能;
代碼實(shí)現(xiàn)
首先需要一個(gè)注解,找個(gè)注解可以理解為一個(gè)坐標(biāo),標(biāo)記該注解的接口都將進(jìn)行訪問(wèn)頻率限制;
package com.yang.prevent; import java.lang.annotation.*; /** * 接口防刷注解 */ @Documented @Target({ElementType.METHOD}) @Retention(RetentionPolicy.RUNTIME) public @interface Prevent { /** * 限制的時(shí)間值(秒)默認(rèn)60s */ long value() default 60; /** * 限制規(guī)定時(shí)間內(nèi)訪問(wèn)次數(shù),默認(rèn)只能訪問(wèn)一次 */ long times() default 1; /** * 提示 */ String message() default ""; /** * 策略 */ PreventStrategy strategy() default PreventStrategy.DEFAULT; }
value就是限制周期,times是在一個(gè)周期內(nèi)訪問(wèn)次數(shù),message是訪問(wèn)頻率過(guò)多時(shí)的提示信息,strategy就是一個(gè)限制策略,是自定義的,如下:
package com.yang.prevent; /** * 防刷策略枚舉 */ public enum PreventStrategy { /** * 默認(rèn)(60s內(nèi)不允許再次請(qǐng)求) */ 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; /** * 防刷切面實(shí)現(xiàn)類 */ @Aspect @Component public class PreventAop { @Resource private RedisTemplate<String, Long> redisTemplate; /** * 切入點(diǎn) */ @Pointcut("@annotation(com.yang.prevent.Prevent)") public void pointcut() {} /** * 處理前 */ @Before("pointcut()") public void joinPoint(JoinPoint joinPoint) throws Exception { // 獲取調(diào)用者ip RequestAttributes requestAttributes = RequestContextHolder.currentRequestAttributes(); HttpServletRequest httpServletRequest = ((ServletRequestAttributes) requestAttributes).getRequest(); String userIP = IpUtils.getUserIP(httpServletRequest); // 獲取調(diào)用接口方法名 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注解(為了使用該注解內(nèi)的參數(shù)) // 執(zhí)行對(duì)應(yīng)策略 entrance(preventAnnotation, userIP, methodFullName); } /** * 通過(guò)prevent注冊(cè)判斷執(zhí)行策略 * @param prevent 該接口的prevent注解對(duì)象 * @param userIP 訪問(wèn)該接口的用戶ip * @param methodFullName 該接口方法名 */ private void entrance(Prevent prevent, String userIP, String methodFullName) throws Exception { PreventStrategy strategy = prevent.strategy(); // 獲取校驗(yàn)策略 if (Objects.requireNonNull(strategy) == PreventStrategy.DEFAULT) { // 默認(rèn)就是default策略,執(zhí)行default策略方法 defaultHandle(userIP, prevent, methodFullName); } else { throw new BusinessException(StatusCode.FORBIDDEN, "無(wú)效的策略"); } } /** * Default測(cè)試執(zhí)行方法 * @param userIP 訪問(wèn)該接口的用戶ip * @param prevent 該接口的prevent注解對(duì)象 * @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(); // 獲取訪問(wèn)限制時(shí)間 long times = prevent.times(); // 獲取訪問(wèn)限制次數(shù) // 限制特定時(shí)間內(nèi)訪問(wèn)特定次數(shù) long count = redisTemplate.opsForValue().increment( RedisKey.PREVENT_METHOD_NAME + base64StrIP + ":" + methodFullName, 1); // 訪問(wèn)次數(shù)+1 if (count == 1) { // 如果訪問(wèn)次數(shù)為1,則重置訪問(wèn)限制時(shí)間(即redis超時(shí)時(shí)間) redisTemplate.expire( RedisKey.PREVENT_METHOD_NAME + base64StrIP + ":" + methodFullName, expire, TimeUnit.SECONDS); } if (count > times) { // 如果訪問(wèn)次數(shù)超出訪問(wèn)限制次數(shù),則禁止訪問(wèn) // 如果有限制信息則使用限制信息,沒(méi)有則使用默認(rèn)限制信息 String errorMessage = !StringUtils.isEmpty(prevent.message()) ? prevent.message() : expire + "秒內(nèi)不允許重復(fù)請(qǐng)求"; throw new BusinessException(StatusCode.FORBIDDEN, errorMessage); } } /** * 對(duì)象轉(zhuǎn)換為base64字符串 * @param obj 對(duì)象值 * @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); } }
注釋寫的很清楚了,這里我簡(jiǎn)單說(shuō)一下關(guān)鍵方法defaultHandle:
1,首先加密ip,原因就是避免ip存在一些特殊字符作為redis的key不合法,該ip是組成redis主鍵的一部分,redis主鍵格式為:polar:prevent:加密ip:方法名
這樣就能區(qū)分不同ip,同一ip下區(qū)分不同方法的訪問(wèn)頻率;
2,expire和times都是從@Prevent注解中獲取的參數(shù),默認(rèn)是60s內(nèi)最多訪問(wèn)1次,可以自定義;
3,然后接口訪問(wèn)次數(shù)+1(該設(shè)備ip下),如果該接口訪問(wèn)次數(shù)為1,則說(shuō)明這是這個(gè)ip第一次訪問(wèn)該接口,或者是該接口的頻率限制已經(jīng)解除,即該接口訪問(wèn)次數(shù)+1前redis中沒(méi)有該ip對(duì)應(yīng)接口的限制記錄,所以需要重新設(shè)置對(duì)應(yīng)超時(shí)時(shí)間,表示新的一輪頻率限制開始;如果訪問(wèn)次數(shù)超過(guò)最大次數(shù),則禁止訪問(wèn)該接口,直到該輪頻率限制結(jié)束,redis緩存的記錄超時(shí)消失,才可以再次訪問(wèn)該接口;
這個(gè)方法理解了其他就不難了,其他方法就是給這個(gè)方法傳參或者作為校驗(yàn)或數(shù)據(jù)處理的;
下面測(cè)試一下,分別標(biāo)記兩個(gè)接口,一個(gè)使用@Prevent默認(rèn)參數(shù),一個(gè)使用自定義參數(shù):
調(diào)用getToleById接口,意思是60s內(nèi)只能調(diào)用該接口1次:
第一次調(diào)用成功,redis鍵值為1
第二次失敗,需要等60s
redis鍵值變成了2
getRoleList是自定義參數(shù),意思是20s內(nèi)最多只能訪問(wèn)該接口5次:
未超出頻率限制
超出頻率限制
總體流程就是這樣了,aop理解好了不難,也比較實(shí)用,可以在自己項(xiàng)目中使用;
到此這篇關(guān)于java中aop實(shí)現(xiàn)接口訪問(wèn)頻率限制的文章就介紹到這了,更多相關(guān)java aop接口訪問(wèn)頻率限制內(nèi)容請(qǐng)搜索腳本之家以前的文章或繼續(xù)瀏覽下面的相關(guān)文章希望大家以后多多支持腳本之家!
相關(guān)文章
基于Java中進(jìn)制的轉(zhuǎn)換函數(shù)詳解
下面小編就為大家?guī)?lái)一篇基于Java中進(jìn)制的轉(zhuǎn)換函數(shù)詳解。小編覺(jué)得挺不錯(cuò)的,現(xiàn)在就分享給大家,也給大家做個(gè)參考。一起跟隨小編過(guò)來(lái)看看吧2017-07-07基于request.getAttribute與request.getParameter的區(qū)別詳解
本篇文章小編為大家介紹,基于request.getAttribute與request.getParameter的區(qū)別詳解。需要的朋友參考下2013-04-04如何將Java枚舉名稱作為注解的屬性值實(shí)現(xiàn)詳解
這篇文章主要為大家介紹了如何將Java枚舉名稱作為注解的屬性值實(shí)現(xiàn)詳解,有需要的朋友可以借鑒參考下,希望能夠有所幫助,祝大家多多進(jìn)步,早日升職加薪2023-05-05Spring Boot console log 格式自定義方式
這篇文章主要介紹了Spring Boot console log 格式自定義方式,具有很好的參考價(jià)值,希望對(duì)大家有所幫助。如有錯(cuò)誤或未考慮完全的地方,望不吝賜教2021-07-07java計(jì)算方差、標(biāo)準(zhǔn)差(均方差)實(shí)例代碼
在本篇文章里小編給大家分享了關(guān)于java計(jì)算方差、標(biāo)準(zhǔn)差(均方差)實(shí)例代碼以及相關(guān)知識(shí)點(diǎn),需要的朋友們可以參考下。2019-08-08使用RestTemplate訪問(wèn)https實(shí)現(xiàn)SSL請(qǐng)求操作
這篇文章主要介紹了使用RestTemplate訪問(wèn)https實(shí)現(xiàn)SSL請(qǐng)求操作,具有很好的參考價(jià)值,希望對(duì)大家有所幫助。如有錯(cuò)誤或未考慮完全的地方,望不吝賜教2021-10-10理解maven命令package、install、deploy的聯(lián)系與區(qū)別
這篇文章主要介紹了理解maven命令package、install、deploy的聯(lián)系與區(qū)別,文中通過(guò)示例代碼介紹的非常詳細(xì),對(duì)大家的學(xué)習(xí)或者工作具有一定的參考學(xué)習(xí)價(jià)值,需要的朋友們下面隨著小編來(lái)一起學(xué)習(xí)學(xué)習(xí)吧2020-07-07Springboot2 session設(shè)置超時(shí)時(shí)間無(wú)效的解決
這篇文章主要介紹了Springboot2 session設(shè)置超時(shí)時(shí)間無(wú)效的解決方案,具有很好的參考價(jià)值,希望對(duì)大家有所幫助。如有錯(cuò)誤或未考慮完全的地方,望不吝賜教2021-07-07