基于php实现网站的反向代理

背景

趁着服务器迁移的时候,对自己的一些网站做了重新的整理。有两个问题:

  1. 域名1和域名2,我都想继续持有并使用。这两个域名我都做了备案,但是域名如果不提供服务,云服务厂商就会经常打电话给我问,说这个域名还用不用啊?如果不用的话要取消备案之类之类。

  2. 域名1,我同时在多家云服务厂商做了域名的绑定。比如腾讯云+百度云。域名1解析到了腾讯云,但是百度云也需要至少有一个2级域名提供服务,并做绑定。

针对这两个问题,解决的方式倒是简单,就是需要针对域名2,以及域名1的某个2级域名,都做一些服务的部署,保持网页可以打开的状态。

我原先的做法,就是分别再搭建一套博客系统,也不用放什么内容,就纯放着,网页能打开就行。但是,毕竟不优雅嘛,而且也要分别搞个数据库。所以我想着干脆,都基于我这个博客网站,做一下反向代理。


即类似于:打开https://domain2.com 和 https://sub.domain1.com 都显示 https://domain1.com 的内容。

方案1

最简单的方式其实是不做反向代理,而是通过DNS服务来做一些事情。


我的网站域名是通过DNSPod进行管理的,而它有个很有意思的功能,就是做url的跳转。


这个功能有两种形式,一个叫做显性url,一个叫做隐性url。显性url很简单,就是类似301、302跳转的效果一样,当我们在浏览器地址栏中输入https://domain2.com 的地址之后,自动跳转到https://domain1.com 这个地址,而地址栏中的地址也同样会发生变化。这样其实不太符合我的预期。


至于隐性url,其实是通过iframe来实现的。也就是说,在页面内点击各种链接跳转,url地址都不会发生任何变化,一直停留在 domain2.com 这个地址上。对于我想保留domain2.com 域名这个初衷来说,其实基本算是满足需要了。


但是稍微有一点小问题,就是这个domain2.com只能通过http访问,而不能通过https协议来访问,这样的话,在浏览器的地址栏中,就会出现网站非安全的小图标提示,看起来不是很爽。再加上iframe技术的应用,对网页搜索引擎的收录自然也不是很友好。

方案2

我们知道,apache、nginx这样的服务器,本身就提供了反向代理的功能。一般来说,我们是基于它做一些服务器隐藏、负载均衡等事情,但是用来做域名的停靠访问也是可以的。


以nginx为例,我们可以针对 domain2.com 去提供一个虚拟主机配置,参考下面的配置代码:

server
{
    // 其他各种配置
    location / {
        proxy_pass https://www.domain1.com;

        #sub_filter '第一个站点的名字' '第二个站点的名字';
        #sub_filter_types text/html;
        #sub_filter_once on;
    }
}

即,我们针对所有的url地址,全部通过代理转发到 https://www.domain1.com 这个域名下,类似于https://www.domain2.com/a/b/c 这样的路径,也同样会实际请求到 https://www.domain1.com/a/b/c 这个实际路径。


其中,如果我们想对页面中的一些文字进行替换,也可以通过sub_filter*这样的一些配置来实现。例如访问 domain1.com 的时候,显示站点名称为“站点1”,换成 domain2.com 访问的时候,显示站点名称为“站点2”。虽然这个替换配置不复杂,但是对于一些基础场景还是适用的。


这个方案比较简单,只需要有一台能够提供服务的nginx即可操作。但是我在使用的时候,又遇到了一点问题。


前面提到,我有的域名是想在百度云上停靠访问的,我百度云的服务器并不是完全可配置的nginx,而是BCH这样一个虚拟主机。百度云在虚拟主机的配置上做了很多阉割、封装,典型的就是proxy_pass这个功能无法使用!


这就有点无语了,还得另想办法。

方案3

百度云的虚拟主机,其实就等同于一个阉割版nginx+php+mysql,php和mysql的功能其实是相对完整的。那么,我干脆就基于php来实现反向代理的逻辑也可以啊!


思路也很简单,总共2个主功能:通过curl访问被代理网站的对应路径,其中path、header、body等信息,都原封不动带过去;并把响应的内容,header、body等都直接输出出来。代码可以直接参考:


