API 接口防刷
顧名思義,想讓某個(gè)接口某個(gè)人在某段時(shí)間內(nèi)只能請(qǐng)求N次嘁圈。
在項(xiàng)目中比較常見(jiàn)的問(wèn)題也有肛炮,那就是連點(diǎn)按鈕導(dǎo)致請(qǐng)求多次,以前在web端有表單重復(fù)提交蚪腐,可以通過(guò)token 來(lái)解決箭昵。
除了上面的方法外,前后端配合的方法』丶荆現(xiàn)在全部由后端來(lái)控制家制。
原理
在你請(qǐng)求的時(shí)候,服務(wù)器通過(guò)redis 記錄下你請(qǐng)求的次數(shù)泡一,如果次數(shù)超過(guò)限制就不給訪問(wèn)颤殴。
在redis 保存的key 是有時(shí)效性的,過(guò)期就會(huì)刪除鼻忠。
代碼實(shí)現(xiàn):
為了讓它看起來(lái)逼格高一點(diǎn)涵但,所以以自定義注解的方式實(shí)現(xiàn)
@RequestLimit
注解
import java.lang.annotation.*;
/**
* 請(qǐng)求限制的自定義注解
*
* @Target 注解可修飾的對(duì)象范圍,ElementType.METHOD 作用于方法帖蔓,ElementType.TYPE 作用于類
* (ElementType)取值有:
* 1.CONSTRUCTOR:用于描述構(gòu)造器
* 2.FIELD:用于描述域
* 3.LOCAL_VARIABLE:用于描述局部變量
* 4.METHOD:用于描述方法
* 5.PACKAGE:用于描述包
* 6.PARAMETER:用于描述參數(shù)
* 7.TYPE:用于描述類矮瘟、接口(包括注解類型) 或enum聲明
* @Retention定義了該Annotation被保留的時(shí)間長(zhǎng)短:某些Annotation僅出現(xiàn)在源代碼中,而被編譯器丟棄塑娇;
* 而另一些卻被編譯在class文件中澈侠;編譯在class文件中的Annotation可能會(huì)被虛擬機(jī)忽略,
* 而另一些在class被裝載時(shí)將被讀嚷癯辍(請(qǐng)注意并不影響class的執(zhí)行哨啃,因?yàn)锳nnotation與class在使用上是被分離的)。
* 使用這個(gè)meta-Annotation可以對(duì) Annotation的“生命周期”限制奇瘦。
* (RetentionPoicy)取值有:
* 1.SOURCE:在源文件中有效(即源文件保留)
* 2.CLASS:在class文件中有效(即class保留)
* 3.RUNTIME:在運(yùn)行時(shí)有效(即運(yùn)行時(shí)保留)
*
* @Inherited
* 元注解是一個(gè)標(biāo)記注解,@Inherited闡述了某個(gè)被標(biāo)注的類型是被繼承的劲弦。
* 如果一個(gè)使用了@Inherited修飾的annotation類型被用于一個(gè)class耳标,則這個(gè)annotation將被用于該class的子類。
*/
@Documented
@Inherited
@Target({ElementType.METHOD,ElementType.TYPE})
@Retention(RetentionPolicy.RUNTIME)
public @interface RequestLimit {
// 在 second 秒內(nèi)邑跪,最大只能請(qǐng)求 maxCount 次
int second() default 1;
int maxCount() default 1;
}
RequestLimitIntercept
攔截器
自定義一個(gè)攔截器次坡,請(qǐng)求之前呼猪,進(jìn)行請(qǐng)求次數(shù)校驗(yàn)
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;
/**
* 請(qǐng)求攔截
*/
@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 對(duì)象所表示的類或接口與指定的 Class 參數(shù)所表示的類或接口是否相同,或是否是其超類或超接口
* isAssignableFrom()方法是判斷是否為某個(gè)類的父類
* instanceof關(guān)鍵字是判斷是否某個(gè)類的子類
*/
if(handler.getClass().isAssignableFrom(HandlerMethod.class)){
//HandlerMethod 封裝方法定義相關(guān)的信息,如類,方法,參數(shù)等
HandlerMethod handlerMethod = (HandlerMethod) handler;
Method method = handlerMethod.getMethod();
// 獲取方法中是否包含注解
RequestLimit methodAnnotation = method.getAnnotation(RequestLimit.class);
//獲取 類中是否包含注解砸琅,也就是controller 是否有注解
RequestLimit classAnnotation = method.getDeclaringClass().getAnnotation(RequestLimit.class);
// 如果 方法上有注解就優(yōu)先選擇方法上的參數(shù)宋距,否則類上的參數(shù)
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);
}
//判斷請(qǐng)求是否受限
public boolean isLimit(HttpServletRequest request,RequestLimit requestLimit){
// 受限的redis 緩存key ,因?yàn)檫@里用瀏覽器做測(cè)試,我就用sessionid 來(lái)做唯一key,如果是app ,可以使用 用戶ID 之類的唯一標(biāo)識(shí)症脂。
String limitKey = request.getServletPath()+request.getSession().getId();
// 從緩存中獲取谚赎,當(dāng)前這個(gè)請(qǐng)求訪問(wèn)了幾次
Integer redisCount = (Integer) redisTemplate.opsForValue().get(limitKey);
if(redisCount == null){
//初始 次數(shù)
redisTemplate.opsForValue().set(limitKey,1,requestLimit.second(), TimeUnit.SECONDS);
}else{
if(redisCount.intValue() >= requestLimit.maxCount()){
return true;
}
// 次數(shù)自增
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);
}
}
攔截器寫好了,但是還得添加注冊(cè)
WebMvcConfig
配置類
因?yàn)槲业氖?code>Springboot2.* 所以只需實(shí)現(xiàn)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
控制層測(cè)試接口壶唤,
使用方式:
- 第一種:直接在類上使用注解
@RequestLimit(maxCount = 5,second = 1)
- 第二種:在方法上使用注解
@RequestLimit(maxCount = 5,second = 1)
maxCount 最大的請(qǐng)求數(shù)、second 代表時(shí)間棕所,單位是秒
默認(rèn)1秒內(nèi)闸盔,每個(gè)接口只能請(qǐng)求一次
@RestController
@RequestMapping("/index")
@RequestLimit(maxCount = 5,second = 1)
public class IndexController {
/**
* @RequestLimit 修飾在方法上,優(yōu)先使用其參數(shù)
* @return
*/
@GetMapping("/test1")
@RequestLimit
public Result test(){
//TODO ...
return Result.ok();
}
/**
* @RequestLimit 修飾在類上琳省,用的是類的參數(shù)
* @return
*/
@GetMapping("/test2")
public Result test2(){
//TODO ...
return Result.ok();
}
}
如果在類和方法上同時(shí)有@RequestLimit
注解 ,以方法上的參數(shù)為準(zhǔn)迎吵,好像注釋有點(diǎn)多了。
代碼地址
完整的代碼针贬,如下地址