接口被狂刷怎么处理

接口被狂刷怎么处理

"紧急止损→多层防御→监控复盘" 👇

🚨 第一步:紧急止损(先保命!黄金 3 分钟)

  1. 快速扩容:立刻将服务实例数拉满,利用云厂商弹性伸缩能力扛住峰值
  2. 熔断降级:非核心接口直接返回默认值 / 降级页,核心接口开启熔断保护
  3. 流量清洗:接入 CDN/WAF,过滤掉明显恶意请求(相同 IP / 设备号高频访问)
  4. 临时封禁:对异常 IP、UID、设备号执行 15 分钟 - 24 小时临时封禁

🛡️ 第二步:构建多层防御体系(长治久安)

这是核心!单一防护必被突破,必须做到层层设防、纵深防御

image

⚡ 核心技术:四大限流算法对比

这是面试必问加分项,必须说清各自适用场景

算法名称 核心思想 优点 缺点 适用场景
固定窗口计数器 统计单位时间内请求数,超过阈值则拒绝 实现简单、性能好 存在临界问题(窗口切换时可能出现 2 倍流量) 要求不高的简单限流
滑动窗口计数器 将时间窗口拆分为多个小格子,滑动统计 解决了临界问题 实现稍复杂,需要存储多个格子的计数 大多数业务场景
漏桶算法 请求像水一样进入漏桶,以固定速率流出 强制匀速处理,平滑突发流量 无法应对突发流量,桶满直接丢弃 消息队列削峰填谷
令牌桶算法 以固定速率生成令牌,请求需要拿到令牌才能执行 既能限流又能应对突发流量 实现相对复杂 互联网接口限流(首选)

💡 大厂首选:Redis+Lua 实现分布式令牌桶算法,或者直接使用 Sentinel/Resilience4j 框架

✨ 进阶优化技巧(拉开差距的加分项)

  1. 分布式限流:网关层做全局限流,应用层做接口级限流,避免单点限流失效
  2. 热点隔离:将热点接口单独部署在独立集群,避免拖垮整个服务
  3. 防爬虫策略:设备指纹识别、User-Agent 检测、Cookie 校验、动态 Token
  4. 缓存防护:布隆过滤器防穿透、热点缓存永不过期、互斥锁防击穿
  5. 灰度发布:新接口先切 10% 流量,观察稳定后再全量发布

🔍 第三步:监控告警与事后复盘

  1. 实时监控:重点关注接口 QPS、响应时间、错误率、服务器 CPU / 内存 / 磁盘
  2. 多级告警:设置梯度阈值(警告→严重→紧急),通过短信 / 电话 / 邮件通知
  3. 事后复盘:攻击结束后分析攻击来源、攻击方式、防护漏洞,输出复盘报告并优化防护策略

📝 总结

接口防刷不是单点问题,而是系统性工程。核心思路是:先止损保命,再层层设防,最后通过监控复盘持续优化。同时要根据业务场景选择合适的限流算法和防护策略,在安全性和用户体验之间找到平衡点。

💻 核心代码实现(带技术亮点标注)

1. Redis+Lua 分布式令牌桶实现(大厂首选 ✅)

技术亮点:利用 Lua 脚本保证 Redis 操作的原子性,避免并发下的计数错误,单实例性能可达 10w+ QPS,支持分布式环境。

第一步:令牌桶核心 Lua 脚本

-- 限流Key(如:rate_limit:user:123:api:/order)
local key = KEYS[1]
-- 桶容量(最大突发流量)
local capacity = tonumber(ARGV[1])
-- 令牌生成速率(个/秒)
local rate = tonumber(ARGV[2])
-- 请求的令牌数(一般为1)
local requested = tonumber(ARGV[3])
-- 当前时间戳(毫秒)
local now = tonumber(ARGV[4])

-- 桶中剩余令牌数
local remaining = tonumber(redis.call('hget', key, 'remaining') or capacity)
-- 上次令牌生成时间
local last_refill = tonumber(redis.call('hget', key, 'last_refill') or now)

