Files
hyperf-micro-svc/app/Common/Trait/ClientIpTrait.php
2025-09-16 18:07:10 +08:00

192 lines
6.2 KiB
PHP
Raw Permalink Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

<?php
namespace App\Common\Trait;
use App\Constants\Common\ClientIpRequestConstant;
use App\Exception\ErrException;
use Hyperf\Di\Annotation\Inject;
use Hyperf\HttpServer\Contract\RequestInterface;
use Symfony\Component\HttpFoundation\HeaderUtils;
use Symfony\Component\HttpFoundation\IpUtils;
trait ClientIpTrait
{
/**
* @var RequestInterface
*/
#[Inject]
protected RequestInterface $request;
/**
* @var string[]
*/
protected static array $trustedProxies = [];
/**
* @var int
*/
private static int $trustedHeaderSet = -1;
/**
* @var array
*/
private array $trustedValuesCache = [];
/**
* @var bool
*/
private bool $isForwardedValid = true;
/**
* @var bool
*/
private static bool $isTrustedRemoteAddr = false;
/**
* 设置可信代理
* @return bool
*/
public static function isTrustedRemoteAddr(): bool
{
return self::$isTrustedRemoteAddr;
}
/**
* 返回客户端IP地址。
* 在返回的数组中最可信的IP地址排在第一位最不可信的IP地址排在最后。“真正的”客户端IP地址是最后一个但这也是最不可信的一个。可信代理被剥离。
* @return array
*/
public function getClientIp(): array
{
$ip = $this->request->server('remote_addr');
if (! $this->isFromTrustedProxy()) return [$ip];
return $this->getTrustedValues(ClientIpRequestConstant::HEADER_X_FORWARDED_FOR, $ip) ?: [$ip];
}
/**
* 判断此请求是否来自受信任的代理。这可以用于确定是否信任
* @return bool
*/
private function isFromTrustedProxy(): bool
{
return (
self::$trustedProxies &&
IpUtils::checkIp($this->request->server('remote_addr',''),self::$trustedProxies) ||
self::isTrustedRemoteAddr()
);
}
/**
* @param int $type
* @param string|null $ip
* @return array|mixed|null[]|string[]
*/
private function getTrustedValues(int $type, ?string $ip = null): mixed
{
$cacheKey = $type . "\0" . ((self::$trustedHeaderSet & $type) ? $this->request->getHeaderLine(ClientIpRequestConstant::TRUSTED_HEADERS[$type]) : '');
$cacheKey .= "\0" . $ip . "\0" . $this->request->getHeaderLine(ClientIpRequestConstant::TRUSTED_HEADERS[ClientIpRequestConstant::HEADER_FORWARDED]);
if (isset($this->trustedValuesCache[$cacheKey])) return $this->trustedValuesCache[$cacheKey];
$clientValues = [];
$forwardedValues = [];
if (
(self::$trustedHeaderSet & $type) &&
$this->request->hasHeader(ClientIpRequestConstant::TRUSTED_HEADERS[$type])
) {
foreach (explode(',', $this->request->getHeaderLine(ClientIpRequestConstant::TRUSTED_HEADERS[$type])) as $value) {
$clientValues[] = ($type === ClientIpRequestConstant::HEADER_X_FORWARDED_PORT ? '0.0.0.0' : trim($value));
}
}
if (
(self::$trustedHeaderSet & ClientIpRequestConstant::HEADER_FORWARDED) &&
(isset(ClientIpRequestConstant::FORWARDED_PARAMS[$type])) &&
$this->request->hasHeader(ClientIpRequestConstant::TRUSTED_HEADERS[ClientIpRequestConstant::HEADER_FORWARDED])
) {
$forward = $this->request->getHeaderLine(ClientIpRequestConstant::TRUSTED_HEADERS[ClientIpRequestConstant::HEADER_FORWARDED]);
$parts = HeaderUtils::split($forward, ',;=');
$param = ClientIpRequestConstant::FORWARDED_PARAMS[$type];
foreach ($parts as $subParts) {
if (($value = HeaderUtils::combine($subParts)[$param] ?? null) === null) continue;
if ($type === ClientIpRequestConstant::HEADER_X_FORWARDED_PORT) {
if (
str_ends_with($value, ']') ||
($value = mb_strrchr($value, ':')) === false
) $value = $this->isSecure() ? ':443' : ':80';
$value = '0.0.0.0' . $value;
}
$forwardedValues[] = $value;
}
}
if (null !== $ip) {
$clientValues = $this->normalizeAndFilterClientIps($clientValues, $ip);
$forwardedValues = $this->normalizeAndFilterClientIps($forwardedValues, $ip);
}
if ($forwardedValues === $clientValues || !$clientValues) return $this->trustedValuesCache[$cacheKey] = $forwardedValues;
if (!$forwardedValues) return $this->trustedValuesCache[$cacheKey] = $clientValues;
if (!$this->isForwardedValid) return (($ip = $this->trustedValuesCache[$cacheKey]) !== null) ? ['0.0.0.0',$ip] : [];
$this->isForwardedValid = false;
throw new ErrException('转发报头无效,请检查服务器配置。');
}
/**
* @return bool
*/
private function isSecure(): bool
{
return $this->request->getHeaderLine(ClientIpRequestConstant::HEADER_X_FORWARDED_PROTO) === 'https'
|| ($this->request->getServerParams()['https'] ?? '') === 'on';
}
/**
* @param array $clientIps
* @param string $ip
* @return array|null[]|string[]
*/
private function normalizeAndFilterClientIps(array $clientIps, string $ip): array
{
if (!$clientIps) return [];
$clientIps[] = $ip;
$firstTrustedIp = null;
foreach ($clientIps as $key => $clientIp) {
if (mb_strpos($clientIp, '.')) {
// ipv4
$i = mb_strpos($clientIp, '.');
if ($i) $clientIps[$key] = $clientIp = mb_substr($clientIp, 0, $i);
} elseif (str_starts_with($clientIp, '[')) {
// ipv6
$i = mb_strpos($clientIp, ']',1);
$clientIps[$key] = $clientIp = mb_substr($clientIp, 1, $i - 1);
}
if (!filter_var($clientIp, FILTER_VALIDATE_IP)) {
unset($clientIps[$key]);
continue;
}
if (IpUtils::checkIp($clientIp, self::$trustedProxies)) {
unset($clientIps[$key]);
$firstTrustedIp ??= $clientIp;
}
}
return $clientIps ? array_reverse($clientIps) : [$firstTrustedIp];
}
}