Redis+攔截器實現(xiàn)接口防刷
前言
我們在瀏覽網(wǎng)站后臺的時候,假如我們頻繁請求,那么網(wǎng)站會提示 “請勿重復提交” 的字樣,那么這個功能究竟有什么用呢,又是如何實現(xiàn)的呢?
其實這就是接口防刷的一種處理方式,通過在一定時間內(nèi)限制同一用戶對同一個接口的請求次數(shù),其目的是為了防止惡意訪問導致服務器和數(shù)據(jù)庫的壓力增大,也可以防止用戶重復提交。
思路分析
接口防刷有很多種實現(xiàn)思路,例如:攔截器/AOP+Redis、攔截器/AOP+本地緩存、前端限制等等很多種實現(xiàn)思路,在這里我們來講一下 攔截器+Redis 的實現(xiàn)方式。
其原理就是 在接口請求前由攔截器攔截下來,然后去 redis 中查詢是否已經(jīng)存在請求了,如果不存在則將請求緩存,若已經(jīng)存在則返回異常。具體可以參考下圖
具體實現(xiàn)
注:以下代碼中的 AjaxResult
為統(tǒng)一返回對象,這里就不貼出代碼了,大家可以根據(jù)自己的業(yè)務場景來編寫。
編寫 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 ****************/ /** * 指定緩存失效時間 * * @param key 鍵 * @param time 時間(秒) * @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 獲取過期時間 * * @param key 鍵 不能為null * @return 時間(秒) 返回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 可以傳一個值 或多個 */ @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è)置時間 * * @param key 鍵 * @param value 值 * @param time 時間(秒) 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 重復請求攔截 * @date 2023-08-10 14:14 */ @Component public class RepeatRequestIntercept extends HandlerInterceptorAdapter { @Autowired private RedisUtils redisUtils; /** * 限定時間 單位:秒 */ private final int seconds = 1; /** * 限定請求次數(shù) */ private final int max = 1; public boolean preHandle(HttpServletRequest request, HttpServletResponse response, Object handler) throws Exception { //判斷請求是否為方法的請求 if (handler instanceof HandlerMethod) { String key = request.getRemoteAddr() + "-" + request.getMethod() + "-" + request.getRequestURL(); Object requestCountObj = redisUtils.get(key); if (Objects.isNull(requestCountObj)) { //若為空則為第一次請求 redisUtils.set(key, 1, seconds); } else { response.setContentType("application/json;charset=utf-8"); ServletOutputStream os = response.getOutputStream(); AjaxResult<Void> result = AjaxResult.error(100, "請求已提交,請勿重復請求"); String jsonString = JSON.toJSONString(result); os.write(jsonString.getBytes()); os.flush(); os.close(); return false; } } return true; } }
然后我們 將攔截器注冊到容器中
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); } }
我們再來編寫一個接口用于測試
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é)果是否符合我們的預期:
1秒內(nèi)的第一次請求:
1秒內(nèi)的第二次請求:
確實已經(jīng)達到了我們的預期,但是如果我們對特定接口進行攔截,或?qū)Σ煌涌诘南薅〝r截時間和次數(shù)不同的話,這種實現(xiàn)方式無法滿足我們的需求,所以我們要提出改進。
改進
我們可以去寫一個自定義的注解,并將 seconds
和 max
設(shè)置為該注解的屬性,再在攔截器中判斷請求的方法是否包含該注解,如果包含則執(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 { /** * 限定時間 */ int seconds() default 1; /** * 限定請求次數(shù) */ int max() default 1; }
改進 RepeatRequestIntercept
/** * @author Bummon * @description 重復請求攔截 * @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 { //判斷請求是否為方法的請求 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)) { //若為空則為第一次請求 redisUtils.set(key, 1, seconds); } else { //限定時間內(nèi)的第n次請求 int requestCount = Integer.parseInt(requestCountObj.toString()); //判斷是否超過最大限定請求次數(shù) if (requestCount < max) { //未超過則請求次數(shù)+1 redisUtils.incr(key, 1); } else { //否則拒絕請求并返回信息 refuse(response); return false; } } } return true; } /** * @param response * @date 2023-08-10 15:25 * @author Bummon * @description 拒絕請求并返回結(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, "請求已提交,請勿重復請求"); String jsonString = JSON.toJSONString(result); os.write(jsonString.getBytes()); os.flush(); os.close(); } }
這樣我們就可以實現(xiàn)我們的需求了。
到此這篇關(guān)于Redis+攔截器實現(xiàn)接口防刷的文章就介紹到這了,更多相關(guān)接口防刷內(nèi)容請搜索腳本之家以前的文章或繼續(xù)瀏覽下面的相關(guān)文章希望大家以后多多支持腳本之家!
相關(guān)文章
使用SpringBoot?+?Redis?實現(xiàn)接口限流的方式
這篇文章主要介紹了SpringBoot?+?Redis?實現(xiàn)接口限流,Redis?除了做緩存,還能干很多很多事情:分布式鎖、限流、處理請求接口冪等,文中給大家提到了限流注解的創(chuàng)建方式,需要的朋友可以參考下2022-05-05基于 Redis 的 JWT令牌失效處理方案(實現(xiàn)步驟)
當用戶登錄狀態(tài)到登出狀態(tài)時,對應的JWT的令牌需要設(shè)置為失效狀態(tài),這時可以使用基于Redis 的黑名單方案來實現(xiàn)JWT令牌失效,本文給大家分享基于 Redis 的 JWT令牌失效處理方案,感興趣的朋友一起看看吧2024-03-03關(guān)于redis可視化工具讀取數(shù)據(jù)亂碼問題
大家來聊一聊在日常操作redis時用的是什么工具,redis提供的一些命令你都了解了嗎,今天通過本文給大家介紹redis可視化工具讀取數(shù)據(jù)亂碼問題,感興趣的朋友跟隨小編一起看看吧2021-07-07