Spring Boot 实现接口防刷的五种方案

在开发 Web 应用时,接口防刷(防止恶意或高频请求)是一个常见的需求。以下是五种常见的实现方案,适用于 Spring Boot 项目:

图片[1]_Spring Boot 实现接口防刷的五种方案_知途无界

方案一:基于 IP 的请求限制

原理

通过记录客户端的 IP 地址,对同一 IP 的请求频率进行限制。

实现方式

  1. 使用 Guava 的 RateLimiter 或 Redis 计数器。
  2. 记录每个 IP 的请求次数,设置时间窗口(如 1 分钟内最多允许 100 次请求)。
  3. 如果超过限制,返回错误响应(如 HTTP 429 Too Many Requests)。

代码示例

@RestController
public class RateLimitController {
private final Map<String, Long> ipRequestMap = new ConcurrentHashMap<>();
private static final int MAX_REQUESTS = 100;
private static final long TIME_WINDOW = 60 * 1000; // 1分钟
@GetMapping("/api/test")
public ResponseEntity<String> testEndpoint(@RequestHeader("X-Forwarded-For") String ip) {
long currentTime = System.currentTimeMillis();
ipRequestMap.putIfAbsent(ip, currentTime);
Long firstRequestTime = ipRequestMap.get(ip);
if (currentTime - firstRequestTime > TIME_WINDOW) {
// 重置时间窗口
ipRequestMap.put(ip, currentTime);
} else if (ipRequestMap.values().stream().filter(time -> time >= firstRequestTime).count() > MAX_REQUESTS) {
return ResponseEntity.status(HttpStatus.TOO_MANY_REQUESTS).body("请求过于频繁");
}
return ResponseEntity.ok("请求成功");
}
}
@RestController
public class RateLimitController {

    private final Map<String, Long> ipRequestMap = new ConcurrentHashMap<>();
    private static final int MAX_REQUESTS = 100;
    private static final long TIME_WINDOW = 60 * 1000; // 1分钟

    @GetMapping("/api/test")
    public ResponseEntity<String> testEndpoint(@RequestHeader("X-Forwarded-For") String ip) {
        long currentTime = System.currentTimeMillis();
        ipRequestMap.putIfAbsent(ip, currentTime);

        Long firstRequestTime = ipRequestMap.get(ip);
        if (currentTime - firstRequestTime > TIME_WINDOW) {
            // 重置时间窗口
            ipRequestMap.put(ip, currentTime);
        } else if (ipRequestMap.values().stream().filter(time -> time >= firstRequestTime).count() > MAX_REQUESTS) {
            return ResponseEntity.status(HttpStatus.TOO_MANY_REQUESTS).body("请求过于频繁");
        }

        return ResponseEntity.ok("请求成功");
    }
}
@RestController public class RateLimitController { private final Map<String, Long> ipRequestMap = new ConcurrentHashMap<>(); private static final int MAX_REQUESTS = 100; private static final long TIME_WINDOW = 60 * 1000; // 1分钟 @GetMapping("/api/test") public ResponseEntity<String> testEndpoint(@RequestHeader("X-Forwarded-For") String ip) { long currentTime = System.currentTimeMillis(); ipRequestMap.putIfAbsent(ip, currentTime); Long firstRequestTime = ipRequestMap.get(ip); if (currentTime - firstRequestTime > TIME_WINDOW) { // 重置时间窗口 ipRequestMap.put(ip, currentTime); } else if (ipRequestMap.values().stream().filter(time -> time >= firstRequestTime).count() > MAX_REQUESTS) { return ResponseEntity.status(HttpStatus.TOO_MANY_REQUESTS).body("请求过于频繁"); } return ResponseEntity.ok("请求成功"); } }

优化建议

  • 使用 Redis 实现分布式请求计数。
  • 配合 Nginx 或网关层进行 IP 限制。

方案二:基于 Token 的限流

原理

为每个用户生成唯一的 Token,Token 附带时间戳和请求次数,服务器验证 Token 的有效性。

实现方式

  1. 用户登录后生成 Token,Token 中包含用户 ID 和请求计数。
  2. 每次请求时验证 Token,并更新计数。
  3. 如果计数超过限制,拒绝请求。

代码示例

@RestController
public class TokenLimitController {
private final Map<String, Integer> tokenMap = new ConcurrentHashMap<>();
@PostMapping("/login")
public String login() {
// 模拟生成 Token
String token = UUID.randomUUID().toString();
tokenMap.put(token, 0);
return token;
}
@GetMapping("/api/test")
public ResponseEntity<String> testEndpoint(@RequestHeader("Authorization") String token) {
if (!tokenMap.containsKey(token)) {
return ResponseEntity.status(HttpStatus.UNAUTHORIZED).body("无效的 Token");
}
int count = tokenMap.compute(token, (k, v) -> v == null ? 0 : v + 1);
if (count > 10) { // 每 Token 限制 10 次请求
return ResponseEntity.status(HttpStatus.TOO_MANY_REQUESTS).body("请求过于频繁");
}
return ResponseEntity.ok("请求成功");
}
}
@RestController
public class TokenLimitController {

