跨境站点机器人风控开发:拦截代购系统恶意刷单与爬虫攻击
跨境站点的风控难点和国内站点不同,用户IP遍布全球、大量用户使用代理IP、动态IP,传统单一IP封禁、频次限制的风控规则很容易误杀正常用户。很多新手开发直接套用国内风控逻辑,导致大量海外正常用户无法注册、下单,严重影响业务转化,这也是跨境风控开发的核心难点。
我在设计Taocarts风控体系时,摒弃了粗暴的IP封禁模式,采用「行为校验+设备指纹+频次限流」的多维风控逻辑,精准区分真实用户与机器人脚本。
一、设备指纹:识别"谁"在访问
设备指纹是风控体系的第一道防线。跨境场景下,用户IP频繁变化,单纯依赖IP根本无法锁定同一设备。我采用服务端与客户端双重指纹采集方案,生成稳定的设备唯一标识。
服务端采集HTTP请求中的稳定特征字段,通过加权哈希生成设备指纹示例代码::
class DeviceFingerprint
{
private $secretKey = 'taocarts_device_secret';
public function generate(array $requestData): string
{
// 清洗UA:去除版本号、随机token等易变部分,保留浏览器类型、内核、OS大类
$ua = $this->cleanUserAgent($requestData['user_agent'] ?? '');
// 提取真实IP(需校验可信代理列表,避免被伪造)
$ip = $this->getRealClientIp($requestData);
// 稳定字段组合:UA + Accept-Language + Accept-Encoding
$fingerprintData = $ua
. ($requestData['accept_language'] ?? '')
. ($requestData['accept_encoding'] ?? '');
// 使用hash_hmac替代md5,防篡改也防暴力反查
return hash_hmac('sha256', $fingerprintData, $this->secretKey);
}
private function cleanUserAgent(string $ua): string
{
// 正则清洗:去掉版本号、随机token等易变部分
$ua = preg_replace('/\d+\.\d+\.\d+/', '', $ua);
$ua = preg_replace('/[a-f0-9]{32}/', '', $ua);
return trim($ua);
}
private function getRealClientIp(array $request): string
{
// 可信代理IP白名单
$trustedProxies = ['10.0.0.0/8', '172.16.0.0/12', '192.168.0.0/16'];
// 优先取HTTP_X_FORWARDED_FOR,取最左非白名单IP
if (!empty($request['http_x_forwarded_for'])) {
$ips = array_map('trim', explode(',', $request['http_x_forwarded_for']));
foreach ($ips as $ip) {
if (!$this->isPrivateIp($ip)) {
return $ip;
}
}
}
// fallback到HTTP_X_REAL_IP,最后才用REMOTE_ADDR
return $request['remote_addr'] ?? '0.0.0.0';
}
}
客户端侧配合JavaScript采集更丰富的浏览器特征(屏幕分辨率、时区、Canvas指纹、WebGL信息等),与服务端指纹交叉验证。Session ID和Cookie不能作为设备标识——它们反映的是"会话状态"而非"设备特征",容易被篡改或共享。
二、行为校验:判断"行为"是否像人
设备指纹解决了"谁在访问"的问题,行为校验则解决"访问是否正常"的问题。通过记录用户浏览轨迹、点击习惯、停留时长、操作间隔等行为数据,精准区分真实用户与机器人脚本。
前端埋点采集用户行为数据示例代码::
// 前端行为采集
class BehaviorCollector {
constructor() {
this.events = [];
this.startTime = Date.now();
this.init();
}
init() {
// 记录鼠标移动轨迹
document.addEventListener('mousemove', (e) => {
this.record('mousemove', {
x: e.clientX,
y: e.clientY,
timestamp: Date.now()
});
});
// 记录点击事件
document.addEventListener('click', (e) => {
this.record('click', {
target: e.target.tagName,
x: e.clientX,
y: e.clientY,
timestamp: Date.now()
});
});
// 记录键盘输入间隔
document.addEventListener('keydown', (e) => {
this.record('keydown', {
key: e.key.length === 1 ? '*' : e.key,
timestamp: Date.now()
});
});
// 页面离开时上报
window.addEventListener('beforeunload', () => {
this.report();
});
}
record(type, data) {
this.events.push({ type, ...data });
// 每50条批量上报一次
if (this.events.length >= 50) {
this.report();
}
}
report() {
if (this.events.length === 0) return;
navigator.sendBeacon('/api/behavior/report', JSON.stringify({
events: this.events,
sessionDuration: Date.now() - this.startTime,
screenWidth: window.screen.width,
screenHeight: window.screen.height
}));
this.events = [];
}
}
new BehaviorCollector();
三、频次限流:控制"频率"防刷单
针对高频风险场景设置专属防护规则,注册、下单、支付、铺货采集四大核心节点重点风控。限流算法我采用滑动窗口而非固定窗口——固定窗口在窗口切换时存在"临界突增"风险(例如第59秒和第60秒各发100次请求,实际1秒内涌入了200次)。
使用Redis有序集合实现滑动窗口限流示例代码::
class SlidingWindowRateLimiter
{
private $redis;
public function __construct($redis)
{
$this->redis = $redis;
}
/**
* 检查是否超过限流阈值
* @param string $key 限流key(如 device_id:123:order)
* @param int $limit 窗口内最大请求数
* @param int $window 时间窗口(秒)
* @return bool true=通过,false=被限流
*/
public function check(string $key, int $limit, int $window): bool
{
$now = microtime(true);
$windowStart = $now - $window;
// 移除窗口外的旧请求记录
$this->redis->zRemRangeByScore($key, 0, $windowStart);
// 获取当前窗口内的请求数
$count = $this->redis->zCard($key);
if ($count >= $limit) {
return false; // 被限流
}
// 记录本次请求(使用微秒时间戳作为member,保证唯一性)
$this->redis->zAdd($key, $now, $now . '_' . uniqid());
$this->redis->expire($key, $window + 1);
return true;
}
}
// 使用示例:限制单设备每日下单不超过50次
$limiter = new SlidingWindowRateLimiter($redis);
$key = "rate:device:" . $deviceId . ":order";
if (!$limiter->check($key, 50, 86400)) {
http_response_code(429);
echo json_encode(['error' => '下单过于频繁,请稍后再试']);
exit;
}
四、爬虫检测:拦截恶意数据抓取
针对高频商品采集请求,自动触发限流与验证码校验。我集成CrawlerDetect库检测已知爬虫——它能够识别超过1000种不同的机器人、爬虫和蜘蛛示例代码:。
// 通过Composer安装: composer require jaybizzle/crawler-detect
require_once 'vendor/autoload.php';
use Jaybizzle\CrawlerDetect\CrawlerDetect;
class CrawlerGuard
{
private $detect;
public function __construct()
{
$this->detect = new CrawlerDetect();
}
public function intercept(): bool
{
// 1. 检测已知爬虫User-Agent
if ($this->detect->isCrawler()) {
$this->logBlock('known_crawler', $_SERVER['HTTP_USER_AGENT']);
return true;
}
// 2. 检测无User-Agent或异常UA(恶意爬虫常伪造或留空)
$ua = $_SERVER['HTTP_USER_AGENT'] ?? '';
if (empty($ua) || strlen($ua) < 10) {
$this->logBlock('empty_ua', $ua);
return true;
}
// 3. 检测高频请求(与限流器联动)
$ip = $_SERVER['REMOTE_ADDR'];
if (!$this->rateLimiter->check("crawler:ip:" . $ip, 100, 60)) {
$this->logBlock('high_frequency', $ip);
return true;
}
return false;
}
// 4. 商品详情页额外防护:校验Referer
public function validateProductAccess(): bool
{
$referer = $_SERVER['HTTP_REFERER'] ?? '';
$host = $_SERVER['HTTP_HOST'];
// 无Referer或非本站来源,触发验证码
if (empty($referer) || !strpos($referer, $host)) {
return $this->requireCaptcha();
}
return true;
}
}
五、风控网关:统一拦截入口
所有风控逻辑集成到统一的中间件/网关层,在请求进入业务逻辑之前完成拦截判断示例代码::
class RiskControlGateway
{
private $deviceFingerprint;
private $behaviorAnalyzer;
private $rateLimiter;
private $crawlerGuard;
/**
* 风控网关入口 - 所有请求先过此处
*/
public function check(string $action, array $context): array
{
// 1. 生成/获取设备指纹
$deviceId = $this->deviceFingerprint->generate($_SERVER);
// 2. 爬虫检测(优先拦截)
if ($this->crawlerGuard->intercept()) {
return ['passed' => false, 'reason' => 'crawler_detected', 'code' => 403];
}
// 3. 频次限流
$rateKey = "rate:" . $action . ":" . $deviceId;
if (!$this->rateLimiter->check($rateKey, $this->getLimit($action), $this->getWindow($action))) {
return ['passed' => false, 'reason' => 'rate_limited', 'code' => 429];
}
// 4. 行为风险评分(敏感操作如注册、支付必检)
if (in_array($action, ['register', 'payment', 'order'])) {
$behavior = $this->getBehaviorData($deviceId);
$analysis = $this->behaviorAnalyzer->analyze($behavior);
if ($analysis['is_risky']) {
// 高风险操作触发二次验证
return [
'passed' => false,
'reason' => 'behavior_risky',
'risk_score' => $analysis['risk_score'],
'require_captcha' => true,
'code' => 428
];
}
}
// 5. 全部通过,记录访问日志
$this->logAccess($deviceId, $action, $context);
return ['passed' => true, 'device_id' => $deviceId];
}
private function getLimit(string $action): int
{
$limits = [
'register' => 3,
'order' => 50,
'payment' => 10,
'product_list' => 100,
'transship' => 20
];
return $limits[$action] ?? 100;
}
private function getWindow(string $action): int
{
$windows = [
'register' => 3600,
'order' => 86400,
'payment' => 600,
'product_list' => 60,
'transship' => 3600
];
return $windows[$action] ?? 60;
}
}
六、适配代购集运业务专属风控
针对代购集运、代购转运业务,系统还需识别以下异常操作示例代码::
class TransshipRiskGuard
{
public function checkTransshipOrder(array $order): array
{
$risks = [];
// 1. 短时间大量提交转运订单
$recentCount = $this->countRecentOrders($order['user_id'], 3600);
if ($recentCount > 20) {
$risks[] = '一小时内提交转运订单超过20单';
}
// 2. 重复提交合包/拆包申请
$duplicateCount = $this->countDuplicatePackageRequests($order['user_id'], 600);
if ($duplicateCount > 5) {
$risks[] = '十分钟内重复提交合包/拆包申请超过5次';
}
// 3. 高频修改收货地址
$addressChanges = $this->countAddressChanges($order['user_id'], 86400);
if ($addressChanges > 3) {
$risks[] = '24小时内修改收货地址超过3次';
}
if (!empty($risks)) {
$this->markRiskyAccount($order['user_id'], $risks);
return ['passed' => false, 'reasons' => $risks];
}
return ['passed' => true];
}
}
七、可视化风控配置与日志
系统支持后台可视化风控配置,管理员可自主调整限流阈值、风控等级,适配平台不同运营阶段的防护需求。同时留存完整风控日志,所有拦截记录、风险账号、异常IP全程溯源:
-- 风控日志表结构示例代码:
CREATE TABLE `risk_logs` (
`id` bigint(20) NOT NULL AUTO_INCREMENT,
`device_id` varchar(64) NOT NULL COMMENT '设备指纹',
`user_id` int(11) DEFAULT NULL COMMENT '用户ID(如有)',
`action` varchar(50) NOT NULL COMMENT '触发动作',
`risk_score` tinyint(4) DEFAULT NULL COMMENT '风险评分',
`block_reason` varchar(255) DEFAULT NULL COMMENT '拦截原因',
`request_uri` varchar(500) DEFAULT NULL COMMENT '请求URI',
`client_ip` varchar(45) DEFAULT NULL COMMENT '客户端IP',
`user_agent` varchar(500) DEFAULT NULL COMMENT 'User-Agent',
`created_at` datetime NOT NULL DEFAULT CURRENT_TIMESTAMP,
PRIMARY KEY (`id`),
KEY `idx_device_id` (`device_id`),
KEY `idx_user_id` (`user_id`),
KEY `idx_created_at` (`created_at`)
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4;