Build Your Own Spring Boot API Guard: Anti‑Duplicate Submissions and Rate Limiting
The article introduces Guardian, a lightweight Spring Boot starter that provides independent anti‑duplicate‑submission and rate‑limiting modules, explains why a custom solution is preferable to raw Redis locks, and walks through annotation and YAML configurations, key generation, response handling, storage options, concurrency safety, and observability.
Overview
Guardian is a lightweight Spring Boot starter that provides two completely independent modules: anti‑duplicate submission and API rate limiting. Each module can be added separately via Maven coordinates
io.github.biggg-guardian:guardian-repeat-submit-spring-boot-starter:1.3.0or
io.github.biggg-guardian:guardian-rate-limit-spring-boot-starter:1.3.0. The source code is hosted at https://github.com/BigGG-Guardian/guardian.
Anti‑Duplicate Submission
Quick start
Add the Maven dependency:
<dependency>
<groupId>io.github.biggg-guardian</groupId>
<artifactId>guardian-repeat-submit-spring-boot-starter</artifactId>
<version>1.3.0</version>
</dependency>Annotate the controller method:
@PostMapping("/submit")
@RepeatSubmit(interval = 10, message = "订单正在处理,请勿重复提交")
public Result submitOrder(@RequestBody OrderDTO order) {
return orderService.submit(order);
}Run the application – no additional code is required.
Within the configured interval (e.g., 10 seconds), a second request with the same user, URL, and request parameters is blocked.
Why not a simple Redis SETNX lock?
Key composition – Using only userId+url cannot distinguish different request bodies. Guardian caches the request body with RepeatableRequestFilter, serialises it to JSON, Base64‑encodes it, and includes it in the key.
Unauthenticated users – When userId is null, Guardian falls back to sessionId and then to client IP, guaranteeing a non‑null key.
Exception handling – If business logic throws an exception while the lock is held, Guardian releases the lock in afterCompletion to avoid false “duplicate submission” errors.
Selective disabling – A whitelist defined by exclude-urls (Ant‑style patterns) has the highest priority and bypasses all anti‑duplicate logic.
YAML bulk configuration
guardian:
repeat-submit:
storage: redis
key-encrypt: md5
urls:
- pattern: /api/order/**
interval: 10
key-scope: user
message: "订单正在处理,请勿重复提交"
- pattern: /api/sms/send
interval: 60
key-scope: ip
exclude-urls:
- /api/public/**
- /api/health key-scopecontrols the protection dimension: user (per user), ip (per IP), or global (shared).
Response modes
exception (default) – Throws RepeatSubmitException for a global exception handler.
json – Returns a JSON payload directly, e.g. {"code":500,"msg":"...","timestamp":...}.
Custom response handling can be provided by defining a RepeatSubmitResponseHandler bean.
Running without Redis
Set storage: local to use an in‑memory ConcurrentHashMap with a daemon thread that cleans expired keys every five minutes.
Context‑path compatibility
When server.servlet.context-path is set, Guardian matches both the full URI and the URI stripped of the context path, so rules work regardless of the prefix.
Internal processing flow
Request → RepeatableRequestFilter (caches body) → RepeatSubmitInterceptor
├─ whitelist check (pass if matched)
├─ YAML rule match
├─ @RepeatSubmit annotation check (pass if none matched)
▼
KeyGenerator (creates key based on scope)
KeyEncrypt (optional MD5)
Storage.tryAcquire()
├─ success → proceed, store key with TTL
└─ failure → response-mode handling (exception or json)
▼
Business execution
├─ normal → key expires naturally
└─ exception → afterCompletion releases lockAPI Rate Limiting
Motivation
Duplicate‑submission protection stops identical requests in a short window but cannot limit high‑frequency calls with different parameters (e.g., a script sending 1000 searches per second). Rate limiting controls request frequency at the API layer.
Quick start
<dependency>
<groupId>io.github.biggg-guardian</groupId>
<artifactId>guardian-rate-limit-spring-boot-starter</artifactId>
<version>1.3.0</version>
</dependency>Supported algorithms:
Sliding window – strict QPS limit.
Token bucket – allows bursts.
Annotation examples:
// Sliding window: max 10 QPS
@RateLimit(qps = 10)
// Token bucket: 5 QPS, burst capacity 20
@RateLimit(qps = 5, capacity = 20, algorithm = RateLimitAlgorithm.TOKEN_BUCKET)YAML bulk configuration
guardian:
rate-limit:
urls:
- pattern: /api/sms/send
qps: 1
rate-limit-scope: ip
message: "短信发送过于频繁"
- pattern: /api/seckill/**
qps: 10
capacity: 50
algorithm: token_bucket
rate-limit-scope: global
exclude-urls:
- /api/public/**YAML rules have higher priority than annotations; whitelist rules have the highest priority.
Sliding window vs. token bucket
Sliding window counts requests within a fixed time window; excess requests are rejected. Guarantees that the request count never exceeds the configured QPS (e.g., qps=10, window=1s).
Token bucket refills tokens at a fixed rate; accumulated tokens allow a burst up to the bucket capacity. Example: qps=5, capacity=20 permits up to 20 instantaneous requests, then continues at 5 QPS.
Illustrative comparison (qps = 10, burst = 20): first 10 requests pass for both algorithms; requests 11‑20 are rejected by sliding window but pass by token bucket; subsequent seconds allow up to 10 requests per second for both.
Controlling token refill rate
qps=10, window=1s→ 10 tokens added each second. qps=10, window=1min → 10 tokens added each minute (≈1 token every 6 seconds).
@RateLimit(qps = 10, window = 1, windowUnit = TimeUnit.MINUTES,
capacity = 10, algorithm = RateLimitAlgorithm.TOKEN_BUCKET)Rate‑limiting dimensions
GLOBAL (default) – a single counter for the entire API (e.g., site‑wide search).
IP – independent counters per client IP (e.g., SMS, captcha).
USER – independent counters per authenticated user (e.g., user‑action limits).
Response handling
exception (default) – Throws RateLimitException for a global handler.
json – Returns a JSON response directly.
Custom handlers can be supplied by defining a RateLimitResponseHandler bean.
Concurrency safety
Redis – Both algorithms use Lua scripts; Redis executes Lua atomically.
Local cache – Uses synchronized blocks at key granularity.
Anti‑duplicate submission also relies on atomic SET NX EX in Redis and atomic methods of ConcurrentHashMap for local storage.
Pluggable architecture
Key generator: RepeatSubmitKeyGenerator / RateLimitKeyGenerator Key encrypt: AbstractKeyEncrypt (shared)
Storage: RepeatSubmitStorage / RateLimitStorage Response handler: RepeatSubmitResponseHandler / RateLimitResponseHandler User context: UserContext (shared)
Observability
Intercept logs can be enabled with log-enabled: true.
Actuator endpoints:
GET /actuator/guardian-repeat-submit → anti‑duplicate statistics
GET /actuator/guardian-rate-limit → rate‑limit statisticsSample rate‑limit statistics JSON includes total request count, pass count, block count, block rate, top blocked APIs, and top requested APIs.
Project structure
guardian-parent
├── guardian-core # shared utilities (UserContext, etc.)
├── guardian-repeat-submit
│ ├── 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 implementation shared by both modules
└── guardian-example # Example applicationModules are independent; include only the starter(s) you need.
Conclusion
Anti‑duplicate submission – Blocks rapid repeat requests, supports annotation and YAML configuration, multiple key scopes (user, IP, global), automatic lock release on exceptions, and context‑path‑aware matching.
API rate limiting – Controls request frequency with sliding‑window or token‑bucket algorithms, supports burst handling, and three scopes (global, IP, user).
Both modules default to Redis storage but can fall back to local in‑memory storage, expose monitoring via Actuator, and allow custom beans for key generation, encryption, storage, and response handling. They provide lightweight protection for Spring Boot applications without requiring heavyweight gateway solutions.
Signed-in readers can open the original source through BestHub's protected redirect.
This article has been distilled and summarized from source material, then republished for learning and reference. If you believe it infringes your rights, please contactand we will review it promptly.
How this landed with the community
Was this worth your time?
0 Comments
Thoughtful readers leave field notes, pushback, and hard-won operational detail here.
