这个问题耗费了不少时间才解决了,搜索的过程中,发现应该是常见的一类问题,但是解决方法好像并不是特别清晰,总之查了好多,碰巧解决!
此外,出现这个问题,是因为一些特殊的配置!一般项目的 https 可能遇不到!
之前写过一遍笔记:
项目 http 升级 https 各种问题总结
https://blog.csdn.net/beyond__devil/article/details/86629562
里面也提到了,我们项目目前的架构:
后端 2 台服务器配置为负载均衡,ssl 证书是直接部署在负载均衡上。
负载均衡的配置:
1)443
1.开启会话保持,植入 Cookie
2.勾选 '附加 HTTP 头字段',勾选上 『通过X-Forwarded-Proto头字段获取SLB的监听协议』(不然微信判断环境不是 HTTPS)
3.选择证书
2)80
1.开启 '监听转发',目的监听选择 'HTTPS:443'
这么做,是让我们输入域名,默认访问的就是 https,不然浏览器默认输入域名,端口默认是 80
后端 2 台服务器配置:
80 端口
因团队没有运维,不过既然阿里云支持这种架构配置,就应该也是一种通用的解决方案。
这种负载均衡架构,是请求指向的是负载均衡,然后由负载均衡再分发给后端的 2 台服务器,负载均衡的角色就是代理。
接着开始今天的正题:
怎么会发现 laravel 分页返回的是 http 而非 https?
在我们的负载均衡这种配置下,支持 https 和 http,http 会重定向到 https,所以即使 laravel 分页链接为 http,然后我们点击下一页,它顶多是请求了2次,先请求了下 http,然后重定向到 https,这个也不算啥问题!
个别页面,我为了体验好点,采用了 ajax,每一页内容以及每一页产生的分页,都是 ajax 请求的。这样分页的链接就是 http,然后我们整个页面是 https,https 页面中请求下一页的内容,因下一页的链接是 http,js 就会报错,导致页面出错!
问题排查:
产生分页链接,laravel 框架这么严谨,应该都会判断是 https 还是 http 协议,进行代码追踪,分页调用的是 laravel 的 Paginator 类,最终定位到的位置是:
Symfony\Component\HttpFoundation\Request
代码分析:
// 获取 uri
public function getUri()
{
if (null !== $qs = $this->getQueryString()) {
$qs = '?'.$qs;
}
return $this->getSchemeAndHttpHost().$this->getBaseUrl().$this->getPathInfo().$qs;
}
// 获取协议和主机名
public function getSchemeAndHttpHost()
{
return $this->getScheme().'://'.$this->getHttpHost();
}
// 获取协议
public function getScheme()
{
return $this->isSecure() ? 'https' : 'http';
}
// 分析是否 https
public function isSecure()
{
if ($this->isFromTrustedProxy() && $proto = $this->getTrustedValues(self::HEADER_X_FORWARDED_PROTO)) {
return \in_array(strtolower($proto[0]), array('https', 'on', 'ssl', '1'), true);
}
$https = $this->server->get('HTTPS');
return !empty($https) && 'off' !== strtolower($https);
}
这里的判断简要分析下:
2种判断:
1.代理判断(我们目前的架构就是这种模式)
// 1>是否是我们信任代理
public function isFromTrustedProxy()
{
return self::$trustedProxies && IpUtils::checkIp($this->server->get('REMOTE_ADDR'), self::$trustedProxies);
}
设置了 $trustedProxies,同时,检查 $_SERVER['REMOTE_ADDR'] 代理 IP 是否是在我们信任的代理 IP 列表中
// 2>根据我们设置的信任的头部集合($trustedHeaderSet),判断 self::HEADER_X_FORWARDED_PROTO 是否在信任的头部集合中,在的话,并返回头部值。
$proto = $this->getTrustedValues(self::HEADER_X_FORWARDED_PROTO)
2.$_SERVER['https'] 是否为 'on'
我们通过 nginx 配置 ssl 时,就是这种情况
我们的阿里云的负载均衡配置,'附加 HTTP 头字段',勾选了2个选项:
1.通过X-Forwarded-For头字段获取客户端真实 IP(默认必须勾选,无法取消)
2.通过X-Forwarded-Proto头字段获取SLB的监听协议(手动勾选,不然微信判断环境不是 HTTPS,同时 symfony 这里判断是否是 https 也需要该头部)
经过上面分析,原理我们清楚了,但是如何解决这个问题:
打印 $_SERVER,发现存在这2个字段:
HTTP_X_FORWARDED_PROTO: https
HTTP_X_FORWARDED_FOR: IPV4 地址
关于 HTTP_X_FORWARDED_* 是个啥东西:
RFC 7239 - Forwarded HTTP Extension
https://tools.ietf.org/html/rfc7239
腾讯云这个开发手册非常不错!!!作为文档查看!!!
https://cloud.tencent.com/developer/section/1190031
Symfony\Component\HttpFoundation\Request 的源码看了好久,半天看不懂,尤其是搜索到的一些资料,关于配置 '自定义的 HTTP 头部':
private static $trustedHeaders = array(
self::HEADER_FORWARDED => 'FORWARDED',
self::HEADER_X_FORWARDED_FOR => 'X_FORWARDED_FOR',
self::HEADER_X_FORWARDED_HOST => 'X_FORWARDED_HOST',
self::HEADER_X_FORWARDED_PROTO => 'X_FORWARDED_PROTO',
self::HEADER_X_FORWARDED_PORT => 'X_FORWARDED_PORT',
);
因为,$_SERVER 中的是 HTTP_X_FORWARDED_,而源码中的没有 'HTTP_' 前缀,导致我以为一直要重新自定义,重新 $trustedHeaders,这一过程,搜索了不少资料:
https://blog.csdn.net/Tianshan2018_Chen/article/details/79884686
https://stackoverflow.com/questions/19967788/laravel-redirect-all-requests-to-https
https://symfony.com/doc/3.2/components/http_foundation/trusting_proxies.html
https://symfony.com/doc/current/deployment/proxies.html
https://segmentfault.com/q/1010000015611503
而且 symfony 版本升级,上面很多提到的方法,都失效了!
最终找到的解决方案从这里找到的:
how to set custom HEADER_X_FORWARD in 4.0(如何自定义 symfony 4.0 的 HEADER_X_FORWARD_* 名称)
https://github.com/fideloper/TrustedProxy/issues/108
提到了 4.0 版本不支持旧版方法改动,作者好像也没有找到方法。但是有一个大神用了另外一个方法,
https://github.com/fideloper/TrustedProxy/issues/108#issuecomment-374883697
扩展了 Fideloper\Proxy\TrustProxies
/**
* The mapping of custom and standard header names.
*
* @var array
*/
protected $aliases = [
// Host header used by ngrok.
'HTTP_X_ORIGINAL_HOST' => 'HTTP_X_FORWARDED_HOST',
// '自定义的' => '标准的'
];
/**
* Handle an incoming request.
*
* ...
*/
public function handle(Request $request, Closure $next)
{
foreach ($this->aliases as $custom => $standard) {
// 一旦发现我们自定义的,我们不修改 symfony 的 $trustedHeaders 为我们自定义的 HTTP 头部 \
// 而是根据 '自定义 => 标准' 的对应关系,依次也设置一个标准的 HTTP 头部的值。
if (! $request->server->has($standard) && $value = $request->server->get($custom)) {
$request->server->set($standard, $value);
$request->headers->set(substr($standard, 5), $value); // Remove "HTTP_" prefix.
}
}
return parent::handle($request, $next);
}
特别强调的是:
$request->server->set($standard, $value);
$request->headers->set(substr($standard, 5), $value);
$server 和 $headers 中的字段,好像是差一个 'HTTP_',查看了下
查看 Symfony 源码:
Symfony\Component\HttpFoundation\Request
$this->server = new ServerBag($server);
$this->headers = new HeaderBag($this->server->getHeaders());
// this->headers 是从 $this->server 获取
Symfony\Component\HttpFoundation\ServerBag
public function getHeaders()
{
$headers = array();
$contentHeaders = array('CONTENT_LENGTH' => true, 'CONTENT_MD5' => true, 'CONTENT_TYPE' => true);
foreach ($this->parameters as $key => $value) {
// 这里也可以看到,确实将 'HTTP_' 前缀去掉了!!!
if (0 === strpos($key, 'HTTP_')) {
$headers[substr($key, 5)] = $value;
}
// CONTENT_* are not prefixed with HTTP_
elseif (isset($contentHeaders[$key])) {
$headers[$key] = $value;
}
}
...
}
所以,最后得出的结论是:
我们 $_SERVER 里的 2 个 HTTP 头本身就是和 Symfony 一致的!
HTTP_X_FORWARDED_PROTO: https
HTTP_X_FORWARDED_FOR: IPV4 地址
未检测为 HTTPS,是因为第一步检测未通过,即:
isFromTrustedProxy()
需要设置:
self::$trustedProxies 为我们信任的 代理IP
我们的 laravel 框架,默认使用的就是 Fideloper\Proxy\TrustProxies 代理,以前都没注意过,看 laravel 文档:
https://laravel.com/docs/5.7/requests#configuring-trusted-proxies
配置信任代理:
app/Http/Middleware/TrustProxies.php
// 信任代理IP,'*' 表示全部信任(这个是 Fideloper\Proxy\TrustProxies 给 Symfony 进行的扩展)
protected $proxies = '*';
// 允许的 HTTP_X_FORWARDED_* 头部,HEADER_X_FORWARDED_ALL 表示支持全部 HTTP_X_FORWARDED_* 头部
protected $headers = Request::HEADER_X_FORWARDED_ALL;
laravel 修复:
app/Http/Middleware/TrustProxies.php
protected $proxies = '*';
如此简单!!!
其他一些内容:
Symfony\Component\HttpFoundation\Request 源码中,自定义的一些状态:
const HEADER_FORWARDED = 0b00001; // When using RFC 7239
const HEADER_X_FORWARDED_FOR = 0b00010;
const HEADER_X_FORWARDED_HOST = 0b00100;
const HEADER_X_FORWARDED_PROTO = 0b01000;
const HEADER_X_FORWARDED_PORT = 0b10000;
const HEADER_X_FORWARDED_ALL = 0b11110; // All "X-Forwarded-*" headers
const HEADER_X_FORWARDED_AWS_ELB = 0b11010; // AWS ELB doesn't send X-Forwarded-Host
以 '0b' 开头,然后在其他方法中,通过 '位运算' 来判断状态,不懂这啥编码,网上搜了一篇类似的,有时间可以研究:
http://bbs.bugcode.cn/t/14935
nginx 负载均衡和反向代理有什么区别:
https://www.cnblogs.com/panxuejun/p/6027792.html