IpRiskService.php 3.8 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143
  1. <?php
  2. namespace App\Services;
  3. use GuzzleHttp\Client;
  4. use Illuminate\Support\Facades\Log;
  5. /**
  6. * IP风险检测服务
  7. *
  8. * 检测逻辑:
  9. * 1. 通过 ip-api.com 获取 IP 的 ASN 和国家信息
  10. * 2. 如果 ASN/ISP 关键字包含常见云厂商 → 标记为可疑
  11. * 3. 如果 IP 不属于美国 → 标记为可疑
  12. */
  13. class IpRiskService
  14. {
  15. // 常见云厂商 ASN/ISP 关键字(小写)
  16. const CLOUD_KEYWORDS = [
  17. 'amazon',
  18. 'aws',
  19. 'google cloud',
  20. 'gcp',
  21. 'microsoft azure',
  22. 'microsoft corporation',
  23. 'digitalocean',
  24. 'linode',
  25. 'vultr',
  26. 'alibaba',
  27. 'tencent',
  28. 'oracle cloud',
  29. 'ovh',
  30. 'hetzner',
  31. 'cloudflare',
  32. 'akamai',
  33. 'fastly',
  34. ];
  35. /**
  36. * 检测 IP 是否存在风险
  37. *
  38. * @param string $ip
  39. * @return array{is_risky: bool, reason: string, ip: string}
  40. */
  41. public function detect($ip)
  42. {
  43. $result = ['is_risky' => false, 'reason' => '', 'ip' => $ip];
  44. if (empty($ip) || $ip === '127.0.0.1' || $ip === '::1') {
  45. return $result;
  46. }
  47. // 私有 IP 范围不检测
  48. if ($this->isPrivateIp($ip)) {
  49. return $result;
  50. }
  51. $info = $this->queryIpInfo($ip);
  52. if (empty($info)) {
  53. return $result;
  54. }
  55. $countryCode = strtoupper($info['countryCode'] ?? '');
  56. $as = strtolower($info['as'] ?? '');
  57. $org = strtolower($info['org'] ?? '');
  58. $isp = strtolower($info['isp'] ?? '');
  59. $combined = $as . ' ' . $org . ' ' . $isp;
  60. // 检测1: 云厂商 ASN/ISP 关键字
  61. $matchedKeyword = $this->matchCloudKeyword($combined);
  62. if ($matchedKeyword) {
  63. $result['is_risky'] = true;
  64. $result['reason'] = "cloud_asn:{$matchedKeyword}";
  65. Log::info("IpRisk: cloud ASN detected", ['ip' => $ip, 'keyword' => $matchedKeyword]);
  66. return $result;
  67. }
  68. // 检测2: 非美国 IP
  69. if ($countryCode !== '' && $countryCode !== 'US') {
  70. $result['is_risky'] = true;
  71. $result['reason'] = "non_us:{$countryCode}";
  72. Log::info("IpRisk: non-US IP detected", ['ip' => $ip, 'country' => $countryCode]);
  73. return $result;
  74. }
  75. return $result;
  76. }
  77. /**
  78. * 调用 ip-api.com 查询 IP 信息
  79. *
  80. * @param string $ip
  81. * @return array|null
  82. */
  83. protected function queryIpInfo($ip)
  84. {
  85. try {
  86. $client = new Client(['timeout' => 5]);
  87. $response = $client->get("http://ip-api.com/json/{$ip}", [
  88. 'query' => ['fields' => 'countryCode,as,org,isp'],
  89. ]);
  90. $data = json_decode($response->getBody()->getContents(), true);
  91. if (isset($data['status']) && $data['status'] === 'fail') {
  92. Log::warning("IpRisk: ip-api query failed", ['ip' => $ip, 'msg' => $data['message'] ?? '']);
  93. return null;
  94. }
  95. return $data;
  96. } catch (\Exception $e) {
  97. Log::warning("IpRisk: ip-api request error", ['ip' => $ip, 'error' => $e->getMessage()]);
  98. return null;
  99. }
  100. }
  101. /**
  102. * 检查是否命中云厂商关键字
  103. *
  104. * @param string $text
  105. * @return string|false 命中的关键字,或 false
  106. */
  107. protected function matchCloudKeyword($text)
  108. {
  109. foreach (self::CLOUD_KEYWORDS as $keyword) {
  110. if (strpos($text, $keyword) !== false) {
  111. return $keyword;
  112. }
  113. }
  114. return false;
  115. }
  116. /**
  117. * 判断是否为私有 IP
  118. *
  119. * @param string $ip
  120. * @return bool
  121. */
  122. protected function isPrivateIp($ip)
  123. {
  124. return filter_var($ip, FILTER_VALIDATE_IP, FILTER_FLAG_NO_PRIV_RANGE | FILTER_FLAG_NO_RES_RANGE) === false;
  125. }
  126. }