$rule = [// 反向代理的目标
			'upstream'	=> 'https://www.poisonbian.com',
			// 设置header中的host
			'host'		=> 'www.poisonbian.com',
			// 改写规则
			'rewrite'	=> [
					// 将字符串aa替换成bb,最多改写N次。如果N=null,则表示所有的全部改写
					['aaaaaaaaaa', 'bbbbbbbbbb', null],
			]
];
$host = $_SERVER['HTTP_HOST'];
$timeout = 5; // seconds
	
$request_method = $_SERVER['REQUEST_METHOD'];
$request_uri = $_SERVER['REQUEST_URI'];

$request_uri = str_replace('proxy_pass/index.php', '', $request_uri);
$request_uri = str_replace('proxy_pass/', '', $request_uri);

$request_headers = getallheaders();
$request_body = file_get_contents('php://input');


// 转发请求
$url = $rule['upstream'] . $request_uri;
$headers = array();
foreach ($request_headers as $key => $value) {
	if ($key === 'Host') {
		$value = $rule['host'];
	}
	$headers[] = $key . ': ' . $value;
}

$ch = curl_init();
curl_setopt($ch, CURLOPT_TIMEOUT, $timeout);
curl_setopt($ch, CURLOPT_CUSTOMREQUEST, $request_method);
curl_setopt($ch, CURLOPT_URL, $url);
curl_setopt($ch, CURLOPT_HTTPHEADER, $headers);
curl_setopt($ch, CURLOPT_POSTFIELDS, $request_body);
curl_setopt($ch, CURLOPT_HEADER, true);
curl_setopt($ch, CURLOPT_FOLLOWLOCATION, false);
curl_setopt($ch, CURLOPT_RETURNTRANSFER, true);
curl_setopt($ch, CURLOPT_SSL_VERIFYPEER, false);
curl_setopt($ch, CURLOPT_ENCODING, ''); // 关闭 gzip

$response = curl_exec($ch);

// 反向代理失败
if ($response === false) {
	header('HTTP/1.1 502 Bad Gateway');
	header('Content-Type: text/plain');
	echo 'Upstream host did not respond.';
	curl_close($ch);
	exit(0);
} 

$header_length = curl_getinfo($ch, CURLINFO_HEADER_SIZE);
$response_headers = explode("\n", substr($response, 0, $header_length));
$response_body = substr($response, $header_length);


foreach ($response_headers as $header) {
	$header = trim($header);
	if ($header === '') {continue;}
	
	if (stripos($header, 'gzip') !== false) {
		continue;
	}
	header($header);
}

foreach ($rule['rewrite'] as $rewrite) {
	$response_body = str_replace($rewrite[0], $rewrite[1], $response_body, $rewrite[2]);
}

echo $response_body;
curl_close($ch);


可以看到这段代码中,除了主功能之外,也增加了str_replace的字符串替换功能,类似nginx的sub_filter相关配置。


不过在做文本替换的过程中,发现了一个小坑。刚开始的时候,无论怎么写替换函数,都没有生效。我把response打出来一看,发现是一坨乱码。仔细分析之后,发现这个是做了gzip压缩后的效果。这样的话,文本内容变得面目全非,自然也没法做替换了。


因此,我在代码中做了调整,请求被代理网站、输出页面内容的时候,都把gzip的对应header直接去掉,这样虽然牺牲了一些网页传输效率,但字符串的替换就能够正常生效了。


最后,还有一个小问题遗留下来。例如我在 https://domain1.com/a/b/c.js 这样一个静态js的地址,如果通过https://domain2.com/ 这个页面来加载,就产生了跨域请求。在原先的nginx配置中,服务器会拒绝这样的访问请求,从而导致https://domain2.com 页面中的js、css、图片、字体等各种静态资源都无法加载生效。


这样,我们该如何解决呢?留下悬念,下篇文章再讲。

本文链接:https://www.poisonbian.com/post/5056.html 转载需授权!

分享到:
原文链接:,转发请注明来源!
「基于php实现网站的反向代理」评论列表

发表评论