API 介面防刷
阿新 • • 發佈:2019-05-13
API 介面防刷
顧名思義,想讓某個介面某個人在某段時間內只能請求N次。
在專案中比較常見的問題也有,那就是連點按鈕導致請求多次,以前在web端有表單重複提交,可以通過token 來解決。
除了上面的方法外,前後端配合的方法。現在全部由後端來控制。
原理
在你請求的時候,伺服器通過redis 記錄下你請求的次數,如果次數超過限制就不給訪問。
在redis 儲存的key 是有時效性的,過期就會刪除。
程式碼實現:
為了讓它看起來逼格高一點,所以以自定義註解的方式實現
@RequestLimit
註解
import java.lang.annotation.*; /** * 請求限制的自定義註解 * * @Target 註解可修飾的物件範圍,ElementType.METHOD 作用於方法,ElementType.TYPE 作用於類 * (ElementType)取值有: * 1.CONSTRUCTOR:用於描述構造器 * 2.FIELD:用於描述域 * 3.LOCAL_VARIABLE:用於描述區域性變數 * 4.METHOD:用於描述方法 * 5.PACKAGE:用於描述包 * 6.PARAMETER:用於描述引數 * 7.TYPE:用於描述類、介面(包括註解型別) 或enum宣告 * @Retention定義了該Annotation被保留的時間長短:某些Annotation僅出現在原始碼中,而被編譯器丟棄; * 而另一些卻被編譯在class檔案中;編譯在class檔案中的Annotation可能會被虛擬機器忽略, * 而另一些在class被裝載時將被讀取(請注意並不影響class的執行,因為Annotation與class在使用上是被分離的)。 * 使用這個meta-Annotation可以對 Annotation的“生命週期”限制。 * (RetentionPoicy)取值有: * 1.SOURCE:在原始檔中有效(即原始檔保留) * 2.CLASS:在class檔案中有效(即class保留) * 3.RUNTIME:在執行時有效(即執行時保留) * * @Inherited * 元註解是一個標記註解,@Inherited闡述了某個被標註的型別是被繼承的。 * 如果一個使用了@Inherited修飾的annotation型別被用於一個class,則這個annotation將被用於該class的子類。 */ @Documented @Inherited @Target({ElementType.METHOD,ElementType.TYPE}) @Retention(RetentionPolicy.RUNTIME) public @interface RequestLimit { // 在 second 秒內,最大隻能請求 maxCount 次 int second() default 1; int maxCount() default 1; }
RequestLimitIntercept
攔截器
自定義一個攔截器,請求之前,進行請求次數校驗
import com.alibaba.fastjson.JSONObject; import lombok.extern.slf4j.Slf4j; import org.springframework.beans.factory.annotation.Autowired; import org.springframework.data.redis.core.RedisTemplate; import org.springframework.stereotype.Component; import org.springframework.web.method.HandlerMethod; import org.springframework.web.servlet.handler.HandlerInterceptorAdapter; import top.lrshuai.limit.annotation.RequestLimit; import top.lrshuai.limit.common.ApiResultEnum; import top.lrshuai.limit.common.Result; import javax.servlet.http.HttpServletRequest; import javax.servlet.http.HttpServletResponse; import java.io.IOException; import java.io.PrintWriter; import java.lang.reflect.Method; import java.util.concurrent.TimeUnit; /** * 請求攔截 */ @Slf4j @Component public class RequestLimitIntercept extends HandlerInterceptorAdapter { @Autowired private RedisTemplate<String,Object> redisTemplate; @Override public boolean preHandle(HttpServletRequest request, HttpServletResponse response, Object handler) throws Exception { /** * isAssignableFrom() 判定此 Class 物件所表示的類或介面與指定的 Class 引數所表示的類或介面是否相同,或是否是其超類或超介面 * isAssignableFrom()方法是判斷是否為某個類的父類 * instanceof關鍵字是判斷是否某個類的子類 */ if(handler.getClass().isAssignableFrom(HandlerMethod.class)){ //HandlerMethod 封裝方法定義相關的資訊,如類,方法,引數等 HandlerMethod handlerMethod = (HandlerMethod) handler; Method method = handlerMethod.getMethod(); // 獲取方法中是否包含註解 RequestLimit methodAnnotation = method.getAnnotation(RequestLimit.class); //獲取 類中是否包含註解,也就是controller 是否有註解 RequestLimit classAnnotation = method.getDeclaringClass().getAnnotation(RequestLimit.class); // 如果 方法上有註解就優先選擇方法上的引數,否則類上的引數 RequestLimit requestLimit = methodAnnotation != null?methodAnnotation:classAnnotation; if(requestLimit != null){ if(isLimit(request,requestLimit)){ resonseOut(response,Result.error(ApiResultEnum.REQUST_LIMIT)); return false; } } } return super.preHandle(request, response, handler); } //判斷請求是否受限 public boolean isLimit(HttpServletRequest request,RequestLimit requestLimit){ // 受限的redis 快取key ,因為這裡用瀏覽器做測試,我就用sessionid 來做唯一key,如果是app ,可以使用 使用者ID 之類的唯一標識。 String limitKey = request.getServletPath()+request.getSession().getId(); // 從快取中獲取,當前這個請求訪問了幾次 Integer redisCount = (Integer) redisTemplate.opsForValue().get(limitKey); if(redisCount == null){ //初始 次數 redisTemplate.opsForValue().set(limitKey,1,requestLimit.second(), TimeUnit.SECONDS); }else{ if(redisCount.intValue() >= requestLimit.maxCount()){ return true; } // 次數自增 redisTemplate.opsForValue().increment(limitKey); } return false; } /** * 回寫給客戶端 * @param response * @param result * @throws IOException */ private void resonseOut(HttpServletResponse response, Result result) throws IOException { response.setCharacterEncoding("UTF-8"); response.setContentType("application/json; charset=utf-8"); PrintWriter out = null ; String json = JSONObject.toJSON(result).toString(); out = response.getWriter(); out.append(json); } }
攔截器寫好了,但是還得添加註冊
WebMvcConfig
配置類
因為我的是Springboot2.*
所以只需實現WebMvcConfigurer
如果是springboot1.*
那就繼承自 WebMvcConfigurerAdapter
然後重寫addInterceptors()
新增自定義攔截器即可。
@Slf4j @Component public class WebMvcConfig implements WebMvcConfigurer { @Autowired private RequestLimitIntercept requestLimitIntercept; @Override public void addInterceptors(InterceptorRegistry registry) { log.info("新增攔截"); registry.addInterceptor(requestLimitIntercept); } }
Controller
控制層測試介面,
使用方式:
- 第一種:直接在類上使用註解
@RequestLimit(maxCount = 5,second = 1)
- 第二種:在方法上使用註解
@RequestLimit(maxCount = 5,second = 1)
maxCount 最大的請求數、second 代表時間,單位是秒
預設1秒內,每個介面只能請求一次
@RestController
@RequestMapping("/index")
@RequestLimit(maxCount = 5,second = 1)
public class IndexController {
/**
* @RequestLimit 修飾在方法上,優先使用其引數
* @return
*/
@GetMapping("/test1")
@RequestLimit
public Result test(){
//TODO ...
return Result.ok();
}
/**
* @RequestLimit 修飾在類上,用的是類的引數
* @return
*/
@GetMapping("/test2")
public Result test2(){
//TODO ...
return Result.ok();
}
}
如果在類和方法上同時有@RequestLimit
註解 ,以方法上的引數為準,好像註釋有點多了。
覺得不錯請點贊支援,歡迎留言或進我的個人群855801563領取【架構資料專題目合集90期】、【BATJTMD大廠JAVA面試真題1000+】,本群專用於學習交流技術、分享面試機會,拒絕廣告,我也會在群內不定