SpringBoot API 接口防刷

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)多了。

代碼地址

完整的代碼针贬,如下地址

Gitee地址:https://gitee.com/rstyro/spring-boot/tree/master/SpringBoot-limit

Github地址:https://github.com/rstyro/Springboot/tree/master/SpringBoot-limit

各位大佬击费,去Github看示例代碼的時(shí)候,如果感覺(jué)有點(diǎn)用的話坚踩,給個(gè)Star唄
最后編輯于
?著作權(quán)歸作者所有,轉(zhuǎn)載或內(nèi)容合作請(qǐng)聯(lián)系作者
  • 序言:七十年代末荡灾,一起剝皮案震驚了整個(gè)濱河市,隨后出現(xiàn)的幾起案子瞬铸,更是在濱河造成了極大的恐慌批幌,老刑警劉巖,帶你破解...
    沈念sama閱讀 207,248評(píng)論 6 481
  • 序言:濱河連續(xù)發(fā)生了三起死亡事件嗓节,死亡現(xiàn)場(chǎng)離奇詭異荧缘,居然都是意外死亡,警方通過(guò)查閱死者的電腦和手機(jī)拦宣,發(fā)現(xiàn)死者居然都...
    沈念sama閱讀 88,681評(píng)論 2 381
  • 文/潘曉璐 我一進(jìn)店門截粗,熙熙樓的掌柜王于貴愁眉苦臉地迎上來(lái),“玉大人鸵隧,你說(shuō)我怎么就攤上這事绸罗。” “怎么了豆瘫?”我有些...
    開(kāi)封第一講書人閱讀 153,443評(píng)論 0 344
  • 文/不壞的土叔 我叫張陵珊蟀,是天一觀的道長(zhǎng)。 經(jīng)常有香客問(wèn)我外驱,道長(zhǎng)育灸,這世上最難降的妖魔是什么腻窒? 我笑而不...
    開(kāi)封第一講書人閱讀 55,475評(píng)論 1 279
  • 正文 為了忘掉前任,我火速辦了婚禮磅崭,結(jié)果婚禮上儿子,老公的妹妹穿的比我還像新娘。我一直安慰自己砸喻,他們只是感情好柔逼,可當(dāng)我...
    茶點(diǎn)故事閱讀 64,458評(píng)論 5 374
  • 文/花漫 我一把揭開(kāi)白布。 她就那樣靜靜地躺著恩够,像睡著了一般卒落。 火紅的嫁衣襯著肌膚如雪。 梳的紋絲不亂的頭發(fā)上蜂桶,一...
    開(kāi)封第一講書人閱讀 49,185評(píng)論 1 284
  • 那天儡毕,我揣著相機(jī)與錄音,去河邊找鬼扑媚。 笑死腰湾,一個(gè)胖子當(dāng)著我的面吹牛,可吹牛的內(nèi)容都是我干的疆股。 我是一名探鬼主播费坊,決...
    沈念sama閱讀 38,451評(píng)論 3 401
  • 文/蒼蘭香墨 我猛地睜開(kāi)眼,長(zhǎng)吁一口氣:“原來(lái)是場(chǎng)噩夢(mèng)啊……” “哼旬痹!你這毒婦竟也來(lái)了附井?” 一聲冷哼從身側(cè)響起,我...
    開(kāi)封第一講書人閱讀 37,112評(píng)論 0 261
  • 序言:老撾萬(wàn)榮一對(duì)情侶失蹤两残,失蹤者是張志新(化名)和其女友劉穎永毅,沒(méi)想到半個(gè)月后,有當(dāng)?shù)厝嗽跇淞掷锇l(fā)現(xiàn)了一具尸體人弓,經(jīng)...
    沈念sama閱讀 43,609評(píng)論 1 300
  • 正文 獨(dú)居荒郊野嶺守林人離奇死亡沼死,尸身上長(zhǎng)有42處帶血的膿包…… 初始之章·張勛 以下內(nèi)容為張勛視角 年9月15日...
    茶點(diǎn)故事閱讀 36,083評(píng)論 2 325
  • 正文 我和宋清朗相戀三年,在試婚紗的時(shí)候發(fā)現(xiàn)自己被綠了崔赌。 大學(xué)時(shí)的朋友給我發(fā)了我未婚夫和他白月光在一起吃飯的照片意蛀。...
    茶點(diǎn)故事閱讀 38,163評(píng)論 1 334
  • 序言:一個(gè)原本活蹦亂跳的男人離奇死亡,死狀恐怖健芭,靈堂內(nèi)的尸體忽然破棺而出县钥,到底是詐尸還是另有隱情,我是刑警寧澤慈迈,帶...
    沈念sama閱讀 33,803評(píng)論 4 323
  • 正文 年R本政府宣布若贮,位于F島的核電站,受9級(jí)特大地震影響,放射性物質(zhì)發(fā)生泄漏兜看。R本人自食惡果不足惜,卻給世界環(huán)境...
    茶點(diǎn)故事閱讀 39,357評(píng)論 3 307
  • 文/蒙蒙 一狭瞎、第九天 我趴在偏房一處隱蔽的房頂上張望细移。 院中可真熱鬧,春花似錦熊锭、人聲如沸弧轧。這莊子的主人今日做“春日...
    開(kāi)封第一講書人閱讀 30,357評(píng)論 0 19
  • 文/蒼蘭香墨 我抬頭看了看天上的太陽(yáng)精绎。三九已至,卻和暖如春锌妻,著一層夾襖步出監(jiān)牢的瞬間代乃,已是汗流浹背。 一陣腳步聲響...
    開(kāi)封第一講書人閱讀 31,590評(píng)論 1 261
  • 我被黑心中介騙來(lái)泰國(guó)打工仿粹, 沒(méi)想到剛下飛機(jī)就差點(diǎn)兒被人妖公主榨干…… 1. 我叫王不留搁吓,地道東北人。 一個(gè)月前我還...
    沈念sama閱讀 45,636評(píng)論 2 355
  • 正文 我出身青樓吭历,卻偏偏與公主長(zhǎng)得像堕仔,于是被迫代替她去往敵國(guó)和親。 傳聞我的和親對(duì)象是個(gè)殘疾皇子晌区,可洞房花燭夜當(dāng)晚...
    茶點(diǎn)故事閱讀 42,925評(píng)論 2 344