一、一次恶意刷单让我们损失了80万2021年618大促前夜有人用脚本疯狂刷我们的新人1元购活动。一秒钟发出2000个请求服务器CPU飙到100%正常用户根本抢不到。活动上线10分钟1元商品被刷走了8000份公司损失80万。更要命的是真正的用户在社交平台上骂我们黑幕。从那以后我们把接口防刷当成了系统的第一道防线。二、限流算法2.1 固定窗口计数器/** * 固定窗口计数器最简单但有临界问题 */ServicepublicclassFixedWindowRateLimiter{AutowiredprivateStringRedisTemplateredisTemplate;/** * 限流检查 * param key 限流键 * param limit 限制次数 * param windowSeconds 窗口时间(秒) */publicbooleanisAllowed(Stringkey,intlimit,intwindowSeconds){longcurrentWindowSystem.currentTimeMillis()/1000/windowSeconds;StringredisKeyrate_limit:key:currentWindow;LongcountredisTemplate.opsForValue().increment(redisKey);if(count1){redisTemplate.expire(redisKey,windowSeconds,TimeUnit.SECONDS);}returncountlimit;}}2.2 滑动窗口计数器/** * 滑动窗口计数器解决临界问题 */ServicepublicclassSlidingWindowRateLimiter{AutowiredprivateStringRedisTemplateredisTemplate;/** * 限流检查 */publicbooleanisAllowed(Stringkey,intlimit,intwindowSeconds){longnowSystem.currentTimeMillis();longwindowStartnow-windowSeconds*1000;StringredisKeyrate_limit:sliding:key;// Lua脚本保证原子性Stringscriptredis.call(ZREMRANGEBYSCORE, KEYS[1], 0, ARGV[1]) local count redis.call(ZCARD, KEYS[1]) if count tonumber(ARGV[2]) then redis.call(ZADD, KEYS[1], ARGV[3], ARGV[3]) redis.call(EXPIRE, KEYS[1], ARGV[4]) return 1 else return 0 end;LongresultredisTemplate.execute(newDefaultRedisScript(script,Long.class),Collections.singletonList(redisKey),String.valueOf(windowStart),// 窗口起始时间String.valueOf(limit),// 限制次数String.valueOf(now),// 当前时间戳作为score和memberString.valueOf(windowSeconds)// 过期时间);returnLong.valueOf(1).equals(result);}}2.3 令牌桶算法/** * 令牌桶算法允许突发流量 */ServicepublicclassTokenBucketRateLimiter{AutowiredprivateStringRedisTemplateredisTemplate;/** * 限流检查 * param key 限流键 * param capacity 桶容量最大令牌数 * param rate 令牌生成速率每秒生成几个 */publicbooleanisAllowed(Stringkey,intcapacity,doublerate){StringredisKeyrate_limit:token:key;// Lua脚本Stringscriptlocal key KEYS[1] local capacity tonumber(ARGV[1]) local rate tonumber(ARGV[2]) local now tonumber(ARGV[3]) local requested tonumber(ARGV[4]) local info redis.call(HMGET, key, tokens, last_time) local tokens tonumber(info[1]) local last_time tonumber(info[2]) if tokens nil then tokens capacity last_time now end local elapsed now - last_time local new_tokens math.min(capacity, tokens elapsed * rate) if new_tokens requested then new_tokens new_tokens - requested redis.call(HMSET, key, tokens, new_tokens, last_time, now) redis.call(EXPIRE, key, 60) return 1 else redis.call(HMSET, key, tokens, new_tokens, last_time, now) redis.call(EXPIRE, key, 60) return 0 end;LongresultredisTemplate.execute(newDefaultRedisScript(script,Long.class),Collections.singletonList(redisKey),String.valueOf(capacity),String.valueOf(rate),String.valueOf(System.currentTimeMillis()/1000.0),1// 每次请求消耗1个令牌);returnLong.valueOf(1).equals(result);}}2.4 Sentinel限流/** * Sentinel限流配置 */ConfigurationpublicclassSentinelConfig{PostConstructpublicvoidinitFlowRules(){ListFlowRulerulesnewArrayList();// 订单创建接口 - QPS限流FlowRuleorderCreateRulenewFlowRule();orderCreateRule.setResource(order-create);orderCreateRule.setGrade(RuleConstant.FLOW_GRADE_QPS);orderCreateRule.setCount(100);// 每秒100次orderCreateRule.setLimitApp(default);rules.add(orderCreateRule);// 秒杀接口 - 线程数限流FlowRuleseckillRulenewFlowRule();seckillRule.setResource(seckill);seckillRule.setGrade(RuleConstant.FLOW_GRADE_THREAD);seckillRule.setCount(50);// 最多50个线程rules.add(seckillRule);FlowRuleManager.loadRules(rules);}}/** * 使用Sentinel限流 */ServiceSlf4jpublicclassOrderService{/** * 创建订单带限流 */publicOrdercreateOrder(CreateOrderRequestrequest){Entryentrynull;try{entrySphU.entry(order-create);// 业务逻辑returndoCreateOrder(request);}catch(BlockExceptione){log.warn(创建订单被限流: userId{},request.getUserId());thrownewBusinessException(系统繁忙请稍后重试);}finally{if(entry!null){entry.exit();}}}}三、接口防刷3.1 IP防刷/** * IP防刷拦截器 */ComponentSlf4jpublicclassIpRateLimitInterceptorimplementsHandlerInterceptor{AutowiredprivateSlidingWindowRateLimiterrateLimiter;OverridepublicbooleanpreHandle(HttpServletRequestrequest,HttpServletResponseresponse,Objecthandler)throwsException{StringipgetClientIp(request);Stringurirequest.getRequestURI();Stringkeyip:uri;// 同一IP同一接口1秒内最多10次if(!rateLimiter.isAllowed(key,10,1)){log.warn(IP限流: ip{}, uri{},ip,uri);response.setStatus(429);response.setContentType(application/json);response.getWriter().write({\code\:429,\message\:\请求过于频繁\});returnfalse;}returntrue;}privateStringgetClientIp(HttpServletRequestrequest){Stringiprequest.getHeader(X-Forwarded-For);if(ipnull||ip.isEmpty()){iprequest.getHeader(X-Real-IP);}if(ipnull||ip.isEmpty()){iprequest.getRemoteAddr();}returnip;}}3.2 用户防刷/** * 用户防刷基于Token机制 */ServiceSlf4jpublicclassUserAntiBrushService{AutowiredprivateStringRedisTemplateredisTemplate;/** * 检查用户请求频率 */publicbooleancheckUserRate(LonguserId,Stringaction){Stringkeyanti_brush:userId:action;// 1秒内最多5次LongcountredisTemplate.opsForValue().increment(key);if(count1){redisTemplate.expire(key,1,TimeUnit.SECONDS);}if(count5){log.warn(用户请求过于频繁: userId{}, action{}, count{},userId,action,count);returnfalse;}returntrue;}/** * 活动防刷同一用户同一活动只能参与一次 */publicbooleancheckActivityParticipation(LonguserId,LongactivityId){Stringkeyactivity:activityId:user:userId;BooleanisNewredisTemplate.opsForValue().setIfAbsent(key,1,24,TimeUnit.HOURS);returnBoolean.TRUE.equals(isNew);}/** * 设备指纹防刷 */publicbooleancheckDeviceFingerprint(Stringfingerprint,Stringaction){Stringkeyanti_brush:device:fingerprint:action;// 同一设备1小时内最多50次LongcountredisTemplate.opsForValue().increment(key);if(count1){redisTemplate.expire(key,1,TimeUnit.HOURS);}returncount50;}}3.3 验证码防刷/** * 验证码防刷 */ServiceSlf4jpublicclassCaptchaAntiBrushService{AutowiredprivateStringRedisTemplateredisTemplate;/** * 触发验证码的条件 */publicbooleanneedCaptcha(LonguserId,Stringaction){Stringkeycaptcha:check:userId:action;StringcountStrredisTemplate.opsForValue().get(key);intcountcountStrnull?0:Integer.parseInt(countStr);// 5次以内不需要验证码returncount5;}/** * 增加请求计数 */publicvoidincrementRequestCount(LonguserId,Stringaction){Stringkeycaptcha:check:userId:action;redisTemplate.opsForValue().increment(key);redisTemplate.expire(key,10,TimeUnit.MINUTES);}/** * 验证码验证成功后重置计数 */publicvoidresetRequestCount(LonguserId,Stringaction){Stringkeycaptcha:check:userId:action;redisTemplate.delete(key);}}四、踩坑实录坑1限流键设计不合理用用户ID做限流键但攻击者注册了大量账号每个账号都有限流额度。解决多维度限流IP用户设备指纹综合判断。坑2限流阈值太低正常流量被误限影响业务。解决根据历史流量数据设置阈值预留30%余量。坑3验证码被破解图形验证码被OCR破解短信验证码被接码平台截获。解决使用滑块验证、行为验证等更安全的方案。坑4分布式限流不一致每个节点各自限流总限流数是单节点的N倍。解决使用Redis集中式限流或使用令牌桶算法。坑5限流后的响应不友好直接返回429用户不知道怎么回事。解决返回友好的提示信息告知用户何时可以重试。五、总结限流防刷方案维度方案算法令牌桶 滑动窗口 固定窗口IP防刷滑动窗口限流用户防刷多维度限流活动防刷Redis SETNX验证码行为验证 图形验证最佳实践多维度限流不要只看一个维度阈值根据历史数据设置预留余量限流后返回友好提示异常流量自动触发验证码监控限流指标及时调整血的教训限流不是限制业务是保护业务。被刷一次的损失远超限流配置的开发成本。思考题你的系统有没有被恶意刷过用了什么防刷方案个人观点仅供参考
【架构实战】接口防刷与限流:保护系统的第一道防线
发布时间:2026/6/3 15:32:23
一、一次恶意刷单让我们损失了80万2021年618大促前夜有人用脚本疯狂刷我们的新人1元购活动。一秒钟发出2000个请求服务器CPU飙到100%正常用户根本抢不到。活动上线10分钟1元商品被刷走了8000份公司损失80万。更要命的是真正的用户在社交平台上骂我们黑幕。从那以后我们把接口防刷当成了系统的第一道防线。二、限流算法2.1 固定窗口计数器/** * 固定窗口计数器最简单但有临界问题 */ServicepublicclassFixedWindowRateLimiter{AutowiredprivateStringRedisTemplateredisTemplate;/** * 限流检查 * param key 限流键 * param limit 限制次数 * param windowSeconds 窗口时间(秒) */publicbooleanisAllowed(Stringkey,intlimit,intwindowSeconds){longcurrentWindowSystem.currentTimeMillis()/1000/windowSeconds;StringredisKeyrate_limit:key:currentWindow;LongcountredisTemplate.opsForValue().increment(redisKey);if(count1){redisTemplate.expire(redisKey,windowSeconds,TimeUnit.SECONDS);}returncountlimit;}}2.2 滑动窗口计数器/** * 滑动窗口计数器解决临界问题 */ServicepublicclassSlidingWindowRateLimiter{AutowiredprivateStringRedisTemplateredisTemplate;/** * 限流检查 */publicbooleanisAllowed(Stringkey,intlimit,intwindowSeconds){longnowSystem.currentTimeMillis();longwindowStartnow-windowSeconds*1000;StringredisKeyrate_limit:sliding:key;// Lua脚本保证原子性Stringscriptredis.call(ZREMRANGEBYSCORE, KEYS[1], 0, ARGV[1]) local count redis.call(ZCARD, KEYS[1]) if count tonumber(ARGV[2]) then redis.call(ZADD, KEYS[1], ARGV[3], ARGV[3]) redis.call(EXPIRE, KEYS[1], ARGV[4]) return 1 else return 0 end;LongresultredisTemplate.execute(newDefaultRedisScript(script,Long.class),Collections.singletonList(redisKey),String.valueOf(windowStart),// 窗口起始时间String.valueOf(limit),// 限制次数String.valueOf(now),// 当前时间戳作为score和memberString.valueOf(windowSeconds)// 过期时间);returnLong.valueOf(1).equals(result);}}2.3 令牌桶算法/** * 令牌桶算法允许突发流量 */ServicepublicclassTokenBucketRateLimiter{AutowiredprivateStringRedisTemplateredisTemplate;/** * 限流检查 * param key 限流键 * param capacity 桶容量最大令牌数 * param rate 令牌生成速率每秒生成几个 */publicbooleanisAllowed(Stringkey,intcapacity,doublerate){StringredisKeyrate_limit:token:key;// Lua脚本Stringscriptlocal key KEYS[1] local capacity tonumber(ARGV[1]) local rate tonumber(ARGV[2]) local now tonumber(ARGV[3]) local requested tonumber(ARGV[4]) local info redis.call(HMGET, key, tokens, last_time) local tokens tonumber(info[1]) local last_time tonumber(info[2]) if tokens nil then tokens capacity last_time now end local elapsed now - last_time local new_tokens math.min(capacity, tokens elapsed * rate) if new_tokens requested then new_tokens new_tokens - requested redis.call(HMSET, key, tokens, new_tokens, last_time, now) redis.call(EXPIRE, key, 60) return 1 else redis.call(HMSET, key, tokens, new_tokens, last_time, now) redis.call(EXPIRE, key, 60) return 0 end;LongresultredisTemplate.execute(newDefaultRedisScript(script,Long.class),Collections.singletonList(redisKey),String.valueOf(capacity),String.valueOf(rate),String.valueOf(System.currentTimeMillis()/1000.0),1// 每次请求消耗1个令牌);returnLong.valueOf(1).equals(result);}}2.4 Sentinel限流/** * Sentinel限流配置 */ConfigurationpublicclassSentinelConfig{PostConstructpublicvoidinitFlowRules(){ListFlowRulerulesnewArrayList();// 订单创建接口 - QPS限流FlowRuleorderCreateRulenewFlowRule();orderCreateRule.setResource(order-create);orderCreateRule.setGrade(RuleConstant.FLOW_GRADE_QPS);orderCreateRule.setCount(100);// 每秒100次orderCreateRule.setLimitApp(default);rules.add(orderCreateRule);// 秒杀接口 - 线程数限流FlowRuleseckillRulenewFlowRule();seckillRule.setResource(seckill);seckillRule.setGrade(RuleConstant.FLOW_GRADE_THREAD);seckillRule.setCount(50);// 最多50个线程rules.add(seckillRule);FlowRuleManager.loadRules(rules);}}/** * 使用Sentinel限流 */ServiceSlf4jpublicclassOrderService{/** * 创建订单带限流 */publicOrdercreateOrder(CreateOrderRequestrequest){Entryentrynull;try{entrySphU.entry(order-create);// 业务逻辑returndoCreateOrder(request);}catch(BlockExceptione){log.warn(创建订单被限流: userId{},request.getUserId());thrownewBusinessException(系统繁忙请稍后重试);}finally{if(entry!null){entry.exit();}}}}三、接口防刷3.1 IP防刷/** * IP防刷拦截器 */ComponentSlf4jpublicclassIpRateLimitInterceptorimplementsHandlerInterceptor{AutowiredprivateSlidingWindowRateLimiterrateLimiter;OverridepublicbooleanpreHandle(HttpServletRequestrequest,HttpServletResponseresponse,Objecthandler)throwsException{StringipgetClientIp(request);Stringurirequest.getRequestURI();Stringkeyip:uri;// 同一IP同一接口1秒内最多10次if(!rateLimiter.isAllowed(key,10,1)){log.warn(IP限流: ip{}, uri{},ip,uri);response.setStatus(429);response.setContentType(application/json);response.getWriter().write({\code\:429,\message\:\请求过于频繁\});returnfalse;}returntrue;}privateStringgetClientIp(HttpServletRequestrequest){Stringiprequest.getHeader(X-Forwarded-For);if(ipnull||ip.isEmpty()){iprequest.getHeader(X-Real-IP);}if(ipnull||ip.isEmpty()){iprequest.getRemoteAddr();}returnip;}}3.2 用户防刷/** * 用户防刷基于Token机制 */ServiceSlf4jpublicclassUserAntiBrushService{AutowiredprivateStringRedisTemplateredisTemplate;/** * 检查用户请求频率 */publicbooleancheckUserRate(LonguserId,Stringaction){Stringkeyanti_brush:userId:action;// 1秒内最多5次LongcountredisTemplate.opsForValue().increment(key);if(count1){redisTemplate.expire(key,1,TimeUnit.SECONDS);}if(count5){log.warn(用户请求过于频繁: userId{}, action{}, count{},userId,action,count);returnfalse;}returntrue;}/** * 活动防刷同一用户同一活动只能参与一次 */publicbooleancheckActivityParticipation(LonguserId,LongactivityId){Stringkeyactivity:activityId:user:userId;BooleanisNewredisTemplate.opsForValue().setIfAbsent(key,1,24,TimeUnit.HOURS);returnBoolean.TRUE.equals(isNew);}/** * 设备指纹防刷 */publicbooleancheckDeviceFingerprint(Stringfingerprint,Stringaction){Stringkeyanti_brush:device:fingerprint:action;// 同一设备1小时内最多50次LongcountredisTemplate.opsForValue().increment(key);if(count1){redisTemplate.expire(key,1,TimeUnit.HOURS);}returncount50;}}3.3 验证码防刷/** * 验证码防刷 */ServiceSlf4jpublicclassCaptchaAntiBrushService{AutowiredprivateStringRedisTemplateredisTemplate;/** * 触发验证码的条件 */publicbooleanneedCaptcha(LonguserId,Stringaction){Stringkeycaptcha:check:userId:action;StringcountStrredisTemplate.opsForValue().get(key);intcountcountStrnull?0:Integer.parseInt(countStr);// 5次以内不需要验证码returncount5;}/** * 增加请求计数 */publicvoidincrementRequestCount(LonguserId,Stringaction){Stringkeycaptcha:check:userId:action;redisTemplate.opsForValue().increment(key);redisTemplate.expire(key,10,TimeUnit.MINUTES);}/** * 验证码验证成功后重置计数 */publicvoidresetRequestCount(LonguserId,Stringaction){Stringkeycaptcha:check:userId:action;redisTemplate.delete(key);}}四、踩坑实录坑1限流键设计不合理用用户ID做限流键但攻击者注册了大量账号每个账号都有限流额度。解决多维度限流IP用户设备指纹综合判断。坑2限流阈值太低正常流量被误限影响业务。解决根据历史流量数据设置阈值预留30%余量。坑3验证码被破解图形验证码被OCR破解短信验证码被接码平台截获。解决使用滑块验证、行为验证等更安全的方案。坑4分布式限流不一致每个节点各自限流总限流数是单节点的N倍。解决使用Redis集中式限流或使用令牌桶算法。坑5限流后的响应不友好直接返回429用户不知道怎么回事。解决返回友好的提示信息告知用户何时可以重试。五、总结限流防刷方案维度方案算法令牌桶 滑动窗口 固定窗口IP防刷滑动窗口限流用户防刷多维度限流活动防刷Redis SETNX验证码行为验证 图形验证最佳实践多维度限流不要只看一个维度阈值根据历史数据设置预留余量限流后返回友好提示异常流量自动触发验证码监控限流指标及时调整血的教训限流不是限制业务是保护业务。被刷一次的损失远超限流配置的开发成本。思考题你的系统有没有被恶意刷过用了什么防刷方案个人观点仅供参考