-- 计算从上次到现在生成的令牌数
local delta = math.max(0, now - last_refill) * rate / 1000
-- 更新剩余令牌数(不超过桶容量)
local new_remaining = math.min(capacity, remaining + delta)
-- 更新最后刷新时间
local new_last_refill = now

-- 判断是否有足够的令牌
if new_remaining >= requested then
    new_remaining = new_remaining - requested
    -- 存储新状态,设置过期时间(避免冷Key占用内存)
    redis.call('hmset', key, 'remaining', new_remaining, 'last_refill', new_last_refill)
    redis.call('expire', key, 3600)
    -- 返回1表示允许通过
    return 1
else
    -- 返回0表示拒绝
    return 0
end

第二步:Java 调用工具类

@Component
public class RedisRateLimiter {
    @Autowired
    private StringRedisTemplate redisTemplate;
    
    // 预加载Lua脚本,避免每次编译
    private final DefaultRedisScript<Long> rateLimitScript;
    
    public RedisRateLimiter() {
        rateLimitScript = new DefaultRedisScript<>();
        rateLimitScript.setScriptSource(new ResourceScriptSource(new ClassPathResource("rate_limit.lua")));
        rateLimitScript.setResultType(Long.class);
    }
    
    /**
     * 尝试获取令牌
     * @param key 限流唯一标识(接口+IP+UID)
     * @param capacity 桶容量
     * @param rate 令牌生成速率(个/秒)
     * @return true=允许通过,false=限流
     */
    public boolean tryAcquire(String key, int capacity, int rate) {
        List<String> keys = Collections.singletonList(key);
        Long result = redisTemplate.execute(
            rateLimitScript,
            keys,
            String.valueOf(capacity),
            String.valueOf(rate),
            "1", // 每次请求1个令牌
            String.valueOf(System.currentTimeMillis())
        );
        return result != null && result == 1;
    }
}

2. Sentinel 注解式限流(SpringCloud 项目首选)

技术亮点:注解式开发,侵入性极低;支持控制台动态调整规则,无需重启服务;内置熔断、降级、热点参数限流等功能。

@RestController
@RequestMapping("/order")
public class OrderController {
    @Autowired
    private OrderService orderService;
    
    // 定义限流资源,指定限流异常处理器
    @SentinelResource(
        value = "createOrder",
        blockHandler = "createOrderBlockHandler",
        fallback = "createOrderFallback"
    )
    @PostMapping("/create")
    public ResponseEntity<OrderVO> createOrder(@RequestBody OrderDTO orderDTO) {
        return ResponseEntity.ok(orderService.createOrder(orderDTO));
    }
    
    // 限流触发时的处理方法
    public ResponseEntity<OrderVO> createOrderBlockHandler(OrderDTO orderDTO, BlockException e) {
        return ResponseEntity.status(429).body(OrderVO.error("系统繁忙,请稍后再试"));
    }
    
    // 业务异常降级方法
    public ResponseEntity<OrderVO> createOrderFallback(OrderDTO orderDTO, Throwable e) {
        return ResponseEntity.status(500).body(OrderVO.error("订单创建失败,请稍后再试"));
    }
}

3. Guava 布隆过滤器防穿透(防恶意查询不存在的 Key)

技术亮点:空间效率极高(100 万数据仅需约 1MB 内存),查询速度 O (1),可有效拦截 99% 以上的恶意穿透请求。

@Component
public class BloomFilterService {
    // 初始化布隆过滤器:预期插入100万条数据,误判率0.01%
    private final BloomFilter<String> orderIdFilter = BloomFilter.create(
        Funnels.stringFunnel(Charset.defaultCharset()),
        1000000,
        0.0001
    );
    
    // 系统启动时初始化所有有效订单ID到布隆过滤器
    @PostConstruct
    public void init() {
        List<String> allOrderIds = orderMapper.getAllValidOrderIds();
        allOrderIds.forEach(orderIdFilter::put);
    }
    
