顧名思義,想讓某個接口某我的在某段時間內只能請求N次。 在項目中比較常見的問題也有,那就是連點按鈕致使請求屢次,之前在web端有表單重複提交,能夠經過token 來解決。 除了上面的方法外,先後端配合的方法。如今所有由後端來控制。java
在你請求的時候,服務器經過redis 記錄下你請求的次數,若是次數超過限制就不給訪問。 在redis 保存的key 是有時效性的,過時就會刪除。web
爲了讓它看起來逼格高一點,因此以自定義註解的方式實現redis
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;
}
複製代碼
自定義一個攔截器,請求以前,進行請求次數校驗spring
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);
}
}
複製代碼
攔截器寫好了,可是還得添加註冊json
由於個人是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 控制層測試接口,瀏覽器
maxCount 最大的請求數、second 表明時間,單位是秒緩存
默認1秒內,每一個接口只能請求一次springboot
@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註解 ,以方法上的參數爲準,好像註釋有點多了。bash