最新国产好看的视频,伊人天堂AV在线,国产Aaaaaa视频,蜜臀视频在线观看一区,人妻av色图,密臀久久久精品影片,青青视频免费观看毛片,久草在线观看视,国产三级精品色情在线

SpringBoot 接口防護(防重提交 + 限流)

 更新時間:2026年03月25日 10:29:10   作者:BigGGGuardian  
Guardian 是一個輕量級 Spring Boot API 請求層防護框架,提供防重復(fù)提交和接口限流兩大能力,本文就來詳細(xì)的介紹一下SpringBoot 接口防護(防重提交 + 限流),感興趣的可以了解一下

前言

事情是這樣的,前段時間在公司項目里又寫了一遍防重復(fù)提交的邏輯——Redis 加鎖、拼 Key、設(shè)過期時間、處理異常釋放鎖……寫到一半我就煩了,這套東西每個項目都要來一遍,而且每次寫法還不太一樣,維護起來頭大。

限流也是,要么上 Sentinel 搞一套,要么自己寫個攔截器糊一個,代碼散得到處都是。

想了想,干脆自己封一個 Starter。搞著搞著就把防重復(fù)提交和接口限流都做了,打包發(fā)到 Maven Central 開源了。

項目叫 Guardian,一個輕量級的 Spring Boot API 請求層防護框架,目前 v1.3.0。兩個功能完全獨立,用哪個引哪個,互不依賴。

項目地址(源碼 + 示例 + 文檔全在里面):

一、防重復(fù)提交

先看效果

三步搞定:

第一步,引依賴:

<dependency>
    <groupId>io.github.biggg-guardian</groupId>
    <artifactId>guardian-repeat-submit-spring-boot-starter</artifactId>
    <version>1.3.0</version>
</dependency>

第二步,加注解:

@PostMapping("/submit")
@RepeatSubmit(interval = 10, message = "訂單正在處理,請勿重復(fù)提交")
public Result submitOrder(@RequestBody OrderDTO order) {
    return orderService.submit(order);
}

第三步,沒了。啟動項目就生效了。

10 秒內(nèi)同一個用戶、同一個接口、同樣的請求參數(shù),第二次請求會被直接攔截。

為什么不直接用 Redis 加個鎖?

你肯定想說:"這不就是 Redis setnx 嘛,我自己寫也行。"

確實能寫,但你想想實際項目里會遇到的問題:

1. Key 怎么拼?

userId + url 夠不夠?如果同一個用戶對同一個接口傳了不同的參數(shù)呢?比如下單接口,買商品 A 和買商品 B 應(yīng)該算兩次不同的請求,不能攔截。

所以防重 Key 要把請求參數(shù)也算進去。但 POST 請求的 body 是個流,讀了一次就沒了,你還得處理 HttpServletRequestWrapper 的問題。

Guardian 內(nèi)置了 RepeatableRequestFilter,自動緩存請求體,Key 生成時會把請求參數(shù)做 JSON 序列化 + Base64 編碼拼進去。

2. 用戶沒登錄怎么辦?

很多防重方案直接用 userId 作為 Key 的一部分,但用戶沒登錄的時候 userId 是 null,Key 就亂了。

Guardian 的處理是:已登錄用 userId → 沒登錄用 sessionId → 沒 session 用客戶端 IP。三級降級,永遠(yuǎn)不會出現(xiàn) null。

3. 業(yè)務(wù)異常了鎖不釋放怎么辦?

比如用戶提交訂單,業(yè)務(wù)代碼報了個異常,但防重鎖已經(jīng)設(shè)了 10 秒。結(jié)果用戶修正數(shù)據(jù)重新提交,被告知"請勿重復(fù)提交"——這體驗就很差了。

Guardian 在攔截器的 afterCompletion 里做了處理:如果請求拋了異常,自動釋放鎖。正常完成的請求才讓鎖自然過期。

4. 有些接口不需要防重怎么辦?