    private final Map<String, Integer> tokenMap = new ConcurrentHashMap<>();

    @PostMapping("/login")
    public String login() {
        // 模拟生成 Token
        String token = UUID.randomUUID().toString();
        tokenMap.put(token, 0);
        return token;
    }

    @GetMapping("/api/test")
    public ResponseEntity<String> testEndpoint(@RequestHeader("Authorization") String token) {
        if (!tokenMap.containsKey(token)) {
            return ResponseEntity.status(HttpStatus.UNAUTHORIZED).body("无效的 Token");
        }

        int count = tokenMap.compute(token, (k, v) -> v == null ? 0 : v + 1);
        if (count > 10) { // 每 Token 限制 10 次请求
            return ResponseEntity.status(HttpStatus.TOO_MANY_REQUESTS).body("请求过于频繁");
        }

        return ResponseEntity.ok("请求成功");
    }
}
@RestController public class TokenLimitController { private final Map<String, Integer> tokenMap = new ConcurrentHashMap<>(); @PostMapping("/login") public String login() { // 模拟生成 Token String token = UUID.randomUUID().toString(); tokenMap.put(token, 0); return token; } @GetMapping("/api/test") public ResponseEntity<String> testEndpoint(@RequestHeader("Authorization") String token) { if (!tokenMap.containsKey(token)) { return ResponseEntity.status(HttpStatus.UNAUTHORIZED).body("无效的 Token"); } int count = tokenMap.compute(token, (k, v) -> v == null ? 0 : v + 1); if (count > 10) { // 每 Token 限制 10 次请求 return ResponseEntity.status(HttpStatus.TOO_MANY_REQUESTS).body("请求过于频繁"); } return ResponseEntity.ok("请求成功"); } }

优化建议

  • 使用 JWT 或 Redis 存储 Token。
  • 配合前端实现 Token 的自动刷新。

方案三:基于滑动窗口算法的限流

原理

滑动窗口算法通过记录每个请求的时间戳,动态计算时间窗口内的请求次数。

实现方式

  1. 使用 LinkedHashMap 或 Redis 的 Sorted Set 存储请求时间戳。
  2. 每次请求时移除窗口外的旧时间戳,计算剩余时间戳的数量。
  3. 如果数量超过限制,拒绝请求。

代码示例

@RestController
public class SlidingWindowController {
private final Map<String, LinkedList<Long>> ipRequestMap = new ConcurrentHashMap<>();
private static final int MAX_REQUESTS = 100;
private static final long TIME_WINDOW = 60 * 1000; // 1分钟
@GetMapping("/api/test")
public ResponseEntity<String> testEndpoint(@RequestHeader("X-Forwarded-For") String ip) {
long currentTime = System.currentTimeMillis();
ipRequestMap.computeIfAbsent(ip, k -> new LinkedList<>()).add(currentTime);
LinkedList<Long> requests = ipRequestMap.get(ip);
while (!requests.isEmpty() && requests.peekFirst() < currentTime - TIME_WINDOW) {
requests.pollFirst();
}
if (requests.size() > MAX_REQUESTS) {
return ResponseEntity.status(HttpStatus.TOO_MANY_REQUESTS).body("请求过于频繁");
}
return ResponseEntity.ok("请求成功");
}
}
@RestController
public class SlidingWindowController {

    private final Map<String, LinkedList<Long>> ipRequestMap = new ConcurrentHashMap<>();
    private static final int MAX_REQUESTS = 100;
    private static final long TIME_WINDOW = 60 * 1000; // 1分钟

    @GetMapping("/api/test")
    public ResponseEntity<String> testEndpoint(@RequestHeader("X-Forwarded-For") String ip) {
        long currentTime = System.currentTimeMillis();
        ipRequestMap.computeIfAbsent(ip, k -> new LinkedList<>()).add(currentTime);

        LinkedList<Long> requests = ipRequestMap.get(ip);
        while (!requests.isEmpty() && requests.peekFirst() < currentTime - TIME_WINDOW) {
            requests.pollFirst();
        }

        if (requests.size() > MAX_REQUESTS) {
            return ResponseEntity.status(HttpStatus.TOO_MANY_REQUESTS).body("请求过于频繁");
        }

        return ResponseEntity.ok("请求成功");
    }
}
@RestController public class SlidingWindowController { private final Map<String, LinkedList<Long>> ipRequestMap = new ConcurrentHashMap<>(); private static final int MAX_REQUESTS = 100; private static final long TIME_WINDOW = 60 * 1000; // 1分钟 @GetMapping("/api/test") public ResponseEntity<String> testEndpoint(@RequestHeader("X-Forwarded-For") String ip) { long currentTime = System.currentTimeMillis(); ipRequestMap.computeIfAbsent(ip, k -> new LinkedList<>()).add(currentTime); LinkedList<Long> requests = ipRequestMap.get(ip); while (!requests.isEmpty() && requests.peekFirst() < currentTime - TIME_WINDOW) { requests.pollFirst(); } if (requests.size() > MAX_REQUESTS) { return ResponseEntity.status(HttpStatus.TOO_MANY_REQUESTS).body("请求过于频繁"); } return ResponseEntity.ok("请求成功"); } }

优化建议

