溫馨提示×

溫馨提示×

您好,登錄后才能下訂單哦!

密碼登錄×
登錄注冊×
其他方式登錄
點(diǎn)擊 登錄注冊 即表示同意《億速云用戶服務(wù)條款》

API 接口防刷

發(fā)布時間:2020-06-22 04:53:06 來源:網(wǎng)絡(luò) 閱讀:506 作者:java架構(gòu)師1 欄目:編程語言

API 接口防刷
顧名思義,想讓某個接口某個人在某段時間內(nèi)只能請求N次。
在項目中比較常見的問題也有,那就是連點(diǎn)按鈕導(dǎo)致請求多次,以前在web端有表單重復(fù)提交,可以通過token 來解決。
除了上面的方法外,前后端配合的方法。現(xiàn)在全部由后端來控制。
原理
在你請求的時候,服務(wù)器通過redis 記錄下你請求的次數(shù),如果次數(shù)超過限制就不給訪問。
在redis 保存的key 是有時效性的,過期就會刪除。
代碼實(shí)現(xiàn):
為了讓它看起來逼格高一點(diǎn),所以以自定義注解的方式實(shí)現(xiàn)

@RequestLimit 注解

import java.lang.annotation.*;

/**
 * 請求限制的自定義注解
 *
 * @Target 注解可修飾的對象范圍,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被保留的時間長短:某些Annotation僅出現(xiàn)在源代碼中,而被編譯器丟棄;
 * 而另一些卻被編譯在class文件中;編譯在class文件中的Annotation可能會被虛擬機(jī)忽略,
 * 而另一些在class被裝載時將被讀?。ㄕ堊⒁獠⒉挥绊慶lass的執(zhí)行,因?yàn)锳nnotation與class在使用上是被分離的)。
 * 使用這個meta-Annotation可以對 Annotation的“生命周期”限制。
 * (RetentionPoicy)取值有:
 *     1.SOURCE:在源文件中有效(即源文件保留)
 *     2.CLASS:在class文件中有效(即class保留)
 *     3.RUNTIME:在運(yùn)行時有效(即運(yùn)行時保留)
 *
 * @Inherited
 * 元注解是一個標(biāo)記注解,@Inherited闡述了某個被標(biāo)注的類型是被繼承的。
 * 如果一個使用了@Inherited修飾的annotation類型被用于一個class,則這個annotation將被用于該class的子類。
 */
@Documented
@Inherited
@Target({ElementType.METHOD,ElementType.TYPE})
@Retention(RetentionPolicy.RUNTIME)
public @interface RequestLimit {
    // 在 second 秒內(nèi),最大只能請求 maxCount 次
    int second() default 1;
    int maxCount() default 1;
}

RequestLimitIntercept 攔截器
自定義一個攔截器,請求之前,進(jìn)行請求次數(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;

/**
 * 請求攔截
 */
@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 參數(shù)所表示的類或接口是否相同,或是否是其超類或超接口
         * isAssignableFrom()方法是判斷是否為某個類的父類
         * instanceof關(guān)鍵字是判斷是否某個類的子類
         */
        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);
    }
    //判斷請求是否受限
    public boolean isLimit(HttpServletRequest request,RequestLimit requestLimit){
        // 受限的redis 緩存key ,因?yàn)檫@里用瀏覽器做測試,我就用sessionid 來做唯一key,如果是app ,可以使用 用戶ID 之類的唯一標(biāo)識。
        String limitKey = request.getServletPath()+request.getSession().getId();
        // 從緩存中獲取,當(dāng)前這個請求訪問了幾次
        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);
    }
}

攔截器寫好了,但是還得添加注冊

WebMvcConfig 配置類
因?yàn)槲业氖荢pringboot2. 所以只需實(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
控制層測試接口,
使用方式:

第一種:直接在類上使用注解@RequestLimit(maxCount = 5,second = 1)

第二種:在方法上使用注解@RequestLimit(maxCount = 5,second = 1)

maxCount 最大的請求數(shù)、second 代表時間,單位是秒

默認(rèn)1秒內(nèi),每個接口只能請求一次

@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();
    }
}

如果在類和方法上同時有@RequestLimit注解 ,以方法上的參數(shù)為準(zhǔn),好像注釋有點(diǎn)多了。

向AI問一下細(xì)節(jié)

免責(zé)聲明:本站發(fā)布的內(nèi)容(圖片、視頻和文字)以原創(chuàng)、轉(zhuǎn)載和分享為主,文章觀點(diǎn)不代表本網(wǎng)站立場,如果涉及侵權(quán)請聯(lián)系站長郵箱:is@yisu.com進(jìn)行舉報,并提供相關(guān)證據(jù),一經(jīng)查實(shí),將立刻刪除涉嫌侵權(quán)內(nèi)容。

AI