修复认证与基础安全链路
This commit is contained in:
@@ -1,42 +1,106 @@
|
||||
<?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
|
||||
{
|
||||
/**
|
||||
* 文件功能:强制信任并解析 CDN 传导的真实客户端 IP。
|
||||
* 解决 Herd 环境 / Nginx 本地反代时,丢失 X-Forwarded-For 导致全员 IP 变成 127.0.0.1 的问题。
|
||||
* 处理进入应用的请求,并在可信代理场景下覆写客户端 IP。
|
||||
*/
|
||||
public function handle(Request $request, Closure $next): Response
|
||||
{
|
||||
// 优先采纳 Cloudflare 的 CF-Connecting-IP
|
||||
if ($request->hasHeader('CF-Connecting-IP')) {
|
||||
$realIp = $request->header('CF-Connecting-IP');
|
||||
}
|
||||
// 腾讯云 EdgeOne CDN 自定义回源头部(后台配置名:EO-Client-IP)
|
||||
elseif ($request->hasHeader('EO-Client-IP')) {
|
||||
$realIp = $request->header('EO-Client-IP');
|
||||
}
|
||||
// 其他国内 CDN 厂商(阿里云 DCDN 等)通用头部
|
||||
elseif ($request->hasHeader('X-Real-IP')) {
|
||||
$realIp = $request->header('X-Real-IP');
|
||||
}
|
||||
// 最后兜底:取 X-Forwarded-For 最左边第一个(真实客户端)IP
|
||||
// 格式为 "真实客户端, CDN节点1, CDN节点2"
|
||||
elseif ($request->hasHeader('X-Forwarded-For')) {
|
||||
$realIp = trim(explode(',', $request->header('X-Forwarded-For'))[0]);
|
||||
}
|
||||
$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;
|
||||
}
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user