107 lines
3.0 KiB
PHP
107 lines
3.0 KiB
PHP
<?php
|
|
|
|
/**
|
|
* 文件功能:在可信代理场景下解析客户端真实 IP。
|
|
*
|
|
* 仅当当前请求明确来自配置中的反向代理 / CDN 节点时,
|
|
* 才会采信其透传的真实客户端 IP 头,避免外部客户端伪造来源。
|
|
*/
|
|
|
|
namespace App\Http\Middleware;
|
|
|
|
use Closure;
|
|
use Illuminate\Http\Request;
|
|
use Symfony\Component\HttpFoundation\IpUtils;
|
|
use Symfony\Component\HttpFoundation\Response;
|
|
|
|
/**
|
|
* 类功能:为可信代理请求恢复真实客户端 IP。
|
|
*/
|
|
class CloudflareProxies
|
|
{
|
|
/**
|
|
* 处理进入应用的请求,并在可信代理场景下覆写客户端 IP。
|
|
*/
|
|
public function handle(Request $request, Closure $next): Response
|
|
{
|
|
$realIp = $this->resolveTrustedClientIp($request);
|
|
|
|
if (! empty($realIp)) {
|
|
// 仅在确认上游代理可信且透传 IP 合法时,才覆写 request()->ip() 的来源。
|
|
$request->server->set('REMOTE_ADDR', $realIp);
|
|
$request->headers->set('X-Forwarded-For', $realIp);
|
|
}
|
|
|
|
return $next($request);
|
|
}
|
|
|
|
/**
|
|
* 从可信代理头中解析真实客户端 IP。
|
|
*/
|
|
private function resolveTrustedClientIp(Request $request): ?string
|
|
{
|
|
$remoteAddress = (string) $request->server->get('REMOTE_ADDR', '');
|
|
if (! $this->isTrustedProxy($remoteAddress)) {
|
|
return null;
|
|
}
|
|
|
|
foreach (['CF-Connecting-IP', 'EO-Client-IP', 'X-Real-IP'] as $headerName) {
|
|
$resolvedIp = $this->sanitizeIp($request->header($headerName));
|
|
if ($resolvedIp !== null) {
|
|
return $resolvedIp;
|
|
}
|
|
}
|
|
|
|
return $this->extractForwardedForIp($request->header('X-Forwarded-For'));
|
|
}
|
|
|
|
/**
|
|
* 判断当前请求是否来自受信代理节点。
|
|
*/
|
|
private function isTrustedProxy(string $remoteAddress): bool
|
|
{
|
|
if ($this->sanitizeIp($remoteAddress) === null) {
|
|
return false;
|
|
}
|
|
|
|
$trustedProxies = config('app.trusted_proxies', ['127.0.0.1', '::1']);
|
|
foreach ($trustedProxies as $trustedProxy) {
|
|
$trustedProxy = trim((string) $trustedProxy);
|
|
if ($trustedProxy !== '' && IpUtils::checkIp($remoteAddress, $trustedProxy)) {
|
|
return true;
|
|
}
|
|
}
|
|
|
|
return false;
|
|
}
|
|
|
|
/**
|
|
* 从 X-Forwarded-For 头中提取最左侧的合法 IP。
|
|
*/
|
|
private function extractForwardedForIp(?string $forwardedFor): ?string
|
|
{
|
|
if (! is_string($forwardedFor) || $forwardedFor === '') {
|
|
return null;
|
|
}
|
|
|
|
foreach (explode(',', $forwardedFor) as $candidateIp) {
|
|
$resolvedIp = $this->sanitizeIp($candidateIp);
|
|
if ($resolvedIp !== null) {
|
|
return $resolvedIp;
|
|
}
|
|
}
|
|
|
|
return null;
|
|
}
|
|
|
|
/**
|
|
* 校验并标准化 IP 文本。
|
|
*/
|
|
private function sanitizeIp(?string $ip): ?string
|
|
{
|
|
$normalizedIp = trim((string) $ip);
|
|
|
|
return filter_var($normalizedIp, FILTER_VALIDATE_IP) ? $normalizedIp : null;
|
|
}
|
|
}
|