| 123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152 |
- <?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 '未知操作系统';
- }
- }
|