    // 判断订单ID是否可能存在
    public boolean mightContain(String orderId) {
        return orderIdFilter.mightContain(orderId);
    }
    
    // 新增订单时同步到布隆过滤器
    public void addOrderId(String orderId) {
        orderIdFilter.put(orderId);
    }
}

🎯 核心技术难点与解决方案(面试必问加分项 ⭐)

技术难点 问题描述 解决方案 核心技术亮点
分布式限流原子性问题 普通 Redis 命令(get+set)在并发下会出现计数错误,导致限流失效 Redis+Lua 原子脚本Redisson 分布式限流组件 Lua 脚本一次性执行所有逻辑,Redis 单线程保证原子性,无并发问题
热点 Key 突刺问题 某个热点接口 / 商品被瞬间百万级访问,普通限流无法扛住,拖垮整个集群 1. 热点接口单独集群部署2. 本地缓存 + Caffeine+Redis 多级缓存3. 热点 Key 提前预热4. Sentinel 热点参数限流 本地缓存扛住 90% 以上的热点请求,避免 Redis 和数据库被打穿
缓存穿透 / 击穿 / 雪崩 大量不存在的 Key 直接打到数据库;热点 Key 过期瞬间大量请求并发;大量 Key 同时过期 穿透:布隆过滤器 + 空值缓存击穿:互斥锁 + 热点 Key 永不过期雪崩:缓存过期时间加随机值 + 熔断降级 分层防护,从根本上解决缓存三大问题
恶意请求绕过前端防护 攻击者直接调用后端接口,绕过前端验证码、按钮防重复点击等措施 1. 网关层请求签名校验(Timestamp+Nonce+Sign)2. 设备指纹识别3. 动态 Token(每次请求刷新)4. 用户行为分析(异常操作封禁) 从请求合法性层面拦截恶意流量,而不仅仅是限流
限流粒度难以平衡 全局限流太粗会误伤正常用户,用户级限流太细会占用大量内存 多级限流架构:网关层:全局限流(QPS 10w)应用层:接口级限流(QPS 1w)用户层:用户级限流(QPS 10) 层层递进,既保证系统稳定性,又最大限度减少误伤
服务雪崩效应 一个下游服务挂了,导致上游所有调用它的服务都被拖垮,最终整个系统瘫痪 1. 服务隔离(线程池隔离 / 信号量隔离)2. 熔断降级(Sentinel/Hystrix)3. 超时控制(所有接口设置合理超时时间) 故障隔离,防止故障蔓延,保证核心服务可用

📝 补充总结

接口防刷本质上是 "攻防对抗" 的过程,没有一劳永逸的方案。核心思路是:用最小的成本拦截最多的恶意流量,同时保证正常用户的体验

真实面试模拟

真实面试模拟

👨‍💼 面试官:

来,同学,聊个实际的。假设咱们刚上线一个领券活动,现在发现接口被狂刷,疑似有人用脚本疯狂抢券。你作为后端开发,会怎么从系统层面入手去分析和防御?

🧑‍💻 候选人:

首先我不会一上来就加代码,会先界定一下“狂刷”到底是哪种类型。

  • 同一用户高频请求薅羊毛?
  • 还是大量不同IP抓数据的爬虫?
  • 或者是单纯重放同一个请求

看日志、结合业务指标,基本就能定位。假设就是登录用户用脚本高频抢券,那我就围绕“如何让重复提交失效”和“如何识别自动化行为”去分层设防。

👨‍💼 面试官:

分层?具体怎么分层,说说你的防御体系图。

🧑‍💻 候选人:

我会搭一个纵深防御的漏斗,每一层解决不同维度的问题,哪怕上层被绕过,下层照样兜底 👇

image

👨‍💼 面试官:

图不错。那拆开说说,前端这一层你能做什么?

🧑‍💻 候选人:

前端其实只是第一层过滤,不信任但有用 🤞

  • 防抖/节流:按钮点击后 2 秒内灰显,脚本很难绕过拖拽
  • 一次性 page_token:页面加载时下发 token,提交即失效,防止表单重复提交
  • 滑动验证码:高风险动作直接弹出,脚本成本一下就上去了

当然这一层防君子不防小人,核心防线还得在后端。

👨‍💼 面试官:

那网关层怎么扛?如果人家直接调用 API 绕过前端呢?

🧑‍💻 候选人:

网关做无差别限流和清洗,这是第一个硬门槛。

Nginx + LuaSpring Cloud Gateway + Redis 实现:

  • IP 令牌桶限流:单 IP 每秒最多 20 次,超限返回 429
  • 动态黑名单:连续触发限流 N 次,自动封禁该 IP 1 小时
  • UA/设备指纹校验:请求里缺少正常浏览器特征,直接咔嚓 🚫

伪代码大概这样:

if (redis.incr("ratelimit:ip:" + ip) > 20) {
    response.setStatus(429);
    return;
}
redis.expire("ratelimit:ip:" + ip, 1, TimeUnit.SECONDS);

网关就能挡掉大部分无脑脚本。

👨‍💼 面试官:

如果攻击者换 IP 或者用肉鸡,业务层还能怎么办?

🧑‍💻 候选人:

这就是业务层要精准控制的地方,维度可以很丰富 👍

a) 用户/接口级分布式限流

SentinelRedisson RRateLimiter,比如同一个 userId + 领券接口,1 秒只能通过 1 次。超限返回“操作太频繁,请稍后再试~” ⏳,不丢用户体验。

b) 幂等性设计(防重放核心)

强制客户端传全局唯一 requestId 或业务幂等键(如 userId + couponId + activityId),后端用 Redis SETNX 做轻量锁:

String key = "idem:submit:" + userId + couponId;
boolean ok = redis.setIfAbsent(key, "1", 5, TimeUnit.SECONDS);
if (!ok) return "请勿重复提交";

数据库再加唯一索引兜底,最后一道防线 💾。

c) 风控逐级加压

用户触发限流 → 弹出图形验证码;验证码多次失败 → 短时封禁帐号,同步上报风控系统;再深一层,用 Flink 实时计算设备、行为轨迹、历史风险分,高分直接拒绝,连验证码都不给。

这样就算 IP 换了,帐号和设备指纹也跑不掉。

👨‍💼 面试官:

思路挺完整。不过你觉得防刷就是拼命堵吗?有没有更高级的玩法?

🧑‍💻 候选人:

对,纯堵容易把正常用户误伤。我的理解是:防刷本质是成本对抗 💸

让刷子付出更高成本,同时几乎不影响真实用户:

  • 新手首券必须绑定实名手机号 📱
  • 异常行为不发券,改为“人工审核后发放” 🎫
  • 高频访问不封死,改为响应加盐降级,返回缓存数据但隐藏关键信息
  • 建立帐号信用体系,信用分低的享受的权益就少

这样攻击者算一笔账,发现不划算,自然放弃。

👨‍💼 面试官:

很好,最后用一段话总结下你的完整方案吧。

🧑‍💻 候选人:

我会从 前端防抖→网关IP限流→业务幂等+用户级限流→风控验证码→数据库唯一索引 五层漏斗来设计。

前端减少无意重复;网关挡掉大部分恶意流量;业务层用 Redis 做分布式限流和请求幂等,防止同一用户狂刷;风控系统自动对异常升级验证码或短时封禁;数据库唯一索引最后兜底。核心是分层过滤、逐级加压,同时用成本对抗思维去平衡安全和体验 😊。

👨‍💼 面试官:

不错,体系已经有了。那能不能贴出几段你认为最有技术亮点的核心代码?不用全写,挑最体现你功底的来。

🧑‍💻 候选人:

没问题,我挑三段:IP黑名单动态管理、业务层分布式限流、幂等注解+AOP,这三块最能体现高并发下的原子性和扩展性。

