接口被恶意狂刷,如何处理
引言:接口防护的重要性
某个接口突然被大量请求轰炸,导致系统响应缓慢甚至崩溃?或者用户恶意刷接口获取优惠券、红包等福利?再或者爬虫程序疯狂调用接口抓取数据,严重影响正常用户使用?
这就是接口防护的典型难题。今天我们就来聊聊如何构建一个完整的接口防护体系,让你的系统远离恶意刷接口的困扰。
接口被刷的常见场景
1. 登录接口被暴力破解
攻击者使用字典攻击,尝试大量用户名密码组合。
2. 短信验证码接口被刷
恶意用户反复请求验证码,导致短信费用飙升。
3. 优惠券接口被刷
利用漏洞大量领取优惠券,造成经济损失。
4. 商品抢购接口被刷
使用脚本抢购限量商品,影响公平性。
5. API接口被爬虫攻击
大量请求抓取数据,影响正常业务。
防护策略总览
1. 限流策略
- 固定窗口限流:单位时间内限制请求数
- 滑动窗口限流:更精确的时间窗口
- 令牌桶限流:平滑的请求处理
- 漏桶限流:恒定的处理速率
2. 认证策略
- 图形验证码:人机验证
- 滑块验证:行为验证
- 短信验证:手机验证
- 生物识别:指纹、人脸等
3. 黑白名单
- IP黑名单:阻止恶意IP访问
- 用户黑名单:阻止恶意用户
- 设备黑名单:阻止恶意设备
技术实现方案
1. 基于Redis的限流实现
@Component
public class RateLimiterService {
@Autowired
private RedisTemplate<String, Object> redisTemplate;
/**
* 固定窗口限流
*/
public boolean isAllowed(String key, int limit, int windowSeconds) {
String luaScript =
"local key = KEYS[1]\n" +
"local limit = tonumber(ARGV[1])\n" +
"local window = tonumber(ARGV[2])\n" +
"local current = redis.call('GET', key)\n" +
"if current == false then\n" +
" redis.call('SET', key, 1)\n" +
" redis.call('EXPIRE', key, window)\n" +
" return 1\n" +
"end\n" +
"current = tonumber(current)\n" +
"if current < limit then\n" +
" redis.call('INCR', key)\n" +
" return 1\n" +
"else\n" +
" return 0\n" +
"end";
RedisScript<Long> script = new DefaultRedisScript<>(luaScript, Long.class);
Long result = (Long) redisTemplate.execute(script,
Arrays.asList(key), String.valueOf(limit), String.valueOf(windowSeconds));
return result == 1;
}
/**
* 滑动窗口限流
*/
public boolean isAllowedSlidingWindow(String key, int limit, int windowSeconds) {
String luaScript =
"local key = KEYS[1]\n" +
"local limit = tonumber(ARGV[1])\n" +
"local window = tonumber(ARGV[2])\n" +
"local current = redis.call('TIME')[1]\n" +
"local pipeline = redis.call('ZRANGEBYSCORE', key, current - window, current)\n" +
"if #pipeline >= limit then\n" +
" return 0\n" +
"end\n" +
"redis.call('ZADD', key, current, current .. math.random(1000000))\n" +
"redis.call('ZREMRANGEBYSCORE', key, 0, current - window)\n" +
"redis.call('EXPIRE', key, window)\n" +
"return 1";
RedisScript<Long> script = new DefaultRedisScript<>(luaScript, Long.class);
Long result = (Long) redisTemplate.execute(script,
Arrays.asList(key), String.valueOf(limit), String.valueOf(windowSeconds));
return result == 1;
}
}
2. 自定义限流注解
@Target(ElementType.METHOD)
@Retention(RetentionPolicy.RUNTIME)
public @interface RateLimit {
String key() default ""; // 限流键
int limit() default 10; // 限制次数
int window() default 60; // 时间窗口(秒)
String message() default "请求过于频繁,请稍后再试"; // 提示信息
}
3. 限流拦截器
@Aspect
@Component
public class RateLimitAspect {
@Autowired
private RateLimiterService rateLimiterService;
@Around("@annotation(rateLimit)")
public Object around(ProceedingJoinPoint point, RateLimit rateLimit) throws Throwable {
// 构建限流键
String key = buildRateLimitKey(point, rateLimit);
// 检查是否允许访问
boolean allowed = rateLimiterService.isAllowed(key, rateLimit.limit(), rateLimit.window());
if (!allowed) {
throw new RateLimitException(rateLimit.message());
}
return point.proceed();
}
private String buildRateLimitKey(ProceedingJoinPoint point, RateLimit rateLimit) {
String key = rateLimit.key();
if (StringUtils.isEmpty(key)) {
// 默认使用方法名
MethodSignature signature = (MethodSignature) point.getSignature();
key = signature.getMethod().getName();
}
// 添加用户标识
HttpServletRequest request = getCurrentRequest();
String userId = getCurrentUserId(request);
if (userId != null) {
key = "rate_limit:" + key + ":" + userId;
} else {
// 使用IP地址
String ip = getIpAddress(request);
key = "rate_limit:" + key + ":" + ip;
}
return key;
}
private HttpServletRequest getCurrentRequest() {
RequestAttributes attributes = RequestContextHolder.getRequestAttributes();
return ((ServletRequestAttributes) attributes).getRequest();
}
private String getIpAddress(HttpServletRequest request) {
String ip = request.getHeader("X-Forwarded-For");
if (ip == null || ip.isEmpty() || "unknown".equalsIgnoreCase(ip)) {
ip = request.getHeader("X-Real-IP");
}
if (ip == null || ip.isEmpty() || "unknown".equalsIgnoreCase(ip)) {
ip = request.getRemoteAddr();
}
return ip;
}
}
4. IP黑名单管理
@Service
public class IpBlacklistService {
@Autowired
private RedisTemplate<String, Object> redisTemplate;
/**
* 检查IP是否在黑名单中
*/
public boolean isBlacklisted(String ip) {
return Boolean.TRUE.equals(
redisTemplate.opsForSet().isMember("ip:blacklist", ip));
}
/**
* 添加IP到黑名单
*/
public void addToBlacklist(String ip, String reason) {
redisTemplate.opsForSet().add("ip:blacklist", ip);
// 记录封禁原因
redisTemplate.opsForHash().put("ip:blacklist:reason", ip, reason);
}
/**
* 从黑名单移除
*/
public void removeFromBlacklist(String ip) {
redisTemplate.opsForSet().remove("ip:blacklist", ip);
redisTemplate.opsForHash().delete("ip:blacklist:reason", ip);
}
/**
* 自动封禁策略
*/
public void autoBlacklist(String ip, String action) {
String key = "attack:count:" + ip + ":" + action;
// 增加攻击计数
Long count = redisTemplate.opsForValue().increment(key);
if (count != null && count >= 10) { // 10次攻击后封禁
addToBlacklist(ip, "自动封禁: " + action + "攻击");
// 设置封禁时长
redisTemplate.expire(key, Duration.ofHours(24));
} else {
// 设置计数器过期时间
redisTemplate.expire(key, Duration.ofMinutes(10));
}
}
}
5. 图形验证码
@RestController
public class CaptchaController {
@Autowired
private RedisTemplate<String, Object> redisTemplate;
@GetMapping("/captcha")
public ResponseEntity<CaptchaResponse> generateCaptcha() {
// 生成验证码
String code = generateRandomCode(4);
String captchaId = UUID.randomUUID().toString();
// 存储验证码到Redis
redisTemplate.opsForValue().set("captcha:" + captchaId, code, Duration.ofMinutes(5));
// 生成验证码图片
BufferedImage image = createCaptchaImage(code);
// 返回验证码ID和图片
CaptchaResponse response = new CaptchaResponse();
response.setCaptchaId(captchaId);
response.setImageData(imageToBase64(image));
return ResponseEntity.ok(response);
}
@PostMapping("/captcha/verify")
public ResponseEntity<Boolean> verifyCaptcha(@RequestBody CaptchaVerifyRequest request) {
String storedCode = (String) redisTemplate.opsForValue().get("captcha:" + request.getCaptchaId());
if (storedCode != null && storedCode.equalsIgnoreCase(request.getCode())) {
// 验证成功,删除验证码
redisTemplate.delete("captcha:" + request.getCaptchaId());
return ResponseEntity.ok(true);
}
return ResponseEntity.ok(false);
}
private String generateRandomCode(int length) {
String chars = "0123456789ABCDEFGHIJKLMNOPQRSTUVWXYZ";
StringBuilder sb = new StringBuilder();
Random random = new Random();
for (int i = 0; i < length; i++) {
sb.append(chars.charAt(random.nextInt(chars.length())));
}
return sb.toString();
}
private BufferedImage createCaptchaImage(String code) {
int width = 120;
int height = 40;
BufferedImage image = new BufferedImage(width, height, BufferedImage.TYPE_INT_RGB);
Graphics2D g = image.createGraphics();
// 设置背景色
g.setColor(Color.WHITE);
g.fillRect(0, 0, width, height);
// 设置字体
g.setFont(new Font("Arial", Font.BOLD, 24));
// 绘制验证码
for (int i = 0; i < code.length(); i++) {
g.setColor(new Color(random.nextInt(255), random.nextInt(255), random.nextInt(255)));
g.drawString(String.valueOf(code.charAt(i)), 20 + i * 20, 25);
}
// 添加干扰线
for (int i = 0; i < 5; i++) {
g.setColor(new Color(random.nextInt(255), random.nextInt(255), random.nextInt(255)));
g.drawLine(random.nextInt(width), random.nextInt(height),
random.nextInt(width), random.nextInt(height));
}
g.dispose();
return image;
}
private String imageToBase64(BufferedImage image) {
try {
ByteArrayOutputStream baos = new ByteArrayOutputStream();
ImageIO.write(image, "png", baos);
byte[] imageBytes = baos.toByteArray();
return Base64.getEncoder().encodeToString(imageBytes);
} catch (IOException e) {
throw new RuntimeException("验证码图片转换失败", e);
}
}
}
6. 全局防护拦截器
@Component
public class GlobalProtectionInterceptor implements HandlerInterceptor {
@Autowired
private IpBlacklistService ipBlacklistService;
@Autowired
private RateLimiterService rateLimiterService;
@Override
public boolean preHandle(HttpServletRequest request,
HttpServletResponse response,
Object handler) throws Exception {
String ip = getIpAddress(request);
// 检查IP是否在黑名单中
if (ipBlacklistService.isBlacklisted(ip)) {
response.setStatus(403);
response.getWriter().write("您的IP已被封禁");
return false;
}
// 检查全局限流
String globalKey = "global:rate_limit:" + ip;
if (!rateLimiterService.isAllowed(globalKey, 1000, 60)) { // 每分钟最多1000次请求
response.setStatus(429);
response.getWriter().write("请求过于频繁");
return false;
}
return true;
}
private String getIpAddress(HttpServletRequest request) {
String ip = request.getHeader("X-Forwarded-For");
if (ip == null || ip.isEmpty() || "unknown".equalsIgnoreCase(ip)) {
ip = request.getHeader("X-Real-IP");
}
if (ip == null || ip.isEmpty() || "unknown".equalsIgnoreCase(ip)) {
ip = request.getRemoteAddr();
}
return ip;
}
}
高级防护策略
1. 行为分析
@Service
public class BehaviorAnalysisService {
@Autowired
private RedisTemplate<String, Object> redisTemplate;
/**
* 分析用户行为
*/
public boolean analyzeBehavior(String userId, String action, HttpServletRequest request) {
String ip = getIpAddress(request);
// 记录用户行为
String behaviorKey = "behavior:" + userId + ":" + action;
redisTemplate.opsForValue().increment(behaviorKey);
redisTemplate.expire(behaviorKey, Duration.ofMinutes(5));
// 记录IP行为
String ipBehaviorKey = "ip_behavior:" + ip + ":" + action;
redisTemplate.opsForValue().increment(ipBehaviorKey);
redisTemplate.expire(ipBehaviorKey, Duration.ofMinutes(5));
// 检查异常行为
Long userCount = redisTemplate.opsForValue().get(behaviorKey);
Long ipCount = redisTemplate.opsForValue().get(ipBehaviorKey);
// 如果用户在短时间内进行大量操作,可能是异常行为
if (userCount != null && userCount > 50) {
// 记录异常行为
recordAnomaly(userId, action, "用户行为异常", request);
return false;
}
// 如果IP在短时间内进行大量操作,可能是爬虫
if (ipCount != null && ipCount > 100) {
// 记录异常行为并加入IP黑名单
recordAnomaly(ip, action, "IP行为异常", request);
ipBlacklistService.addToBlacklist(ip, "IP行为异常");
return false;
}
return true;
}
private void recordAnomaly(String target, String action, String reason, HttpServletRequest request) {
AnomalyRecord record = AnomalyRecord.builder()
.target(target)
.action(action)
.reason(reason)
.ip(getIpAddress(request))
.timestamp(new Date())
.build();
// 存储异常记录
String key = "anomaly:" + System.currentTimeMillis();
redisTemplate.opsForValue().set(key, record, Duration.ofDays(7));
}
}
2. 设备指纹识别
@Service
public class DeviceFingerprintService {
/**
* 生成设备指纹
*/
public String generateFingerprint(HttpServletRequest request) {
StringBuilder sb = new StringBuilder();
// 获取请求头信息
sb.append(request.getHeader("User-Agent"));
sb.append(request.getHeader("Accept-Language"));
sb.append(request.getHeader("Accept-Encoding"));
sb.append(request.getHeader("Accept"));
// 获取其他信息
sb.append(request.getRemoteAddr());
sb.append(request.getHeader("X-Forwarded-For"));
// 计算MD5
return DigestUtils.md5DigestAsHex(sb.toString().getBytes());
}
/**
* 检查设备是否异常
*/
public boolean isDeviceNormal(String fingerprint, String action) {
String key = "device:" + fingerprint + ":" + action;
Long count = redisTemplate.opsForValue().increment(key);
if (count != null && count > 100) { // 同一设备同一操作超过100次
return false;
}
// 设置过期时间
redisTemplate.expire(key, Duration.ofHours(1));
return true;
}
}
监控与告警
1. 防护指标监控
@Component
public class ProtectionMetricsCollector {
private final MeterRegistry meterRegistry;
public void recordRateLimitHit(String endpoint) {
Counter.builder("rate_limit_hits_total")
.tag("endpoint", endpoint)
.register(meterRegistry)
.increment();
}
public void recordBlacklistHit(String ip) {
Counter.builder("blacklist_hits_total")
.tag("ip", ip)
.register(meterRegistry)
.increment();
}
public void recordAnomaly(String type, String target) {
Counter.builder("anomaly_detected_total")
.tag("type", type)
.tag("target", target)
.register(meterRegistry)
.increment();
}
}
2. 异常告警
@Component
public class ProtectionAlertService {
/**
* 检查是否需要告警
*/
public void checkAndAlert() {
// 检查限流命中率
Long rateLimitHits = getRateLimitHitsInLastMinute();
if (rateLimitHits > 1000) { // 一分钟内限流命中超过1000次
sendAlert("限流告警", "一分钟内限流命中超过1000次,请检查系统");
}
// 检查黑名单命中率
Long blacklistHits = getBlacklistHitsInLastMinute();
if (blacklistHits > 100) { // 一分钟内黑名单命中超过100次
sendAlert("黑名单告警", "一分钟内黑名单命中超过100次,请检查攻击情况");
}
// 检查异常行为
Long anomalyCount = getAnomalyCountInLastHour();
if (anomalyCount > 50) { // 一小时内异常行为超过50次
sendAlert("异常行为告警", "一小时内检测到超过50次异常行为");
}
}
private void sendAlert(String title, String message) {
// 发送告警通知(邮件、短信、钉钉等)
// 实现告警发送逻辑
}
}
最佳实践
1. 分层防护策略
- 第一层:全局限流,防止大规模攻击
- 第二层:接口限流,保护关键接口
- 第三层:认证验证,确保用户合法性
- 第四层:行为分析,识别异常模式
2. 限流参数配置
@Configuration
public class RateLimitConfig {
// 登录接口限流:每分钟最多5次
@Bean
@Qualifier("loginRateLimit")
public RateLimitRule loginRateLimit() {
return RateLimitRule.builder()
.limit(5)
.window(60)
.build();
}
// 验证码接口限流:每分钟最多3次
@Bean
@Qualifier("captchaRateLimit")
public RateLimitRule captchaRateLimit() {
return RateLimitRule.builder()
.limit(3)
.window(60)
.build();
}
// 普通接口限流:每分钟最多100次
@Bean
@Qualifier("normalRateLimit")
public RateLimitRule normalRateLimit() {
return RateLimitRule.builder()
.limit(100)
.window(60)
.build();
}
}
3. 动态调整策略
@Service
public class DynamicProtectionService {
/**
* 根据系统负载动态调整防护策略
*/
public void adjustProtectionStrategy() {
// 获取系统负载信息
double systemLoad = getSystemLoad();
if (systemLoad > 0.8) { // 系统负载过高
// 收紧限流策略
tightenRateLimit();
} else if (systemLoad < 0.3) { // 系统负载较低
// 放宽限流策略
loosenRateLimit();
}
}
private void tightenRateLimit() {
// 收紧限流参数
// 实现逻辑...
}
private void loosenRateLimit() {
// 放宽限流参数
// 实现逻辑...
}
}
总结
接口防护是一个系统工程,需要从多个维度进行考虑:
- 预防为主:建立完善的限流机制
- 检测及时:实时监控异常行为
- 响应迅速:快速封禁恶意请求
- 持续优化:根据攻击模式调整策略
记住,防护不是一成不变的,需要根据业务特点和攻击模式持续优化。掌握了这些技巧,你就能构建一个坚固的防护体系,让恶意刷接口的行为无处遁形!
0 评论