反爬虫
1、为了避免网站的数据被别人大规模盗取
2、为了避免频繁的爬虫请求导致系统过载
手段
可以参考这篇文章
总有坏人想爬我网站的数据,看我用这 10 - 编程导航 - 程序员编程学习交流社区
1、使用协议条款
2、限制请求条件
3、统计访问频率和封禁
4、多级处理策略+自动处理(警告、短期封禁、长期封禁)
5、自动告警+人工处理
。。。
如何通过统计访问频率来实现?
1、hotkey探测
统计访问频率的目的就是为了对访问的流量进行限制。
那么如果系统中已经引入了hotkey, 我们就可以使用热点key探测来实现。
比如当用户获取某个资源的时候,我们以业务内容+用户来做为key,并且在控制台给这个key配置一个热点规则
这样在业务中,就可以通过判断这个key是不是热点key,来分析当前请求是不是合法
2、sentinel限流
限流框架中提供了对热点参数进行限流配置的功能,我们可以将用户ID做为热点参数,如果当前用户的请求过多超过配置的规则
那么就可以在降级方法中进行限制。
3、本地计数器(单机)
前面两种方法都必须依赖第三方框架。如果项目本身没有引入,那么代价就比较大。
可以使用jdk自带的本地计数器来对需要反爬的资源请求进行统计。
请看示例
import java.util.concurrent.ConcurrentHashMap;
import java.util.concurrent.TimeUnit;
import java.util.concurrent.atomic.LongAdder;
public class RequestRateLimiter {
// 用于存储每个用户的访问次数计数器
private ConcurrentHashMap<String, LongAdder> userRequestCounts = new ConcurrentHashMap<>();
// 统计的时间间隔(比如每分钟重置一次计数)
private final long interval;
public RequestRateLimiter(long intervalInSeconds) {
this.interval = intervalInSeconds;
startResetTask();
}
/**
* 每次用户访问时调用此方法
* @param userId 用户的唯一标识符
*/
public void recordRequest(String userId) {
// 获取或者初始化用户的访问计数器
userRequestCounts.computeIfAbsent(userId, key -> new LongAdder()).increment();
}
/**
* 获取用户的当前访问次数
* @param userId 用户的唯一标识符
* @return 用户的访问次数
*/
public long getRequestCount(String userId) {
return userRequestCounts.getOrDefault(userId, new LongAdder()).sum();
}
/**
* 定期重置每个用户的访问计数器
*/
private void startResetTask() {
// 定时任务,每隔指定的时间间隔重置计数
new Thread(() -> {
while (true) {
try {
TimeUnit.SECONDS.sleep(interval); // 等待指定的时间间隔
} catch (InterruptedException e) {
Thread.currentThread().interrupt();
return;
}
// 重置每个用户的计数器
userRequestCounts.clear();
}
}).start();
}
public static void main(String[] args) {
RequestRateLimiter limiter = new RequestRateLimiter(60); // 每分钟重置一次
// 模拟用户请求
limiter.recordRequest("user1");
limiter.recordRequest("user1");
System.out.println("User1's current request count: " + limiter.getRequestCount("user1")); // 输出 2
}
}
4、基于redis进行统计(方案3的分布式版本)
使用redis,就是要定义一个key,然后每次请求来了,对key的value进行+1操作。
下面是示例
//假设使用 jedis 客户端
// 使用 Redis 的 INCR 操作增加当前秒的访问次数
jedis.incr(redisKey);
// 设置过期时间(TTL),例如只保存60秒的数据
jedis.expire(redisKey, 60); // 60秒后自动过期
上面这种方法,因为是两步操作,且没有事务,不是原子性操作
当并发流量非常大的的时候,可能会出现计数异常
如果要在redis中实现原子操作,就要借助lua脚本
String luaScript =
"if redis.call('exists', KEYS[1]) == 1 then " +
" return redis.call('incr', KEYS[1]); " +
"else " +
" redis.call('set', KEYS[1], 1); " +
" redis.call('expire', KEYS[1], 180); " + // 设置 180 秒过期时间
" return 1; " +
"end";
实际方法
public long incrAndGetCounter(String key, int timeInterval, TimeUnit unit, int expireTimeInSeconds){
if (StrUtil.isBlank(key)){
return 0;
}
// 根据时间粒度生成key
long timeFactor;
switch (unit) {
case SECONDS:
timeFactor = Instant.now().getEpochSecond() / timeInterval;
break;
case MINUTES:
timeFactor = Instant.now().getEpochSecond() / (timeInterval * 60);
break;
case HOURS:
timeFactor = Instant.now().getEpochSecond() / (timeInterval * 3600);
break;
default:
throw new IllegalArgumentException("unsupported timeUnit");
}
String redisKey = StrUtil.format("{}:{}", key, timeFactor);
// lua脚本
String luaScript =
"if redis.call('exists', KEYS[1]) == 1 then " +
"return redis.call('incr', KEYS[1]); " +
"else " +
"redis.call('set', KEYS[1], 1); " +
"redis.call('expire', KEYS[1], ARGV[1]); " +
"return 1; " +
"end ";
// 执行脚本
RScript script = redisClient.getScript(IntegerCodec.INSTANCE);
Object result = script.eval(
RScript.Mode.READ_WRITE,
luaScript,
RScript.ReturnType.INTEGER,
Collections.singletonList(redisKey),
expireTimeInSeconds
);
return (long) result;
}有了检测方法之后,就是具体嵌入业务中
可以使用切面编程的方法,通过自定义注解,这样就可以在需要进行反爬虫的接口上添加注解,用来进行爬虫检测
示例如下:
自定义注解:
@Target(ElementType.METHOD)
@Retention(RetentionPolicy.RUNTIME)
public @interface CrawlerDetect {
}切面方法:
@Around("@annotation(crawlerDetect)")
public Object around(ProceedingJoinPoint joinPoint, CrawlerDetect crawlerDetect) throws Throwable {
RequestAttributes requestAttributes = RequestContextHolder.currentRequestAttributes();
HttpServletRequest request = ((ServletRequestAttributes) requestAttributes).getRequest();
// 当前登录用户
User loginUser = userService.getLoginUser(request);
// 爬虫检测
crawlerDetectManager.crawlerDetect(loginUser.getId());
// 通过权限校验,放行
return joinPoint.proceed();
}