1.為什么要限流
當我們設計接口時,需要考慮的因素有很多,其中例如如在設計獲取短信驗證碼的接口時糜芳,第一個想到的就是脐瑰,接口如何去實現(xiàn)訪問控制妖枚,好比如我只能讓你1分鐘之內(nèi)最多請求1次,或者其他規(guī)則蚪黑,這樣很大程度上對接口起到一定的保護盅惜。防止對接口的惡意請求,減少不必要的資源浪費忌穿。當然并非所有接口都需要做這些限制抒寂,這也需要根據(jù)實際業(yè)務而定。
2.怎么限流
基于springboot而言掠剑,我們想到的是通過redis的自加:incr來實現(xiàn)屈芜。我們可以通過用戶的唯一標識來設計成redis的key,值為單位時間內(nèi)用戶的請求次數(shù)。
3.實現(xiàn)
基于朴译,是什么井佑,為什么,怎么做三部曲眠寿,閑話不多說躬翁,直接上代碼。
/**
* 注解用戶訪問控制
* 在 second 秒內(nèi)盯拱,最大只能請求 maxCount 次
* @author Json
* @date 2022/3/9 19:33
*/
@Documented
@Inherited
@Target({ElementType.METHOD,ElementType.TYPE})
@Retention(RetentionPolicy.RUNTIME)
public @interface RequestLimit {
/**
* 通過redis 實現(xiàn)的 這里指定redisKey 否則默認用戶id標識 做實現(xiàn)
*/
String redisKey();
/**
* 時間
*/
int second() default 1;
/**
* 最大請求量
*/
int maxCount() default 1;
/**
* 錯誤文案
*/
String errorMsg();
}
再寫個攔截器
@Slf4j
@Component
public class RequestLimitIntercept implements HandlerInterceptor {
private RedisTemplate<String, Object> redisTemplate;
@Autowired
public void setRedisTemplate(RedisTemplate<String, Object> redisTemplate) {
this.redisTemplate = redisTemplate;
}
@Override
public boolean preHandle(@NotNull HttpServletRequest request, @NotNull HttpServletResponse response, @NotNull Object handler) throws Exception {
response.setCharacterEncoding("UTF-8");
response.setContentType("application/json");
//HandlerMethod 封裝方法定義相關的信息,如類,方法,參數(shù)等
if (!(handler instanceof HandlerMethod)) {
return true;
}
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)) {
Result<String> result = new ResultUtil<String>().setErrorMsg(requestLimit.errorMsg());
response.getWriter().write(JSONObject.toJSON(result).toString());
return false;
}
}
return true;
}
/**
* 判斷請求是否受限
*
* @param request HttpServletRequest
* @param requestLimit RequestLimit
* @return true 允許 false 不允許
*/
public boolean isLimit(HttpServletRequest request, RequestLimit requestLimit) {
// 受限的redis 緩存key ,因為這里用瀏覽器做測試狡逢,我就用sessionid 來做唯一key,如果是app ,可以使用 用戶ID 之類的唯一標識宁舰。
String redisKey = requestLimit.redisKey();
String limitKey;
if (StringUtils.isNotBlank(redisKey)) {
limitKey = redisKey;
}else {
limitKey = request.getServletPath() + request.getSession().getId();
}
// 從緩存中獲取,當前這個請求訪問了幾次
Object obj = redisTemplate.opsForValue().get(limitKey);
if (obj == null) {
//初始 次數(shù)
redisTemplate.opsForValue().set(limitKey, "1", requestLimit.second(), TimeUnit.SECONDS);
} else {
if (Integer.parseInt(String.valueOf(obj)) >= requestLimit.maxCount()) {
return true;
}
// 次數(shù)自增 +1
redisTemplate.opsForValue().increment(limitKey);
}
return false;
}
}
再接口上我們加上上面的注解奢浑,即可實現(xiàn)單位時間內(nèi)的訪問控制蛮艰,當然這里只說了大概的原理,可以自行根據(jù)業(yè)務去改造雀彼,當然還有其他辦法去實現(xiàn)類似的效果壤蚜,只是redis做起來會更方便,更好详羡。