Apache HTTP服务器 2.0版本
现有的语种: en
Originally written by
Ralf S. Engelschall <[email protected]>
December 1997
本文是mod_rewrite
的参考文档,阐述在实际应用中如何解决网管所面临的基于URL的典型问题,并详细描述如何配置URL重写规则集以解决问题。
Apache的mod_rewrite
是提供了强大URL操作的杀手级的模块,可以实现几乎所有你梦想的URL操作类型,其代价是你必须接受其复杂性,因为mod_rewrite
的主要障碍就是初学者不容易理解和运用,即使是Apache专家有时也会发掘出mod_rewrite
的新用途。
换句话说:对mod_rewrite
,或者是打退堂鼓永不再用,或者是喜欢它并一生受用。本文试图通过对已有方案的表述来创造一个成功的开端,以免你放弃。
我自己创造和收集了许多实践方案,不要有畏惧心理,从这些例子开始学习URL重写的黑匣子吧。
mod_alias
和mod_userdir
时要增加[PT]
标志,或者重写.htaccess
而不是单个服务器中的规则集。对一个特定的规则集应该首先去理解而后再去用以避免出问题。在有些网站服务器上,一个资源会拥有多个URL,在实际应用和发布中应该被使用的是规范的URL,其他的则是简写或者是内部使用的。无论用户在请求中使用什么形式的URL,他最终看见的都应该是规范的URL。
对所有的不规范的URL执行一个外部的HTTP重定向,以改变它在浏览器地址栏中的显示及其后继的请求。下例中的规则集用规范的/u/user
替换/~user
,并修正了/u/user
所遗漏的后缀的斜杠。
RewriteRule ^/~([^/]+)/?(.*) /u/$1/$2 [R] RewriteRule ^/([uge])/([^/]+)$ /$1/$2/ [R]
RewriteCond %{HTTP_HOST} !^fully\.qualified\.domain\.name [NC] RewriteCond %{HTTP_HOST} !^$ RewriteCond %{SERVER_PORT} !^80$ RewriteRule ^/(.*) http://fully.qualified.domain.name:%{SERVER_PORT}/$1 [L,R] RewriteCond %{HTTP_HOST} !^fully\.qualified\.domain\.name [NC] RewriteCond %{HTTP_HOST} !^$ RewriteRule ^/(.*) http://fully.qualified.domain.name/$1 [L,R]
DocumentRoot
通常,网站服务器的DocumentRoot
直接对应于URL"/
",但是,它常常不是处于最高一级,而可能只是众多数据池中的一个实体。比如,在Intranet站点中,有/e/www/
(WWW的主页)、/e/sww/
(Intranet的主页)等等,而DocumentRoot
指向了/e/www/
,则必须保证此数据池中的所有内嵌的图片和其他元素对后继请求有效。
只须重定向URL /
到/e/www/
即可。这个方案看起来很简单,但只是有了mod_rewrite
模块的支持,它才简单,因为传统的URL Aliases机制(由mod_alias
及其相关模块提供)只是作了一个前缀匹配,DocumentRoot
是一个对所有URL的前缀,因而无法实现这样的重定向。而用mod_rewrite
的确很简单:
RewriteEngine on RewriteRule ^/$ /e/www/ [R]
每个网管对引用目录后缀斜杠的问题都有一本苦经,如果遗漏了,服务器会产生一个错误,因为如果请求是/~quux/foo
而不是/~quux/foo/
,服务器会去找一个叫foo
的文件,而它是一个目录,所以就报错了。事实上,大多数情况下,它自己会试图修正这个错误,但是有时候需要你手工纠正,比如,在重写了许多CGI脚本中的复杂的URL以后。
解决这个微妙问题的方案是让服务器自动添加后缀的斜杠。对此,必须使用一个外部的重定向,使浏览器正确地处理后继的对诸如图片的请求。如果仅仅作一个内部的重写,可能只对目录页面有效,而对内嵌有使用相对URL的图片的页面则无效,因为浏览器有请求内嵌目标的可能。比如,如果不用外部重定向,/~quux/foo/index.html
页面中对image.gif
的请求,其结果将是/~quux/image.gif
!。
所以,应该这样写:
RewriteEngine on RewriteBase /~quux/ RewriteRule ^foo$ foo/ [R]
又懒又疯狂的做法是把这些写入其宿主目录中的顶级.htaccess
中,但是须注意,如此会带来一些处理上的开销。
RewriteEngine on RewriteBase /~quux/ RewriteCond %{REQUEST_FILENAME} -d RewriteRule ^(.+[^/])$ $1/ [R]
我们希望在一个Intranet集群网站中,对所有WWW服务器建立一个同类的一致性的URL规划,也就是,所有的URL(对单个服务器来说,是本地的依赖于此服务器的!)是独立于服务器的!我们需要的是一个具有独立于服务器的一致性规划的WWW名称空间,即,URL不需要包含正确的物理的目标服务器,而由集群本身来自动定位物理的目标主机。
首先,目标服务器的信息来自(产生)于包含有用户、组以及实体的外部地图,其格式形如:
user1 server_of_user1 user2 server_of_user2 : :
这些信息被存入map.xxx-to-host
文件。其次,如果URL在一个服务器上无效,需要引导所有的服务器重定向URL
/u/user/anypath /g/group/anypath /e/entity/anypath
到
http://physical-host/u/user/anypath http://physical-host/g/group/anypath http://physical-host/e/entity/anypath
以下规则集依靠地图文件来完成这个操作(假定,如果一个用户在地图中没有对应的项,则使用server0为默认服务器):
RewriteEngine on RewriteMap user-to-host txt:/path/to/map.user-to-host RewriteMap group-to-host txt:/path/to/map.group-to-host RewriteMap entity-to-host txt:/path/to/map.entity-to-host RewriteRule ^/u/([^/]+)/?(.*) http://${user-to-host:$1|server0}/u/$1/$2 RewriteRule ^/g/([^/]+)/?(.*) http://${group-to-host:$1|server0}/g/$1/$2 RewriteRule ^/e/([^/]+)/?(.*) http://${entity-to-host:$1|server0}/e/$1/$2 RewriteRule ^/([uge])/([^/]+)/?$ /$1/$2/.www/ RewriteRule ^/([uge])/([^/]+)/([^.]+.+) /$1/$2/.www/$3\
通常,许多网管在建立一个新的网站服务器时,都会有这样的要求:重定向一个网站服务器上的所有宿主目录到另一个网站服务器。
很简单,用mod_rewrite
。在老的网站服务器上重定向所有的URL /~user/anypath
到http://newserver/~user/anypath
。
RewriteEngine on RewriteRule ^/~(.+) http://newserver/~$1 [R,L]
一些拥有几千个用户的网站通常都使用结构化的宿主目录规划,即,每个宿主目录位于一个带有特定前缀比如其用户名的第一个字符的子目录下。那么,/~foo/anypath
代表/home/f/foo/.www/anypath
,而/~bar/anypath
代表/home/b/bar/.www/anypath
。
可以使用下列规则集来扩展~以达到上述目的。
RewriteEngine on RewriteRule ^/~(([a-z])[a-z0-9]+)(.*) /home/$2/$1/.www$3
这是一个不加雕琢的例子:一个大量使用针对目录的规则集以实现平滑观感,而从来不用调整数据结构的杀手级的应用。背景:net.sw从1992年开始,存放了我收集的免费的有效的Unix软件包。它是我的爱好也是我的工作,因为在学习计算机科学的同时,业余时间还做了多年的系统和网络的管理员。每周我都需要整理软件,因而建立了一个层次很深的目录结构来存放各种软件包:
drwxrwxr-x 2 netsw users 512 Aug 3 18:39 Audio/ drwxrwxr-x 2 netsw users 512 Jul 9 14:37 Benchmark/ drwxrwxr-x 12 netsw users 512 Jul 9 00:34 Crypto/ drwxrwxr-x 5 netsw users 512 Jul 9 00:41 Database/ drwxrwxr-x 4 netsw users 512 Jul 30 19:25 Dicts/ drwxrwxr-x 10 netsw users 512 Jul 9 01:54 Graphic/ drwxrwxr-x 5 netsw users 512 Jul 9 01:58 Hackers/ drwxrwxr-x 8 netsw users 512 Jul 9 03:19 InfoSys/ drwxrwxr-x 3 netsw users 512 Jul 9 03:21 Math/ drwxrwxr-x 3 netsw users 512 Jul 9 03:24 Misc/ drwxrwxr-x 9 netsw users 512 Aug 1 16:33 Network/ drwxrwxr-x 2 netsw users 512 Jul 9 05:53 Office/ drwxrwxr-x 7 netsw users 512 Jul 9 09:24 SoftEng/ drwxrwxr-x 7 netsw users 512 Jul 9 12:17 System/ drwxrwxr-x 12 netsw users 512 Aug 3 20:15 Typesetting/ drwxrwxr-x 10 netsw users 512 Jul 9 14:08 X11/
1996年7月,我决定通过一个漂亮的Web接口公开我的收藏。“漂亮”是指提供一个接口以直接浏览整个目录结构,同时不对这个结构做任何改变 - 甚至也不在结构顶部放置CGI脚本。为什么呢?因为这个结构还要能够被FTP访问,而且我不希望其中有任何Web或者CGI的成分。
这个方案分为两个部分:第一个部分,是用于在空闲时间建立所有目录页面的CGI脚本集。我把它们放在/e/netsw/.www/
,如下:
-rw-r--r-- 1 netsw users 1318 Aug 1 18:10 .wwwacl drwxr-xr-x 18 netsw users 512 Aug 5 15:51 DATA/ -rw-rw-rw- 1 netsw users 372982 Aug 5 16:35 LOGFILE -rw-r--r-- 1 netsw users 659 Aug 4 09:27 TODO -rw-r--r-- 1 netsw users 5697 Aug 1 18:01 netsw-about.html -rwxr-xr-x 1 netsw users 579 Aug 2 10:33 netsw-access.pl -rwxr-xr-x 1 netsw users 1532 Aug 1 17:35 netsw-changes.cgi -rwxr-xr-x 1 netsw users 2866 Aug 5 14:49 netsw-home.cgi drwxr-xr-x 2 netsw users 512 Jul 8 23:47 netsw-img/ -rwxr-xr-x 1 netsw users 24050 Aug 5 15:49 netsw-lsdir.cgi -rwxr-xr-x 1 netsw users 1589 Aug 3 18:43 netsw-search.cgi -rwxr-xr-x 1 netsw users 1885 Aug 1 17:41 netsw-tree.cgi -rw-r--r-- 1 netsw users 234 Jul 30 16:35 netsw-unlimit.lst
其中的DATA/
子目录包含了上述目录结构,即实在的net.sw,由rdist
在需要的时候自动更新。第二个部分的遗留问题是:如何连接这两个结构为一个平滑观感的URL树?我希望在运行适当的CGI脚本而使用各种URL的时候,使用户感觉不到DATA/
目录的存在。方案如下:首先,我把下列配置放在服务器上DocumentRoot
中的针对目录的配置文件里,以重写公布的URL /net.sw/
为内部路径 /e/netsw
:
RewriteRule ^net.sw$ net.sw/ [R] RewriteRule ^net.sw/(.*)$ e/netsw/$1
第一条规则是针对遗漏后缀斜杠的请求的!第二条规则才是真正实现功能的。接着,就是放在针对目录的配置文件/e/netsw/.www/.wwwacl
中的杀手级的配置了:
Options ExecCGI FollowSymLinks Includes MultiViews RewriteEngine on # we are reached via /net.sw/ prefix RewriteBase /net.sw/ # first we rewrite the root dir to # the handling cgi script RewriteRule ^$ netsw-home.cgi [L] RewriteRule ^index\.html$ netsw-home.cgi [L] # strip out the subdirs when # the browser requests us from perdir pages RewriteRule ^.+/(netsw-[^/]+/.+)$ $1 [L] # and now break the rewriting for local files RewriteRule ^netsw-home\.cgi.* - [L] RewriteRule ^netsw-changes\.cgi.* - [L] RewriteRule ^netsw-search\.cgi.* - [L] RewriteRule ^netsw-tree\.cgi$ - [L] RewriteRule ^netsw-about\.html$ - [L] RewriteRule ^netsw-img/.*$ - [L] # anything else is a subdir which gets handled # by another cgi script RewriteRule !^netsw-lsdir\.cgi.* - [C] RewriteRule (.*) netsw-lsdir.cgi/$1
阅读提示:
L
(最后),和无对应项('-
')!
(非),和标志C
(链)mod_imap
许多人都希望在从NCSA网站服务器向较现代的Apache网站服务器转移中实现平滑过渡,即希望老的NCSA imagemap
程序能在Apache的较现代的mod_imap
支持下正常运作。但问题在于,到处都是通过/cgi-bin/imagemap/path/to/page.map
引用imagemap
程序的连接,而在Apache下,应该写成/path/to/page.map
。
使用全局规则在空闲时间去除所有这些请求的前缀:
RewriteEngine on RewriteRule ^/cgi-bin/imagemap(.*) $1 [PT]
有时会有必要使网站服务器在多个目录中搜索页面,对此,MultiViews或者其他技术无能为力。
编制一个明确的规则集以搜索目录中的文件。
RewriteEngine on # first try to find it in custom/... # ...and if found stop and be happy: RewriteCond /your/docroot/dir1/%{REQUEST_FILENAME} -f RewriteRule ^(.+) /your/docroot/dir1/$1 [L] # second try to find it in pub/... # ...and if found stop and be happy: RewriteCond /your/docroot/dir2/%{REQUEST_FILENAME} -f RewriteRule ^(.+) /your/docroot/dir2/$1 [L] # else go on for other Alias or ScriptAlias directives, # etc. RewriteRule ^(.+) - [PT]
如果希望保持请求之间的状态信息,但又不希望使用CGI来包装所有页面,而只通过分离URL中的有用信息来编码。
可以用一个规则集来分离出状态信息,并设置环境变量以备此后用于XSSI或CGI。如此,一个/foo/S=java/bar/
的URL会被解析为/foo/bar/
,而环境变量STATUS
则被设置为"java"。
RewriteEngine on RewriteRule ^(.*)/S=([^/]+)/(.*) $1/$3 [E=STATUS:$2]
如果需要为用户username支持一个www.username.host.domain.com
的主页,但不是用在此机器上建虚拟主机的方法,而是用仅在此机器上增加一个DNS记录的方法实现。
对HTTP/1.0的请求,这是无法实现的;但是对HTTP/1.1的在HTTP头中包含有主机名的请求,可以用以下规则集来内部地重写http://www.username.host.com/anypath
为/home/username/anypath
:
RewriteEngine on RewriteCond %{HTTP_HOST} ^www\.[^.]+\.host\.com$ RewriteRule ^(.+) %{HTTP_HOST}$1 [C] RewriteRule ^www\.([^.]+)\.host\.com(.*) /home/$1$2
对不是来自本地域ourdomain.com
的外来访问者的请求,重定向其宿主目录URL到另一个网站服务器www.somewhere.com
,有时这种做法也会用在虚拟主机的上下文中。
只须一个重写条件:
RewriteEngine on RewriteCond %{REMOTE_HOST} !^.+\.ourdomain\.com$ RewriteRule ^(/~.+) http://www.somewhere.com/$1 [R,L]
如何重写URL以重定向对网站服务器A的失败请求到服务器B,是一个常见的问题。一般,可以用Perl写的CGI脚本通过ErrorDocument
来解决,此外,还有mod_rewrite
方案。但是须注意,这种方法的执行效率不如用ErrorDocument
的CGI脚本!
第一种方案,有最好的性能而灵活性欠佳,出错概率小所以安全:
RewriteEngine on RewriteCond /your/docroot/%{REQUEST_FILENAME} !-f RewriteRule ^(.+) http://webserverB.dom/$1
但是其问题在于,它只对位于DocumentRoot
中的页面有效。虽然可以增加更多的条件(比如同时还处理宿主目录,等等),但是还有一个更好的方法:
RewriteEngine on RewriteCond %{REQUEST_URI} !-U RewriteRule ^(.+) http://webserverB.dom/$1
这种方法使用了mod_rewrite
提供的“向前参照(look-ahead)”的功能,是一种对所有URL类型都有效而且安全的方法。但是,对网站服务器的性能会有影响,所以如果网站服务器有一个强大的CPU,那就用这个方法。而在慢速机器上,可以用第一种方法,或者用性能更好的ErrorDocument
CGI脚本。
有时候,我们会需要更多的对重定向URL的(有关字符转义机制方面的)控制。通常,Apache内核中的URL转义函数uri_escape()
同时还会对anchor转义,即,类似"url#anchor
"的URL,因此,你不能用mod_rewrite
对此类URL直接重定向。那么如何实现呢?
必须用NPH-CGI脚本使它自己重定向,因为对NPH(non-parseable headers [无须解析的HTTP头])不会发生转义操作。首先,在针对服务器的配置中(应该位于所有重写规则的最后),引入一种新的URL类型xredirect:
:
RewriteRule ^xredirect:(.+) /path/to/nph-xredirect.cgi/$1 \ [T=application/x-httpd-cgi,L]
以强制所有带xredirect:
前缀的URL被传送到如下的nph-xredirect.cgi
程序:
#!/path/to/perl ## ## nph-xredirect.cgi -- NPH/CGI script for extended redirects ## Copyright (c) 1997 Ralf S. Engelschall, All Rights Reserved. ## $| = 1; $url = $ENV{'PATH_INFO'}; print "HTTP/1.0 302 Moved Temporarily\n"; print "Server: $ENV{'SERVER_SOFTWARE'}\n"; print "Location: $url\n"; print "Content-type: text/html\n"; print "\n"; print "<html>\n"; print "<head>\n"; print "<title>302 Moved Temporarily (EXTENDED)</title>\n"; print "</head>\n"; print "<body>\n"; print "<h1>Moved Temporarily (EXTENDED)</h1>\n"; print "The document has moved <a HREF=\"$url\">here</a>.<p>\n"; print "</body>\n"; print "</html>\n"; ##EOF##
这是一种可以重定向所有URL类型的方法,包括不被mod_rewrite
直接支持的类型。所以,还可以这样重定向news:newsgroup
:
RewriteRule ^anyurl xredirect:news:newsgroup
[R]
或[R,L]
,因为xredirect:
会在稍后被其特殊的传送规则扩展。你知道http://www.perl.com/CPAN的CPAN(Comprehensive Perl Archive Network)吗?它实现了一个重定向以提供,全世界的CPAN镜像中离访问者最近的一个FTP站点,也可以称之为FTP访问多路复用服务。CPAN是通过CGI脚本实现的,那么用mod_rewrite
如何实现呢?
首先,我们注意到mod_rewrite
从3.0.0版本开始,还可以重写"ftp:
"类型。其次,对客户端顶级域名的路径最近的求取可以用RewriteMap
实现。利用链式规则集,并用顶级域名作为查找多路复用地图的键,可以这样做:
RewriteEngine on RewriteMap multiplex txt:/path/to/map.cxan RewriteRule ^/CxAN/(.*) %{REMOTE_HOST}::$1 [C] RewriteRule ^.+\.([a-zA-Z]+)::(.*)$ ${multiplex:$1|ftp.default.dom}$2 [R,L]
## ## map.cxan -- Multiplexing Map for CxAN ## de ftp://ftp.cxan.de/CxAN/ uk ftp://ftp.cxan.uk/CxAN/ com ftp://ftp.cxan.com/CxAN/ : ##EOF##
在页面内容依时间不同而变化的场合,比如重定向特定页面,许多网管仍然采用CGI脚本的方法,如何用mod_rewrite
来实现呢?
有许多类似TIME_xxx
的变量可以用在重写条件中,利用<STRING
,>STRING
和=STRING
的类型比较,并加以连接,就可以实现依赖于时间的重写:
RewriteEngine on RewriteCond %{TIME_HOUR}%{TIME_MIN} >0700 RewriteCond %{TIME_HOUR}%{TIME_MIN} <1900 RewriteRule ^foo\.html$ foo.day.html RewriteRule ^foo\.html$ foo.night.html
此例使URL foo.html
在07:00-19:00
时指向foo.day.html
,而在其余时间,则指向foo.night.html
,对主页是一个不错的功能...
在转变了大批.html
文件为.phtml,使
文档.YYYY
过渡成为文档.XXXX
后,如何保持URL的向前兼容(仍然虚拟地存在)?
只须按基准文件名重写,并测试带有新的扩展名的文件是否存在,如果存在,则用新的,否则,仍然用原来的。
# backward compatibility ruleset for # rewriting document.html to document.phtml # when and only when document.phtml exists # but no longer document.html RewriteEngine on RewriteBase /~quux/ # parse out basename, but remember the fact RewriteRule ^(.*)\.html$ $1 [C,E=WasHTML:yes] # rewrite to document.phtml if exists RewriteCond %{REQUEST_FILENAME}.phtml -f RewriteRule ^(.*)$ $1.phtml [S=1] # else reverse the previous basename cutout RewriteCond %{ENV:WasHTML} ^yes$ RewriteRule ^(.*)$ $1.html
假定已经把文件bar.html
改名为foo.html
,需要对老的URL向前兼容,即让用户仍然可以使用老的URL,而感觉不到文件被改名了。
通过以下规则内部地重写老的URL为新的:
RewriteEngine on RewriteBase /~quux/ RewriteRule ^foo\.html$ bar.html
仍然假定已经把文件bar.html
改名为foo.html
,需要对老的URL向前兼容,但是要让用户得到文件被改名的暗示,即,其浏览器的地址栏中显示的是新的URL。
作一个HTTP的强制重定向以改变浏览器和用户界面上的显示:
RewriteEngine on RewriteBase /~quux/ RewriteRule ^foo\.html$ bar.html [R]
至少对重要的顶级页面,有时候有必要提供依赖于浏览器的最佳的内容,即对最新的Netscape提供最大化的版本,对Lynx提供最小化的版本,而对其他的浏览器则提供一个功能一般的版本。
对此,内容协商无能为力,因为浏览器不提供其那种形式的类型,所以只能在HTTP头"User-Agent"上想办法。以下规则集可以完成这个操作:如果HTTP头"User-Agent"以"Mozilla/3"开头,则页面foo.html
被重写为foo.NS.html
,而后重写操作终止;如果是"Lynx"或者版本号为1和2的"Mozilla",则重写为foo.20.html
;而其他所有的浏览器收到的页面则是foo.32.html
:
RewriteCond %{HTTP_USER_AGENT} ^Mozilla/3.* RewriteRule ^foo\.html$ foo.NS.html [L] RewriteCond %{HTTP_USER_AGENT} ^Lynx/.* [OR] RewriteCond %{HTTP_USER_AGENT} ^Mozilla/[12].* RewriteRule ^foo\.html$ foo.20.html [L] RewriteRule ^foo\.html$ foo.32.html [L]
假定,需要在我们的名称空间里加入其他远程主机的页面。对FTP服务器,可以用mirror
程序以在本地机器上维持一个对远程数据的最新的拷贝;对网站服务器,可以用类似的用于HTTP的webcopy
程序。但这两种技术都有一个主要的缺点:此本地拷贝必须通过这个程序的执行来更新。所以,比较好的方法是,不采用静态镜像,而采用动态镜像,即,在有数据请求时自动更新(远程主机上更新的数据)。
为此,使用Proxy Throughput功能(flag [P]
),以映射远程页面甚至整个远程网络区域到我们的名称空间:
RewriteEngine on RewriteBase /~quux/ RewriteRule ^hotsheet/(.*)$ http://www.tstimpreso.com/hotsheet/$1 [P]
RewriteEngine on RewriteBase /~quux/ RewriteRule ^usa-news\.html$ http://www.quux-corp.com/news/index.html [P]
RewriteEngine on RewriteCond /mirror/of/remotesite/$1 -U RewriteRule ^http://www\.remotesite\.com/(.*)$ /mirror/of/remotesite/$1
这是一种在受防火墙保护的(内部的)Intranet(www2.quux-corp.dom
)上保存和维护实际数据,而虚拟地运行企业级(外部的)Internet网站服务器(www.quux-corp.dom
)的巧妙的方法。这种方法是外部服务器在空闲时间从内部服务器取得被请求的数据。
首先,必须确保防火墙对内部服务器的保护,并只允许此外部服务器取得数据。对包过滤(packet-filtering)防火墙,可以如下制定防火墙规则:
ALLOW Host www.quux-corp.dom Port >1024 --> Host www2.quux-corp.dom Port 80 DENY Host * Port * --> Host www2.quux-corp.dom Port 80
按你的实际配置,只要对上例稍作调整即可。接着,建立通过代理后台获取丢失数据的mod_rewrite
规则:
RewriteRule ^/~([^/]+)/?(.*) /home/$1/.www/$2 RewriteCond %{REQUEST_FILENAME} !-f RewriteCond %{REQUEST_FILENAME} !-d RewriteRule ^/home/([^/]+)/.www/?(.*) http://www2.quux-corp.dom/~$1/pub/$2 [P]
如何均衡www.foo.com
的负载到www[0-5].foo.com
(一共是6个服务器)?
这个问题有许多可能的解决方案,在此,我们讨论通称为“基于DNS(DNS-based)的”方案,和特殊的使用mod_rewrite
的方案:
最简单的方法是用BIND
的DNS循环特性,只要按惯例设置www[0-9].foo.com
的DNS的A(地址)记录,如:
www0 IN A 1.2.3.1 www1 IN A 1.2.3.2 www2 IN A 1.2.3.3 www3 IN A 1.2.3.4 www4 IN A 1.2.3.5 www5 IN A 1.2.3.6
然后,增加以下各项:
www IN CNAME www0.foo.com. IN CNAME www1.foo.com. IN CNAME www2.foo.com. IN CNAME www3.foo.com. IN CNAME www4.foo.com. IN CNAME www5.foo.com. IN CNAME www6.foo.com.
注意,上述看起来似乎是错误的,但事实上,它的确是BIND
中的一个预期的特性,而且也可以这样用。无论如何,现在www.foo.com
已经被解析,BIND
可以给出www0-www6
- 虽然每次在次序上会有轻微的置换/循环,客户端的请求可以被分散到各个服务器。可是,这并不是一个优秀的负载均衡方案,因为,DNS解析信息可以被网络中其他名称服务器缓冲,而一旦www.foo.com
被解析为wwwN.foo.com
,则其后继请求都将被送往www.foo.com
。但是最终结果是正确的,因为请求的总量的确被分散到各个服务器了
一种成熟的基于DNS的负载均衡方法是使用http://www.stanford.edu/~schemers/docs/lbnamed/lbnamed.html的lbnamed
程序,它是一个Perl 5程序,带有若干辅助工具,实现了真正的基于DNS的负载均衡。
这是一个使用mod_rewrite
及其代理吞吐特性的方法。首先,在DNS记录中,将www0.foo.com
固定为www.foo.com
,如下:
www IN CNAME www0.foo.com.
其次,将www0.foo.com
转换为一个专职代理服务器,即,由这个机器把所有到来的URL通过内部代理分散到另外5个服务器(www1-www5
)。为此,必须建立一个规则集,对所有URL调用一个负载均衡脚本lb.pl
。
RewriteEngine on RewriteMap lb prg:/path/to/lb.pl RewriteRule ^/(.+)$ ${lb:$1} [P,L]
以下是lb.pl
:
#!/path/to/perl ## ## lb.pl -- load balancing script ## $| = 1; $name = "www"; # the hostname base $first = 1; # the first server (not 0 here, because 0 is myself) $last = 5; # the last server in the round-robin $domain = "foo.dom"; # the domainname $cnt = 0; while (<STDIN>) { $cnt = (($cnt+1) % ($last+1-$first)); $server = sprintf("%s%d.%s", $name, $cnt+$first, $domain); print "http://$server/$_"; } ##EOF##
www0.foo.com
似乎也会超载呀?答案是:没错,它的确会超载,但是它超载的仅仅是简单的代理吞吐请求!所有诸如SSI、CGI、ePerl等等的处理完全是由其他机器完成的,这个才是要点。还有一个硬件解决方案。Cisco有一个叫LocalDirector的东西,实现了TCP/IP层的负载均衡,事实上,它是一个位于网站集群前端的电路级网关。如果你有足够资金而且的确需要高性能的解决方案,那么可以用这个。
## ## apache-rproxy.conf -- Apache configuration for Reverse Proxy Usage ## # server type ServerType standalone Listen 8000 MinSpareServers 16 StartServers 16 MaxSpareServers 16 MaxClients 16 MaxRequestsPerChild 100 # server operation parameters KeepAlive on MaxKeepAliveRequests 100 KeepAliveTimeout 15 Timeout 400 IdentityCheck off HostnameLookups off # paths to runtime files PidFile /path/to/apache-rproxy.pid LockFile /path/to/apache-rproxy.lock ErrorLog /path/to/apache-rproxy.elog CustomLog /path/to/apache-rproxy.dlog "%{%v/%T}t %h -> %{SERVER}e URL: %U" # unused paths ServerRoot /tmp DocumentRoot /tmp CacheRoot /tmp RewriteLog /dev/null TransferLog /dev/null TypesConfig /dev/null AccessConfig /dev/null ResourceConfig /dev/null # speed up and secure processing <Directory /> Options -FollowSymLinks -SymLinksIfOwnerMatch AllowOverride None </Directory> # the status page for monitoring the reverse proxy <Location /apache-rproxy-status> SetHandler server-status </Location> # enable the URL rewriting engine RewriteEngine on RewriteLogLevel 0 # define a rewriting map with value-lists where # mod_rewrite randomly chooses a particular value RewriteMap server rnd:/path/to/apache-rproxy.conf-servers # make sure the status page is handled locally # and make sure no one uses our proxy except ourself RewriteRule ^/apache-rproxy-status.* - [L] RewriteRule ^(http|ftp)://.* - [F] # now choose the possible servers for particular URL types RewriteRule ^/(.*\.(cgi|shtml))$ to://${server:dynamic}/$1 [S=1] RewriteRule ^/(.*)$ to://${server:static}/$1 # and delegate the generated URL by passing it # through the proxy module RewriteRule ^to://([^/]+)/(.*) http://$1/$2 [E=SERVER:$1,P,L] # and make really sure all other stuff is forbidden # when it should survive the above rules... RewriteRule .* - [F] # enable the Proxy module without caching ProxyRequests on NoCache * # setup URL reverse mapping for redirect reponses ProxyPassReverse / http://www1.foo.dom/ ProxyPassReverse / http://www2.foo.dom/ ProxyPassReverse / http://www3.foo.dom/ ProxyPassReverse / http://www4.foo.dom/ ProxyPassReverse / http://www5.foo.dom/ ProxyPassReverse / http://www6.foo.dom/
## ## apache-rproxy.conf-servers -- Apache/mod_rewrite selection table ## # list of backend servers which serve static # pages (HTML files and Images, etc.) static www1.foo.dom|www2.foo.dom|www3.foo.dom|www4.foo.dom # list of backend servers which serve dynamically # generated page (CGI programs or mod_perl scripts) dynamic www5.foo.dom|www6.foo.dom
网上有许多很技巧的CGI程序,但是用法晦涩,许多网管弃之不用。即使是Apache的MEME类型的动作处理器,也仅仅在CGI程序不需要在其输入中包含特殊URL(PATH_INFO
和QUERY_STRINGS
)时才很好用。首先,配置一种新的后缀为.scgi
(for secure CGI)文件类型,其处理器是很常见的cgiwrap
程序。问题是:如果使用同类URL规划(见上述),而用户宿主目录中的一个文件的URL是/u/user/foo/bar.scgi
,可是cgiwrap
要求的URL的格式是/~user/foo/bar.scgi/
,以下规则解决了这个问题:
RewriteRule ^/[uge]/([^/]+)/\.www/(.+)\.scgi(.*) ... ... /internal/cgi/user/cgiwrap/~$1/$2.scgi$3 [NS,T=application/x-http-cgi]
另外,假设需要使用其他程序:wwwlog
(显示access.log
中的一个URL子树)和wwwidx
(对一个URL子树运行Glimpse),则必须对这些程序提供URL区域作为其操作对象。比如,对/u/user/foo/
执行swwidx
程序的超链是这样的:
/internal/cgi/user/swwidx?i=/u/user/foo/
其缺点是,必须同时硬编码超链中的区域和CGI的路径,如果重组了这个区域,就需要花费大量时间来修改各个超链。
方案是用一个特殊的新的URL格式,自动拼装CGI参数:
RewriteRule ^/([uge])/([^/]+)(/?.*)/\* /internal/cgi/user/wwwidx?i=/$1/$2$3/ RewriteRule ^/([uge])/([^/]+)(/?.*):log /internal/cgi/user/wwwlog?f=/$1/$2$3
现在,这个搜索到/u/user/foo/
的超链简化成了:
HREF="*"
它会被内部地自动转换为
/internal/cgi/user/wwwidx?i=/u/user/foo/
如此,可以为使用:log
的超链,拼装出调用CGI程序的参数。
如何无缝转换静态页面foo.html
为动态的foo.cgi
,而不为浏览器/用户所察觉。
只须重写此URL为CGI-script,以强制为可以作为CGI-script运行的正确的MIME类型。如此,对/~quux/foo.html
的请求其实会执行/~quux/foo.cgi
。
RewriteEngine on RewriteBase /~quux/ RewriteRule ^foo\.html$ foo.cgi [T=application/x-httpd-cgi]
这是一个很难解的功能:动态生成的静态页面,即,它应该作为静态页面发送(从文件系统中读出,然后直接发出去),但是如果它丢失了,则由服务器动态生成。如此,可以静态地提供CGI生成的页面,除非有人(或者是一个cronjob)删除了这些静态页面,而且其内容可以得到更新。
RewriteCond %{REQUEST_FILENAME} !-s RewriteRule ^page\.html$ page.cgi [T=application/x-httpd-cgi,L]
这样,如果page.html
不存在或者文件大小为null,则对page.html
的请求会导致page.cgi
的运行。其中奥妙在于,page.cgi
是一个将输出写入page.html
的(同时也写入STDOUT
)的常规的CGI脚本,执行完毕,服务器则将page.html
的内容发出。如果网管需要强制更新其内容,只须删除page.html
即可(通常由一个cronjob完成)。
建立一个复杂的页面,能够在用编辑器写了一个更新的版本时自动在浏览器上得到刷新,这不是很好吗?这可能吗?
这是可行的! 这需要综合利用MIME多成分、网站服务器的NPH和mod_rewrite
的URL操控特性。首先,建立一个新的URL特性:对在文件系统中更新时需要刷新的所有URL加上:refresh
。
RewriteRule ^(/[uge]/[^/]+/?.*):refresh /internal/cgi/apache/nph-refresh?f=$1
然后,修改URL
/u/foo/bar/page.html:refresh
以内部地操控此URL
/internal/cgi/apache/nph-refresh?f=/u/foo/bar/page.html
接着就是NPH-CGI脚本部分了。虽然,人们常说"left as an exercise to the reader";-),我还是给出答案了。
#!/sw/bin/perl ## ## nph-refresh -- NPH/CGI script for auto refreshing pages ## Copyright (c) 1997 Ralf S. Engelschall, All Rights Reserved. ## $| = 1; # split the QUERY_STRING variable @pairs = split(/&/, $ENV{'QUERY_STRING'}); foreach $pair (@pairs) { ($name, $value) = split(/=/, $pair); $name =~ tr/A-Z/a-z/; $name = 'QS_' . $name; $value =~ s/%([a-fA-F0-9][a-fA-F0-9])/pack("C", hex($1))/eg; eval "\$$name = \"$value\""; } $QS_s = 1 if ($QS_s eq ''); $QS_n = 3600 if ($QS_n eq ''); if ($QS_f eq '') { print "HTTP/1.0 200 OK\n"; print "Content-type: text/html\n\n"; print "<b>ERROR</b>: No file given\n"; exit(0); } if (! -f $QS_f) { print "HTTP/1.0 200 OK\n"; print "Content-type: text/html\n\n"; print "<b>ERROR</b>: File $QS_f not found\n"; exit(0); } sub print_http_headers_multipart_begin { print "HTTP/1.0 200 OK\n"; $bound = "ThisRandomString12345"; print "Content-type: multipart/x-mixed-replace;boundary=$bound\n"; &print_http_headers_multipart_next; } sub print_http_headers_multipart_next { print "\n--$bound\n"; } sub print_http_headers_multipart_end { print "\n--$bound--\n"; } sub displayhtml { local($buffer) = @_; $len = length($buffer); print "Content-type: text/html\n"; print "Content-length: $len\n\n"; print $buffer; } sub readfile { local($file) = @_; local(*FP, $size, $buffer, $bytes); ($x, $x, $x, $x, $x, $x, $x, $size) = stat($file); $size = sprintf("%d", $size); open(FP, "<$file"); $bytes = sysread(FP, $buffer, $size); close(FP); return $buffer; } $buffer = &readfile($QS_f); &print_http_headers_multipart_begin; &displayhtml($buffer); sub mystat { local($file) = $_[0]; local($time); ($x, $x, $x, $x, $x, $x, $x, $x, $x, $mtime) = stat($file); return $mtime; } $mtimeL = &mystat($QS_f); $mtime = $mtime; for ($n = 0; $n < $QS_n; $n++) { while (1) { $mtime = &mystat($QS_f); if ($mtime ne $mtimeL) { $mtimeL = $mtime; sleep(2); $buffer = &readfile($QS_f); &print_http_headers_multipart_next; &displayhtml($buffer); sleep(5); $mtimeL = &mystat($QS_f); last; } sleep($QS_s); } } &print_http_headers_multipart_end; exit(0); ##EOF##
Apache的<VirtualHost>
功能很强,在有几十个虚拟主机的情况下运行得很好,但是如果你是ISP,需要提供几百个虚拟主机,那么这就不是一个最佳的选择了。
为此,需要用代理吞吐(Proxy Throughput)功能(flag [P]
)映射远程页面甚至整个远程网络区域到自己的名称空间:
## ## vhost.map ## www.vhost1.dom:80 /path/to/docroot/vhost1 www.vhost2.dom:80 /path/to/docroot/vhost2 : www.vhostN.dom:80 /path/to/docroot/vhostN
## ## httpd.conf ## : # use the canonical hostname on redirects, etc. UseCanonicalName on : # add the virtual host in front of the CLF-format CustomLog /path/to/access_log "%{VHOST}e %h %l %u %t \"%r\" %>s %b" : # enable the rewriting engine in the main server RewriteEngine on # define two maps: one for fixing the URL and one which defines # the available virtual hosts with their corresponding # DocumentRoot. RewriteMap lowercase int:tolower RewriteMap vhost txt:/path/to/vhost.map # Now do the actual virtual host mapping # via a huge and complicated single rule: # # 1. make sure we don't map for common locations RewriteCond %{REQUEST_URL} !^/commonurl1/.* RewriteCond %{REQUEST_URL} !^/commonurl2/.* : RewriteCond %{REQUEST_URL} !^/commonurlN/.* # # 2. make sure we have a Host header, because # currently our approach only supports # virtual hosting through this header RewriteCond %{HTTP_HOST} !^$ # # 3. lowercase the hostname RewriteCond ${lowercase:%{HTTP_HOST}|NONE} ^(.+)$ # # 4. lookup this hostname in vhost.map and # remember it only when it is a path # (and not "NONE" from above) RewriteCond ${vhost:%1} ^(/.*)$ # # 5. finally we can map the URL to its docroot location # and remember the virtual host for logging puposes RewriteRule ^/(.*)$ %1/$1 [E=VHOST:${lowercase:%{HTTP_HOST}}] :
如何阻止一个完全匿名的robot取得特定网络区域的页面?一个/robots.txt
文件可以包含若干"Robot Exclusion Protocol(robot排除协议)"的行,但不足以阻止此类robot。
可以用一个规则集以拒绝对网络区域/~quux/foo/arc/
(对一个很深的目录区域进行列表可能会使服务器产生很大的负载)的访问。还必须确保仅阻止特定的robot,就是说,仅仅阻止robot访问主机是不够的,这样会同时也阻止了用户访问该主机。为此,就需要对HTTP头的User-Agent信息作匹配。
RewriteCond %{HTTP_USER_AGENT} ^NameOfBadRobot.* RewriteCond %{REMOTE_ADDR} ^123\.45\.67\.[8-9]$ RewriteRule ^/~quux/foo/arc/.+ - [F]
假设,http://www.quux-corp.de/~quux/
有一些内嵌图片的页面,这些图片很好,所以就有人用超链连到他们自己的页面中了。由于这样徒然增加了我们的服务器的流量,因此,我们不愿意这种事情发生。
虽然,我们不能100%地保护这些图片不被写入别人的页面,但至少可以对发出HTTP Referer头的浏览器加以限制。
RewriteCond %{HTTP_REFERER} !^$ RewriteCond %{HTTP_REFERER} !^http://www.quux-corp.de/~quux/.*$ [NC] RewriteRule .*\.gif$ - [F]
RewriteCond %{HTTP_REFERER} !^$ RewriteCond %{HTTP_REFERER} !.*/foo-with-gif\.html$ RewriteRule ^inlined-in-foo\.gif$ - [F]
如何拒绝一批外部列表中的主机对我们服务器的使用?
For Apache >= 1.3b6:
RewriteEngine on RewriteMap hosts-deny txt:/path/to/hosts.deny RewriteCond ${hosts-deny:%{REMOTE_HOST}|NOT-FOUND} !=NOT-FOUND [OR] RewriteCond ${hosts-deny:%{REMOTE_ADDR}|NOT-FOUND} !=NOT-FOUND RewriteRule ^/.* - [F]
For Apache <= 1.3b6:
RewriteEngine on RewriteMap hosts-deny txt:/path/to/hosts.deny RewriteRule ^/(.*)$ ${hosts-deny:%{REMOTE_HOST}|NOT-FOUND}/$1 RewriteRule !^NOT-FOUND/.* - [F] RewriteRule ^NOT-FOUND/(.*)$ ${hosts-deny:%{REMOTE_ADDR}|NOT-FOUND}/$1 RewriteRule !^NOT-FOUND/.* - [F] RewriteRule ^NOT-FOUND/(.*)$ /$1
## ## hosts.deny ## ## ATTENTION! This is a map, not a list, even when we treat it as such. ## mod_rewrite parses it for key/value pairs, so at least a ## dummy value "-" must be present for each entry. ## 193.102.180.41 - bsdti1.sdm.de - 192.76.162.40 -
如何拒绝某个主机或者来自特定主机的用户使用Apache代理?
首先,要确保Apache网站服务器在编译时配置文件中mod_rewrite
在mod_proxy
的下面(!),使它在mod_proxy
之前被调用。然后,如下拒绝某个主机...
RewriteCond %{REMOTE_HOST} ^badhost\.mydomain\.com$ RewriteRule !^http://[^/.]\.mydomain.com.* - [F]
...如下拒绝user@host-dependent:
RewriteCond %{REMOTE_IDENT}@%{REMOTE_HOST} ^badguy@badhost\.mydomain\.com$ RewriteRule !^http://[^/.]\.mydomain.com.* - [F]
有时候,会需要一种非常特殊的认证,即,对一组明确指定的用户,允许其访问,而没有(在使用mod_access
的基本认证方法时可能会出现的)任何提示。
可是使用一个重写条件列表来排除所有的朋友:
RewriteCond %{REMOTE_IDENT}@%{REMOTE_HOST} !^[email protected]\.com$ RewriteCond %{REMOTE_IDENT}@%{REMOTE_HOST} !^friend2@client2.quux-corp\.com$ RewriteCond %{REMOTE_IDENT}@%{REMOTE_HOST} !^friend3@client3.quux-corp\.com$ RewriteRule ^/~quux/only-for-friends/ - [F]
如何配置一个基于HTTP头"Referer"的反射器以反射到任意数量的提交页面?
使用这个很技巧的规则集...
RewriteMap deflector txt:/path/to/deflector.map RewriteCond %{HTTP_REFERER} !="" RewriteCond ${deflector:%{HTTP_REFERER}} ^-$ RewriteRule ^.* %{HTTP_REFERER} [R,L] RewriteCond %{HTTP_REFERER} !="" RewriteCond ${deflector:%{HTTP_REFERER}|NOT-FOUND} !=NOT-FOUND RewriteRule ^.* ${deflector:%{HTTP_REFERER}} [R,L]
... 并结合对应的重写地图:
## ## deflector.map ## http://www.badguys.com/bad/index.html - http://www.badguys.com/bad/index2.html - http://www.badguys.com/bad/index3.html http://somewhere.com/
它可以自动将请求(在地图中指定了"-
"值的时候)反射回其提交页面,或者(在地图中URL有第二个参数时)反射到一个特定的URL。
一个常见的问题: 如何解决似乎无法用mod_rewrite
解决的FOO/BAR/QUUX/之类的问题?
可以使用一个与RewriteMap
功能相同的外部RewriteMap
程序,一旦它在Apache启动时被执行,则从STDIN
接收被请求的URL,并将处理过(通常是重写过的)的URL(以相同顺序!)在STDOUT
输出。
RewriteEngine on RewriteMap quux-map prg:/path/to/map.quux.pl RewriteRule ^/~quux/(.*)$ /~quux/${quux-map:$1}
#!/path/to/perl # disable buffered I/O which would lead # to deadloops for the Apache server $| = 1; # read URLs one per line from stdin and # generate substitution URL on stdout while (<>) { s|^foo/|bar/|; print $_; }
这是一个作演示的例子,只是把所有的URL /~quux/foo/...
重写为/~quux/bar/...
,而事实上,可以把它修改以获得任何你需要的功能。但是要注意,虽然一般用户都可以使用,可是只有系统管理员才可以定义这样的地图。
现有的语种: en