How to Build a Lightweight Spring Boot API Firewall for Real‑Time Protection
This article walks through the design and implementation of a lightweight API firewall embedded in a Spring Boot application, covering background pain points, architecture, core capabilities such as black‑white list, rate limiting, online configuration, detailed Java code, front‑end console, deployment, and practical use cases.
1. Background and Pain Points
When developing backend services, we often encounter problems such as malicious traffic flooding an interface, lack of fine‑grained protection, hard‑coded risk policies, and chaotic black‑white list management.
Interface malicious traffic: a query interface is called massively in a short time, exhausting DB connections and crashing the service.
Lack of fine‑grained protection: only coarse global rate limiting exists; high‑value APIs (order, payment, export) have no dedicated protection.
Risk policies hard to change: rules are hard‑coded; each change requires code modification, packaging and deployment, raising ops cost.
Black‑white list chaos: temporary block rules are scattered in Nginx or database without a unified entry for developers, ops and testers.
Many enterprises introduce API gateways such as Kong, Spring Cloud Gateway, or Nginx+Lua, but these solutions are often too heavyweight:
Additional component increases deployment complexity.
High learning and usage cost, unsuitable for small teams.
Overkill for single‑machine or internal small systems.
Therefore a very practical demand emerges: embed a lightweight API firewall directly in a Spring Boot application to achieve rate limiting, risk control and online configuration without external gateways.
2. Design Idea
Goal: embed a protection layer in Spring Boot that performs a “pre‑check” for every API request.
Core capabilities:
Black‑white list: support IP whitelist (higher priority) and blacklist.
Rate‑limiting strategies:
QPS limit (e.g., each interface no more than 100 requests per second).
Time‑window limit (e.g., a single user no more than 60 calls per minute).
Burst control to avoid short‑term spikes.
Risk control rules: time‑window based access frequency, e.g., “single IP may access at most 10 times within 60 seconds”.
Online configuration: a front‑end console to modify rules in real time without restart.
Low intrusion: integrate as Interceptor or Filter without affecting existing business logic.
Architecture diagram:
┌────────────────────┐
│ API Firewall │ ← Intercept layer (black‑white list / rate‑limit / risk)
└────────────────────┘
↓
┌────────────────────┐
│ Application API │
└────────────────────┘3. Technology Selection
Spring Boot Interceptor: best for request pre‑validation with low intrusion.
Guava Cache (or Caffeine): high‑performance in‑memory cache with expiration and concurrency support.
AtomicInteger + time window: simple and efficient counter‑based rate limiting.
Spring Boot Actuator: provides health check and monitoring endpoints.
Front‑end UI with Tailwind CSS + Chart.js: modern responsive management page.
For distributed scenarios, Redis + Lua can be added later; the current focus is single‑machine lightweight protection.
4. Core Implementation
4.1 Rule Entity
@Data
@NoArgsConstructor
@AllArgsConstructor
@Builder
public class FirewallRule {
/** primary key */
private Long id;
/** rule name */
private String ruleName;
/** API path pattern */
private String apiPattern;
/** QPS limit */
private Integer qpsLimit;
/** per‑user limit within time window */
private Integer userLimit;
/** time window (seconds) */
private Integer timeWindow;
/** enabled flag */
private Boolean enabled;
/** description */
private String description;
/** creation time */
private LocalDateTime createdTime;
/** update time */
private LocalDateTime updatedTime;
/** check if API path matches this rule */
public boolean matches(String apiPath) {
if (apiPattern == null || apiPath == null) {
return false;
}
// support wildcard matching
String pattern = apiPattern.replace("**", ".*").replace("*", "[^/]*");
return apiPath.matches(pattern);
}
}4.2 Rule Manager
@Slf4j
@Service
public class RuleManager {
@Autowired
private FirewallRuleMapper ruleMapper;
@Autowired
private FirewallBlacklistMapper blacklistMapper;
@Autowired
private FirewallWhitelistMapper whitelistMapper;
/** rule cache */
private final Map<String, FirewallRule> ruleCache = new ConcurrentHashMap<>();
/** blacklist cache */
private final Map<String, FirewallBlacklist> blacklistCache = new ConcurrentHashMap<>();
/** whitelist cache */
private final Map<String, FirewallWhitelist> whitelistCache = new ConcurrentHashMap<>();
@PostConstruct
public void init() {
log.info("Initializing firewall rule manager...");
refreshRules();
refreshBlacklist();
refreshWhitelist();
log.info("Firewall rule manager initialized, loaded rules: {}, blacklist: {}, whitelist: {}",
ruleCache.size(), blacklistCache.size(), whitelistCache.size());
}
public FirewallRule getMatchingRule(String apiPath) {
if (apiPath == null) {
return null;
}
// exact match first
FirewallRule exactMatch = ruleCache.get(apiPath);
if (exactMatch != null && exactMatch.isEffectiveEnabled()) {
return exactMatch;
}
// pattern match
for (FirewallRule rule : ruleCache.values()) {
if (rule.isEffectiveEnabled() && rule.matches(apiPath)) {
return rule;
}
}
return null;
}
public boolean isBlacklisted(String ipAddress) {
if (ipAddress == null) {
return false;
}
for (FirewallBlacklist blacklist : blacklistCache.values()) {
if (blacklist.isValid() && blacklist.matches(ipAddress)) {
return true;
}
}
return false;
}
public boolean isWhitelisted(String ipAddress) {
if (ipAddress == null) {
return false;
}
for (FirewallWhitelist whitelist : whitelistCache.values()) {
if (whitelist.isValid() && whitelist.matches(ipAddress)) {
return true;
}
}
return false;
}
@Scheduled(fixedRate = 300000) // every 5 minutes
public void scheduledRefresh() {
try {
refreshRules();
refreshBlacklist();
refreshWhitelist();
log.debug("Scheduled firewall rule cache refresh completed");
} catch (Exception e) {
log.error("Scheduled firewall rule cache refresh failed", e);
}
}
// other CRUD methods omitted for brevity
}4.3 Interceptor Logic
@Slf4j
@Component
public class FirewallInterceptor implements HandlerInterceptor {
@Autowired
private RuleManager ruleManager;
@Autowired
private FirewallService firewallService;
@Value("${firewall.default.qps-limit:100}")
private int defaultQpsLimit;
@Value("${firewall.default.user-limit:1000}")
private int defaultUserLimit;
@Value("${firewall.default.time-window:60}")
private int defaultTimeWindow;
/** QPS limit cache */
private final Cache<String, AtomicInteger> qpsCache = CacheBuilder.newBuilder()
.maximumSize(10000)
.expireAfterWrite(1, TimeUnit.MINUTES)
.build();
/** user limit cache */
private final Cache<String, AtomicInteger> userCache = CacheBuilder.newBuilder()
.maximumSize(10000)
.expireAfterWrite(1, TimeUnit.HOURS)
.build();
@Override
public boolean preHandle(HttpServletRequest request, HttpServletResponse response, Object handler) throws Exception {
long startTime = System.currentTimeMillis();
String ipAddress = getClientIpAddress(request);
String apiPath = request.getRequestURI();
String userAgent = request.getHeader("User-Agent");
String method = request.getMethod();
log.debug("Firewall check: IP={}, API={}, Method={}", ipAddress, apiPath, method);
try {
// whitelist
if (ruleManager.isWhitelisted(ipAddress)) {
log.debug("IP {} in whitelist, allow", ipAddress);
logAccess(ipAddress, apiPath, userAgent, method, 200, null, startTime);
return true;
}
// blacklist
if (ruleManager.isBlacklisted(ipAddress)) {
log.warn("IP {} in blacklist, reject", ipAddress);
blockRequest(response, "IP address is blacklisted", 403);
logAccess(ipAddress, apiPath, userAgent, method, 403, "IP blacklist blocked", startTime);
return false;
}
// rule
FirewallRule rule = ruleManager.getMatchingRule(apiPath);
if (rule == null) {
rule = createDefaultRule(apiPath);
}
// QPS check
if (!checkQpsLimit(ipAddress, apiPath, rule)) {
log.warn("IP {} exceeds QPS limit {} on {}", ipAddress, rule.getEffectiveQpsLimit(), apiPath);
blockRequest(response, "Too many requests, please try later", 429);
logAccess(ipAddress, apiPath, userAgent, method, 429, "QPS blocked", startTime);
return false;
}
// user limit check
if (!checkUserLimit(ipAddress, rule)) {
log.warn("IP {} exceeds user limit {}", ipAddress, rule.getEffectiveUserLimit());
blockRequest(response, "Request count exceeded, please try later", 429);
logAccess(ipAddress, apiPath, userAgent, method, 429, "User limit blocked", startTime);
return false;
}
// normal access
logAccess(ipAddress, apiPath, userAgent, method, 200, null, startTime);
log.debug("IP {} passed firewall check for {}", ipAddress, apiPath);
return true;
} catch (Exception e) {
log.error("Firewall interceptor exception: IP={}, API={}", ipAddress, apiPath, e);
logAccess(ipAddress, apiPath, userAgent, method, 500, "System error", startTime);
return true; // allow to avoid service disruption
}
}
private boolean checkQpsLimit(String ip, String api, FirewallRule rule) {
int qpsLimit = rule.getEffectiveQpsLimit();
if (qpsLimit <= 0) return true;
String key = ip + ":" + api;
AtomicInteger counter = qpsCache.getIfPresent(key);
if (counter == null) {
counter = new AtomicInteger(0);
qpsCache.put(key, counter);
}
return counter.incrementAndGet() <= qpsLimit;
}
private boolean checkUserLimit(String ip, FirewallRule rule) {
int userLimit = rule.getEffectiveUserLimit();
if (userLimit <= 0) return true;
AtomicInteger counter = userCache.getIfPresent(ip);
if (counter == null) {
counter = new AtomicInteger(0);
userCache.put(ip, counter);
}
return counter.incrementAndGet() <= userLimit;
}
private String getClientIpAddress(HttpServletRequest request) {
String xForwardedFor = request.getHeader("X-Forwarded-For");
if (xForwardedFor != null && !xForwardedFor.isEmpty() && !"unknown".equalsIgnoreCase(xForwardedFor)) {
return xForwardedFor.split(",")[0].trim();
}
String xRealIp = request.getHeader("X-Real-IP");
if (xRealIp != null && !xRealIp.isEmpty() && !"unknown".equalsIgnoreCase(xRealIp)) {
return xRealIp;
}
return request.getRemoteAddr();
}
// blockRequest, logAccess, createDefaultRule omitted for brevity
}4.4 Online Configuration API
@Slf4j
@RestController
@RequestMapping("/api/firewall")
@CrossOrigin(origins = "*")
public class FirewallController {
@Autowired
private RuleManager ruleManager;
@Autowired
private FirewallService firewallService;
@Autowired
private FirewallInterceptor firewallInterceptor;
@GetMapping("/rules")
public ResponseEntity<List<FirewallRule>> getRules() {
try {
List<FirewallRule> rules = ruleManager.getAllRules();
return ResponseEntity.ok(rules);
} catch (Exception e) {
log.error("Failed to get firewall rules", e);
return ResponseEntity.status(500).build();
}
}
@PostMapping("/rules")
public ResponseEntity<Map<String, Object>> saveRule(@RequestBody FirewallRule rule) {
Map<String, Object> result = new HashMap<>();
try {
boolean success = ruleManager.saveRule(rule);
if (success) {
result.put("success", true);
result.put("message", "Rule saved successfully");
return ResponseEntity.ok(result);
} else {
result.put("success", false);
result.put("message", "Rule save failed");
return ResponseEntity.status(500).body(result);
}
} catch (Exception e) {
log.error("Failed to save firewall rule", e);
result.put("success", false);
result.put("message", "System error: " + e.getMessage());
return ResponseEntity.status(500).body(result);
}
}
@DeleteMapping("/rules/{id}")
public ResponseEntity<Map<String, Object>> deleteRule(@PathVariable Long id) {
Map<String, Object> result = new HashMap<>();
try {
boolean success = ruleManager.deleteRule(id);
if (success) {
result.put("success", true);
result.put("message", "Rule deleted successfully");
return ResponseEntity.ok(result);
} else {
result.put("success", false);
result.put("message", "Rule delete failed");
return ResponseEntity.status(500).body(result);
}
} catch (Exception e) {
log.error("Failed to delete firewall rule", e);
result.put("success", false);
result.put("message", "System error: " + e.getMessage());
return ResponseEntity.status(500).body(result);
}
}
// other endpoints for blacklist, logs, statistics omitted for brevity
}5. Front‑End Console
A modern management page built with HTML5, Tailwind CSS and JavaScript provides full firewall management functions.
<!DOCTYPE html>
<html lang="zh-CN">
<head>
<meta charset="UTF-8">
<title>Web防火墙控制台</title>
<script src="https://cdn.tailwindcss.com"></script>
<link href="https://cdnjs.cloudflare.com/ajax/libs/font-awesome/6.0.0/css/all.min.css" rel="stylesheet">
<script src="https://cdn.jsdelivr.net/npm/chart.js"></script>
</head>
<body class="bg-gray-50 font-sans">
<!-- navigation and dashboard omitted for brevity -->
</body>
</html>6. Configuration File (application.yml)
server:
port: 8080
servlet:
context-path: /
spring:
application:
name: springboot-firewall
jackson:
serialization:
write-dates-as-timestamps: false
time-zone: GMT+8
date-format: yyyy-MM-ddHH:mm:ss
datasource:
url: jdbc:h2:mem:firewall;DB_CLOSE_DELAY=-1;DB_CLOSE_ON_EXIT=FALSE
driver-class-name: org.h2.Driver
username: sa
password:
h2:
console:
enabled: true
path: /h2-console
settings:
web-allow-others: true
sql:
init:
mode: always
schema-locations: classpath:schema.sql
data-locations: classpath:data.sql
mybatis:
mapper-locations: classpath:mapper/*.xml
type-aliases-package: com.example.firewall.entity
configuration:
map-underscore-to-camel-case: true
log-impl: org.apache.ibatis.logging.stdout.StdOutImpl
management:
endpoints:
web:
exposure:
include: health,info,metrics
endpoint:
health:
show-details: always
logging:
level:
com.example.firewall: DEBUG
org.springframework.web: INFO
pattern:
console: "%d{yyyy-MM-dd HH:mm:ss} [%thread] %-5level %logger{36} - %msg%n"
firewall:
enabled: true
default-qps-limit: 100
default-user-limit: 60
cache-size: 1000
exclude-paths:
- /firewall/**
- /h2-console/**
- /actuator/**
- /static/**
- /favicon.ico7. Practical Scenarios
7.1 Small‑to‑medium System Protection
e‑commerce order interface:
- ruleName: "订单保护"
apiPattern: "/api/orders/**"
qpsLimit: 50
userLimit: 5
timeWindow: 60
enabled: true
description: "防止恶意刷单和重复下单"Content platform comment/like interfaces:
- ruleName: "内容互动限制"
apiPattern: "/api/comments/**,/api/likes/**"
qpsLimit: 200
userLimit: 20
timeWindow: 300
enabled: true
description: "防止垃圾评论和刷赞行为"7.2 Burst Traffic Handling
During a flash‑sale, dynamically add a rule:
POST /api/firewall/rules
{
"ruleName": "秒杀活动保护",
"apiPattern": "/api/seckill/**",
"qpsLimit": 1000,
"userLimit": 1,
"timeWindow": 60,
"enabled": true,
"description": "秒杀活动期间特殊限制"
}Manual blacklist example:
FirewallBlacklist blacklist = new FirewallBlacklist();
blacklist.setIpAddress("192.168.1.100");
blacklist.setReason("恶意爬虫行为");
blacklist.setExpireTime(LocalDateTime.now().plusHours(24));
blacklist.setEnabled(true);
ruleManager.addBlacklist(blacklist);7.3 Deployment and Operations
Docker image:
FROM openjdk:11-jre-slim
VOLUME /tmp
COPY target/springboot-firewall-1.0.jar app.jar
EXPOSE 8080
ENTRYPOINT ["java","-jar","/app.jar"]Prometheus metrics integration:
management:
metrics:
tags:
application: firewall
export:
prometheus:
enabled: trueConclusion
This article implements a lightweight API firewall based on Spring Boot that provides real‑time protection through interceptor mechanisms, Guava Cache for high‑performance in‑memory rate limiting, and fine‑grained black‑white list management. A modern web console enables online rule configuration and immediate effect, offering a concise and efficient solution for small‑to‑medium projects.
Code Ape Tech Column
Former Ant Group P8 engineer, pure technologist, sharing full‑stack Java, job interview and career advice through a column. Site: java-family.cn
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.
