false, 'reason' => '', 'ip' => $ip]; if (empty($ip) || $ip === '127.0.0.1' || $ip === '::1') { return $result; } // 私有 IP 范围不检测 if ($this->isPrivateIp($ip)) { return $result; } $info = $this->queryIpInfo($ip); if (empty($info)) { return $result; } $countryCode = strtoupper($info['countryCode'] ?? ''); $as = strtolower($info['as'] ?? ''); $org = strtolower($info['org'] ?? ''); $isp = strtolower($info['isp'] ?? ''); $combined = $as . ' ' . $org . ' ' . $isp; // 检测1: 云厂商 ASN/ISP 关键字 $matchedKeyword = $this->matchCloudKeyword($combined); if ($matchedKeyword) { $result['is_risky'] = true; $result['reason'] = "cloud_asn:{$matchedKeyword}"; Log::info("IpRisk: cloud ASN detected", ['ip' => $ip, 'keyword' => $matchedKeyword]); return $result; } // 检测2: 非美国 IP if ($countryCode !== '' && $countryCode !== 'US') { $result['is_risky'] = true; $result['reason'] = "non_us:{$countryCode}"; Log::info("IpRisk: non-US IP detected", ['ip' => $ip, 'country' => $countryCode]); return $result; } return $result; } /** * 调用 ip-api.com 查询 IP 信息 * * @param string $ip * @return array|null */ protected function queryIpInfo($ip) { try { $client = new Client(['timeout' => 5]); $response = $client->get("http://ip-api.com/json/{$ip}", [ 'query' => ['fields' => 'countryCode,as,org,isp'], ]); $data = json_decode($response->getBody()->getContents(), true); if (isset($data['status']) && $data['status'] === 'fail') { Log::warning("IpRisk: ip-api query failed", ['ip' => $ip, 'msg' => $data['message'] ?? '']); return null; } return $data; } catch (\Exception $e) { Log::warning("IpRisk: ip-api request error", ['ip' => $ip, 'error' => $e->getMessage()]); return null; } } /** * 检查是否命中云厂商关键字 * * @param string $text * @return string|false 命中的关键字,或 false */ protected function matchCloudKeyword($text) { foreach (self::CLOUD_KEYWORDS as $keyword) { if (strpos($text, $keyword) !== false) { return $keyword; } } return false; } /** * 判断是否为私有 IP * * @param string $ip * @return bool */ protected function isPrivateIp($ip) { return filter_var($ip, FILTER_VALIDATE_IP, FILTER_FLAG_NO_PRIV_RANGE | FILTER_FLAG_NO_RES_RANGE) === false; } }