1️⃣ 网关层:IP黑名单 + 动态令牌桶(Redis + Lua)

直接用 Lua 脚本保证「判断-计数-封禁」的原子性,避免竞态窗口。

-- ratelimit.lua:IP限流+自动拉黑
local ip = KEYS[1]
local limit = tonumber(ARGV[1])   -- 每秒限制次数
local ban_seconds = tonumber(ARGV[2]) -- 封禁时长
local blacklist_key = "blacklist:" .. ip
local rate_key = "rate:" .. ip

-- 1. 检查是否已在黑名单
if redis.call("exists", blacklist_key) == 1 then
    return -1  -- 已被封禁
end

-- 2. 滑动窗口计数(简化令牌桶)
local current = redis.call("incr", rate_key)
if current == 1 then
    redis.call("expire", rate_key, 1)  -- 1秒窗口
end

-- 3. 超限则加入黑名单
if current > limit then
    redis.call("set", blacklist_key, 1)
    redis.call("expire", blacklist_key, ban_seconds)
    return -1
end
return 1  -- 放行

亮点 🎯:

  • 整个逻辑在 Redis 服务端原子执行,无并发漏洞
  • 令牌桶用 incr + expire 简单实现滑动窗口,性能极高。
  • 超限自动拉黑,不需要额外后端定时任务。

2️⃣ 业务层:用户+接口维度的分布式限流(Redisson RRateLimiter)

Redisson 内置了令牌桶实现,支持集群,省去自己造轮子。

@Service
public class RateLimitService {
    @Autowired
    private RedissonClient redissonClient;

    /**
     * 尝试获取令牌
     * @param userId 用户ID
     * @param apiKey 接口标识
     * @param permits 每次消耗令牌数
     * @return 是否允许
     */
    public boolean tryAcquire(String userId, String apiKey, long permits) {
        String key = "limit:" + apiKey + ":" + userId;
        RRateLimiter limiter = redissonClient.getRateLimiter(key);
        
        // 初始化:每秒生成1个令牌,最大桶容量5(允许小突发)
        limiter.trySetRate(RateType.OVERALL, 1, 1, RateIntervalUnit.SECONDS);
        
        return limiter.tryAcquire(permits);
    }
}

亮点 🎯:

  • 利用 Redisson 分布式对象,天然支持多服务实例
  • trySetRate 只首次设置,后续复用,避免重复初始化开销。
  • 桶容量可调,适应突发流量,减少误杀。

3️⃣ 幂等拦截:自定义注解 + AOP(防重放利器)

通过声明式注解,零侵入给任何接口加上幂等保护。

// 注解定义
@Target(ElementType.METHOD)
@Retention(RetentionPolicy.RUNTIME)
public @interface Idempotent {
    /** 幂等键前缀 */
    String prefix();
    /** 键过期秒数 */
    int expire() default 5;
    /** SpEL表达式,从参数中提取业务唯一标识 */
    String keySpEL();
}
// AOP 切面实现
@Aspect
@Component
public class IdempotentAspect {
    @Autowired
    private StringRedisTemplate redisTemplate;

    @Around("@annotation(idempotent)")
    public Object around(ProceedingJoinPoint joinPoint, Idempotent idempotent) throws Throwable {
        // 1. 解析 SpEL 获取业务幂等键
        String key = parseSpEL(idempotent.keySpEL(), joinPoint);
        String redisKey = "idem:" + idempotent.prefix() + ":" + key;

        // 2. SETNX 原子占位
        Boolean success = redisTemplate.opsForValue()
                .setIfAbsent(redisKey, "1", Duration.ofSeconds(idempotent.expire()));
        
        if (Boolean.FALSE.equals(success)) {
            return ApiResult.fail("请勿重复提交");
        }

        // 3. 执行原业务
        try {
            return joinPoint.proceed();
        } catch (Exception e) {
            // 业务异常可主动删除幂等键,允许重试
            redisTemplate.delete(redisKey);
            throw e;
        }
    }
}
// 业务使用,简单到飞起
@Idempotent(prefix = "claimCoupon", keySpEL = "#req.userId + ':' + #req.couponId")
public Result claimCoupon(ClaimReq req) {
    // 核心领券逻辑
}

