Redis+攔截器實(shí)現(xiàn)接口防刷
前言
我們?cè)跒g覽網(wǎng)站后臺(tái)的時(shí)候,假如我們頻繁請(qǐng)求,那么網(wǎng)站會(huì)提示 “請(qǐng)勿重復(fù)提交” 的字樣,那么這個(gè)功能究竟有什么用呢,又是如何實(shí)現(xiàn)的呢?
其實(shí)這就是接口防刷的一種處理方式,通過在一定時(shí)間內(nèi)限制同一用戶對(duì)同一個(gè)接口的請(qǐng)求次數(shù),其目的是為了防止惡意訪問導(dǎo)致服務(wù)器和數(shù)據(jù)庫的壓力增大,也可以防止用戶重復(fù)提交。
思路分析
接口防刷有很多種實(shí)現(xiàn)思路,例如:攔截器/AOP+Redis、攔截器/AOP+本地緩存、前端限制等等很多種實(shí)現(xiàn)思路,在這里我們來講一下 攔截器+Redis 的實(shí)現(xiàn)方式。
其原理就是 在接口請(qǐng)求前由攔截器攔截下來,然后去 redis 中查詢是否已經(jīng)存在請(qǐng)求了,如果不存在則將請(qǐng)求緩存,若已經(jīng)存在則返回異常。具體可以參考下圖
具體實(shí)現(xiàn)
注:以下代碼中的 AjaxResult
為統(tǒng)一返回對(duì)象,這里就不貼出代碼了,大家可以根據(jù)自己的業(yè)務(wù)場(chǎng)景來編寫。
編寫 RedisUtils
import com.apply.core.exception.MyRedidsException; import org.springframework.beans.factory.annotation.Autowired; import org.springframework.data.redis.core.RedisTemplate; import org.springframework.stereotype.Component; import org.springframework.util.CollectionUtils; import java.util.Collections; import java.util.List; import java.util.Map; import java.util.Set; import java.util.concurrent.TimeUnit; /** * Redis工具類 */ @Component public class RedisUtils { @Autowired private RedisTemplate<String, Object> redisTemplate; /****************** common start ****************/ /** * 指定緩存失效時(shí)間 * * @param key 鍵 * @param time 時(shí)間(秒) * @return */ public boolean expire(String key, long time) { try { if (time > 0) { redisTemplate.expire(key, time, TimeUnit.SECONDS); } return true; } catch (Exception e) { e.printStackTrace(); return false; } } /** * 根據(jù)key 獲取過期時(shí)間 * * @param key 鍵 不能為null * @return 時(shí)間(秒) 返回0代表為永久有效 */ public long getExpire(String key) { return redisTemplate.getExpire(key, TimeUnit.SECONDS); } /** * 判斷key是否存在 * * @param key 鍵 * @return true 存在 false不存在 */ public boolean hasKey(String key) { try { return redisTemplate.hasKey(key); } catch (Exception e) { e.printStackTrace(); return false; } } /** * 刪除緩存 * * @param key 可以傳一個(gè)值 或多個(gè) */ @SuppressWarnings("unchecked") public void del(String... key) { if (key != null && key.length > 0) { if (key.length == 1) { redisTemplate.delete(key[0]); } else { redisTemplate.delete(CollectionUtils.arrayToList(key)); } } } /****************** common end ****************/ /****************** String start ****************/ /** * 普通緩存獲取 * * @param key 鍵 * @return 值 */ public Object get(String key) { return key == null ? null : redisTemplate.opsForValue().get(key); } /** * 普通緩存放入 * * @param key 鍵 * @param value 值 * @return true成功 false失敗 */ public boolean set(String key, Object value) { try { redisTemplate.opsForValue().set(key, value); return true; } catch (Exception e) { e.printStackTrace(); return false; } } /** * 普通緩存放入并設(shè)置時(shí)間 * * @param key 鍵 * @param value 值 * @param time 時(shí)間(秒) time要大于0 如果time小于等于0 將設(shè)置無限期 * @return true成功 false 失敗 */ public boolean set(String key, Object value, long time) { try { if (time > 0) { redisTemplate.opsForValue().set(key, value, time, TimeUnit.SECONDS); } else { set(key, value); } return true; } catch (Exception e) { e.printStackTrace(); return false; } } /** * 遞增 * * @param key 鍵 * @param delta 要增加幾(大于0) * @return */ public long incr(String key, long delta) { if (delta < 0) { throw new MyRedidsException("遞增因子必須大于0"); } return redisTemplate.opsForValue().increment(key, delta); } /** * 遞減 * * @param key 鍵 * @param delta 要減少幾(小于0) * @return */ public long decr(String key, long delta) { if (delta < 0) { throw new MyRedidsException("遞減因子必須大于0"); } return redisTemplate.opsForValue().increment(key, -delta); } /****************** String end ****************/ }
定義Interceptor
import com.alibaba.fastjson.JSON; import com.apply.common.utils.redis.RedisUtils; import com.apply.common.validator.annotation.AccessLimit; import com.apply.core.http.AjaxResult; import org.springframework.beans.factory.annotation.Autowired; import org.springframework.stereotype.Component; import org.springframework.web.method.HandlerMethod; import org.springframework.web.servlet.handler.HandlerInterceptorAdapter; import javax.servlet.ServletOutputStream; import javax.servlet.http.HttpServletRequest; import javax.servlet.http.HttpServletResponse; import java.io.IOException; import java.util.Objects; /** * @author Bummon * @description 重復(fù)請(qǐng)求攔截 * @date 2023-08-10 14:14 */ @Component public class RepeatRequestIntercept extends HandlerInterceptorAdapter { @Autowired private RedisUtils redisUtils; /** * 限定時(shí)間 單位:秒 */ private final int seconds = 1; /** * 限定請(qǐng)求次數(shù) */ private final int max = 1; public boolean preHandle(HttpServletRequest request, HttpServletResponse response, Object handler) throws Exception { //判斷請(qǐng)求是否為方法的請(qǐng)求 if (handler instanceof HandlerMethod) { String key = request.getRemoteAddr() + "-" + request.getMethod() + "-" + request.getRequestURL(); Object requestCountObj = redisUtils.get(key); if (Objects.isNull(requestCountObj)) { //若為空則為第一次請(qǐng)求 redisUtils.set(key, 1, seconds); } else { response.setContentType("application/json;charset=utf-8"); ServletOutputStream os = response.getOutputStream(); AjaxResult<Void> result = AjaxResult.error(100, "請(qǐng)求已提交,請(qǐng)勿重復(fù)請(qǐng)求"); String jsonString = JSON.toJSONString(result); os.write(jsonString.getBytes()); os.flush(); os.close(); return false; } } return true; } }
然后我們 將攔截器注冊(cè)到容器中
import com.apply.common.validator.intercept.RepeatRequestIntercept; import com.apply.core.base.entity.Constants; import org.springframework.beans.factory.annotation.Autowired; import org.springframework.context.annotation.Configuration; import org.springframework.web.servlet.config.annotation.CorsRegistry; import org.springframework.web.servlet.config.annotation.InterceptorRegistry; import org.springframework.web.servlet.config.annotation.ResourceHandlerRegistry; import org.springframework.web.servlet.config.annotation.WebMvcConfigurer; /** * @author Bummon * @description * @date 2023-08-10 14:17 */ @Configuration public class WebConfig implements WebMvcConfigurer { @Autowired private RepeatRequestIntercept repeatRequestIntercept; @Override public void addInterceptors(InterceptorRegistry registry) { registry.addInterceptor(repeatRequestIntercept); } }
我們?cè)賮砭帉懸粋€(gè)接口用于測(cè)試
import com.apply.common.validator.annotation.AccessLimit; import org.springframework.web.bind.annotation.GetMapping; import org.springframework.web.bind.annotation.RequestMapping; import org.springframework.web.bind.annotation.RestController; /** * @author Bummon * @description * @date 2023-08-10 14:35 */ @RestController public class TestController { @GetMapping("/test") public String test(){ return "SUCCESS"; } }
最后我們來看一下結(jié)果是否符合我們的預(yù)期:
1秒內(nèi)的第一次請(qǐng)求:
1秒內(nèi)的第二次請(qǐng)求:
確實(shí)已經(jīng)達(dá)到了我們的預(yù)期,但是如果我們對(duì)特定接口進(jìn)行攔截,或?qū)Σ煌涌诘南薅〝r截時(shí)間和次數(shù)不同的話,這種實(shí)現(xiàn)方式無法滿足我們的需求,所以我們要提出改進(jìn)。
改進(jìn)
我們可以去寫一個(gè)自定義的注解,并將 seconds
和 max
設(shè)置為該注解的屬性,再在攔截器中判斷請(qǐng)求的方法是否包含該注解,如果包含則執(zhí)行攔截方法,如果不包含則直接返回。
自定義注解 RequestLimit
import java.lang.annotation.ElementType; import java.lang.annotation.Retention; import java.lang.annotation.RetentionPolicy; import java.lang.annotation.Target; /** * @author Bummon * @description 冪等性注解 * @date 2023-08-10 15:10 */ @Retention(RetentionPolicy.RUNTIME) @Target(ElementType.METHOD) public @interface RequestLimit { /** * 限定時(shí)間 */ int seconds() default 1; /** * 限定請(qǐng)求次數(shù) */ int max() default 1; }
改進(jìn) RepeatRequestIntercept
/** * @author Bummon * @description 重復(fù)請(qǐng)求攔截 * @date 2023-08-10 15:14 */ @Component public class RepeatRequestIntercept extends HandlerInterceptorAdapter { @Autowired private RedisUtils redisUtils; @Override public boolean preHandle(HttpServletRequest request, HttpServletResponse response, Object handler) throws Exception { //判斷請(qǐng)求是否為方法的請(qǐng)求 if (handler instanceof HandlerMethod) { HandlerMethod hm = (HandlerMethod) handler; //獲取方法中是否有冪等性注解 RequestLimit anno = hm.getMethodAnnotation(RequestLimit.class); //若注解為空則直接返回 if (Objects.isNull(anno)) { return true; } int seconds = anno.seconds(); int max = anno.max(); String key = request.getRemoteAddr() + "-" + request.getMethod() + "-" + request.getRequestURL(); Object requestCountObj = redisUtils.get(key); if (Objects.isNull(requestCountObj)) { //若為空則為第一次請(qǐng)求 redisUtils.set(key, 1, seconds); } else { //限定時(shí)間內(nèi)的第n次請(qǐng)求 int requestCount = Integer.parseInt(requestCountObj.toString()); //判斷是否超過最大限定請(qǐng)求次數(shù) if (requestCount < max) { //未超過則請(qǐng)求次數(shù)+1 redisUtils.incr(key, 1); } else { //否則拒絕請(qǐng)求并返回信息 refuse(response); return false; } } } return true; } /** * @param response * @date 2023-08-10 15:25 * @author Bummon * @description 拒絕請(qǐng)求并返回結(jié)果 */ private void refuse(HttpServletResponse response) throws IOException { response.setContentType("application/json;charset=utf-8"); ServletOutputStream os = response.getOutputStream(); AjaxResult<Void> result = AjaxResult.error(100, "請(qǐng)求已提交,請(qǐng)勿重復(fù)請(qǐng)求"); String jsonString = JSON.toJSONString(result); os.write(jsonString.getBytes()); os.flush(); os.close(); } }
這樣我們就可以實(shí)現(xiàn)我們的需求了。
到此這篇關(guān)于Redis+攔截器實(shí)現(xiàn)接口防刷的文章就介紹到這了,更多相關(guān)接口防刷內(nèi)容請(qǐng)搜索腳本之家以前的文章或繼續(xù)瀏覽下面的相關(guān)文章希望大家以后多多支持腳本之家!
相關(guān)文章
使用SpringBoot?+?Redis?實(shí)現(xiàn)接口限流的方式
這篇文章主要介紹了SpringBoot?+?Redis?實(shí)現(xiàn)接口限流,Redis?除了做緩存,還能干很多很多事情:分布式鎖、限流、處理請(qǐng)求接口冪等,文中給大家提到了限流注解的創(chuàng)建方式,需要的朋友可以參考下2022-05-05基于 Redis 的 JWT令牌失效處理方案(實(shí)現(xiàn)步驟)
當(dāng)用戶登錄狀態(tài)到登出狀態(tài)時(shí),對(duì)應(yīng)的JWT的令牌需要設(shè)置為失效狀態(tài),這時(shí)可以使用基于Redis 的黑名單方案來實(shí)現(xiàn)JWT令牌失效,本文給大家分享基于 Redis 的 JWT令牌失效處理方案,感興趣的朋友一起看看吧2024-03-03Java實(shí)現(xiàn)多級(jí)緩存的方法詳解
對(duì)于高并發(fā)系統(tǒng)來說,有三個(gè)重要的機(jī)制來保障其高效運(yùn)行,它們分別是:緩存、限流和熔斷,所以本文就來和大家探討一下多級(jí)緩存的實(shí)現(xiàn)方法,希望對(duì)大家有所幫助2024-02-02關(guān)于redis可視化工具讀取數(shù)據(jù)亂碼問題
大家來聊一聊在日常操作redis時(shí)用的是什么工具,redis提供的一些命令你都了解了嗎,今天通過本文給大家介紹redis可視化工具讀取數(shù)據(jù)亂碼問題,感興趣的朋友跟隨小編一起看看吧2021-07-07