1115 lines
60 KiB
PHP
1115 lines
60 KiB
PHP
|
|
<?php
|
|||
|
|
/**==*==*==*==*==*==*==*==*==*==*==*==*==*==*==*==*==*==*==*==*==*==*==*==**/
|
|||
|
|
|
|||
|
|
// 风险评估类
|
|||
|
|
class RiskAnalyzer {
|
|||
|
|
public $countries;
|
|||
|
|
public $countriesByContinent;
|
|||
|
|
public $langLabels;
|
|||
|
|
public $countryLangMap;
|
|||
|
|
public $commonResolutions;
|
|||
|
|
public $timezones;
|
|||
|
|
public $countryTimezoneMap;
|
|||
|
|
public $unqiePixels;
|
|||
|
|
|
|||
|
|
private $riskLevel = 0;
|
|||
|
|
private $riskFactors = [];
|
|||
|
|
private $fingerprint;
|
|||
|
|
private $fingerprintData;
|
|||
|
|
|
|||
|
|
public function __construct($data) {
|
|||
|
|
|
|||
|
|
$this->fingerprintData = $data;
|
|||
|
|
$this->hashFingerprint();
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
private function getScreenWidth() {
|
|||
|
|
$screen = isset($this->fingerprintData['screen']) ? $this->fingerprintData['screen'] : null;
|
|||
|
|
if (!is_array($screen)) {
|
|||
|
|
return 0;
|
|||
|
|
}
|
|||
|
|
if (isset($screen['width'])) {
|
|||
|
|
return (int) $screen['width'];
|
|||
|
|
}
|
|||
|
|
return (int) (isset($screen[0]) ? $screen[0] : 0);
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
private function hasTouchscreen() {
|
|||
|
|
$touch = isset($this->fingerprintData['touchscreen']) ? $this->fingerprintData['touchscreen'] : array(0, false, false);
|
|||
|
|
if (!is_array($touch)) {
|
|||
|
|
return false;
|
|||
|
|
}
|
|||
|
|
return ($touch[0] > 0 || !empty($touch[1]) || !empty($touch[2]));
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
private function uaIndicatesTablet($ua) {
|
|||
|
|
return (bool) preg_match('/iPad|Tablet|SM-T|Kindle|Silk\/|PlayBook|Tab(?:let)?\b/i', $ua);
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
private function uaIndicatesMobile($ua) {
|
|||
|
|
return (bool) preg_match(
|
|||
|
|
'/iPhone|iPod|Android.*Mobile|Mobile\/|IEMobile|webOS|BlackBerry|Opera Mini|Mobile Safari/i',
|
|||
|
|
$ua
|
|||
|
|
);
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
private function platformIndicatesMobile() {
|
|||
|
|
$platform = isset($this->fingerprintData['platform']) ? (string) $this->fingerprintData['platform'] : '';
|
|||
|
|
return $platform !== '' && (bool) preg_match('/iPhone|iPod|Android/i', $platform);
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
private function platformIndicatesTablet() {
|
|||
|
|
$platform = isset($this->fingerprintData['platform']) ? (string) $this->fingerprintData['platform'] : '';
|
|||
|
|
return $platform !== '' && (bool) preg_match('/iPad/i', $platform);
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
/**
|
|||
|
|
* 推断设备类型(browser=null 时不能默认 desktop)
|
|||
|
|
*/
|
|||
|
|
private function getDeviceType() {
|
|||
|
|
$ua = isset($this->fingerprintData['userAgent']) ? (string) $this->fingerprintData['userAgent'] : '';
|
|||
|
|
|
|||
|
|
if ($this->uaIndicatesTablet($ua) || $this->platformIndicatesTablet()) {
|
|||
|
|
return 'tablet';
|
|||
|
|
}
|
|||
|
|
if ($this->uaIndicatesMobile($ua) || $this->platformIndicatesMobile()) {
|
|||
|
|
return 'mobile';
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
$parsedType = '';
|
|||
|
|
if (!empty($this->fingerprintData['browser']['device']['type'])) {
|
|||
|
|
$parsedType = strtolower((string) $this->fingerprintData['browser']['device']['type']);
|
|||
|
|
}
|
|||
|
|
if (in_array($parsedType, array('mobile', 'tablet', 'wearable', 'smarttv', 'console', 'embedded', 'xr'), true)) {
|
|||
|
|
return $parsedType;
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
if ($this->hasTouchscreen()) {
|
|||
|
|
$screenWidth = $this->getScreenWidth();
|
|||
|
|
if ($screenWidth > 0 && $screenWidth < 1024) {
|
|||
|
|
return 'mobile';
|
|||
|
|
}
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
if ($parsedType !== '') {
|
|||
|
|
return $parsedType;
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
return 'desktop';
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
private function isMobileDeviceType($deviceType) {
|
|||
|
|
return in_array($deviceType, array('mobile', 'tablet'), true);
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
/**
|
|||
|
|
* 移动端 screen 与 availScreen 相同是正常现象,不能作为桌面风险信号
|
|||
|
|
*/
|
|||
|
|
private function shouldApplyDesktopScreenHeuristics() {
|
|||
|
|
if ($this->isMobileDeviceType($this->getDeviceType())) {
|
|||
|
|
return false;
|
|||
|
|
}
|
|||
|
|
if ($this->hasTouchscreen()) {
|
|||
|
|
return false;
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
$ua = isset($this->fingerprintData['userAgent']) ? (string) $this->fingerprintData['userAgent'] : '';
|
|||
|
|
if ($this->uaIndicatesMobile($ua) || $this->uaIndicatesTablet($ua)) {
|
|||
|
|
return false;
|
|||
|
|
}
|
|||
|
|
if ($this->platformIndicatesMobile() || $this->platformIndicatesTablet()) {
|
|||
|
|
return false;
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
return true;
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
private function getOsName() {
|
|||
|
|
$platform = isset($this->fingerprintData['platform']) ? (string) $this->fingerprintData['platform'] : '';
|
|||
|
|
if (preg_match('/iPad|iPhone|iPod/i', $platform)) {
|
|||
|
|
return 'iOS';
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
if (!empty($this->fingerprintData['browser']['os']['name'])) {
|
|||
|
|
$os = (string) $this->fingerprintData['browser']['os']['name'];
|
|||
|
|
if (preg_match('/macOS/i', $os) && ($this->platformIndicatesMobile() || $this->platformIndicatesTablet())) {
|
|||
|
|
return 'iOS';
|
|||
|
|
}
|
|||
|
|
return $os;
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
$ua = isset($this->fingerprintData['userAgent']) ? (string) $this->fingerprintData['userAgent'] : '';
|
|||
|
|
if (preg_match('/Windows/i', $ua)) {
|
|||
|
|
return 'Windows';
|
|||
|
|
}
|
|||
|
|
if (preg_match('/Android/i', $ua)) {
|
|||
|
|
return 'Android';
|
|||
|
|
}
|
|||
|
|
if (preg_match('/iPhone|iPad|iOS/i', $ua)) {
|
|||
|
|
return 'iOS';
|
|||
|
|
}
|
|||
|
|
if (preg_match('/Mac OS X|Macintosh/i', $ua)) {
|
|||
|
|
if ($this->platformIndicatesMobile() || $this->platformIndicatesTablet() || $this->hasTouchscreen()) {
|
|||
|
|
return 'iOS';
|
|||
|
|
}
|
|||
|
|
return 'macOS';
|
|||
|
|
}
|
|||
|
|
return null;
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
// 执行完整风险评估
|
|||
|
|
public function analyze() {
|
|||
|
|
$this->checkWebDriver();
|
|||
|
|
$this->checkUserAgent();
|
|||
|
|
$this->checkWebGLRenderer();
|
|||
|
|
$this->checkHardwareConcurrency();
|
|||
|
|
$this->checkDeviceMemory();
|
|||
|
|
$this->checkTouchscreenConsistency();
|
|||
|
|
$this->checkScreenResolution();
|
|||
|
|
$this->checkBrowserFeatures();
|
|||
|
|
$this->checkTimezoneAnomalies();
|
|||
|
|
$this->checkLanguages();
|
|||
|
|
$this->checkHostname();
|
|||
|
|
$this->checkFonts();
|
|||
|
|
|
|||
|
|
// 限制风险等级在0-100之间
|
|||
|
|
$this->riskLevel = min(100, max(0, $this->riskLevel));
|
|||
|
|
|
|||
|
|
return [
|
|||
|
|
'fingerprint' => $this->fingerprint,
|
|||
|
|
'risk_level' => $this->riskLevel,
|
|||
|
|
'risk_factors' => $this->riskFactors,
|
|||
|
|
'risk_category' => $this->getRiskCategory(),
|
|||
|
|
'risk_description' => $this->getRiskDescription()
|
|||
|
|
];
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
public function hashFingerprint() {
|
|||
|
|
if (empty($this->fingerprintData['canvas']) || !is_array($this->fingerprintData['canvas'])) {
|
|||
|
|
return;
|
|||
|
|
}
|
|||
|
|
if ($this->fingerprintData['canvas'][0] == 'canvas winding:yes') {
|
|||
|
|
$this->fingerprint = md5($this->fingerprintData['canvas'][0] . $this->fingerprintData['canvas'][1]);
|
|||
|
|
} else {
|
|||
|
|
$this->fingerprint = NULL;
|
|||
|
|
$this->riskLevel += 50;
|
|||
|
|
$this->riskFactors[] = "检测CANVAS无效";
|
|||
|
|
}
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
// 检查WebDriver状态(强风险指标)
|
|||
|
|
private function checkWebDriver() {
|
|||
|
|
if (!empty($this->fingerprintData['webdriver'])) {
|
|||
|
|
$this->riskLevel += 50;
|
|||
|
|
$this->riskFactors[] = "检测到WebDriver,通常与自动化工具相关";
|
|||
|
|
}
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
// 检查User Agent中的自动化工具特征
|
|||
|
|
private function checkUserAgent() {
|
|||
|
|
if (empty($this->fingerprintData['userAgent'])) return;
|
|||
|
|
|
|||
|
|
$userAgent = strtolower($this->fingerprintData['userAgent']);
|
|||
|
|
$botIndicators = [
|
|||
|
|
'bot', 'crawl', 'spider', 'slurp', 'search', 'semrush',
|
|||
|
|
'webdriver', 'selenium', 'phantom', 'headless',
|
|||
|
|
'chromedriver', 'geckodriver', 'robot', 'scrap', 'screenshot', 'monitoring',
|
|||
|
|
'AdsBot-Google','facebookexternalhit','Google-Adwords-Instant', 'emulator', 'simulator'
|
|||
|
|
];
|
|||
|
|
|
|||
|
|
// 检测爬虫特征
|
|||
|
|
foreach ($botIndicators as $indicator) {
|
|||
|
|
if (stripos($userAgent, $indicator) !== false) {
|
|||
|
|
$this->riskLevel += 30;
|
|||
|
|
$this->riskFactors[] = "User Agent包含自动化工具特征: {$indicator}";
|
|||
|
|
}
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
// 检查无头浏览器特征
|
|||
|
|
if (stripos($userAgent, 'headlesschrome') !== false) {
|
|||
|
|
$this->riskLevel += 40;
|
|||
|
|
$this->riskFactors[] = "检测到无头Chrome浏览器,通常用于自动化";
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
// 检测用户代理异常
|
|||
|
|
$browsers = ['Chrome', 'Firefox', 'Safari', 'Edge'];
|
|||
|
|
$os = ['Windows', 'Mac', 'Android', 'iOS'];
|
|||
|
|
|
|||
|
|
$browserDetected = false;
|
|||
|
|
$osDetected = false;
|
|||
|
|
|
|||
|
|
foreach ($browsers as $browser) {
|
|||
|
|
if (stripos($this->fingerprintData['userAgent'], $browser) !== false) {
|
|||
|
|
$browserDetected = true;
|
|||
|
|
break;
|
|||
|
|
}
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
foreach ($os as $osName) {
|
|||
|
|
if (stripos($this->fingerprintData['userAgent'], $osName) !== false) {
|
|||
|
|
$osDetected = true;
|
|||
|
|
break;
|
|||
|
|
}
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
if (!$browserDetected || !$osDetected) {
|
|||
|
|
$this->riskLevel += 15;
|
|||
|
|
$this->riskFactors[] = "异常用户代理: 无法识别浏览器或操作系统";
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
if (isset($this->fingerprintData['automation'])) {
|
|||
|
|
if ($this->fingerprintData['automation']['phantom'] || $this->fingerprintData['automation']['selenium']) {
|
|||
|
|
$this->riskLevel += 30;
|
|||
|
|
$this->riskFactors[] = '检测到PhantomJS或Selenium相关组件。';
|
|||
|
|
}
|
|||
|
|
// UA声称是Chrome,但没有`window.chrome`对象,非常可疑
|
|||
|
|
if (strpos($userAgent, 'Chrome') !== false && !$this->fingerprintData['automation']['isChrome']) {
|
|||
|
|
$this->riskLevel += 20;
|
|||
|
|
$this->riskFactors[] = 'UA声称是Chrome,但没有`window.chrome`对象,非常可疑';
|
|||
|
|
}
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
$renderer = isset($this->fingerprintData['renderer']) ? $this->fingerprintData['renderer'] : '';
|
|||
|
|
$os = $this->getOsName();
|
|||
|
|
if ($renderer === '' || $os === null) {
|
|||
|
|
return;
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
// 检查是否为Windows系统
|
|||
|
|
if (preg_match('/Windows/i', $os)) {
|
|||
|
|
// Windows 系统的 GPU 应该是 NVIDIA, AMD, Intel, 或者 Microsoft Basic Render Driver
|
|||
|
|
// 绝对不应该是 Apple
|
|||
|
|
if (stripos($renderer, 'apple') !== false) {
|
|||
|
|
$this->riskLevel += 10;
|
|||
|
|
$this->riskFactors[] = "CRITICAL: Windows User-Agent but an Apple GPU renderer was detected. Clear sign of spoofing.";
|
|||
|
|
}
|
|||
|
|
}
|
|||
|
|
// 检查是否为macOS系统
|
|||
|
|
elseif (preg_match('/macOS/i', $os)) {
|
|||
|
|
// macOS 系统的 GPU 应该是 Apple, Intel, 或 AMD (在旧款Mac Pro上)
|
|||
|
|
// 现代Mac上几乎不可能是 NVIDIA
|
|||
|
|
if (strpos($renderer, 'nvidia') !== false) {
|
|||
|
|
$this->riskLevel += 10;
|
|||
|
|
$this->riskFactors[] = "CRITICAL: macOS User-Agent but an NVIDIA GPU renderer was detected. Highly inconsistent.";
|
|||
|
|
}
|
|||
|
|
// Mac的renderer不应包含Direct3D
|
|||
|
|
if (stripos($renderer, 'direct3d') !== false) {
|
|||
|
|
$this->riskLevel += 10;
|
|||
|
|
$this->riskFactors[] = "CRITICAL: macOS User-Agent but renderer string contains 'Direct3D', which is Windows-specific.";
|
|||
|
|
}
|
|||
|
|
}
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
// 检查WebGL渲染器是否为虚拟环境特征
|
|||
|
|
private function checkWebGLRenderer() {
|
|||
|
|
if (empty($this->fingerprintData['renderer'])) return;
|
|||
|
|
|
|||
|
|
$vendor = $this->fingerprintData['vendor'];
|
|||
|
|
$renderer = $this->fingerprintData['renderer'];
|
|||
|
|
$virtualRenderers = [
|
|||
|
|
'llvmpipe', 'software', 'virtual', 'qemu', 'mesa', 'swiftshader', 'VMware', 'VirtualBox', 'KVM', 'Xen', 'Hyper-V', 'AWS', 'Google Compute', 'Azure', 'Headless', 'OpenGL', 'Parallels'
|
|||
|
|
];
|
|||
|
|
|
|||
|
|
foreach ($virtualRenderers as $virt) {
|
|||
|
|
if (stripos($renderer, $virt) !== false) {
|
|||
|
|
$this->riskLevel += 30;
|
|||
|
|
$this->riskFactors[] = "检测到可能的虚拟环境图形渲染器: {$this->fingerprintData['renderer']}";
|
|||
|
|
}
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
if (empty($this->fingerprintData['webgl']) || !is_array($this->fingerprintData['webgl'])) {
|
|||
|
|
return;
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
$extensions = explode(';', $this->fingerprintData['webgl'][1]);
|
|||
|
|
if (count($extensions) < 28) {
|
|||
|
|
$this->riskLevel += 10;
|
|||
|
|
$this->riskFactors[] = "WebGL扩展数量较少.";
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
$lineWidthRangeStr = str_replace("webgl aliased line width range:", "", $this->fingerprintData['webgl'][2]);
|
|||
|
|
$lineWidthRange = $lineWidthRangeStr ? json_decode($lineWidthRangeStr, true) : [0,0];
|
|||
|
|
|
|||
|
|
if (is_array($lineWidthRange) && $lineWidthRange[0] == 1 && $lineWidthRange[1] == 1) {
|
|||
|
|
$this->riskLevel += 2;
|
|||
|
|
$this->riskFactors[] = "Aliased line width range is [1, 1]";
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
$MAX_TEXTURE_SIZE = str_replace("webgl max texture size:", "", $this->fingerprintData['webgl'][15]);
|
|||
|
|
if (isset($this->fingerprintData['webgl'][15]) && $MAX_TEXTURE_SIZE < 8192) {
|
|||
|
|
$this->riskLevel += 2;
|
|||
|
|
$this->riskFactors[] = "SUSPICIOUS: MAX_TEXTURE_SIZE is low ($MAX_TEXTURE_SIZE).";
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
$MAX_FRAGMENT_UNIFORM_VECTORS = str_replace("webgl max fragment uniform vectors:", "", $this->fingerprintData['webgl'][12]);
|
|||
|
|
if (isset($this->fingerprintData['webgl'][12]) && $MAX_FRAGMENT_UNIFORM_VECTORS < 1024) {
|
|||
|
|
$this->riskLevel += 2;
|
|||
|
|
$this->riskFactors[] = "SUSPICIOUS: MAX_FRAGMENT_UNIFORM_VECTORS is low ($MAX_FRAGMENT_UNIFORM_VECTORS).";
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
$MAX_VARYING_VECTORS = str_replace("webgl max varying vectors:", "", $this->fingerprintData['webgl'][16]);
|
|||
|
|
if (isset($this->fingerprintData['webgl'][16]) && $MAX_VARYING_VECTORS < 16) {
|
|||
|
|
$this->riskLevel += 2;
|
|||
|
|
$this->riskFactors[] = "SUSPICIOUS: MAX_VARYING_VECTORS is low ($MAX_VARYING_VECTORS).";
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
$max_viewport_dims_str = str_replace("webgl max viewport dims:", "", $this->fingerprintData['webgl'][20]);
|
|||
|
|
$MAX_VIEWPORT_DIMS = $max_viewport_dims_str ? json_decode($max_viewport_dims_str, true) : [0,0];
|
|||
|
|
|
|||
|
|
if (isset($this->fingerprintData['webgl'][20]) && is_array($MAX_VIEWPORT_DIMS) && $MAX_VIEWPORT_DIMS[0] < 8192) {
|
|||
|
|
$this->riskLevel += 2;
|
|||
|
|
$this->riskFactors[] = "SUSPICIOUS: MAX_VIEWPORT_DIMS is low.";
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
$MAX_ANISOTROPY = str_replace("webgl max anisotropy:", "", $this->fingerprintData['webgl'][9]);
|
|||
|
|
if (isset($this->fingerprintData['webgl'][9]) && (int) $MAX_ANISOTROPY <= 2) {
|
|||
|
|
$this->riskLevel += 5;
|
|||
|
|
$this->riskFactors[] = "SUSPICIOUS: Anisotropic filtering is not supported.";
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
|
|||
|
|
// 规则4:上下文配置检查
|
|||
|
|
// 前端请求了抗锯齿,检查后端实际是否获得
|
|||
|
|
$antialiasing = str_replace("webgl antialiasing:", "", $this->fingerprintData['webgl'][5]);
|
|||
|
|
if (isset($this->fingerprintData['webgl'][5]) && $antialiasing != 'yes') {
|
|||
|
|
$this->riskLevel += 10;
|
|||
|
|
$this->riskFactors[] = "SUSPICIOUS: Antialiasing was requested but not enabled, common in software renderers.";
|
|||
|
|
}
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
// 检查IP的Hostname
|
|||
|
|
private function checkHostname() {
|
|||
|
|
$ip = isset($this->fingerprintData['ip']) ? trim((string) $this->fingerprintData['ip']) : '';
|
|||
|
|
if ($ip === '' || !filter_var($ip, FILTER_VALIDATE_IP)) {
|
|||
|
|
return;
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
$hostname = @gethostbyaddr($ip);
|
|||
|
|
if ($hostname && $hostname !== $ip) {
|
|||
|
|
if (preg_match('/\.googlebot\.com$/i', $hostname) || preg_match('/\.google\.com$/i', $hostname)) {
|
|||
|
|
$this->riskLevel += 30;
|
|||
|
|
$this->riskFactors[] = "检测到是Google爬虫";
|
|||
|
|
} elseif (preg_match('/\.facebook\.com$/i', $hostname)) {
|
|||
|
|
$this->riskLevel += 30;
|
|||
|
|
$this->riskFactors[] = "检测到是Facebook爬虫";
|
|||
|
|
} elseif (preg_match('/\.tiktok\.com$/i', $hostname)) {
|
|||
|
|
$this->riskLevel += 30;
|
|||
|
|
$this->riskFactors[] = "检测到是Tiktok爬虫";
|
|||
|
|
}
|
|||
|
|
}
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
// 检查硬件并发数是否异常低
|
|||
|
|
private function checkHardwareConcurrency() {
|
|||
|
|
if (empty($this->fingerprintData['hardwareConcurrency'])) return;
|
|||
|
|
|
|||
|
|
$cores = $this->fingerprintData['hardwareConcurrency'];
|
|||
|
|
if ($cores <= 2) {
|
|||
|
|
$this->riskLevel += 15;
|
|||
|
|
$this->riskFactors[] = "硬件并发数异常低: {$cores}核,可能是虚拟机环境";
|
|||
|
|
}
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
// 检查设备内存是否异常低
|
|||
|
|
private function checkDeviceMemory() {
|
|||
|
|
if (empty($this->fingerprintData['deviceMemory'])) return;
|
|||
|
|
|
|||
|
|
if ($this->fingerprintData['deviceMemory'] < 2) {
|
|||
|
|
$this->riskLevel += 10;
|
|||
|
|
$this->riskFactors[] = "低设备内存: " . $this->fingerprintData['deviceMemory'] . "GB";
|
|||
|
|
}
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
// 检查触摸屏与设备类型是否一致
|
|||
|
|
private function checkTouchscreenConsistency() {
|
|||
|
|
$deviceType = $this->getDeviceType();
|
|||
|
|
$isMobile = $this->isMobileDeviceType($deviceType);
|
|||
|
|
$hasTouchscreen = $this->hasTouchscreen();
|
|||
|
|
|
|||
|
|
// 移动设备没有触摸屏
|
|||
|
|
if ($isMobile && !$hasTouchscreen) {
|
|||
|
|
$this->riskLevel += 20;
|
|||
|
|
$this->riskFactors[] = "移动设备报告没有触摸屏,可能是自动化环境";
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
// 桌面设备有触摸屏但分辨率异常低
|
|||
|
|
if ($deviceType == 'desktop' && $hasTouchscreen) {
|
|||
|
|
$screenWidth = $this->getScreenWidth();
|
|||
|
|
if ($screenWidth > 0 && $screenWidth < 1024) {
|
|||
|
|
$this->riskLevel += 5;
|
|||
|
|
$this->riskFactors[] = "桌面设备有触摸屏但分辨率异常低,可能是模拟环境";
|
|||
|
|
}
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
// 移动设备仿真器,没有加速和陀螺仪
|
|||
|
|
if ($isMobile && !empty($this->fingerprintData['isEmulators'])) {
|
|||
|
|
$this->riskLevel += 15;
|
|||
|
|
$this->riskFactors[] = "设备可能是仿真器";
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
// 检查是否为移动端
|
|||
|
|
if ($isMobile) {
|
|||
|
|
$renderer = isset($this->fingerprintData['renderer']) ? $this->fingerprintData['renderer'] : '';
|
|||
|
|
if ($renderer !== '' && preg_match('/(nvidia|geforce|radeon)/i', $renderer)) {
|
|||
|
|
$this->riskLevel += 10;
|
|||
|
|
$this->riskFactors[] = "CRITICAL: Mobile User-Agent but a desktop-class GPU (NVIDIA/AMD) was detected.";
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
$os = $this->getOsName();
|
|||
|
|
if ($os !== null && !preg_match('/Android/i', $os) && !preg_match('/iOS/i', $os)) {
|
|||
|
|
$this->riskLevel += 10;
|
|||
|
|
$this->riskFactors[] = "移动端仅支持安卓与IOS";
|
|||
|
|
}
|
|||
|
|
}
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
// 检查屏幕分辨率是否在常见范围内
|
|||
|
|
private function checkScreenResolution() {
|
|||
|
|
if (empty($this->fingerprintData['screen']) || !is_array($this->fingerprintData['screen'])) return;
|
|||
|
|
|
|||
|
|
$screenSize = $this->fingerprintData['screen'];
|
|||
|
|
$screenWidth = (int) (isset($screenSize[0]) ? $screenSize[0] : (isset($screenSize['width']) ? $screenSize['width'] : 0));
|
|||
|
|
$screenHeight = (int) (isset($screenSize[1]) ? $screenSize[1] : (isset($screenSize['height']) ? $screenSize['height'] : 0));
|
|||
|
|
|
|||
|
|
$availScreenSize = isset($this->fingerprintData['availScreen']) ? $this->fingerprintData['availScreen'] : array(0, 0);
|
|||
|
|
$availScreenWidth = (int) (isset($availScreenSize[0]) ? $availScreenSize[0] : (isset($availScreenSize['width']) ? $availScreenSize['width'] : 0));
|
|||
|
|
$availScreenHeight = (int) (isset($availScreenSize[1]) ? $availScreenSize[1] : (isset($availScreenSize['height']) ? $availScreenSize['height'] : 0));
|
|||
|
|
|
|||
|
|
// 分辨率过小
|
|||
|
|
if ($screenWidth < 300 || $screenHeight < 400) {
|
|||
|
|
$this->riskLevel += 15;
|
|||
|
|
$this->riskFactors[] = "分辨率异常[宽或高过小]";
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
// 仅真实桌面环境:手机/平板 screen 与 availScreen 相同是正常现象
|
|||
|
|
if ($this->shouldApplyDesktopScreenHeuristics()) {
|
|||
|
|
if ($screenWidth === $availScreenWidth && $screenHeight === $availScreenHeight) {
|
|||
|
|
$this->riskLevel += 20;
|
|||
|
|
$this->riskFactors[] = "分辨率异常[桌面设备可用分辨率完全相同]";
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
$widthDiff = $screenWidth - $availScreenWidth;
|
|||
|
|
$heightDiff = $screenHeight - $availScreenHeight;
|
|||
|
|
|
|||
|
|
if ($widthDiff < 10 && $heightDiff < 10 && $screenWidth > 1000) {
|
|||
|
|
$this->riskLevel += 10;
|
|||
|
|
$this->riskFactors[] = "分辨率异常[3]";
|
|||
|
|
}
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
// 分辨率小于0
|
|||
|
|
if ($screenWidth <= 0 || $screenHeight <= 0 ||
|
|||
|
|
$availScreenWidth <= 0 || $availScreenHeight <= 0) {
|
|||
|
|
$this->riskLevel += 40;
|
|||
|
|
$this->riskFactors[] = "检测到无效或零分辨率";
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
// 常见分辨率与已知设备不匹配(unqiePixels 未配置时跳过)
|
|||
|
|
if (is_array($this->unqiePixels) && !empty($this->unqiePixels)) {
|
|||
|
|
if (array_search($screenWidth . "×" . $screenHeight . "@" . $this->fingerprintData['devicePixelRatio'], $this->unqiePixels) === false) {
|
|||
|
|
if (array_search($screenWidth . "×" . $screenHeight, $this->unqiePixels) === false) {
|
|||
|
|
$this->riskLevel += 10;
|
|||
|
|
$this->riskFactors[] = "非常见分辨率";
|
|||
|
|
}
|
|||
|
|
}
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
// 检测设备像素比异常
|
|||
|
|
if (isset($this->fingerprintData['devicePixelRatio']) &&
|
|||
|
|
($this->fingerprintData['devicePixelRatio'] < 0.5 || $this->fingerprintData['devicePixelRatio'] > 5)) {
|
|||
|
|
$this->riskLevel += 20;
|
|||
|
|
$this->riskFactors[] = "异常设备像素比: " . $this->fingerprintData['devicePixelRatio'];
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
// 初始状态下,视口不重合或有缩放,几乎不可能是机器人
|
|||
|
|
if (isset($this->fingerprintData['scale'], $this->fingerprintData['visualViewportOffset']) &&
|
|||
|
|
is_array($this->fingerprintData['visualViewportOffset']) &&
|
|||
|
|
($this->fingerprintData['scale'] != 1 || $this->fingerprintData['visualViewportOffset'][0] != 0)) {
|
|||
|
|
$this->riskLevel -= 10;
|
|||
|
|
$this->riskFactors[] = '初始状态下,视口不重合或有缩放,几乎不可能是机器人';
|
|||
|
|
}
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
// 检查浏览器特性支持是否合理
|
|||
|
|
private function checkBrowserFeatures() {
|
|||
|
|
$deviceType = $this->getDeviceType();
|
|||
|
|
|
|||
|
|
// 检查WebGL和WebGPU支持是否匹配现代浏览器
|
|||
|
|
$webglSupported = !empty ($this->fingerprintData['webgl']);
|
|||
|
|
$isModernBrowser = preg_match('/chrome\/(80|8[1-9]|9[0-9]|1[0-9]{2})|firefox\/(75|7[6-9]|8[0-9]|9[0-9])/i', $this->fingerprintData['userAgent'] ?? '');
|
|||
|
|
|
|||
|
|
if ($isModernBrowser && !$webglSupported) {
|
|||
|
|
$this->riskLevel += 10;
|
|||
|
|
$this->riskFactors[] = "现代浏览器但不支持WebGL,可能是受限环境";
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
// 检查WebRTC支持
|
|||
|
|
if (empty($this->fingerprintData['experimental'][0]['isWebRTCSupported'])) {
|
|||
|
|
$this->riskLevel += 10;
|
|||
|
|
$this->riskFactors[] = "不支持WebRTC,可能是自动化环境";
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
// 检查WebGPU支持
|
|||
|
|
$webGPUSupported = !empty ($this->fingerprintData['webGPU']);
|
|||
|
|
if ($webGPUSupported) {
|
|||
|
|
$webGPU = $this->fingerprintData['webGpuData'];
|
|||
|
|
if ($webGPU['adapterInfo'] && isset($webGPU['adapterInfo']['description']) && strpos($webGPU['adapterInfo']['description'], 'SwiftShader') !== false) {
|
|||
|
|
$this->riskLevel += 50; // 强信号:软件渲染
|
|||
|
|
$this->riskFactors[] = '软件渲染';
|
|||
|
|
} else if ($webGPU['adapterInfo'] && isset($webGPU['adapterInfo']['description']) && preg_match('/(VMware|VirtualBox|Parallels)/i', $webGPU['adapterInfo']['description'])) {
|
|||
|
|
$this->riskLevel += 30; // 强信号:虚拟机
|
|||
|
|
$this->riskFactors[] = '虚拟机';
|
|||
|
|
}
|
|||
|
|
// 基准测试时间过长 (例如超过500ms,具体阈值需要测试确定)
|
|||
|
|
if ($webGPU['benchmark']['durationMs'] > 500) {
|
|||
|
|
$this->riskLevel += 50;
|
|||
|
|
$this->riskFactors[] = '渲染过慢';
|
|||
|
|
}
|
|||
|
|
} else {
|
|||
|
|
if( $deviceType == 'desktop' ) {
|
|||
|
|
$this->riskLevel += 15;
|
|||
|
|
$this->riskFactors[] = 'PC不支持WEBGPU';
|
|||
|
|
}
|
|||
|
|
}
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
// 检查语言异常
|
|||
|
|
private function checkLanguages() {
|
|||
|
|
if (empty($this->fingerprintData['languages']) || empty($this->fingerprintData['language'])) return;
|
|||
|
|
|
|||
|
|
// 检测国家与时区是否匹配
|
|||
|
|
$supportLangs = $this->getSupportLang($this->fingerprintData['ciso']);
|
|||
|
|
$intersect = !empty(array_intersect($supportLangs, array_map('strtolower', $this->fingerprintData['languages'])));
|
|||
|
|
|
|||
|
|
if( ! $intersect ) {
|
|||
|
|
$this->riskLevel += 10;
|
|||
|
|
$this->riskFactors[] = "语言不属于IP所在国家: " . join('; ', $supportLangs);
|
|||
|
|
}
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
// 检查时区异常
|
|||
|
|
private function checkTimezoneAnomalies() {
|
|||
|
|
if (empty($this->fingerprintData['timezone']) || empty($this->fingerprintData['timezoneOffset'])) return;
|
|||
|
|
|
|||
|
|
// 根据时区偏移计算预期时区
|
|||
|
|
$offsetHours = $this->fingerprintData['timezoneOffset'] / 60 * -1;
|
|||
|
|
$expectedTimezones = $this->getTimezonesByOffset($offsetHours);
|
|||
|
|
|
|||
|
|
// 检查报告的时区是否与偏移量匹配
|
|||
|
|
$found = false;
|
|||
|
|
foreach ($expectedTimezones as $tz) {
|
|||
|
|
if (stripos($this->fingerprintData['timezone'], $tz) !== false) {
|
|||
|
|
$found = true;
|
|||
|
|
break;
|
|||
|
|
}
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
if (!$found) {
|
|||
|
|
$this->riskLevel += 5;
|
|||
|
|
$this->riskFactors[] = "时区与偏移量不匹配,可能是伪造的环境";
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
// 检测时区异常
|
|||
|
|
$timezoneOffset = abs($this->fingerprintData['timezoneOffset']);
|
|||
|
|
if ($timezoneOffset % 60 !== 0 && $timezoneOffset % 60 !== 30 && $timezoneOffset % 60 !== 45) {
|
|||
|
|
$this->riskLevel += 10;
|
|||
|
|
$this->riskFactors[] = "异常时区偏移: " . $timezoneOffset . "分钟";
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
// 检测国家与时区是否匹配
|
|||
|
|
$supportTZ = $this->getSupportTZ($this->fingerprintData['ciso']);
|
|||
|
|
|
|||
|
|
if( ! in_array(strtolower($this->fingerprintData['timezone']), $supportTZ) ) {
|
|||
|
|
$this->riskLevel += 10;
|
|||
|
|
$this->riskFactors[] = "时区不属于IP所在国家: " . join('; ', $supportTZ);
|
|||
|
|
}
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
// 根据时区偏移获取可能的时区
|
|||
|
|
private function getTimezonesByOffset($offset) {
|
|||
|
|
$timezones = [
|
|||
|
|
'-12' => ['Pacific/Kwajalein'],
|
|||
|
|
'-11' => ['Pacific/Midway', 'Pacific/Niue', 'Pacific/Pago_Pago'],
|
|||
|
|
'-10' => ['Pacific/Honolulu', 'Pacific/Rarotonga', 'Pacific/Tahiti'],
|
|||
|
|
'-9' => ['America/Anchorage', 'Pacific/Gambier'],
|
|||
|
|
'-8.5' => ['Pacific/Marquesas'],
|
|||
|
|
'-8' => ['America/Vancouver', 'America/Tijuana', 'America/Los_Angeles', 'Pacific/Pitcairn', 'America/Whitehorse'],
|
|||
|
|
'-7' => [ 'America/Dawson_Creek', 'America/Denver', 'America/Phoenix', 'America/Edmonton', 'America/Hermosillo', 'America/Mazatlan', 'America/Yellowknife'],
|
|||
|
|
'-6' => ['America/Chicago', 'America/Winnipeg', 'America/Belize', 'America/Mexico_City', 'America/Chihuahua', 'America/Costa_Rica', 'America/Regina', 'America/El_Salvador', 'America/Tegucigalpa', 'America/Managua', 'America/Guatemala', 'Pacific/Galapagos'],
|
|||
|
|
'-5' => ['America/New_York', 'America/Bogota', 'America/Nassau', 'America/Lima', 'America/Jamaica', 'America/Iqaluit', 'America/Havana', 'America/Guayaquil', 'America/Cayman', 'America/Cancun', 'Pacific/Easter', 'America/Toronto', 'America/Rio_Branco', 'America/Port-au-Prince', 'America/Panama'],
|
|||
|
|
'-4' => ['America/Curacao','America/Martinique','America/Manaus','America/La_Paz','America/Halifax','America/Guyana','America/Grand_Turk','America/Port_of_Spain','America/Santo_Domingo','America/Thule','America/Boa_Vista','America/Barbados','Atlantic/Bermuda','America/Puerto_Rico','America/Porto_Velho'],
|
|||
|
|
'-3.5' => ['America/Caracas'],
|
|||
|
|
'-3' => ['Atlantic/Azores', 'Antarctica/Palmer', 'America/Cuiaba', 'America/Santiago', 'America/Recife', 'America/Paramaribo', 'America/Montevideo', 'America/Miquelon', 'America/Maceio', 'America/Godthab', 'America/Fortaleza', 'America/Cayenne', 'America/Belem', 'America/Bahia', 'America/Argentina/Buenos_Aires', 'America/Araguaina', 'Atlantic/Stanley', 'America/Asuncion', 'America/Campo_Grande', 'Antarctica/Rothera'],
|
|||
|
|
'-2' => ['America/Sao_Paulo', 'Atlantic/South_Georgia', 'America/St_Johns', 'America/Noronha'],
|
|||
|
|
'-1' => ['Atlantic/Cape_Verde', 'Atlantic/Azores', 'America/Scoresbysund'],
|
|||
|
|
'0' => ['UTC', 'Europe/London', 'Africa/Bissau', 'Etc/Greenwich', 'Atlantic/Reykjavik', 'Atlantic/Faroe', 'Atlantic/Canary', 'America/Danmarkshavn', 'Africa/Monrovia', 'Africa/El_Aaiun', 'Africa/Casablanca', 'Europe/Dublin', 'Africa/Accra', 'Africa/Abidjan', 'Europe/Lisbon'],
|
|||
|
|
'1' => ['Europe/Paris', 'Africa/Lagos', 'Europe/Prague', 'Europe/Rome', 'Europe/Stockholm', 'Africa/Algiers', 'Europe/Vienna', 'Europe/Warsaw', 'Europe/Zurich', 'Africa/Ceuta', 'Africa/Ndjamena', 'Africa/Tunis', 'Europe/Tirane', 'Europe/Amsterdam', 'Europe/Andorra', 'Europe/Belgrade', 'Europe/Berlin', 'Europe/Brussels', 'Europe/Budapest', 'Europe/Copenhagen', 'Europe/Gibraltar', 'Europe/Luxembourg', 'Europe/Madrid', 'Europe/Malta', 'Europe/Monaco', 'Europe/Oslo'],
|
|||
|
|
'2' => ['Europe/Istanbul', 'Africa/Cairo', 'Asia/Jerusalem', 'Europe/Vilnius', 'Europe/Tallinn', 'Africa/Johannesburg', 'Africa/Maputo', 'Africa/Tripoli', 'Asia/Amman', 'Asia/Beirut', 'Asia/Damascus', 'Asia/Gaza', 'Africa/Windhoek', 'Asia/Nicosia', 'Europe/Athens', 'Europe/Bucharest', 'Europe/Chisinau', 'Europe/Helsinki', 'Europe/Kaliningrad', 'Europe/Kiev', 'Europe/Riga', 'Europe/Sofia'],
|
|||
|
|
'3' => ['Europe/Moscow', 'Asia/Riyadh', 'Europe/Minsk', 'Asia/Qatar', 'Asia/Baghdad', 'Antarctica/Syowa', 'Africa/Nairobi', 'Africa/Khartoum'],
|
|||
|
|
'3.5' => ['Asia/Tehran'],
|
|||
|
|
'4' => ['Asia/Dubai', 'Asia/Baku', 'Indian/Mauritius', 'Indian/Mahe', 'Europe/Samara', 'Indian/Reunion', 'Asia/Tbilisi', 'Asia/Yerevan'],
|
|||
|
|
'4.5' => ['Asia/Kabul'],
|
|||
|
|
'5'=> ['Asia/Karachi', 'Asia/Tashkent', 'Indian/Kerguelen', 'Asia/Yekaterinburg', 'Asia/Dushanbe', 'Asia/Ashgabat', 'Asia/Aqtobe', 'Asia/Aqtau', 'Indian/Maldives', 'Antarctica/Mawson'],
|
|||
|
|
'5.5' => ['Asia/Calcutta', 'Asia/Colombo', 'Asia/Kolkata'],
|
|||
|
|
'5.75' => ['Asia/Kathmandu'],
|
|||
|
|
'6' => ['Indian/Chagos', 'Asia/Thimphu', 'Asia/Dhaka', 'Asia/Bishkek', 'Asia/Almaty', 'Antarctica/Vostok', 'Asia/Omsk'],
|
|||
|
|
'6.5' => ['Asia/Rangoon', 'Indian/Cocos'],
|
|||
|
|
'7' => ['Asia/Bangkok', 'Indian/Christmas', 'Asia/Saigon', 'Asia/Krasnoyarsk', 'Asia/Jakarta', 'Asia/Hovd', 'Antarctica/Davis'],
|
|||
|
|
'8' => ['Asia/Choibalsan', 'Asia/Taipei', 'Asia/Singapore', 'Australia/Perth', 'Asia/Manila', 'Asia/Makassar', 'Asia/Macau', 'Asia/Kuala_Lumpur', 'Asia/Irkutsk', 'Asia/Hong_Kong', 'Asia/Ulaanbaatar', 'Asia/Brunei', 'Antarctica/Casey', 'Asia/Shanghai', 'Asia/Beijing'],
|
|||
|
|
'8.5' => ['Asia/Pyongyang'],
|
|||
|
|
'9' => ['Asia/Yakutsk', 'Asia/Tokyo', 'Asia/Seoul', 'Asia/Jayapura', 'Asia/Dili', 'Pacific/Palau'],
|
|||
|
|
'9.5' => ['Australia/Darwin'],
|
|||
|
|
'10' => ['Pacific/Guam', 'Pacific/Chuuk', 'Asia/Vladivostok', 'Asia/Magadan', 'Antarctica/DumontDUrville', 'Pacific/Port_Moresby', 'Australia/Brisbane'],
|
|||
|
|
'10.5' => ['Australia/Adelaide'],
|
|||
|
|
'11' => ['Australia/Sydney', 'Asia/Magadan', 'Pacific/Noumea', 'Pacific/Kosrae', 'Pacific/Guadalcanal', 'Pacific/Efate', 'Pacific/Pohnpei', 'Australia/Hobart', 'Pacific/Norfolk'],
|
|||
|
|
'12' => ['Pacific/Funafuti', 'Pacific/Nauru', 'Pacific/Tarawa', 'Pacific/Wake', 'Asia/Kamchatka', 'Pacific/Kwajalein', 'Pacific/Majuro', 'Pacific/Wallis'],
|
|||
|
|
'13' => ['Pacific/Enderbury', 'Pacific/Tongatapu', 'Pacific/Fakaofo', 'Pacific/Auckland', 'Pacific/Fiji']
|
|||
|
|
];
|
|||
|
|
|
|||
|
|
return isset($timezones["$offset"]) ? $timezones["$offset"] : [];
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
// 根据国家获取一般设置的语言
|
|||
|
|
private function getSupportLang($country_code) {
|
|||
|
|
$browserLanguages = [
|
|||
|
|
"AF" => ["ps-af", "fa-af", "uz-af"], // 阿富汗: 普什图语, 达里语, 乌兹别克语
|
|||
|
|
"AL" => ["sq-al", "sq"], // 阿尔巴尼亚: 阿尔巴尼亚语
|
|||
|
|
"DZ" => ["ar-dz", "fr-dz", "ar", "fr"], // 阿尔及利亚: 阿拉伯语, 法语
|
|||
|
|
"AD" => ["ca-ad", "ca"], // 安道尔: 加泰罗尼亚语
|
|||
|
|
"AO" => ["pt-ao", "pt"], // 安哥拉: 葡萄牙语
|
|||
|
|
"AR" => ["es-ar", "es"], // 阿根廷: 西班牙语
|
|||
|
|
"AM" => ["hy-am", "hy"], // 亚美尼亚: 亚美尼亚语
|
|||
|
|
"AU" => ["en-au", "en"], // 澳大利亚: 英语
|
|||
|
|
"AT" => ["de-at", "de"], // 奥地利: 德语
|
|||
|
|
"AZ" => ["az-latn-az", "az"], // 阿塞拜疆: 阿塞拜疆语 (拉丁文)
|
|||
|
|
"BH" => ["ar-bh", "ar"], // 巴林: 阿拉伯语
|
|||
|
|
"BD" => ["bn-bd", "bn"], // 孟加拉国: 孟加拉语
|
|||
|
|
"BY" => ["be-by", "ru-by", "be", "ru"], // 白俄罗斯: 白俄罗斯语, 俄语
|
|||
|
|
"BE" => ["nl-be", "fr-be", "de-be", "nl", "fr", "de"], // 比利时: 荷兰语, 法语, 德语
|
|||
|
|
"BZ" => ["en-bz", "es-bz", "en", "es"], // 伯利兹: 英语, 西班牙语
|
|||
|
|
"BO" => ["es-bo", "qu-bo", "ay-bo", "es", "qu", "ay"], // 玻利维亚: 西班牙语, 盖丘亚语, 艾马拉语
|
|||
|
|
"BA" => ["bs-ba", "hr-ba", "sr-ba", "bs", "hr", "sr"], // 波斯尼亚和黑塞哥维那: 波斯尼亚语, 克罗地亚语, 塞尔维亚语
|
|||
|
|
"BR" => ["pt-br", "pt"], // 巴西: 葡萄牙语
|
|||
|
|
"BN" => ["ms-bn", "ms"], // 文莱: 马来语
|
|||
|
|
"BG" => ["bg-bg", "bg"], // 保加利亚: 保加利亚语
|
|||
|
|
"KH" => ["km-kh", "km"], // 柬埔寨: 高棉语
|
|||
|
|
"CA" => ["en-ca", "fr-ca", "en", "fr"], // 加拿大: 英语, 法语
|
|||
|
|
"CL" => ["es-cl", "es"], // 智利: 西班牙语
|
|||
|
|
"CN" => ["zh-cn", "zh"], // 中国大陆: 简体中文
|
|||
|
|
"CO" => ["es-co", "es"], // 哥伦比亚: 西班牙语
|
|||
|
|
"CR" => ["es-cr", "es"], // 哥斯达黎加: 西班牙语
|
|||
|
|
"HR" => ["hr-hr", "hr"], // 克罗地亚: 克罗地亚语
|
|||
|
|
"CU" => ["es-cu", "es"], // 古巴: 西班牙语
|
|||
|
|
"CY" => ["el-cy", "tr-cy", "el", "tr"], // 塞浦路斯: 希腊语, 土耳其语
|
|||
|
|
"CZ" => ["cs-cz", "cs"], // 捷克共和国: 捷克语
|
|||
|
|
"DK" => ["da-dk", "da"], // 丹麦: 丹麦语
|
|||
|
|
"DO" => ["es-do", "es"], // 多米尼加共和国: 西班牙语
|
|||
|
|
"EC" => ["es-ec", "es"], // 厄瓜多尔: 西班牙语
|
|||
|
|
"EG" => ["ar-eg", "ar"], // 埃及: 阿拉伯语
|
|||
|
|
"SV" => ["es-sv", "es"], // 萨尔瓦多: 西班牙语
|
|||
|
|
"EE" => ["et-ee", "et"], // 爱沙尼亚: 爱沙尼亚语
|
|||
|
|
"ET" => ["am-et", "om-et", "ti-et", "am", "om", "ti"], // 埃塞俄比亚: 阿姆哈拉语, 奥罗莫语, 提格里尼亚语
|
|||
|
|
"FI" => ["fi-fi", "sv-fi", "fi", "sv"], // 芬兰: 芬兰语, 瑞典语
|
|||
|
|
"FR" => ["fr-fr", "fr"], // 法国: 法语
|
|||
|
|
"GE" => ["ka-ge", "ka"], // 格鲁吉亚: 格鲁吉亚语
|
|||
|
|
"DE" => ["de-de", "de"], // 德国: 德语
|
|||
|
|
"GR" => ["el-gr", "el"], // 希腊: 希腊语
|
|||
|
|
"GT" => ["es-gt", "es"], // 危地马拉: 西班牙语
|
|||
|
|
"HN" => ["es-hn", "es"], // 洪都拉斯: 西班牙语
|
|||
|
|
"HK" => ["zh-hk", "en-hk", "zh-tw", "zh", "en"], // 香港: 繁体中文, 英语
|
|||
|
|
"HU" => ["hu-hu", "hu"], // 匈牙利: 匈牙利语
|
|||
|
|
"IS" => ["is-is", "is"], // 冰岛: 冰岛语
|
|||
|
|
"IN" => ["hi-in", "en-in", "ta-in", "te-in", "bn-in", "hi", "en", "ta", "te", "bn"], // 印度: 印地语, 英语, 泰米尔语, 泰卢固语, 孟加拉语
|
|||
|
|
"ID" => ["id-id", "id"], // 印度尼西亚: 印度尼西亚语
|
|||
|
|
"IR" => ["fa-ir", "fa"], // 伊朗: 波斯语
|
|||
|
|
"IQ" => ["ar-iq", "ku-iq", "ar", "ku"], // 伊拉克: 阿拉伯语, 库尔德语
|
|||
|
|
"IE" => ["en-ie", "ga-ie", "en", "ga"], // 爱尔兰: 英语, 爱尔兰语
|
|||
|
|
"IL" => ["he-il", "ar-il", "en-il", "he", "ar", "en"], // 以色列: 希伯来语, 阿拉伯语, 英语
|
|||
|
|
"IT" => ["it-it", "de-it", "it", "de"], // 意大利: 意大利语, 德语
|
|||
|
|
"JM" => ["en-jm", "en"], // 牙买加: 英语
|
|||
|
|
"JP" => ["ja-jp", "ja"], // 日本: 日语
|
|||
|
|
"JO" => ["ar-jo", "ar"], // 约旦: 阿拉伯语
|
|||
|
|
"KZ" => ["kk-kz", "ru-kz", "kk", "ru"], // 哈萨克斯坦: 哈萨克语, 俄语
|
|||
|
|
"KE" => ["sw-ke", "en-ke", "sw", "en"], // 肯尼亚: 斯瓦希里语, 英语
|
|||
|
|
"KR" => ["ko-kr", "ko"], // 韩国: 韩语
|
|||
|
|
"KW" => ["ar-kw", "ar"], // 科威特: 阿拉伯语
|
|||
|
|
"LA" => ["lo-la", "lo"], // 老挝: 老挝语
|
|||
|
|
"LV" => ["lv-lv", "lv"], // 拉脱维亚: 拉脱维亚语
|
|||
|
|
"LB" => ["ar-lb", "fr-lb", "ar", "fr"], // 黎巴嫩: 阿拉伯语, 法语
|
|||
|
|
"LI" => ["de-li", "de"], // 列支敦士登: 德语
|
|||
|
|
"LT" => ["lt-lt", "lt"], // 立陶宛: 立陶宛语
|
|||
|
|
"LU" => ["fr-lu", "de-lu", "lb-lu", "fr", "de", "lb"], // 卢森堡: 法语, 德语, 卢森堡语
|
|||
|
|
"MO" => ["zh-mo", "pt-mo", "zh-hk", "zh", "pt"], // 澳门: 中文, 葡萄牙语
|
|||
|
|
"MY" => ["ms-my", "en-my", "zh-my", "ms", "en", "zh"], // 马来西亚: 马来语, 英语, 中文
|
|||
|
|
"MX" => ["es-mx", "es"], // 墨西哥: 西班牙语
|
|||
|
|
"MD" => ["ro-md", "ru-md", "ro", "ru"], // 摩尔多瓦: 罗马尼亚语, 俄语
|
|||
|
|
"MC" => ["fr-mc", "it-mc", "fr", "it"], // 摩纳哥: 法语, 意大利语
|
|||
|
|
"MN" => ["mn-mn", "mn"], // 蒙古: 蒙古语
|
|||
|
|
"MA" => ["ar-ma", "fr-ma", "ar", "fr"], // 摩洛哥: 阿拉伯语, 法语
|
|||
|
|
"NL" => ["nl-nl", "fy-nl", "nl", "fy"], // 荷兰: 荷兰语, 弗里斯语
|
|||
|
|
"NZ" => ["en-nz", "mi-nz", "en", "mi"], // 新西兰: 英语, 毛利语
|
|||
|
|
"NI" => ["es-ni", "es"], // 尼加拉瓜: 西班牙语
|
|||
|
|
"NG" => ["en-ng", "ha-ng", "ig-ng", "yo-ng", "en", "ha", "ig", "yo"], // 尼日利亚: 英语, 豪萨语, 伊博语, 约鲁巴语
|
|||
|
|
"NO" => ["nb-no", "nn-no", "se-no", "nb", "nn", "se"], // 挪威: 书面挪威语, 新挪威语, 萨米语
|
|||
|
|
"OM" => ["ar-om", "ar"], // 阿曼: 阿拉伯语
|
|||
|
|
"PK" => ["ur-pk", "en-pk", "pa-pk", "ur", "en", "pa"], // 巴基斯坦: 乌尔都语, 英语, 旁遮普语
|
|||
|
|
"PA" => ["es-pa", "es"], // 巴拿马: 西班牙语
|
|||
|
|
"PY" => ["es-py", "gn-py", "es", "gn"], // 巴拉圭: 西班牙语, 瓜拉尼语
|
|||
|
|
"PE" => ["es-pe", "qu-pe", "ay-pe", "es", "qu", "ay"], // 秘鲁: 西班牙语, 盖丘亚语, 艾马拉语
|
|||
|
|
"PH" => ["en-ph", "fil-ph", "en", "fil"], // 菲律宾: 英语, 菲律宾语
|
|||
|
|
"PL" => ["pl-pl", "pl"], // 波兰: 波兰语
|
|||
|
|
"PT" => ["pt-pt", "pt"], // 葡萄牙: 葡萄牙语
|
|||
|
|
"PR" => ["es-pr", "en-pr", "es", "en"], // 波多黎各: 西班牙语, 英语
|
|||
|
|
"QA" => ["ar-qa", "ar"], // 卡塔尔: 阿拉伯语
|
|||
|
|
"RO" => ["ro-ro", "ro"], // 罗马尼亚: 罗马尼亚语
|
|||
|
|
"RU" => ["ru-ru", "tt-ru", "ru", "tt"], // 俄罗斯: 俄语, 鞑靼语
|
|||
|
|
"SA" => ["ar-sa", "ar"], // 沙特阿拉伯: 阿拉伯语
|
|||
|
|
"RS" => ["sr-cyrl-rs", "sr-latn-rs", "sr"], // 塞尔维亚: 塞尔维亚语 (西里尔文, 拉丁文)
|
|||
|
|
"SG" => ["en-sg", "zh-sg", "ms-sg", "ta-sg", "en", "zh", "ms", "ta"], // 新加坡: 英语, 中文, 马来语, 泰米尔语
|
|||
|
|
"SK" => ["sk-sk", "sk"], // 斯洛伐克: 斯洛伐克语
|
|||
|
|
"SI" => ["sl-si", "sl"], // 斯洛文尼亚: 斯洛文尼亚语
|
|||
|
|
"ZA" => ["en-za", "af-za", "zu-za", "xh-za", "en", "af", "zu", "xh"], // 南非: 英语, 南非荷兰语, 祖鲁语, 科萨语
|
|||
|
|
"ES" => ["es-es", "ca-es", "gl-es", "eu-es", "es", "ca", "gl", "eu"], // 西班牙: 西班牙语, 加泰罗尼亚语, 加利西亚语, 巴斯克语
|
|||
|
|
"LK" => ["si-lk", "ta-lk", "si", "ta"], // 斯里兰卡: 僧伽罗语, 泰米尔语
|
|||
|
|
"SE" => ["sv-se", "sv"], // 瑞典: 瑞典语
|
|||
|
|
"CH" => ["de-ch", "fr-ch", "it-ch", "rm-ch", "de", "fr", "it", "rm"], // 瑞士: 德语, 法语, 意大利语, 罗曼什语
|
|||
|
|
"SY" => ["ar-sy", "ar"], // 叙利亚: 阿拉伯语
|
|||
|
|
"TW" => ["zh-tw", "zh"], // 台湾: 繁体中文
|
|||
|
|
"TH" => ["th-th", "th"], // 泰国: 泰语
|
|||
|
|
"TT" => ["en-tt", "en"], // 特立尼达和多巴哥: 英语
|
|||
|
|
"TN" => ["ar-tn", "fr-tn", "ar", "fr"], // 突尼斯: 阿拉伯语, 法语
|
|||
|
|
"TR" => ["tr-tr", "tr"], // 土耳其: 土耳其语
|
|||
|
|
"UA" => ["uk-ua", "ru-ua", "uk", "ru"], // 乌克兰: 乌克兰语, 俄语
|
|||
|
|
"AE" => ["ar-ae", "en-ae", "ar", "en"], // 阿拉伯联合酋长国: 阿拉伯语, 英语
|
|||
|
|
"GB" => ["en-gb", "cy-gb", "gd-gb", "en", "cy", "gd"], // 英国: 英语, 威尔士语, 苏格兰盖尔语
|
|||
|
|
"US" => ["en-us", "es-us", "en", "es"], // 美国: 英语, 西班牙语
|
|||
|
|
"UY" => ["es-uy", "es"], // 乌拉圭: 西班牙语
|
|||
|
|
"UZ" => ["uz-latn-uz", "ru-uz", "uz", "ru"], // 乌兹别克斯坦: 乌兹别克语 (拉丁文), 俄语
|
|||
|
|
"VE" => ["es-ve", "es"], // 委内瑞拉: 西班牙语
|
|||
|
|
"VN" => ["vi-vn", "vi"], // 越南: 越南语
|
|||
|
|
"YE" => ["ar-ye", "ar"], // 也门: 阿拉伯语
|
|||
|
|
"ZW" => ["en-zw", "sn-zw", "nd-zw", "en", "sn", "nd"], // 津巴布韦: 英语, 修纳语, 恩德贝勒语
|
|||
|
|
];
|
|||
|
|
|
|||
|
|
return isset($browserLanguages[$country_code]) ? $browserLanguages[$country_code] : [];
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
// 根据国家获取一般设置的时区
|
|||
|
|
private function getSupportTZ($country_code) {
|
|||
|
|
$countryTimezoneMap = [
|
|||
|
|
'AD' => ['Europe/Andorra'],
|
|||
|
|
'AE' => ['Asia/Dubai'],
|
|||
|
|
'AF' => ['Asia/Kabul'],
|
|||
|
|
'AG' => ['America/Antigua'],
|
|||
|
|
'AI' => ['America/Anguilla'],
|
|||
|
|
'AL' => ['Europe/Tirane'],
|
|||
|
|
'AM' => ['Asia/Yerevan'],
|
|||
|
|
'AO' => ['Africa/Luanda'],
|
|||
|
|
'AQ' => ['Antarctica/Casey', 'Antarctica/Davis', 'Antarctica/DumontDUrville', 'Antarctica/Macquarie', 'Antarctica/Mawson', 'Antarctica/McMurdo', 'Antarctica/Palmer', 'Antarctica/Rothera', 'Antarctica/South_Pole', 'Antarctica/Syowa', 'Antarctica/Troll', 'Antarctica/Vostok'],
|
|||
|
|
'AR' => ['America/Argentina/Buenos_Aires', 'America/Argentina/Catamarca', 'America/Argentina/ComodRivadavia', 'America/Argentina/Cordoba', 'America/Argentina/Jujuy', 'America/Argentina/La_Rioja', 'America/Argentina/Mendoza', 'America/Argentina/Rio_Gallegos', 'America/Argentina/Salta', 'America/Argentina/San_Juan', 'America/Argentina/San_Luis', 'America/Argentina/Tucuman', 'America/Argentina/Ushuaia', 'America/Buenos_Aires', 'America/Catamarca', 'America/Cordoba', 'America/Jujuy', 'America/Mendoza', 'America/Rosario'],
|
|||
|
|
'AS' => ['Pacific/Pago_Pago', 'Pacific/Samoa', 'US/Samoa'],
|
|||
|
|
'AT' => ['Europe/Vienna'],
|
|||
|
|
'AU' => ['Australia/ACT', 'Australia/Adelaide', 'Australia/Brisbane', 'Australia/Broken_Hill', 'Australia/Canberra', 'Australia/Currie', 'Australia/Darwin', 'Australia/Eucla', 'Australia/Hobart', 'Australia/LHI', 'Australia/Lindeman', 'Australia/Lord_Howe', 'Australia/Melbourne', 'Australia/NSW', 'Australia/North', 'Australia/Perth', 'Australia/Queensland', 'Australia/South', 'Australia/Sydney', 'Australia/Tasmania', 'Australia/Victoria', 'Australia/West', 'Australia/Yancowinna'],
|
|||
|
|
'AW' => ['America/Aruba'],
|
|||
|
|
'AX' => ['Europe/Mariehamn'],
|
|||
|
|
'AZ' => ['Asia/Baku'],
|
|||
|
|
'BA' => ['Europe/Sarajevo'],
|
|||
|
|
'BB' => ['America/Barbados'],
|
|||
|
|
'BD' => ['Asia/Dacca', 'Asia/Dhaka'],
|
|||
|
|
'BE' => ['Europe/Brussels'],
|
|||
|
|
'BF' => ['Africa/Ouagadougou'],
|
|||
|
|
'BG' => ['Europe/Sofia'],
|
|||
|
|
'BH' => ['Asia/Bahrain'],
|
|||
|
|
'BI' => ['Africa/Bujumbura'],
|
|||
|
|
'BJ' => ['Africa/Porto-Novo'],
|
|||
|
|
'BL' => ['America/St_Barthelemy'],
|
|||
|
|
'BM' => ['Atlantic/Bermuda'],
|
|||
|
|
'BN' => ['Asia/Brunei'],
|
|||
|
|
'BO' => ['America/La_Paz'],
|
|||
|
|
'BQ' => ['America/Kralendijk'],
|
|||
|
|
'BR' => ['America/Araguaina', 'America/Bahia', 'America/Belem', 'America/Boa_Vista', 'America/Campo_Grande', 'America/Cuiaba', 'America/Eirunepe', 'America/Fortaleza', 'America/Maceio', 'America/Manaus', 'America/Noronha', 'America/Porto_Acre', 'America/Porto_Velho', 'America/Recife', 'America/Rio_Branco', 'America/Santarem', 'America/Sao_Paulo', 'Brazil/Acre', 'Brazil/DeNoronha', 'Brazil/East', 'Brazil/West'],
|
|||
|
|
'BS' => ['America/Nassau'],
|
|||
|
|
'BT' => ['Asia/Thimphu'],
|
|||
|
|
'BW' => ['Africa/Gaborone'],
|
|||
|
|
'BY' => ['Europe/Minsk'],
|
|||
|
|
'BZ' => ['America/Belize'],
|
|||
|
|
'CA' => ['America/Atikokan', 'America/Blanc-Sablon', 'America/Cambridge_Bay', 'America/Coral_Harbour', 'America/Creston', 'America/Dawson', 'America/Dawson_Creek', 'America/Edmonton', 'America/Fort_Nelson', 'America/Glace_Bay', 'America/Goose_Bay', 'America/Halifax', 'America/Inuvik', 'America/Iqaluit', 'America/Moncton', 'America/Montreal', 'America/Nipigon', 'America/Pangnirtung', 'America/Rainy_River', 'America/Rankin_Inlet', 'America/Regina', 'America/Resolute', 'America/St_Johns', 'America/Swift_Current', 'America/Thunder_Bay', 'America/Toronto', 'America/Vancouver', 'America/Whitehorse', 'America/Winnipeg', 'America/Yellowknife', 'Canada/Atlantic', 'Canada/Central', 'Canada/Eastern', 'Canada/Mountain', 'Canada/Newfoundland', 'Canada/Pacific', 'Canada/Saskatchewan', 'Canada/Yukon'],
|
|||
|
|
'CC' => ['Indian/Cocos'],
|
|||
|
|
'CD' => ['Africa/Kinshasa', 'Africa/Lubumbashi'],
|
|||
|
|
'CF' => ['Africa/Bangui'],
|
|||
|
|
'CG' => ['Africa/Brazzaville'],
|
|||
|
|
'CH' => ['Europe/Zurich'],
|
|||
|
|
'CI' => ['Africa/Abidjan'],
|
|||
|
|
'CK' => ['Pacific/Rarotonga'],
|
|||
|
|
'CL' => ['America/Punta_Arenas', 'America/Santiago', 'Chile/Continental', 'Chile/EasterIsland', 'Pacific/Easter'],
|
|||
|
|
'CM' => ['Africa/Douala'],
|
|||
|
|
'CN' => ['Asia/Chongqing', 'Asia/Chungking', 'Asia/Harbin', 'Asia/Kashgar', 'Asia/Shanghai', 'Asia/Urumqi', 'PRC'],
|
|||
|
|
'CO' => ['America/Bogota'],
|
|||
|
|
'CR' => ['America/Costa_Rica'],
|
|||
|
|
'CU' => ['America/Havana', 'Cuba'],
|
|||
|
|
'CV' => ['Atlantic/Cape_Verde'],
|
|||
|
|
'CW' => ['America/Curacao'],
|
|||
|
|
'CX' => ['Indian/Christmas'],
|
|||
|
|
'CY' => ['Asia/Famagusta', 'Asia/Nicosia', 'Europe/Nicosia'],
|
|||
|
|
'CZ' => ['Europe/Prague'],
|
|||
|
|
'DE' => ['Europe/Berlin', 'Europe/Busingen'],
|
|||
|
|
'DJ' => ['Africa/Djibouti'],
|
|||
|
|
'DK' => ['Europe/Copenhagen'],
|
|||
|
|
'DM' => ['America/Dominica'],
|
|||
|
|
'DO' => ['America/Santo_Domingo'],
|
|||
|
|
'DZ' => ['Africa/Algiers'],
|
|||
|
|
'EC' => ['America/Guayaquil', 'Pacific/Galapagos'],
|
|||
|
|
'EE' => ['Europe/Tallinn'],
|
|||
|
|
'EG' => ['Africa/Cairo', 'Egypt'],
|
|||
|
|
'EH' => ['Africa/El_Aaiun'],
|
|||
|
|
'ER' => ['Africa/Asmara'],
|
|||
|
|
'ES' => ['Africa/Ceuta', 'Atlantic/Canary', 'Europe/Madrid'],
|
|||
|
|
'ET' => ['Africa/Addis_Ababa'],
|
|||
|
|
'FI' => ['Europe/Helsinki'],
|
|||
|
|
'FJ' => ['Pacific/Fiji'],
|
|||
|
|
'FK' => ['Atlantic/Stanley'],
|
|||
|
|
'FM' => ['Pacific/Chuuk', 'Pacific/Kosrae', 'Pacific/Pohnpei', 'Pacific/Ponape', 'Pacific/Truk', 'Pacific/Yap'],
|
|||
|
|
'FO' => ['Atlantic/Faeroe', 'Atlantic/Faroe'],
|
|||
|
|
'FR' => ['Europe/Paris'],
|
|||
|
|
'GA' => ['Africa/Libreville'],
|
|||
|
|
'GB' => ['Europe/Belfast', 'Europe/London', 'GB', 'GB-Eire'],
|
|||
|
|
'GD' => ['America/Grenada'],
|
|||
|
|
'GE' => ['Asia/Tbilisi'],
|
|||
|
|
'GF' => ['America/Cayenne'],
|
|||
|
|
'GG' => ['Europe/Guernsey'],
|
|||
|
|
'GH' => ['Africa/Accra'],
|
|||
|
|
'GI' => ['Europe/Gibraltar'],
|
|||
|
|
'GL' => ['America/Danmarkshavn', 'America/Godthab', 'America/Nuuk', 'America/Scoresbysund', 'America/Thule'],
|
|||
|
|
'GM' => ['Africa/Banjul'],
|
|||
|
|
'GN' => ['Africa/Conakry'],
|
|||
|
|
'GP' => ['America/Guadeloupe'],
|
|||
|
|
'GQ' => ['Africa/Malabo'],
|
|||
|
|
'GR' => ['Europe/Athens'],
|
|||
|
|
'GS' => ['Atlantic/South_Georgia'],
|
|||
|
|
'GT' => ['America/Guatemala'],
|
|||
|
|
'GU' => ['Pacific/Guam'],
|
|||
|
|
'GW' => ['Africa/Bissau'],
|
|||
|
|
'GY' => ['America/Guyana'],
|
|||
|
|
'HK' => ['Asia/Hong_Kong', 'Hongkong'],
|
|||
|
|
'HN' => ['America/Tegucigalpa'],
|
|||
|
|
'HR' => ['Europe/Zagreb'],
|
|||
|
|
'HT' => ['America/Port-au-Prince'],
|
|||
|
|
'HU' => ['Europe/Budapest'],
|
|||
|
|
'ID' => ['Asia/Jakarta', 'Asia/Jayapura', 'Asia/Makassar', 'Asia/Pontianak', 'Asia/Ujung_Pandang'],
|
|||
|
|
'IE' => ['Eire', 'Europe/Dublin'],
|
|||
|
|
'IL' => ['Asia/Jerusalem', 'Asia/Tel_Aviv', 'Israel'],
|
|||
|
|
'IM' => ['Europe/Isle_of_Man'],
|
|||
|
|
'IN' => ['Asia/Calcutta', 'Asia/Kolkata'],
|
|||
|
|
'IO' => ['Indian/Chagos'],
|
|||
|
|
'IQ' => ['Asia/Baghdad'],
|
|||
|
|
'IR' => ['Asia/Tehran', 'Iran'],
|
|||
|
|
'IS' => ['Atlantic/Reykjavik', 'Iceland'],
|
|||
|
|
'IT' => ['Europe/Rome'],
|
|||
|
|
'JE' => ['Europe/Jersey'],
|
|||
|
|
'JM' => ['America/Jamaica', 'Jamaica'],
|
|||
|
|
'JO' => ['Asia/Amman'],
|
|||
|
|
'JP' => ['Asia/Tokyo', 'Japan'],
|
|||
|
|
'KE' => ['Africa/Nairobi'],
|
|||
|
|
'KG' => ['Asia/Bishkek'],
|
|||
|
|
'KH' => ['Asia/Phnom_Penh'],
|
|||
|
|
'KI' => ['Pacific/Enderbury', 'Pacific/Kanton', 'Pacific/Kiritimati', 'Pacific/Tarawa'],
|
|||
|
|
'KM' => ['Indian/Comoro'],
|
|||
|
|
'KN' => ['America/St_Kitts'],
|
|||
|
|
'KP' => ['Asia/Pyongyang'],
|
|||
|
|
'KR' => ['Asia/Seoul', 'ROK'],
|
|||
|
|
'KW' => ['Asia/Kuwait'],
|
|||
|
|
'KY' => ['America/Cayman'],
|
|||
|
|
'KZ' => ['Asia/Almaty', 'Asia/Aqtau', 'Asia/Aqtobe', 'Asia/Atyrau', 'Asia/Oral', 'Asia/Qostanay', 'Asia/Qyzylorda'],
|
|||
|
|
'LA' => ['Asia/Vientiane'],
|
|||
|
|
'LB' => ['Asia/Beirut'],
|
|||
|
|
'LC' => ['America/St_Lucia'],
|
|||
|
|
'LI' => ['Europe/Vaduz'],
|
|||
|
|
'LK' => ['Asia/Colombo'],
|
|||
|
|
'LR' => ['Africa/Monrovia'],
|
|||
|
|
'LS' => ['Africa/Maseru'],
|
|||
|
|
'LT' => ['Europe/Vilnius'],
|
|||
|
|
'LU' => ['Europe/Luxembourg'],
|
|||
|
|
'LV' => ['Europe/Riga'],
|
|||
|
|
'LY' => ['Africa/Tripoli', 'Libya'],
|
|||
|
|
'MA' => ['Africa/Casablanca'],
|
|||
|
|
'MC' => ['Europe/Monaco'],
|
|||
|
|
'MD' => ['Europe/Chisinau', 'Europe/Tiraspol'],
|
|||
|
|
'ME' => ['Europe/Podgorica'],
|
|||
|
|
'MF' => ['America/Marigot'],
|
|||
|
|
'MG' => ['Indian/Antananarivo'],
|
|||
|
|
'MH' => ['Kwajalein', 'Pacific/Kwajalein', 'Pacific/Majuro'],
|
|||
|
|
'MK' => ['Europe/Skopje'],
|
|||
|
|
'ML' => ['Africa/Bamako', 'Africa/Timbuktu'],
|
|||
|
|
'MM' => ['Asia/Rangoon', 'Asia/Yangon'],
|
|||
|
|
'MN' => ['Asia/Choibalsan', 'Asia/Hovd', 'Asia/Ulaanbaatar', 'Asia/Ulan_Bator'],
|
|||
|
|
'MO' => ['Asia/Macao', 'Asia/Macau'],
|
|||
|
|
'MP' => ['Pacific/Saipan'],
|
|||
|
|
'MQ' => ['America/Martinique'],
|
|||
|
|
'MR' => ['Africa/Nouakchott'],
|
|||
|
|
'MS' => ['America/Montserrat'],
|
|||
|
|
'MT' => ['Europe/Malta'],
|
|||
|
|
'MU' => ['Indian/Mauritius'],
|
|||
|
|
'MV' => ['Indian/Maldives'],
|
|||
|
|
'MW' => ['Africa/Blantyre'],
|
|||
|
|
'MX' => ['America/Bahia_Banderas', 'America/Cancun', 'America/Chihuahua', 'America/Ciudad_Juarez', 'America/Ensenada', 'America/Hermosillo', 'America/Matamoros', 'America/Mazatlan', 'America/Merida', 'America/Mexico_City', 'America/Monterrey', 'America/Ojinaga', 'America/Santa_Isabel', 'America/Tijuana', 'Mexico/BajaNorte', 'Mexico/BajaSur', 'Mexico/General'],
|
|||
|
|
'MY' => ['Asia/Kuala_Lumpur', 'Asia/Kuching'],
|
|||
|
|
'MZ' => ['Africa/Maputo'],
|
|||
|
|
'NA' => ['Africa/Windhoek'],
|
|||
|
|
'NC' => ['Pacific/Noumea'],
|
|||
|
|
'NE' => ['Africa/Niamey'],
|
|||
|
|
'NF' => ['Pacific/Norfolk'],
|
|||
|
|
'NG' => ['Africa/Lagos'],
|
|||
|
|
'NI' => ['America/Managua'],
|
|||
|
|
'NL' => ['Europe/Amsterdam'],
|
|||
|
|
'NO' => ['Europe/Oslo'],
|
|||
|
|
'NP' => ['Asia/Kathmandu', 'Asia/Katmandu'],
|
|||
|
|
'NR' => ['Pacific/Nauru'],
|
|||
|
|
'NU' => ['Pacific/Niue'],
|
|||
|
|
'NZ' => ['Antarctica/McMurdo', 'Antarctica/South_Pole', 'NZ', 'NZ-CHAT', 'Pacific/Auckland', 'Pacific/Chatham'],
|
|||
|
|
'OM' => ['Asia/Muscat'],
|
|||
|
|
'PA' => ['America/Panama'],
|
|||
|
|
'PE' => ['America/Lima'],
|
|||
|
|
'PF' => ['Pacific/Gambier', 'Pacific/Marquesas', 'Pacific/Tahiti'],
|
|||
|
|
'PG' => ['Pacific/Bougainville', 'Pacific/Port_Moresby'],
|
|||
|
|
'PH' => ['Asia/Manila'],
|
|||
|
|
'PK' => ['Asia/Karachi'],
|
|||
|
|
'PL' => ['Europe/Warsaw', 'Poland'],
|
|||
|
|
'PM' => ['America/Miquelon'],
|
|||
|
|
'PN' => ['Pacific/Pitcairn'],
|
|||
|
|
'PR' => ['America/Puerto_Rico'],
|
|||
|
|
'PS' => ['Asia/Gaza', 'Asia/Hebron'],
|
|||
|
|
'PT' => ['Atlantic/Azores', 'Atlantic/Madeira', 'Europe/Lisbon', 'Portugal'],
|
|||
|
|
'PW' => ['Pacific/Palau'],
|
|||
|
|
'PY' => ['America/Asuncion'],
|
|||
|
|
'QA' => ['Asia/Qatar'],
|
|||
|
|
'RE' => ['Indian/Reunion'],
|
|||
|
|
'RO' => ['Europe/Bucharest'],
|
|||
|
|
'RS' => ['Europe/Belgrade'],
|
|||
|
|
'RU' => ['Asia/Anadyr', 'Asia/Barnaul', 'Asia/Chita', 'Asia/Irkutsk', 'Asia/Kamchatka', 'Asia/Khandyga', 'Asia/Krasnoyarsk', 'Asia/Magadan', 'Asia/Novokuznetsk', 'Asia/Novosibirsk', 'Asia/Omsk', 'Asia/Sakhalin', 'Asia/Srednekolymsk', 'Asia/Tomsk', 'Asia/Ust-Nera', 'Asia/Vladivostok', 'Asia/Yakutsk', 'Asia/Yekaterinburg', 'Europe/Astrakhan', 'Europe/Kaliningrad', 'Europe/Kirov', 'Europe/Moscow', 'Europe/Samara', 'Europe/Saratov', 'Europe/Ulyanovsk', 'Europe/Volgograd', 'W-SU'],
|
|||
|
|
'RW' => ['Africa/Kigali'],
|
|||
|
|
'SA' => ['Asia/Riyadh'],
|
|||
|
|
'SB' => ['Pacific/Guadalcanal'],
|
|||
|
|
'SC' => ['Indian/Mahe'],
|
|||
|
|
'SD' => ['Africa/Khartoum'],
|
|||
|
|
'SE' => ['Europe/Stockholm'],
|
|||
|
|
'SG' => ['Asia/Singapore', 'Singapore'],
|
|||
|
|
'SH' => ['Atlantic/St_Helena'],
|
|||
|
|
'SI' => ['Europe/Ljubljana'],
|
|||
|
|
'SJ' => ['Arctic/Longyearbyen', 'Atlantic/Jan_Mayen'],
|
|||
|
|
'SK' => ['Europe/Bratislava'],
|
|||
|
|
'SL' => ['Africa/Freetown'],
|
|||
|
|
'SM' => ['Europe/San_Marino'],
|
|||
|
|
'SN' => ['Africa/Dakar'],
|
|||
|
|
'SO' => ['Africa/Mogadishu'],
|
|||
|
|
'SR' => ['America/Paramaribo'],
|
|||
|
|
'SS' => ['Africa/Juba'],
|
|||
|
|
'ST' => ['Africa/Sao_Tome'],
|
|||
|
|
'SV' => ['America/El_Salvador'],
|
|||
|
|
'SX' => ['America/Lower_Princes'],
|
|||
|
|
'SY' => ['Asia/Damascus'],
|
|||
|
|
'SZ' => ['Africa/Mbabane'],
|
|||
|
|
'TC' => ['America/Grand_Turk'],
|
|||
|
|
'TD' => ['Africa/Ndjamena'],
|
|||
|
|
'TF' => ['Indian/Kerguelen'],
|
|||
|
|
'TG' => ['Africa/Lome'],
|
|||
|
|
'TH' => ['Asia/Bangkok'],
|
|||
|
|
'TJ' => ['Asia/Dushanbe'],
|
|||
|
|
'TK' => ['Pacific/Fakaofo'],
|
|||
|
|
'TL' => ['Asia/Dili'],
|
|||
|
|
'TM' => ['Asia/Ashgabat', 'Asia/Ashkhabad'],
|
|||
|
|
'TN' => ['Africa/Tunis'],
|
|||
|
|
'TO' => ['Pacific/Tongatapu'],
|
|||
|
|
'TR' => ['Asia/Istanbul', 'Europe/Istanbul', 'Turkey'],
|
|||
|
|
'TT' => ['America/Port_of_Spain'],
|
|||
|
|
'TV' => ['Pacific/Funafuti'],
|
|||
|
|
'TW' => ['Asia/Taipei', 'ROC'],
|
|||
|
|
'TZ' => ['Africa/Dar_es_Salaam'],
|
|||
|
|
'UA' => ['Europe/Kiev', 'Europe/Kyiv', 'Europe/Simferopol', 'Europe/Uzhgorod', 'Europe/Zaporozhye'],
|
|||
|
|
'UG' => ['Africa/Kampala'],
|
|||
|
|
'UM' => ['Pacific/Johnston', 'Pacific/Midway', 'Pacific/Wake'],
|
|||
|
|
'US' => ['America/Adak', 'America/Anchorage', 'America/Atka', 'America/Boise', 'America/Chicago', 'America/Denver', 'America/Detroit', 'America/Fort_Wayne', 'America/Indiana/Indianapolis', 'America/Indiana/Knox', 'America/Indiana/Marengo', 'America/Indiana/Petersburg', 'America/Indiana/Tell_City', 'America/Indiana/Vevay', 'America/Indiana/Vincennes', 'America/Indiana/Winamac', 'America/Indianapolis', 'America/Juneau', 'America/Kentucky/Louisville', 'America/Kentucky/Monticello', 'America/Knox_IN', 'America/Los_Angeles', 'America/Louisville', 'America/Menominee', 'America/Metlakatla', 'America/New_York', 'America/Nome', 'America/North_Dakota/Beulah', 'America/North_Dakota/Center', 'America/North_Dakota/New_Salem', 'America/Phoenix', 'America/Sitka', 'America/Yakutat', 'CST6CDT', 'EST5EDT', 'Navajo', 'PST8PDT', 'US/Alaska', 'US/Aleutian', 'US/Arizona', 'US/Central', 'US/East-Indiana', 'US/Eastern', 'US/Hawaii', 'US/Indiana-Starke', 'US/Michigan', 'US/Mountain', 'US/Pacific'],
|
|||
|
|
'UY' => ['America/Montevideo'],
|
|||
|
|
'UZ' => ['Asia/Samarkand', 'Asia/Tashkent'],
|
|||
|
|
'VA' => ['Europe/Vatican'],
|
|||
|
|
'VC' => ['America/St_Vincent'],
|
|||
|
|
'VE' => ['America/Caracas'],
|
|||
|
|
'VG' => ['America/Tortola', 'America/Virgin'],
|
|||
|
|
'VI' => ['America/St_Thomas'],
|
|||
|
|
'VN' => ['Asia/Ho_Chi_Minh', 'Asia/Saigon'],
|
|||
|
|
'VU' => ['Pacific/Efate'],
|
|||
|
|
'WF' => ['Pacific/Wallis'],
|
|||
|
|
'WS' => ['Pacific/Apia'],
|
|||
|
|
'YE' => ['Asia/Aden'],
|
|||
|
|
'YT' => ['Indian/Mayotte'],
|
|||
|
|
'ZA' => ['Africa/Johannesburg'],
|
|||
|
|
'ZM' => ['Africa/Lusaka'],
|
|||
|
|
'ZW' => ['Africa/Harare'],
|
|||
|
|
];
|
|||
|
|
|
|||
|
|
return isset($countryTimezoneMap[$country_code]) ? array_map('strtolower', $countryTimezoneMap[$country_code]) : [];
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
// 判断字体
|
|||
|
|
private function checkFonts() {
|
|||
|
|
$fonts = !empty($this->fingerprintData['fonts']) ? $this->fingerprintData['fonts'] : [];
|
|||
|
|
if (empty($fonts)) {
|
|||
|
|
return;
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
$os = $this->getOsName();
|
|||
|
|
if ($os === null) {
|
|||
|
|
return;
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
if (preg_match('/Windows/i', $os)) {
|
|||
|
|
// Windows 系统应该有 Calibri 或 Segoe UI,但不应该有 San Francisco
|
|||
|
|
if (!in_array('Calibri', $fonts) && !in_array('Segoe UI', $fonts)) {
|
|||
|
|
$this->riskLevel += 20;
|
|||
|
|
$this->riskFactors[] = "SUSPICIOUS: Windows User-Agent but common Windows fonts like Calibri/Segoe UI are missing.";
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
if (in_array('San Francisco', $fonts)) {
|
|||
|
|
$this->riskLevel += 20;
|
|||
|
|
$this->riskFactors[] = "SUSPICIOUS: Windows User-Agent but macOS-specific font 'San Francisco' was detected.";
|
|||
|
|
}
|
|||
|
|
}
|
|||
|
|
elseif (preg_match('/macOS/i', $os)) {
|
|||
|
|
// macOS 系统应该有 Helvetica Neue 或 San Francisco,但不应该有 Calibri
|
|||
|
|
if (!in_array('Helvetica Neue', $fonts) && !in_array('San Francisco', $fonts)) {
|
|||
|
|
$this->riskLevel += 20;
|
|||
|
|
$this->riskFactors[] = "SUSPICIOUS: macOS User-Agent but common macOS fonts are missing.";
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
if (in_array('Calibri', $fonts)) {
|
|||
|
|
$this->riskLevel += 20;
|
|||
|
|
$this->riskFactors[] = "SUSPICIOUS: macOS User-Agent but Windows-specific font 'Calibri' was detected.";
|
|||
|
|
}
|
|||
|
|
}
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
|
|||
|
|
// 获取风险等级分类
|
|||
|
|
private function getRiskCategory() {
|
|||
|
|
if ($this->riskLevel < 20) {
|
|||
|
|
return '低风险';
|
|||
|
|
} elseif ($this->riskLevel < 50) {
|
|||
|
|
return '中等风险';
|
|||
|
|
} else {
|
|||
|
|
return '高风险';
|
|||
|
|
}
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
// 获取风险等级描述
|
|||
|
|
private function getRiskDescription() {
|
|||
|
|
switch ($this->getRiskCategory()) {
|
|||
|
|
case '低风险':
|
|||
|
|
return '看起来是正常的用户访问,未检测到明显的自动化工具特征';
|
|||
|
|
case '中等风险':
|
|||
|
|
return '检测到一些可能与自动化工具相关的特征,建议进一步观察';
|
|||
|
|
case '高风险':
|
|||
|
|
return '很可能是自动化工具、爬虫或恶意访问,建议采取限制措施';
|
|||
|
|
default:
|
|||
|
|
return '无法确定风险等级';
|
|||
|
|
}
|
|||
|
|
}
|
|||
|
|
}
|