|
|
@@ -0,0 +1,152 @@
|
|
|
+<?php
|
|
|
+
|
|
|
+namespace App\Helpers;
|
|
|
+
|
|
|
+/**
|
|
|
+ * 登录客户端信息:国家/地区、浏览器、操作系统
|
|
|
+ */
|
|
|
+class LoginClientInfo
|
|
|
+{
|
|
|
+ /**
|
|
|
+ * 解析请求中的客户端信息
|
|
|
+ */
|
|
|
+ public static function fromRequest(?string $ip = null, ?string $userAgent = null): array
|
|
|
+ {
|
|
|
+ $ip = $ip ?: (string)request()->ip();
|
|
|
+ $userAgent = $userAgent ?? (string)request()->userAgent();
|
|
|
+
|
|
|
+ return [
|
|
|
+ 'login_ip' => $ip,
|
|
|
+ 'country' => self::resolveCountry($ip),
|
|
|
+ 'browser' => self::parseBrowser($userAgent),
|
|
|
+ 'os' => self::parseOs($userAgent),
|
|
|
+ 'user_agent' => mb_substr($userAgent, 0, 500),
|
|
|
+ ];
|
|
|
+ }
|
|
|
+
|
|
|
+ /**
|
|
|
+ * 国家/地区:优先 Cloudflare 头,其次 ip-api
|
|
|
+ */
|
|
|
+ public static function resolveCountry(string $ip): string
|
|
|
+ {
|
|
|
+ if ($ip === '' || $ip === '127.0.0.1' || $ip === '::1') {
|
|
|
+ return '本地';
|
|
|
+ }
|
|
|
+
|
|
|
+ $cfCountry = (string)request()->header('CF-IPCountry', '');
|
|
|
+ if ($cfCountry !== '' && strtoupper($cfCountry) !== 'XX') {
|
|
|
+ return strtoupper($cfCountry);
|
|
|
+ }
|
|
|
+
|
|
|
+ try {
|
|
|
+ $url = 'http://ip-api.com/json/' . urlencode($ip) . '?fields=status,country,regionName,city&lang=zh-CN';
|
|
|
+ $ctx = stream_context_create([
|
|
|
+ 'http' => [
|
|
|
+ 'timeout' => 1.5,
|
|
|
+ 'method' => 'GET',
|
|
|
+ ],
|
|
|
+ ]);
|
|
|
+ $raw = @file_get_contents($url, false, $ctx);
|
|
|
+ if ($raw === false) {
|
|
|
+ return '';
|
|
|
+ }
|
|
|
+ $data = json_decode($raw, true);
|
|
|
+ if (!is_array($data) || ($data['status'] ?? '') !== 'success') {
|
|
|
+ return '';
|
|
|
+ }
|
|
|
+ $parts = array_filter([
|
|
|
+ $data['country'] ?? '',
|
|
|
+ $data['regionName'] ?? '',
|
|
|
+ $data['city'] ?? '',
|
|
|
+ ]);
|
|
|
+ return implode(' ', $parts);
|
|
|
+ } catch (\Throwable $e) {
|
|
|
+ return '';
|
|
|
+ }
|
|
|
+ }
|
|
|
+
|
|
|
+ public static function parseBrowser(string $ua): string
|
|
|
+ {
|
|
|
+ if ($ua === '') {
|
|
|
+ return '未知';
|
|
|
+ }
|
|
|
+
|
|
|
+ $map = [
|
|
|
+ 'Edg' => 'Edge',
|
|
|
+ 'Edge' => 'Edge',
|
|
|
+ 'OPR' => 'Opera',
|
|
|
+ 'Opera' => 'Opera',
|
|
|
+ 'Chrome' => 'Chrome',
|
|
|
+ 'Firefox' => 'Firefox',
|
|
|
+ 'Safari' => 'Safari',
|
|
|
+ 'MSIE' => 'IE',
|
|
|
+ 'Trident' => 'IE',
|
|
|
+ 'MicroMessenger' => '微信',
|
|
|
+ 'QQBrowser' => 'QQ浏览器',
|
|
|
+ 'UCBrowser' => 'UC浏览器',
|
|
|
+ 'Telegram' => 'Telegram',
|
|
|
+ ];
|
|
|
+
|
|
|
+ foreach ($map as $needle => $name) {
|
|
|
+ if (stripos($ua, $needle) === false) {
|
|
|
+ continue;
|
|
|
+ }
|
|
|
+ if ($needle === 'Safari' && stripos($ua, 'Chrome') !== false) {
|
|
|
+ continue;
|
|
|
+ }
|
|
|
+ if ($needle === 'Chrome' && (stripos($ua, 'Edg') !== false || stripos($ua, 'OPR') !== false)) {
|
|
|
+ continue;
|
|
|
+ }
|
|
|
+ $version = '';
|
|
|
+ if (preg_match('/' . preg_quote($needle, '/') . '[\/\s]?([\d.]+)/i', $ua, $m)) {
|
|
|
+ $version = $m[1];
|
|
|
+ } elseif ($needle === 'Trident' && preg_match('/rv:([\d.]+)/i', $ua, $m)) {
|
|
|
+ $version = $m[1];
|
|
|
+ }
|
|
|
+ return $version !== '' ? "{$name}({$version})" : $name;
|
|
|
+ }
|
|
|
+
|
|
|
+ return '未知浏览器';
|
|
|
+ }
|
|
|
+
|
|
|
+ public static function parseOs(string $ua): string
|
|
|
+ {
|
|
|
+ if ($ua === '') {
|
|
|
+ return '未知';
|
|
|
+ }
|
|
|
+
|
|
|
+ $rules = [
|
|
|
+ '/windows nt 10/i' => 'Windows 10/11',
|
|
|
+ '/windows nt 6\.3/i' => 'Windows 8.1',
|
|
|
+ '/windows nt 6\.2/i' => 'Windows 8',
|
|
|
+ '/windows nt 6\.1/i' => 'Windows 7',
|
|
|
+ '/windows nt 6\.0/i' => 'Windows Vista',
|
|
|
+ '/windows nt 5\.1|windows xp/i' => 'Windows XP',
|
|
|
+ '/windows phone/i' => 'Windows Phone',
|
|
|
+ '/android/i' => 'Android',
|
|
|
+ '/iphone|ipad|ipod/i' => 'iOS',
|
|
|
+ '/mac os x/i' => 'Mac OS',
|
|
|
+ '/macintosh/i' => 'Mac OS',
|
|
|
+ '/linux/i' => 'Linux',
|
|
|
+ '/ubuntu/i' => 'Ubuntu',
|
|
|
+ '/cros/i' => 'Chrome OS',
|
|
|
+ ];
|
|
|
+
|
|
|
+ foreach ($rules as $pattern => $name) {
|
|
|
+ if (preg_match($pattern, $ua)) {
|
|
|
+ if ($name === 'Android' && preg_match('/Android\s+([\d.]+)/i', $ua, $m)) {
|
|
|
+ return 'Android ' . $m[1];
|
|
|
+ }
|
|
|
+ if ($name === 'iOS' && preg_match('/OS\s+([\d_]+)/i', $ua, $m)) {
|
|
|
+ return 'iOS ' . str_replace('_', '.', $m[1]);
|
|
|
+ }
|
|
|
+ if ($name === 'Mac OS' && preg_match('/Mac OS X\s+([\d_]+)/i', $ua, $m)) {
|
|
|
+ return 'Mac OS ' . str_replace('_', '.', $m[1]);
|
|
|
+ }
|
|
|
+ return $name;
|
|
|
+ }
|
|
|
+ }
|
|
|
+
|
|
|
+ return '未知操作系统';
|
|
|
+ }
|
|
|
+}
|