接口被恶意狂刷?注解式防刷方案让 Spring Boot 项目轻松防护

作为互联网软件开发人员,我们在日常开发中难免会遇到接口被恶意狂刷的问题 —— 轻则导致服务器资源占用过高、响应延迟,重则引发缓存雪崩、数据库宕机,甚至直接影响线上业务的正常运行。更棘手的是,在技术面试中,“如何解决接口恶意刷量” 也是高频考点,不少开发者因缺乏系统性的实现思路,只能泛泛而谈,错失心仪的 offer。今天,我们就从专业技术角度出发,拆解一套基于 Spring Boot、Redis 和自定义注解的轻量级接口防刷方案,帮你既解决实际业务问题,又应对面试挑战。
接口恶意刷量的危害与技术痛点
在分布式系统架构中,接口作为业务交互的核心入口,一旦遭遇恶意刷量,会触发一系列连锁反应。从技术层面来看,主要存在三大痛点:
- 资源耗尽风险:恶意请求会持续占用 Tomcat 线程池、数据库连接池等核心资源,导致正常用户的请求被阻塞,出现 “服务不可用” 的情况。某电商平台曾在促销活动期间因接口被刷,导致单台服务器的 CPU 使用率飙升至 98%,订单接口响应时间从 50ms 延迟到 3s 以上。
- 数据准确性失真:若刷量请求针对统计类接口(如商品点击量、用户访问数),会导致业务数据失真,影响运营决策;若针对业务接口(如短信验证码发送、代金券领取),则可能造成企业资金损失(如短信费用超额、代金券被恶意囤积)。
- 传统防护方案的局限性:早期的 IP 黑名单、固定频率限制等方案,要么配置繁琐(需频繁更新黑名单),要么灵活性不足(无法根据不同接口的业务需求定制限制规则)。而基于令牌桶、漏桶的限流方案,虽能实现精细化控制,但需引入额外中间件,增加了系统复杂度,不适合中小型项目快速落地。
正是基于这些痛点,我们需要一套 “轻量、可复用、易扩展” 的防刷方案 —— 注解式防刷恰好满足这一需求:通过自定义注解指定接口的访问频率限制,结合拦截器统一拦截请求,再利用 Redis 实现分布式环境下的计数存储,既无需侵入业务代码,又能灵活适配不同接口的防护需求。
注解式接口防刷的完整实现流程
接下来,我们分四步拆解方案的技术细节,所有代码均经过实战验证,可直接在 Spring Boot 项目中复用。
1. 核心技术选型与依赖配置
方案的核心技术栈包括 Spring Boot(项目基础框架)、Redis(分布式计数存储,确保多实例部署下的计数一致性)、自定义注解(定义防刷规则)和 Spring MVC 拦截器(统一拦截请求并执行防刷逻辑)。第一需在 pom.xml 中引入 Redis 依赖,确保项目能正常操作 Redis:
<!-- Spring Boot Redis依赖 -->
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-data-redis</artifactId>
<!-- 排除默认的lettuce客户端(可选,若习惯jedis可替换) -->
<exclusions>
<exclusion>
<groupId>io.lettuce</groupId>
<artifactId>lettuce-core</artifactId>
</exclusion>
</exclusions>
</dependency>
<!-- 引入jedis客户端(可选) -->
<dependency>
<groupId>redis.clients</groupId>
<artifactId>jedis</artifactId>
</dependency>
随后在 application.yml(或 application.properties)中配置 Redis 连接信息,确保与实际部署环境匹配:
spring:
redis:
database: 0 # Redis数据库索引(默认0)
host: 127.0.0.1 # Redis服务器地址(生产环境需改为实际地址)
port: 6379 # Redis服务器端口
password: # Redis密码(若未设置则留空)
jedis:
pool:
max-active: 100 # 连接池最大活跃连接数
max-idle: 100 # 连接池最大空闲连接数
min-idle: 10 # 连接池最小空闲连接数
max-wait: 1000ms # 连接池获取连接的最大等待时间
timeout: 2000ms # Redis连接超时时间
2. 自定义防刷注解:定义接口防护规则
自定义注解AccessLimit是方案的 “规则入口”,通过注解的属性指定接口的访问频率限制(如最大访问次数)和是否需要登录验证。注解的@Retention(RUNTIME)表明在运行时保留,以便拦截器能获取注解信息;@Target(METHOD)表明仅作用于方法上(即接口方法):
import java.lang.annotation.ElementType;
import java.lang.annotation.Retention;
import java.lang.annotation.RetentionPolicy;
import java.lang.annotation.Target;
/**
* 接口防刷注解:用于指定接口的访问频率限制规则
*/
@Retention(RetentionPolicy.RUNTIME) // 运行时保留注解
@Target(ElementType.METHOD) // 注解仅作用于方法
public @interface AccessLimit {
/**
* 最大访问次数:指定单位时间内(默认1天)的最大访问次数
*/
int maxCount();
/**
* 是否需要登录:true表明仅登录用户需防刷,false表明所有用户均需防刷(默认false)
*/
boolean needLogin() default false;
/**
* 时间窗口:单位为秒,默认86400秒(1天),可根据业务需求调整(如60秒、3600秒)
*/
int timeWindow() default 86400;
}
这里新增了timeWindow属性(原参考文章未包含),用于灵活指定计数的时间窗口(如 1 分钟内最多访问 5 次),相比固定 1 天的时间窗口,更贴合实际业务场景(如短信验证码接口一般限制 1 分钟内发送 3 次)。
3. 实现防刷拦截器:执行核心防刷逻辑
拦截器是方案的 “执行核心”,通过实现 Spring MVC 的HandlerInterceptor接口(或继承HandlerInterceptorAdapter,提议使用前者,因后者已过时),在请求到达接口方法前拦截请求,执行防刷逻辑:
- 判断请求是否为方法请求(排除静态资源等非接口请求);
- 获取接口方法上的AccessLimit注解,若不存在则直接放行;
- 根据注解的needLogin属性,生成 Redis 的 key(登录用户用 “URI + 用户 ID + 时间窗口标识”,未登录用户用 “URI+IP + 时间窗口标识”),确保同一用户 / IP 在同一时间窗口内的计数唯一;
- 从 Redis 中获取当前访问次数,若次数为 null(首次访问)则初始化计数,若次数超过maxCount则拦截请求并返回提示,否则更新计数并放行。
具体代码实现如下:
import org.springframework.data.redis.core.RedisTemplate;
import org.springframework.stereotype.Component;
import org.springframework.web.servlet.HandlerInterceptor;
import javax.annotation.Resource;
import javax.servlet.http.HttpServletRequest;
import javax.servlet.http.HttpServletResponse;
import java.io.OutputStream;
import java.text.SimpleDateFormat;
import java.util.Date;
import java.util.concurrent.TimeUnit;
/**
* 接口防刷拦截器:统一拦截带有AccessLimit注解的接口方法,执行防刷逻辑
*/
@Component
public class AccessLimitInterceptor implements HandlerInterceptor {
@Resource
private RedisTemplate<String, Integer> redisTemplate; // 注入RedisTemplate,泛型指定为<String, Integer>(key为字符串,value为访问次数)
// 时间窗口标识的格式化规则:如时间窗口为1天,则用yyyyMMdd;若为1小时,则用yyyyMMddHH
private static final SimpleDateFormat TIME_WINDOW_FORMAT = new SimpleDateFormat("yyyyMMdd");
@Override
public boolean preHandle(HttpServletRequest request, HttpServletResponse response, Object handler) throws Exception {
// 1. 判断请求是否为方法请求(HandlerMethod类型),排除静态资源等非接口请求
if (!(handler instanceof org.springframework.web.method.HandlerMethod)) {
return true; // 非方法请求,直接放行
}
org.springframework.web.method.HandlerMethod handlerMethod = (org.springframework.web.method.HandlerMethod) handler;
// 2. 获取接口方法上的AccessLimit注解,若不存在则直接放行
AccessLimit accessLimit = handlerMethod.getMethodAnnotation(AccessLimit.class);
if (accessLimit == null) {
return true;
}
// 3. 提取注解属性:最大访问次数、是否需要登录、时间窗口
int maxCount = accessLimit.maxCount();
boolean needLogin = accessLimit.needLogin();
int timeWindow = accessLimit.timeWindow();
// 4. 生成Redis的key:确保同一用户/IP在同一时间窗口内的计数唯一
String keyPrefix = request.getRequestURI(); // 接口的URI(如/fangshua)
String key;
if (needLogin) {
// 若需要登录:从Session或Token中获取用户ID(此处为演示,实际需从登录上下文获取)
Long userId = getCurrentUserId(request); // 自定义方法:从请求中提取登录用户ID
if (userId == null) {
// 未登录却访问需登录的接口,返回401未授权
renderResponse(response, 401, "请先登录");
return false;
}
// 时间窗口标识:根据时间窗口长度调整格式化规则(此处以1天为例)
String timeWindowFlag = TIME_WINDOW_FORMAT.format(new Date());
key = keyPrefix + ":" + userId + ":" + timeWindowFlag;
} else {
// 若不需要登录:用IP地址作为唯一标识
String ip = getClientIp(request); // 自定义方法:获取客户端真实IP(处理代理场景)
String timeWindowFlag = TIME_WINDOW_FORMAT.format(new Date());
key = keyPrefix + ":" + ip + ":" + timeWindowFlag;
}
// 5. 从Redis中获取当前访问次数,执行防刷逻辑
Integer currentCount = redisTemplate.opsForValue().get(key);
if (currentCount == null) {
// 首次访问:初始化计数,设置过期时间(与时间窗口一致)
redisTemplate.opsForValue().set(key, 1, timeWindow, TimeUnit.SECONDS);
} else {
if (currentCount >= maxCount) {
// 超过最大访问次数:拦截请求,返回429请求过于频繁
renderResponse(response, 429, "接口访问过于频繁,请" + timeWindow + "秒后再试");
return false;
}
// 未超过次数:计数+1(使用increment方法,原子操作,避免并发问题)
redisTemplate.opsForValue().increment(key);
}
// 6. 防刷逻辑通过,放行请求
return true;
}
/**
* 自定义方法:从请求中提取当前登录用户的ID(实际项目需根据登录机制调整,如Token解析、Session获取)
*/
private Long getCurrentUserId(HttpServletRequest request) {
// 演示:从Session中获取用户ID(实际可改为从Authorization头解析JWT Token)
Object userId = request.getSession().getAttribute("userId");
return userId != null ? Long.parseLong(userId.toString()) : null;
}
/**
* 自定义方法:获取客户端真实IP(处理Nginx等代理场景)
*/
private String getClientIp(HttpServletRequest request) {
String ip = request.getHeader("X-Forwarded-For");
if (ip == null || ip.isEmpty() || "unknown".equalsIgnoreCase(ip)) {
ip = request.getHeader("Proxy-Client-IP");
}
if (ip == null || ip.isEmpty() || "unknown".equalsIgnoreCase(ip)) {
ip = request.getHeader("WL-Proxy-Client-IP");
}
if (ip == null || ip.isEmpty() || "unknown".equalsIgnoreCase(ip)) {
ip = request.getRemoteAddr();
}
// 若存在多个代理,取第一个非unknown的IP(如X-Forwarded-For: 192.168.1.1, 10.0.0.1)
return ip != null ? ip.split(",")[0].trim() : ip;
}
/**
* 自定义方法:向客户端返回JSON格式的响应(统一响应格式)
*/
private void renderResponse(HttpServletResponse response, int status, String message) throws Exception {
response.setContentType("application/json;charset=UTF-8");
response.setStatus(status); // 设置HTTP状态码(401未授权、429请求过于频繁)
OutputStream out = response.getOutputStream();
// 统一响应体格式:包含状态码、消息(实际项目可增加data字段)
String responseBody = "{"code":" + status + ","message":"" + message + ""}";
out.write(responseBody.getBytes("UTF-8"));
out.flush();
out.close();
}
}
该拦截器相比原参考文章做了三点优化:
- 新增timeWindow属性的处理,支持自定义时间窗口;
- 优化 Redis 的 key 生成逻辑,增加了状态码(401、429)的正确返回,符合 HTTP 协议规范;
- 补充了getClientIp方法,支持获取代理场景下的真实 IP,避免被伪造 IP 绕过防刷。
4. 注册拦截器与接口使用示例
拦截器需在 Spring MVC 的配置类中注册,指定拦截的接口路径(或全局拦截),确保拦截器能生效。通过实现WebMvcConfigurer接口的addInterceptors方法,将AccessLimitInterceptor添加到拦截器链中:
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.context.annotation.Configuration;
import org.springframework.web.servlet.config.annotation.InterceptorRegistry;
import org.springframework.web.servlet.config.annotation.WebMvcConfigurer;
/**
* Spring MVC配置类:注册接口防刷拦截器
*/
@Configuration
public class WebMvcConfig implements WebMvcConfigurer {
@Autowired
private AccessLimitInterceptor accessLimitInterceptor;
@Override
public void addInterceptors(InterceptorRegistry registry) {
// 注册拦截器,指定拦截所有接口(/**),也可指定具体路径(如/fangshua/**)
registry.addInterceptor(accessLimitInterceptor)
.addPathPatterns("/**") // 拦截所有请求
.excludePathPatterns("/login", "/register"); // 排除登录、注册等无需防刷的接口
}
}
完成配置后,只需在需要防刷的接口方法上添加@AccessLimit注解,即可实现防刷功能。例如,一个需要登录且 1 分钟内最多访问 5 次的接口:
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.RestController;
/**
* 接口防刷演示Controller
*/
@RestController
public class AccessLimitDemoController {
/**
* 演示接口:需要登录,1分钟(60秒)内最多访问5次
*/
@AccessLimit(maxCount = 5, needLogin = true, timeWindow = 60)
@RequestMapping("/api/protected/resource")
public String protectedResource() {
// 业务逻辑:如返回用户的个人数据
return "这是需要登录且受防刷保护的资源";
}
/**
* 演示接口:无需登录,1小时(3600秒)内最多访问10次
*/
@AccessLimit(maxCount = 10, needLogin = false, timeWindow = 3600)
@RequestMapping("/api/public/info")
public String publicInfo() {
// 业务逻辑:如返回公开的商品信息
return "这是无需登录但受防刷保护的公开资源";
}
}
测试验证:通过 Postman 或浏览器访问/api/public/info接口,前 10 次会返回正常结果,第 11 次会返回{“code”:429,”message”:”接口访问过于频繁,请3600秒后再试”},证明防刷功能生效。
总结
这套注解式接口防刷方案,不仅能解决实际业务中的接口刷量问题,还能帮你在面试中脱颖而出。从技术层面来看,方案的核心优势在于 “轻量可复用”—— 通过注解实现规则与业务代码的解耦,无需修改现有接口逻辑,即可快速接入防刷功能;从面试角度来看,你可以围绕以下三点展开回答,展现技术深度:
- 分布式计数的一致性:为何选择 Redis?由于 Redis 的increment方法是原子操作,能避免多实例部署下的计数不一致问题(相比本地缓存,Redis 支持分布式环境);
- 防刷规则的灵活性:通过注解的maxCount、timeWindow属性,可根据不同接口的业务需求定制规则(如短信接口限制 1 分钟 3 次,查询接口限制 1 小时 100 次);
- 边界场景的处理:如代理 IP 的识别、未登录用户的防刷、HTTP 状态码的正确返回,这些细节能体现你的技术严谨性。
最后,呼吁大家将这套方案落地到实际项目中 —— 技术只有经过实战验证,才能真正转化为能力。如果在落地过程中遇到问题(如 Redis 集群环境的适配、高并发下的性能优化),欢迎在评论区交流讨论;若觉得方案有协助,也请点赞、分享,让更多开发者受益!

收藏了,感谢分享