laravel 项目配置为 https,但分页生成的链接是 http

1,099 阅读4分钟

这个问题耗费了不少时间才解决了,搜索的过程中,发现应该是常见的一类问题,但是解决方法好像并不是特别清晰,总之查了好多,碰巧解决!

此外,出现这个问题,是因为一些特殊的配置!一般项目的 https 可能遇不到!

之前写过一遍笔记: 项目 http 升级 https 各种问题总结 blog.csdn.net/beyond__dev…

里面也提到了,我们项目目前的架构:
	后端 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