在一些特殊的部署环境中,laravel的安全代理机制会造成代理无法正常访问,比如通过前置代理服务器部署访问入口为https://domain.com:10020,然后nginx代理到另一个内网的laravel上,这时候laravel中输出的资源路径都是内网主机的地址,而不是入口地址,资源默认都是全路径而不是相对路径,这与vue3前端的构建方式不一样。
为了安全的解决这个适配问题,建议采用以下方案:
前置代理配置
server {
listen 10020 ssl;
server_name domain.com;
# SSL 配置
ssl_certificate /www/ssl/domain.com.pem;
ssl_certificate_key /www/ssl/domain.com.key;
ssl_protocols TLSv1.1 TLSv1.2;
ssl_ciphers ECDHE-RSA-AES128-GCM-SHA256:ECDHE:ECDH:AES:HIGH:!NULL:!aNULL:!MD5:!ADH:!RC4;
ssl_prefer_server_ciphers on;
location / {
proxy_pass http://172.19.205.1:80; #这里是laravel的地址
# 传递原始请求信息
proxy_set_header Host $host;
proxy_set_header X-Real-IP $remote_addr;
proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;
proxy_set_header X-Forwarded-Proto https; # 关键:协议,这个要设置成固定的,不用从$scheme读取
proxy_set_header X-Forwarded-Host $host; # 关键:域名
proxy_set_header X-Forwarded-Port $server_port; # 关键:端口
proxy_set_header X-Forwarded-Ssl on; # 辅助 SSL 标识
proxy_set_header Front-End-Https on; # 另一种辅助标识
# 超时设置
proxy_connect_timeout 60s;
proxy_send_timeout 60s;
proxy_read_timeout 60s;
}
}
准备阶段
创建一个routes/web.php或修改:
// 仅在调试模式下有效的调试代理路由
if (config('app.debug')) {
Route::get('/debug-proxy', function (Request $request) {
// 获取直接连接到服务器的客户端 IP
$serverRemoteAddr = $_SERVER['REMOTE_ADDR'] ?? 'unknown';
// 获取通过 Laravel 方法获取的客户端 IP
$laravelClientIp = $request->getClientIp();
// 获取所有相关的请求头
$relevantHeaders = [
'x-real-ip' => $request->header('x-real-ip'),
'x-forwarded-for' => $request->header('x-forwarded-for'),
'forwarded' => $request->header('forwarded'),
'forwarded-for' => $request->header('forwarded-for'),
'x-forwarded-host' => $request->header('x-forwarded-host'),
'x-forwarded-proto' => $request->header('x-forwarded-proto'),
'x-forwarded-port' => $request->header('x-forwarded-port'),
'x-forwarded-ssl' => $request->header('x-forwarded-ssl'),
'front-end-https' => $request->header('front-end-https'),
];
return response()->json([
'server_remote_addr' => $serverRemoteAddr,
'laravel_client_ip' => $laravelClientIp,
'relevant_headers' => $relevantHeaders,
'all_headers' => $request->headers->all(),
'trusted_proxies' => $_ENV['TRUSTED_PROXIES'] ?? 'not set',
]);
});
}
通过这个方式来看laravel之前的nginx有没有把头部信息正常传过来,有可能有两个nginx,一个是前置代理服务器,一个是laravel主机的nginx(这个地方我一开始用的caddy,会自动把非正常的https信息给改回http)。
.env配置
# 受信任的代理服务器列表,用于处理转发头信息
# '*' 表示信任所有代理(开发环境)
# 在生产环境中应该指定具体的IP地址或CIDR范围以提高安全性
# 通过/debug-proxy来获取server_remote_addr,并将其设置为可信代理
# 示例: 10.0.0.0/8,172.16.0.0/12,192.168.0.0/16
TRUSTED_PROXIES=127.0.0.1
# TRUSTED_PROXIES=*
TRUSTED_PROXIES这个设置开发环境下可以设置为所有地址,即*,正式环境下最好为精确的前置代理的地址,也就是准备阶段获取到的server_remote_addr的值。
TrustProxies 中间件配置
创建一个app\Http\Middleware\TrustProxies.php
<?php
namespace App\Http\Middleware;
use Illuminate\Http\Middleware\TrustProxies as Middleware;
use Illuminate\Http\Request;
class TrustProxies extends Middleware
{
/**
* 可信代理列表
*
* @var array<int, string>|string|null
*/
protected $proxies;
/**
* 用于检测代理的头部信息
*
* @var int
*/
protected $headers =
Request::HEADER_X_FORWARDED_FOR |
Request::HEADER_X_FORWARDED_HOST |
Request::HEADER_X_FORWARDED_PORT |
Request::HEADER_X_FORWARDED_PROTO |
Request::HEADER_X_FORWARDED_PREFIX |
Request::HEADER_X_FORWARDED_AWS_ELB;
/**
* 构造函数
*
* @return void
*/
public function __construct()
{
// 从 .env 文件中读取可信代理配置
$this->proxies = env('TRUSTED_PROXIES', '*');
}
}
启用TrustProxies
在bootstrap\app.php中增加这个中间件:
->withMiddleware(function (Middleware $middleware) {
$middleware->web(append: [
。。。其他中间件
\App\Http\Middleware\TrustProxies::class,
。。。其他中间件
]);
到这里laravel自身的适配就完成了,但如果还用了swagger-ui,还要让swaager-ui适配一下。
L5SwaggerExtendedServiceProvider
创建一个app\Providers\L5SwaggerExtendedServiceProvider.php
<?php
namespace App\Providers;
use Illuminate\Support\ServiceProvider;
use Illuminate\Support\Facades\Blade;
class L5SwaggerExtendedServiceProvider extends ServiceProvider
{
/**
* Register services.
*/
public function register(): void
{
//
}
/**
* Bootstrap services.
*/
public function boot(): void
{
// 注册自定义 Blade 指令来处理带端口和协议的 Swagger 资源
Blade::directive('l5_swagger_secure_asset', function ($expression) {
return "<?php
// 获取原始资产URL
\$originalUrl = l5_swagger_asset($expression);
// 获取转发相关信息
\$forwardedPort = \$_SERVER['HTTP_X_FORWARDED_PORT'] ?? null;
\$forwardedProto = \$_SERVER['HTTP_X_FORWARDED_PROTO'] ?? null;
\$forwardedHost = \$_SERVER['HTTP_X_FORWARDED_HOST'] ?? null;
// 解析原始URL
\$parsedUrl = parse_url(\$originalUrl);
\$scheme = \$parsedUrl['scheme'] ?? 'http';
\$host = \$parsedUrl['host'] ?? '';
\$port = \$parsedUrl['port'] ?? null;
\$path = \$parsedUrl['path'] ?? '';
\$query = isset(\$parsedUrl['query']) ? '?' . \$parsedUrl['query'] : '';
// 如果原始URL已经有端口,直接返回
if (\$port) {
echo \$originalUrl;
return;
}
// 确定协议
if (\$forwardedProto) {
\$scheme = \$forwardedProto;
}
// 确定主机
if (\$forwardedHost) {
\$host = \$forwardedHost;
}
// 确定端口
if (\$forwardedPort) {
\$port = \$forwardedPort;
} elseif (\$scheme === 'https' && !isset(\$parsedUrl['port'])) {
// HTTPS 默认端口443,通常不显示
\$port = null;
} elseif (\$scheme === 'http' && !isset(\$parsedUrl['port'])) {
// HTTP 默认端口80,通常不显示
\$port = null;
}
// 构造新URL
\$newUrl = \$scheme . '://' . \$host;
if (\$port && ((\$scheme === 'https' && \$port != 443) || (\$scheme === 'http' && \$port != 80))) {
\$newUrl .= ':' . \$port;
}
\$newUrl .= \$path . \$query;
echo \$newUrl;
?>";
});
}
}
注册
在bootstrap\providers.php中添加
return [
。。。其他
App\Providers\L5SwaggerExtendedServiceProvider::class,
];
修改视图
resources\views\vendor\l5-swagger\index.blade.php
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<title>{{config('l5-swagger.documentations.'.$documentation.'.api.title')}}</title>
<link rel="stylesheet" type="text/css" href="@l5_swagger_secure_asset($documentation, 'swagger-ui.css')">
<link rel="icon" type="image/png" href="@l5_swagger_secure_asset($documentation, 'favicon-32x32.png')" sizes="32x32"/>
<link rel="icon" type="image/png" href="@l5_swagger_secure_asset($documentation, 'favicon-16x16.png')" sizes="16x16"/>
<style>
html
{
box-sizing: border-box;
overflow: -moz-scrollbars-vertical;
overflow-y: scroll;
}
*,
*:before,
*:after
{
box-sizing: inherit;
}
body {
margin:0;
background: #fafafa;
}
</style>
@if(config('l5-swagger.defaults.ui.display.dark_mode'))
<style>
body#dark-mode,
#dark-mode .scheme-container {
background: #1b1b1b;
}
#dark-mode .scheme-container,
#dark-mode .opblock .opblock-section-header{
box-shadow: 0 1px 2px 0 rgba(255, 255, 255, 0.15);
}
#dark-mode .operation-filter-input,
#dark-mode .dialog-ux .modal-ux,
#dark-mode input[type=email],
#dark-mode input[type=file],
#dark-mode input[type=password],
#dark-mode input[type=search],
#dark-mode input[type=text],
#dark-mode textarea{
background: #343434;
color: #e7e7e7;
}
#dark-mode .title,
#dark-mode li,
#dark-mode p,
#dark-mode table,
#dark-mode label,
#dark-mode .opblock-tag,
#dark-mode .opblock .opblock-summary-operation-id,
#dark-mode .opblock .opblock-summary-path,
#dark-mode .opblock .opblock-summary-path__deprecated,
#dark-mode h1,
#dark-mode h2,
#dark-mode h3,
#dark-mode h4,
#dark-mode h5,
#dark-mode .btn,
#dark-mode .tab li,
#dark-mode .parameter__name,
#dark-mode .parameter__type,
#dark-mode .prop-format,
#dark-mode .loading-container .loading:after{
color: #e7e7e7;
}
#dark-mode .opblock-description-wrapper p,
#dark-mode .opblock-external-docs-wrapper p,
#dark-mode .opblock-title_normal p,
#dark-mode .response-col_status,
#dark-mode table thead tr td,
#dark-mode table thead tr th,
#dark-mode .response-col_links,
#dark-mode .swagger-ui{
color: wheat;
}
#dark-mode .parameter__extension,
#dark-mode .parameter__in,
#dark-mode .model-title{
color: #949494;
}
#dark-mode table thead tr td,
#dark-mode table thead tr th{
border-color: rgba(120,120,120,.2);
}
#dark-mode .opblock .opblock-section-header{
background: transparent;
}
#dark-mode .opblock.opblock-post{
background: rgba(73,204,144,.25);
}
#dark-mode .opblock.opblock-get{
background: rgba(97,175,254,.25);
}
#dark-mode .opblock.opblock-put{
background: rgba(252,161,48,.25);
}
#dark-mode .opblock.opblock-delete{
background: rgba(249,62,62,.25);
}
#dark-mode .loading-container .loading:before{
border-color: rgba(255,255,255,10%);
border-top-color: rgba(255,255,255,.6);
}
#dark-mode svg:not(:root){
fill: #e7e7e7;
}
</style>
@endif
</head>
<body @if(config('l5-swagger.defaults.ui.display.dark_mode')) id="dark-mode" @endif>
<div id="swagger-ui"></div>
<script src="@l5_swagger_secure_asset($documentation, 'swagger-ui-bundle.js')"></script>
<script src="@l5_swagger_secure_asset($documentation, 'swagger-ui-standalone-preset.js')"></script>
<script>
window.onload = function() {
// 确保 API 文档 URL 包含正确的协议、主机和端口
let urlToDocs = "{!! $urlToDocs !!}";
// 获取转发信息
const forwardedPort = "{{ request()->header('x-forwarded-port') }}";
const forwardedProto = "{{ request()->header('x-forwarded-proto') }}";
const forwardedHost = "{{ request()->header('x-forwarded-host') }}";
const hostHeader = "{{ request()->header('host') }}";
try {
// 解析原始 URL
const url = new URL(urlToDocs);
// 设置协议
if (forwardedProto) {
url.protocol = forwardedProto + ':';
}
// 设置主机
if (forwardedHost) {
// 如果转发主机包含端口,分离主机和端口
if (forwardedHost.includes(':')) {
const parts = forwardedHost.split(':');
url.hostname = parts[0];
if (parts[1]) {
url.port = parts[1];
}
} else {
url.hostname = forwardedHost;
}
}
// 设置端口(如果明确提供了转发端口)
if (forwardedPort) {
url.port = forwardedPort;
} else if (forwardedProto === 'https' && !forwardedPort) {
// HTTPS 默认端口处理
if (url.port === '80' || url.port === '') {
url.port = '';
}
} else if (forwardedProto === 'http' && !forwardedPort) {
// HTTP 默认端口处理
if (url.port === '443' || url.port === '') {
url.port = '';
}
}
urlToDocs = url.toString();
} catch (e) {
console.error('Failed to parse URL:', e);
}
// Build a system
const ui = SwaggerUIBundle({
dom_id: '#swagger-ui',
url: urlToDocs,
operationsSorter: {!! isset($operationsSorter) ? '"' . $operationsSorter . '"' : 'null' !!},
configUrl: {!! isset($configUrl) ? '"' . $configUrl . '"' : 'null' !!},
validatorUrl: {!! isset($validatorUrl) ? '"' . $validatorUrl . '"' : 'null' !!},
oauth2RedirectUrl: "{{ route('l5-swagger.'.$documentation.'.oauth2_callback', [], $useAbsolutePath) }}",
requestInterceptor: function(request) {
request.headers['X-CSRF-TOKEN'] = '{{ csrf_token() }}';
return request;
},
presets: [
SwaggerUIBundle.presets.apis,
SwaggerUIStandalonePreset
],
plugins: [
SwaggerUIBundle.plugins.DownloadUrl
],
layout: "StandaloneLayout",
docExpansion : "{!! config('l5-swagger.defaults.ui.display.doc_expansion', 'none') !!}",
deepLinking: true,
filter: {!! config('l5-swagger.defaults.ui.display.filter') ? 'true' : 'false' !!},
persistAuthorization: "{!! config('l5-swagger.defaults.ui.authorization.persist_authorization') ? 'true' : 'false' !!}",
})
window.ui = ui
@if(in_array('oauth2', array_column(config('l5-swagger.defaults.securityDefinitions.securitySchemes'), 'type')))
ui.initOAuth({
usePkceWithAuthorizationCodeGrant: "{!! (bool)config('l5-swagger.defaults.ui.authorization.oauth2.use_pkce_with_authorization_code_grant') !!}"
})
@endif
}
</script>
</body>
</html>