亮点 🎯:

  • 声明式编程,业务代码零污染。
  • SpEL 灵活组装幂等键,适用任意参数结构。
  • 异常时主动删除键,支持安全重试,兼顾防重和可用性。

👨‍💼 面试官:

代码写得挺工整。那再聊聊,这套防刷体系落地时,你觉得最容易踩坑的技术难点有哪些?你是怎么解决的

🧑‍💻 候选人:

我梳理了四个核心难点,边做边优化出来的 👇

🚧 技术难点 💡 解决方案 🔍 关键手段
1. 分布式限流的一致性与性能矛盾 用 Redis 做集中式计数,Lua 脚本保证原子性;单节点瓶颈则采用本地预限流+远程最终校验的两级模式 Lua 原子化、两级限流
2. 幂等键的粒度定义混乱 和产品统一“业务唯一性”口径,例如领券幂等键 = userId + couponId + 活动Id;技术上用 SpEL 自定义键,数据库加唯一索引双保险 业务语义明确、DB唯一索引兜底
3. 黑名单误杀正常用户 黑名单加入时间窗口内“疑似阈值”而非一超就封;配合验证码柔性挑战,合法用户可通过答题解封;引入设备指纹而不是纯IP,降低NAT下的误伤 软封禁+验证码、设备指纹
4. 防刷规则硬编码,变更需重启 引入配置中心动态下发阈值、黑名单时长、风控开关;甚至用轻量规则引擎(如QLExpress)让运营可配置 配置中心、规则引擎热更新

👨‍💼 面试官:

展开说说第一个难点“分布式限流一致性与性能”,这很关键。

🧑‍💻 候选人:

对,这是个典型的 CAP 权衡问题

  • 如果每次请求都 incr 查 Redis,限流精准,但大流量下 Redis 可能成为瓶颈。
  • 所以我采用了两级限流架构
    • L1 本地:用 Guava RateLimiter 在服务实例内先粗粒度限流,拒绝掉99%的无效请求;
    • L2 远程:只有通过 L1 的请求才去 Redis 做精准计数,这样 Redis 压力骤降。

伪代码:

if (!localLimiter.tryAcquire()) {
    return fastFail("系统繁忙"); // 本地直接扔
}
if (!redisLimiter.tryAcquire(userId, apiKey)) {
    return fastFail("操作频繁");
}
// 正常业务处理

同时如果 Redis 挂了,本地限流还能降级兜底,牺牲一点精度换取可用性

👨‍💼 面试官:

把性能和可靠性都考虑进去了,不错。这个场景还有别的难点吗?

🧑‍💻 候选人:

还有一个容易被忽略的:重放攻击的 RequestId 生成不可信

如果 RequestId 由客户端生成,攻击者完全可以每次都传新 ID 绕过幂等。所以实践中我会 结合服务端签名

  • 客户端先请求一个 antiReplayToken(服务端签发,含时间戳+签名);
  • 提交时带上此 token,后端校验签名和有效期,一次性消费。

这样即使脚本伪造 RequestId,拿不到合法 Token 也没法刷。

这个点经常在面试里被忽略,但线上特别重要 🔐。

👨‍💼 面试官:

非常有深度。看来你对防刷体系的“道”和“术”都把握住了,期待你能来我们团队落地这些方案。今天的面试就到这里,辛苦啦 🎉

🧑‍💻 候选人:

谢谢,我也很期待!如果后面有具体场景,可以再深入探讨 😊🤝


公众号“Rain的Java大神之路”
个人博客“www.javadashen.com”

posted @ 2026-08-26 10:31  Rain的Java大神实战圈  阅读(79)  评论(0)    收藏  举报