  • 使用 Redis 的 ZADD 和 ZREMRANGEBYSCORE 实现分布式滑动窗口。

方案四:使用网关层限流

原理

在 API 网关(如 Nginx、Spring Cloud Gateway、Kong)中配置限流规则,拦截恶意请求。

实现方式

  1. 配置 Nginx 的 limit_req 模块。
  2. 使用 Spring Cloud Gateway 的 RequestRateLimiter 过滤器。

Nginx 示例

http {
limit_req_zone $binary_remote_addr zone=one:10m rate=10r/s;
server {
location /api/ {
limit_req zone=one burst=20 nodelay;
}
}
}
http {
    limit_req_zone $binary_remote_addr zone=one:10m rate=10r/s;

    server {
        location /api/ {
            limit_req zone=one burst=20 nodelay;
        }
    }
}
http { limit_req_zone $binary_remote_addr zone=one:10m rate=10r/s; server { location /api/ { limit_req zone=one burst=20 nodelay; } } }

Spring Cloud Gateway 示例

spring:
cloud:
gateway:
routes:
- id: rate_limiter_route
uri: http://example.org
filters:
- name: RequestRateLimiter
args:
redis-rate-limiter.replenishRate: 10
redis-rate-limiter.burstCapacity: 20
predicates:
- Path=/api/**
spring:
  cloud:
    gateway:
      routes:
      - id: rate_limiter_route
        uri: http://example.org
        filters:
        - name: RequestRateLimiter
          args:
            redis-rate-limiter.replenishRate: 10
            redis-rate-limiter.burstCapacity: 20
        predicates:
        - Path=/api/**
spring: cloud: gateway: routes: - id: rate_limiter_route uri: http://example.org filters: - name: RequestRateLimiter args: redis-rate-limiter.replenishRate: 10 redis-rate-limiter.burstCapacity: 20 predicates: - Path=/api/**

方案五:验证码机制

原理

对于高风险接口(如登录、支付),要求用户输入验证码,防止自动化脚本刷接口。

实现方式

  1. 集成第三方验证码服务(如阿里云、腾讯云)。
  2. 在关键接口前增加验证码验证逻辑。

代码示例

@RestController
public class CaptchaController {
@GetMapping("/api/captcha")
public String generateCaptcha() {
// 生成验证码并返回给前端
return "模拟验证码";
}
@PostMapping("/api/sensitive")
public ResponseEntity<String> sensitiveOperation(@RequestParam String captcha) {
if (!"1234".equals(captcha)) { // 模拟验证码验证
return ResponseEntity.status(HttpStatus.BAD_REQUEST).body("验证码错误");
}
return ResponseEntity.ok("操作成功");
}
}
@RestController
public class CaptchaController {

    @GetMapping("/api/captcha")
    public String generateCaptcha() {
        // 生成验证码并返回给前端
        return "模拟验证码";
    }

    @PostMapping("/api/sensitive")
    public ResponseEntity<String> sensitiveOperation(@RequestParam String captcha) {
        if (!"1234".equals(captcha)) { // 模拟验证码验证
            return ResponseEntity.status(HttpStatus.BAD_REQUEST).body("验证码错误");
        }
        return ResponseEntity.ok("操作成功");
    }
}
@RestController public class CaptchaController { @GetMapping("/api/captcha") public String generateCaptcha() { // 生成验证码并返回给前端 return "模拟验证码"; } @PostMapping("/api/sensitive") public ResponseEntity<String> sensitiveOperation(@RequestParam String captcha) { if (!"1234".equals(captcha)) { // 模拟验证码验证 return ResponseEntity.status(HttpStatus.BAD_REQUEST).body("验证码错误"); } return ResponseEntity.ok("操作成功"); } }

总结与选择

方案适用场景优点缺点
基于 IP 的请求限制简单接口防刷实现简单,无需用户登录无法区分正常用户和恶意用户
基于 Token 的限流用户登录后的接口精准限流,用户体验较好需要用户登录,复杂度较高
滑动窗口算法高并发场景限流精准,资源占用少实现复杂
网关层限流分布式系统性能高,集中管理需要额外部署网关
验证码机制高风险接口防止自动化脚本刷接口用户体验较差

推荐

  • 对于简单接口,优先使用 基于 IP 的请求限制
  • 对于用户登录后的接口,使用 基于 Token 的限流
  • 对于高并发场景,结合 滑动窗口算法 和 网关层限流
  • 对于高风险接口,增加 验证码机制

通过组合多种方案,可以更有效地防止接口被恶意刷取。

© 版权声明
THE END
喜欢就点个赞,支持一下吧!
点赞83 分享
Never,never,never,never give up.
永远不要、不要、不要、不要放弃
评论 抢沙发
头像
欢迎您留下评论!
提交
头像

昵称

取消
昵称表情代码图片

    暂无评论内容