LoginClientInfo.php 4.8 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152
  1. <?php
  2. namespace App\Helpers;
  3. /**
  4. * 登录客户端信息:国家/地区、浏览器、操作系统
  5. */
  6. class LoginClientInfo
  7. {
  8. /**
  9. * 解析请求中的客户端信息
  10. */
  11. public static function fromRequest(?string $ip = null, ?string $userAgent = null): array
  12. {
  13. $ip = $ip ?: (string)request()->ip();
  14. $userAgent = $userAgent ?? (string)request()->userAgent();
  15. return [
  16. 'login_ip' => $ip,
  17. 'country' => self::resolveCountry($ip),
  18. 'browser' => self::parseBrowser($userAgent),
  19. 'os' => self::parseOs($userAgent),
  20. 'user_agent' => mb_substr($userAgent, 0, 500),
  21. ];
  22. }
  23. /**
  24. * 国家/地区:优先 Cloudflare 头,其次 ip-api
  25. */
  26. public static function resolveCountry(string $ip): string
  27. {
  28. if ($ip === '' || $ip === '127.0.0.1' || $ip === '::1') {
  29. return '本地';
  30. }
  31. $cfCountry = (string)request()->header('CF-IPCountry', '');
  32. if ($cfCountry !== '' && strtoupper($cfCountry) !== 'XX') {
  33. return strtoupper($cfCountry);
  34. }
  35. try {
  36. $url = 'http://ip-api.com/json/' . urlencode($ip) . '?fields=status,country,regionName,city&lang=zh-CN';
  37. $ctx = stream_context_create([
  38. 'http' => [
  39. 'timeout' => 1.5,
  40. 'method' => 'GET',
  41. ],
  42. ]);
  43. $raw = @file_get_contents($url, false, $ctx);
  44. if ($raw === false) {
  45. return '';
  46. }
  47. $data = json_decode($raw, true);
  48. if (!is_array($data) || ($data['status'] ?? '') !== 'success') {
  49. return '';
  50. }
  51. $parts = array_filter([
  52. $data['country'] ?? '',
  53. $data['regionName'] ?? '',
  54. $data['city'] ?? '',
  55. ]);
  56. return implode(' ', $parts);
  57. } catch (\Throwable $e) {
  58. return '';
  59. }
  60. }
  61. public static function parseBrowser(string $ua): string
  62. {
  63. if ($ua === '') {
  64. return '未知';
  65. }
  66. $map = [
  67. 'Edg' => 'Edge',
  68. 'Edge' => 'Edge',
  69. 'OPR' => 'Opera',
  70. 'Opera' => 'Opera',
  71. 'Chrome' => 'Chrome',
  72. 'Firefox' => 'Firefox',
  73. 'Safari' => 'Safari',
  74. 'MSIE' => 'IE',
  75. 'Trident' => 'IE',
  76. 'MicroMessenger' => '微信',
  77. 'QQBrowser' => 'QQ浏览器',
  78. 'UCBrowser' => 'UC浏览器',
  79. 'Telegram' => 'Telegram',
  80. ];
  81. foreach ($map as $needle => $name) {
  82. if (stripos($ua, $needle) === false) {
  83. continue;
  84. }
  85. if ($needle === 'Safari' && stripos($ua, 'Chrome') !== false) {
  86. continue;
  87. }
  88. if ($needle === 'Chrome' && (stripos($ua, 'Edg') !== false || stripos($ua, 'OPR') !== false)) {
  89. continue;
  90. }
  91. $version = '';
  92. if (preg_match('/' . preg_quote($needle, '/') . '[\/\s]?([\d.]+)/i', $ua, $m)) {
  93. $version = $m[1];
  94. } elseif ($needle === 'Trident' && preg_match('/rv:([\d.]+)/i', $ua, $m)) {
  95. $version = $m[1];
  96. }
  97. return $version !== '' ? "{$name}({$version})" : $name;
  98. }
  99. return '未知浏览器';
  100. }
  101. public static function parseOs(string $ua): string
  102. {
  103. if ($ua === '') {
  104. return '未知';
  105. }
  106. $rules = [
  107. '/windows nt 10/i' => 'Windows 10/11',
  108. '/windows nt 6\.3/i' => 'Windows 8.1',
  109. '/windows nt 6\.2/i' => 'Windows 8',
  110. '/windows nt 6\.1/i' => 'Windows 7',
  111. '/windows nt 6\.0/i' => 'Windows Vista',
  112. '/windows nt 5\.1|windows xp/i' => 'Windows XP',
  113. '/windows phone/i' => 'Windows Phone',
  114. '/android/i' => 'Android',
  115. '/iphone|ipad|ipod/i' => 'iOS',
  116. '/mac os x/i' => 'Mac OS',
  117. '/macintosh/i' => 'Mac OS',
  118. '/linux/i' => 'Linux',
  119. '/ubuntu/i' => 'Ubuntu',
  120. '/cros/i' => 'Chrome OS',
  121. ];
  122. foreach ($rules as $pattern => $name) {
  123. if (preg_match($pattern, $ua)) {
  124. if ($name === 'Android' && preg_match('/Android\s+([\d.]+)/i', $ua, $m)) {
  125. return 'Android ' . $m[1];
  126. }
  127. if ($name === 'iOS' && preg_match('/OS\s+([\d_]+)/i', $ua, $m)) {
  128. return 'iOS ' . str_replace('_', '.', $m[1]);
  129. }
  130. if ($name === 'Mac OS' && preg_match('/Mac OS X\s+([\d_]+)/i', $ua, $m)) {
  131. return 'Mac OS ' . str_replace('_', '.', $m[1]);
  132. }
  133. return $name;
  134. }
  135. }
  136. return '未知操作系统';
  137. }
  138. }