全局配了防重之后,健康檢查接口、公開查詢接口這些也被攔了。你要么給每個接口單獨控制,要么維護一個白名單。

Guardian 支持 exclude-urls 白名單,AntPath 通配符匹配,優(yōu)先級最高。命中直接放行,不走任何防重邏輯。

注解不夠用?試試 YAML 批量配置

單個接口用注解挺方便,但如果你有 50 個接口都要配防重,一個一個加注解就有點累了。

Guardian 支持在 YAML 里批量配置,用 AntPath 通配符一口氣匹配一批接口:

guardian:
  repeat-submit:
    storage: redis
    key-encrypt: md5
    urls:
      - pattern: /api/order/**
        interval: 10
        key-scope: user
        message: "訂單正在處理,請勿重復(fù)提交"
      - pattern: /api/sms/send
        interval: 60
        key-scope: ip
    exclude-urls:
      - /api/public/**
      - /api/health

幾個要點:

  • YAML 規(guī)則的優(yōu)先級高于注解,同一個接口兩邊都配了以 YAML 為準(zhǔn)
  • 白名單(exclude-urls)優(yōu)先級最高,命中直接放行
  • key-scope 控制防重維度:user(按用戶)、ip(按 IP)、global(全局)

比如短信發(fā)送接口配 key-scope: ip,同一個 IP 60 秒內(nèi)只能發(fā)一次,不管登沒登錄、哪個用戶——這比按用戶維度更合理。

攔截了之后怎么響應(yīng)?

這是我糾結(jié)了挺久的一個設(shè)計點。

一開始只做了拋異常的方式——攔截后拋 RepeatSubmitException,讓業(yè)務(wù)端的全局異常處理器去處理。但后來想到,有些項目可能就想開箱即用,不想為了一個防重還得寫個異常處理器。

所以做了兩種模式:

guardian:
  repeat-submit:
    response-mode: exception  # 默認(rèn),拋異常
    # response-mode: json     # 直接返回 JSON

exception 模式(默認(rèn)):拋 RepeatSubmitException,你在全局異常處理器里接一下:

@RestControllerAdvice
public class GlobalExceptionHandler {
    @ExceptionHandler(RepeatSubmitException.class)
    public Result handleRepeatSubmit(RepeatSubmitException e) {
        return Result.fail(e.getMessage());
    }
}

json 模式:攔截器直接寫 JSON 響應(yīng),默認(rèn)格式是 {"code":500,"msg":"...","timestamp":...}

格式不滿意?注冊一個 RepeatSubmitResponseHandler Bean 就能覆蓋:

@Bean
public RepeatSubmitResponseHandler repeatSubmitResponseHandler() {
    return (request, response, message) -> {
        response.setContentType("application/json;charset=UTF-8");
        response.getWriter().write(JSONUtil.toJsonStr(R.fail(message)));
    };
}

不用 Redis 也能跑

不是每個項目都有 Redis 的。本地開發(fā)環(huán)境、小型單體應(yīng)用,可能就沒有 Redis。

guardian:
  repeat-submit:
    storage: local  # 用本地緩存

切成 local 就行了,底層用 ConcurrentHashMap 實現(xiàn),帶定時過期清理。當(dāng)然生產(chǎn)環(huán)境還是推薦 Redis,支持分布式。

想看 Redis 存儲和本地存儲的具體實現(xiàn)?源碼在 guardian-storage-redisRepeatSubmitLocalStorage。

關(guān)于 context-path 的坑

這個坑我自己踩過。項目配了 server.servlet.context-path: /admin-api,然后 YAML 里配的 URL 規(guī)則死活匹配不上。

排查了一下發(fā)現(xiàn),request.getRequestURI() 返回的是帶 context-path 的完整路徑(比如 /admin-api/order/submit),但 YAML 里配的可能是 /order/submit。

Guardian 的處理是:匹配時同時嘗試完整 URI 和去掉 context-path 后的路徑,兩者有一個匹配上就算命中。所以不管你 YAML 里寫的是 /order/submit 還是 /admin-api/order/submit,都能正確匹配。

防重的內(nèi)部流程

簡單畫一下請求的處理流程:

請求進入
  │
  ▼
RepeatableRequestFilter     ← 緩存請求體,支持重復(fù)讀取
  │
  ▼
RepeatSubmitInterceptor
  ├─ 1. 匹配白名單 → 命中直接放行
  ├─ 2. 匹配 YAML 規(guī)則
  ├─ 3. 檢查 @RepeatSubmit 注解
  │      均未命中 → 放行
  ▼
KeyGenerator                ← 按維度(user/ip/global)生成防重 Key
  │
  ▼
KeyEncrypt                  ← 可選 MD5 加密
  │
  ▼
Storage.tryAcquire()
  ├─ 成功 → 放行,寫入存儲 + 設(shè)置 TTL
  └─ 失敗 → 根據(jù) response-mode 響應(yīng)
      ├─ exception → 拋 RepeatSubmitException
      └─ json → 直接寫 JSON 響應(yīng)
  │
  ▼
業(yè)務(wù)執(zhí)行
  ├─ 正常 → Key 自然過期
  └─ 異常 → afterCompletion 自動釋放

二、接口限流

為什么要做限流?

防重復(fù)提交解決的是"同一個請求短時間內(nèi)被提交多次"的問題,但還有另一類問題它管不了:惡意刷接口。

比如有人寫個腳本一秒鐘請求你的搜索接口 1000 次,防重攔不住(因為每次參數(shù)可能不一樣),這時候就需要限流了。

市面上的限流方案不少,但要么是網(wǎng)關(guān)級別的(Sentinel、Spring Cloud Gateway),要么得寫一堆配置。如果你就是個普通的 Spring Boot 單體應(yīng)用,想給幾個接口加個限流,沒必要引那么重的東西。

Guardian 的限流就是沖著這個場景來的:輕量、注解 + YAML 雙模式、兩種算法可選。

先看效果

<dependency>
    <groupId>io.github.biggg-guardian</groupId>
    <artifactId>guardian-rate-limit-spring-boot-starter</artifactId>
    <version>1.3.0</version>
</dependency>
// 滑動窗口:每秒最多 10 次
@RateLimit(qps = 10)
// 令牌桶:每秒補 5 個令牌,桶容量 20,允許瞬間突發(fā) 20 次
@RateLimit(qps = 5, capacity = 20, algorithm = RateLimitAlgorithm.TOKEN_BUCKET)

同樣支持 YAML 批量配置:

guardian:
  rate-limit:
    urls:
      - pattern: /api/sms/send
        qps: 1
        rate-limit-scope: ip
      - pattern: /api/seckill/**
        qps: 10
        capacity: 50
        algorithm: token_bucket
        rate-limit-scope: global
    exclude-urls:
      - /api/public/**

和防重一樣,注解 + YAML 雙模式,YAML 優(yōu)先級高于注解,白名單優(yōu)先級最高。

滑動窗口 vs 令牌桶

這是限流最常用的兩種算法,Guardian 都支持。

滑動窗口:統(tǒng)計時間窗口內(nèi)的請求次數(shù),超了就拒絕。比如配了 qps=10, window=1s,就是每秒最多 10 次,多了直接打回。

@RateLimit(qps = 10)

特點是嚴(yán)格。窗口內(nèi)絕對不會超過閾值。適合短信發(fā)送、登錄嘗試這種需要精確控制頻率的場景。

令牌桶:桶里裝令牌,按固定速率往里放,請求來了取一個,桶空了就拒絕。桶滿時可以一口氣把令牌全用完。

@RateLimit(qps = 5, capacity = 20, algorithm = RateLimitAlgorithm.TOKEN_BUCKET)

這個配置的意思是:每秒補 5 個令牌,桶最多攢 20 個。平時空閑的時候令牌慢慢攢,突然來一波流量,瞬間可以放過 20 個請求,打完之后回到每秒 5 個的穩(wěn)態(tài)。

特點是允許突發(fā)。適合秒殺、搶購這種"平時沒啥流量,偶爾來一波高峰"的場景。

舉個直觀的例子,都是 qps=10,突然來了 20 個請求:

滑動窗口令牌桶(capacity=20)
第 1-10 個通過通過
第 11-20 個全部拒絕全部通過
之后每秒最多 10 個最多 10 個

補充速率怎么控制?

令牌桶的補充速率通過 qps 和 window 兩個參數(shù)控制:

  • qps=10, window=1s → 每秒補 10 個
  • qps=10, window=1min → 每分鐘補 10 個(約 6 秒補 1 個)
// 每分鐘補 10 個令牌,桶容量 10
@RateLimit(qps = 10, window = 1, windowUnit = TimeUnit.MINUTES,
        capacity = 10, algorithm = RateLimitAlgorithm.TOKEN_BUCKET)

這樣就能實現(xiàn)慢速補充的場景。

限流維度

和防重一樣,限流也支持三種維度:

維度效果典型場景
GLOBAL(默認(rèn))整個接口共用一個計數(shù)器全站搜索接口
IP每個 IP 獨立計數(shù)短信發(fā)送、驗證碼
USER每個用戶獨立計數(shù)用戶操作頻率限制
@RateLimit(qps = 1, rateLimitScope = RateLimitKeyScope.IP, message = "短信發(fā)送過于頻繁")

限流的響應(yīng)處理

和防重一樣,兩種模式:

guardian:
  rate-limit:
    response-mode: exception  # 默認(rèn),拋 RateLimitException
    # response-mode: json     # 直接返回 JSON

也支持自定義響應(yīng)處理器,注冊一個 RateLimitResponseHandler Bean 就行。

三、一些設(shè)計細(xì)節(jié)

并發(fā)安全

限流對并發(fā)安全的要求比防重高。你想,10 個請求同時進來,限流閾值是 5,如果并發(fā)控制沒做好,可能 10 個都放過去了。

Guardian 的處理:

  • Redis:滑動窗口和令牌桶都用 Lua 腳本,Redis 單線程執(zhí)行 Lua 是天然原子的
  • 本地緩存:synchronized 鎖到 Key 粒度,不同 Key 之間互不阻塞

防重那邊也是一樣,Redis 用 SET NX EX 原子操作,本地緩存用 ConcurrentHashMap 的原子方法。

本地緩存的內(nèi)存管理

用 ConcurrentHashMap 做本地存儲有個容易忽略的問題:Key 只進不出,長時間運行內(nèi)存會一直漲。

Guardian 在防重和限流的本地存儲里都加了守護線程,每 5 分鐘掃一次,清理過期的 Key。線程是 daemon 的,不會阻止 JVM 關(guān)閉。源碼可以看 RateLimitLocalStorage。

可插拔架構(gòu)

兩個模塊的核心組件都是面向接口編程的,框架內(nèi)部用 @ConditionalOnMissingBean 做的,你不注冊就用默認(rèn)的,注冊了就用你的:

組件防重復(fù)提交接口限流
Key 生成RepeatSubmitKeyGeneratorRateLimitKeyGenerator
Key 加密AbstractKeyEncryptAbstractKeyEncrypt
存儲RepeatSubmitStorageRateLimitStorage
響應(yīng)處理RepeatSubmitResponseHandlerRateLimitResponseHandler
用戶上下文UserContext(共享)UserContext(共享)

可觀測性

兩個模塊都內(nèi)置了監(jiān)控能力:

攔截日志log-enabled: true 開啟后,攔截/放行都有日志輸出。

Actuator 端點

GET /actuator/guardianRepeatSubmit    → 防重統(tǒng)計
GET /actuator/guardianRateLimit       → 限流統(tǒng)計

限流的統(tǒng)計數(shù)據(jù)長這樣:

{
  "totalRequestCount": 5560,
  "totalPassCount": 5432,
  "totalBlockCount": 128,
  "blockRate": "2.30%",
  "topBlockedApis": { "/api/sms/send": 56 },
  "topRequestApis": { "/api/search": 3200 }
}

項目結(jié)構(gòu)

guardian-parent
├── guardian-core                          # 公共基礎(chǔ)(共享類)
├── guardian-repeat-submit/                # 防重復(fù)提交
│   ├── guardian-repeat-submit-core/
│   └── guardian-repeat-submit-spring-boot-starter/
├── guardian-rate-limit/                   # 接口限流
│   ├── guardian-rate-limit-core/
│   └── guardian-rate-limit-spring-boot-starter/
├── guardian-storage-redis/                # Redis 存儲(多模塊共享)
└── guardian-example/                      # 示例工程

模塊拆分是為了靈活組合。比如你只需要防重就引 guardian-repeat-submit-spring-boot-starter,只需要限流就引 guardian-rate-limit-spring-boot-starter,都需要就兩個都引,互不影響。

guardian-core 放的是兩個模塊都用到的公共類,比如 UserContext、GuardianResponseHandler。guardian-storage-redis 是 Redis 存儲的共享實現(xiàn),兩個模塊的 Redis 存儲都在這里面。

完整的示例代碼在 guardian-example 模塊里,防重 + 限流的各種場景都有,clone 下來直接跑。

總結(jié)

Guardian 做了兩件事:

  1. 防重復(fù)提交:攔截短時間內(nèi)的重復(fù)請求,支持注解 / YAML、用戶 / IP / 全局維度、異常自動釋放、context-path 兼容
  2. 接口限流:控制接口訪問頻率,支持滑動窗口 / 令牌桶、突發(fā)流量處理、三種維度

兩個功能獨立 Starter,核心組件全部可插拔,注冊 Bean 就能替換默認(rèn)實現(xiàn)。Redis 和本地緩存一鍵切換。

如果你的 Spring Boot 項目里需要這些能力,但又不想引 Sentinel 那么重的東西,可以試試。

Maven Central 坐標(biāo)(最新 v1.3.0):

<!-- 防重復(fù)提交 -->
<dependency>
    <groupId>io.github.biggg-guardian</groupId>
    <artifactId>guardian-repeat-submit-spring-boot-starter</artifactId>
    <version>1.3.0</version>
</dependency>
<!-- 接口限流 -->
<dependency>
    <groupId>io.github.biggg-guardian</groupId>
    <artifactId>guardian-rate-limit-spring-boot-starter</artifactId>
    <version>1.3.0</version>
</dependency>

項目地址(README 里有完整配置文檔和更新日志):

GitHubhttps://github.com/BigGG-Guardian/guardian/tree/master/guardian-storage-redis

到此這篇關(guān)于SpringBoot 接口防護(防重提交 + 限流)的文章就介紹到這了,更多相關(guān)SpringBoot 接口防護內(nèi)容請搜索腳本之家以前的文章或繼續(xù)瀏覽下面的相關(guān)文章希望大家以后多多支持腳本之家!

相關(guān)文章

  • Spring?Boot如何處理@Resource示例分析

    Spring?Boot如何處理@Resource示例分析

    這篇文章主要為大家介紹了Spring?Boot如何處理@Resource示例分析,有需要的朋友可以借鑒參考下,希望能夠有所幫助,祝大家多多進步,早日升職加薪
    2023-07-07
  • Java中的反射機制示例詳解

    Java中的反射機制示例詳解

    反射就是把Java類中的各個成分映射成一個個的Java對象。本文將通過示例詳細(xì)講解Java中的反射機制,感興趣的小伙伴可以跟隨小編學(xué)習(xí)一下
    2022-03-03
  • Spring 自動裝配的二義性實例解析

    Spring 自動裝配的二義性實例解析

    這篇文章主要介紹了Spring 自動裝配的二義性實例解析,文中通過示例代碼介紹的非常詳細(xì),對大家的學(xué)習(xí)或者工作具有一定的參考學(xué)習(xí)價值,需要的朋友可以參考下
    2019-11-11
  • 使用arthas命令redefine實現(xiàn)Java熱更新(推薦)

    使用arthas命令redefine實現(xiàn)Java熱更新(推薦)

    今天分享一個非常重要的命令 redefine ,主要作用是加載外部的 .class 文件,用來替換 JVM 已經(jīng)加載的類,總結(jié)起來就是實現(xiàn)了 Java 的熱更新,感興趣的朋友跟隨小編一起看看吧
    2020-05-05
  • Spring Boot 簡介(入門篇)

    Spring Boot 簡介(入門篇)

    Spring Boot是由Pivotal團隊提供的全新框架,其設(shè)計目的是用來簡化新Spring應(yīng)用的初始搭建以及開發(fā)過程。下面通過本文給大家介紹spring boot相關(guān)知識,需要的的朋友參考下吧
    2017-04-04
  • MyBatis Oracle 自增序列的實現(xiàn)方法

    MyBatis Oracle 自增序列的實現(xiàn)方法

    這篇文章給大家分享MyBatis Oracle 自增序列的實現(xiàn)方法及mybatis配置oracle的主鍵自增長的方法,非常不錯具有一定的參考借鑒價值,感興趣的朋友一起看看吧
    2016-11-11
  • 淺析Spring和MyBatis整合及逆向工程

    淺析Spring和MyBatis整合及逆向工程

    這篇文章主要介紹了Spring和MyBatis整合及逆向工程的相關(guān)資料,非常不錯,具有參考借鑒價值,需要的朋友可以參考下
    2016-06-06
  • Java代碼審計的一些基礎(chǔ)知識你知道嗎

    Java代碼審計的一些基礎(chǔ)知識你知道嗎

    這篇文章主要介紹了基于Java的代碼審計功能的基礎(chǔ)知識,小編覺得挺不錯的,現(xiàn)在分享給大家,也給大家做個參考。一起跟隨小編過來看看吧
    2021-09-09
  • java 示例講解循環(huán)語句的使用

    java 示例講解循環(huán)語句的使用

    順序結(jié)構(gòu)的程序語句只能被執(zhí)行一次。如果您想要同樣的操作執(zhí)行多次,就需要使用循環(huán)結(jié)構(gòu),循環(huán)結(jié)構(gòu)就是在循環(huán)條件滿足的情況下,反復(fù)執(zhí)行特定代碼
    2022-04-04
  • idea設(shè)置JVM運行參數(shù)的幾種方式

    idea設(shè)置JVM運行參數(shù)的幾種方式

    對JVM運行參數(shù)進行修改是JVM性能調(diào)優(yōu)的重要手段,本文主要介紹了idea設(shè)置JVM運行參數(shù)的幾種方式,文中通過示例代碼介紹的非常詳細(xì),對大家的學(xué)習(xí)或者工作具有一定的參考學(xué)習(xí)價值,需要的朋友們下面隨著小編來一起學(xué)習(xí)學(xué)習(xí)吧
    2022-04-04

最新評論

大城县| 宜兴市| 沙田区| 耿马| 宿松县| 颍上县| 耿马| 锡林浩特市| 扬州市| 沂南县| 德钦县| 滦平县| 门头沟区| 宁河县| 连江县| 涞源县| 陇南市| 永新县| 丰镇市| 山西省| 龙海市| 高邮市| 大港区| 维西| 靖安县| 赤城县| 环江| 孙吴县| 巴塘县| 奎屯市| 奉化市| 循化| 宣恩县| 丰县| 公主岭市| 通辽市| 昌宁县| 邳州市| 宾川县| 东至县| 迁安市|