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

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

此外,出现这个问题,是因为一些特殊的配置!一般项目的 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

 

  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值