1 為什么要做接口防刷?
如果你的服務(wù)器應(yīng)用,被一些人而已攻擊,寫入腳本不停的刷服務(wù)端的某一個接口,這樣服務(wù)端的壓力聚會非常的,甚至可能給服務(wù)端帶來災(zāi)難,如果是涉及支付相關(guān)的服務(wù)那就更加損失慘重。因此我們就可以做一個接口防刷的功能,如果服務(wù)端接收到了某一個用戶端請求數(shù)在某個時段超過了我們設(shè)定的數(shù)量就可以直接不讓其訪問了,也就做到了接口防刷。
2 API接口防刷原理
原理其實很簡單,在服務(wù)端記錄客戶端的請求次數(shù),如果客戶端請求數(shù)量超過設(shè)置的請求數(shù)就直接不讓訪問了。
3 SpringBoot 實現(xiàn)API接口防刷
為了讓我們的代碼非常優(yōu)雅,這里采用自定義注解的方式來實現(xiàn),同時用redis來記錄客戶端請求次數(shù)。
3.1 自定義API防刷注解
下面是自定義防止客戶端而已刷接口的注解。
代碼示例如下:
package com.test.merservice.controller.manage;
import JAVA.lang.annotation.*;
/**
* API防刷自定義注解(設(shè)置默認每秒只能請求1次)
*/
@Documented
@Inherited
@Target({ElementType.METHOD,ElementType.TYPE})
@Retention(RetentionPolicy.RUNTIME)
public @interface RequestAntiBrushing {
int second() default 1;//時間范圍內(nèi)(秒)
int maxCount() default 1;//最大請求書
}
3.2 自定義API防刷請求攔截器
自定義攔截器來完成API防刷,其主要功能邏輯就是在請求之前攔截請求,驗證請求數(shù)是否超過設(shè)定的限制。
diama.NETic示例如下:
package com.test.merservice.controller.manage;
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 javax.servlet.http.HttpServletRequest;
import javax.servlet.http.HttpServletResponse;
import java.io.IOException;
import java.io.PrintWriter;
import java.lang.reflect.Method;
/**
* 客戶端API防刷攔截器
*/
@Slf4j
@Component
public class RequestAntiBrushingIntercept extends HandlerInterceptorAdapter {
@Autowired
private RedisTemplate<String,Object> redisTemplate;
@Override
public boolean preHandle(HttpServletRequest request, HttpServletResponse response, Object handler) throws Exception {
/**
* instanceof關(guān)鍵字是判斷是否某個類的子類
*/
if(handler.getClass().isAssignableFrom(HandlerMethod.class)){//isAssignableFrom()判定此 Class 對象所表示的類或接口與指定的 Class 參數(shù)所表示的類或接口是否相同,或是否是其超類或超接口(isAssignableFrom()方法是判斷是否為某個類的父類)
//HandlerMethod 封裝方法定義相關(guān)的信息,如類,方法,參數(shù)等
HandlerMethod handlerMethod = (HandlerMethod) handler;
Method method = handlerMethod.getMethod();
// 獲取方法中是否包含注解
RequestAntiBrushing methodAnnotation = method.getAnnotation(RequestAntiBrushing.class);
//獲取 類中是否包含注解,也就是controller 是否有注解
RequestAntiBrushing classAnnotation = method.getDeclaringClass().getAnnotation(RequestAntiBrushing.class);
// 如果 方法上有注解就優(yōu)先選擇方法上的參數(shù),否則類上的參數(shù)
RequestAntiBrushing 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,RequestAntiBrushing requestLimit){
// 受限的redis 緩存key ,因為這里用瀏覽器做測試,我就用sessionid 來做唯一key,如果是App ,可以使用 用戶ID 之類的唯一標(biāo)識。
String limitKey = request.getServletPath()+request.getSession().getId();
// 從緩存中獲取,當(dāng)前這個請求訪問了幾次
//Integer redisCount = (Integer) redisTemplate.opsForValue().get(limitKey);
//下面取原子類,Integer本身是線程不安全的,避免并發(fā)問題
RedisAtomicInteger redisCount = new RedisAtomicInteger(limitKey, this.redisTemplate.getConnectionFactory());
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;
}
/**
* 把結(jié)果返回給客戶端
* @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);
}
}
3.3 自定義攔截器注冊
package com.test.merservice.controller.manage;
import lombok.extern.slf4j.Slf4j;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.stereotype.Component;
import org.springframework.web.servlet.config.annotation.InterceptorRegistry;
import org.springframework.web.servlet.config.annotation.WebMvcConfigurer;
@Slf4j
@Component
public class WebMvcConfig implements WebMvcConfigurer {
@Autowired
private RequestAntiBrushingIntercept requestAntiBrushingIntercept;
@Override
public void addInterceptors(InterceptorRegistry registry) {
log.info("注冊攔截");
registry.addInterceptor(requestAntiBrushingIntercept);
}
}
3.4 API防刷注解使用
注解默認是客戶端每秒最大請求1次,這里注解設(shè)置為每秒最大請求3次。
package com.sllt.merservice.controller.manage;
import io.seata.core.model.Result;
import org.springframework.web.bind.annotation.GetMapping;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.RestController;
@RestController
@RequestMapping("/user/index")
public class IndexController {
@PostMapping("/index")
@RequestAntiBrushing(maxCount = 3,second = 1)
public Result test(){
//調(diào)用service層業(yè)務(wù)省略
return Result.ok();
}
}