教你實現(xiàn)Java接口防刷
前言
我們在瀏覽網(wǎng)站后臺的時候,假如我們頻繁請求,那么網(wǎng)站會提示 “請勿重復(fù)提交” 的字樣,那么這個功能究竟有什么用呢,又是如何實現(xiàn)的呢?
其實這就是接口防刷的一種處理方式,通過在一定時間內(nèi)限制同一用戶對同一個接口的請求次數(shù),其目的是為了防止惡意訪問導(dǎo)致服務(wù)器和數(shù)據(jù)庫的壓力增大,也可以防止用戶重復(fù)提交。
思路分析
接口防刷有很多種實現(xiàn)思路,例如:攔截器/AOP+Redis、攔截器/AOP+本地緩存、前端限制等等很多種實現(xiàn)思路,在這里我們來講一下 攔截器+Redis 的實現(xiàn)方式。
其原理就是 在接口請求前由攔截器攔截下來,然后去 redis 中查詢是否已經(jīng)存在請求了,如果不存在則將請求緩存,若已經(jīng)存在則返回異常。具體可以參考下圖
具體實現(xiàn)
注:以下代碼中的 AjaxResult 為統(tǒng)一返回對象,這里就不貼出代碼了,大家可以根據(jù)自己的業(yè)務(wù)場景來編寫。
編寫 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; /** * @description 重復(fù)請求攔截 * @date 2023-08-13 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, "請求已提交,請勿重復(fù)請求"); 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; /** * @description * @date 2023-08-13 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; /** * @description * @date 2023-08-13 14:35 */ @RestController public class TestController { @GetMapping("/test") public String test(){ return "SUCCESS"; } }
最后我們來看一下結(jié)果是否符合我們的預(yù)期:
1秒內(nèi)的第一次請求:
1秒內(nèi)的第二次請求:
確實已經(jīng)達到了我們的預(yù)期,但是如果我們對特定接口進行攔截,或?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; /** * @description 冪等性注解 * @date 2023-08-13 15:10 */ @Retention(RetentionPolicy.RUNTIME) @Target(ElementType.METHOD) public @interface RequestLimit { /** * 限定時間 */ int seconds() default 1; /** * 限定請求次數(shù) */ int max() default 1; }
改進 RepeatRequestIntercept
/** * @description 重復(fù)請求攔截 * @date 2023-08-13 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; }
/** * @date 2023-08-14 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, "請求已提交,請勿重復(fù)請求"); String jsonString = JSON.toJSONString(result); os.write(jsonString.getBytes()); os.flush(); os.close(); } }
這樣我們就可以實現(xiàn)我們的需求了。
到此這篇關(guān)于教你實現(xiàn)Java接口防刷的文章就介紹到這了,更多相關(guān)Java接口防刷內(nèi)容請搜索腳本之家以前的文章或繼續(xù)瀏覽下面的相關(guān)文章希望大家以后多多支持腳本之家!
相關(guān)文章
淺析如何在SpringBoot中實現(xiàn)數(shù)據(jù)脫敏
脫敏是指在不改變原數(shù)據(jù)結(jié)構(gòu)的前提下,通過某種方式處理數(shù)據(jù),使數(shù)據(jù)不能直接暴露用戶的真實信息,下面我們就來看看SpringBoot中實現(xiàn)數(shù)據(jù)脫敏的具體方法吧2024-03-03基于spring cloud多個消費端重復(fù)定義feign client的問題
這篇文章主要介紹了spring cloud多個消費端重復(fù)定義feign client的問題,具有很好的參考價值,希望對大家有所幫助。如有錯誤或未考慮完全的地方,望不吝賜教2021-10-10Spring Cloud Gateway打造可擴展的微服務(wù)網(wǎng)關(guān)
微服務(wù)網(wǎng)關(guān)是一個位于客戶端和后端微服務(wù)之間的服務(wù)器,用于處理所有與客戶端的通信,Spring Cloud Gateway都是一個值得考慮的選擇,它將幫助您更好地管理和保護您的微服務(wù),感興趣的朋友一起看看吧2023-11-11IDEA巧用Postfix Completion讓碼速起飛(小技巧)
這篇文章主要介紹了IDEA巧用Postfix Completion讓碼速起飛,文中通過示例代碼介紹的非常詳細,對大家的學(xué)習(xí)或者工作具有一定的參考學(xué)習(xí)價值,需要的朋友們下面隨著小編來一起學(xué)習(xí)學(xué)習(xí)吧2020-08-08