Apache mod_rewrite重写规则到底怎么写才不绕晕?
本文目录
- mod_rewrite到底在Apache请求里的哪一步动手?
- RewriteRule的正则到底怎么读?
- 标志位L、R、QSA、NC这些到底各管什么?
- RewriteCond怎么给规则加前提条件?
- 为什么 .htaccess里的重写会陷入死循环?
- 想看rewrite到底怎么走的,怎么调试?
- mod_rewrite、mod_alias的Redirect、和反代该怎么分工?
- 常见问题解答
- RewriteRule里的L标志和END标志到底有什么区别?
- 为什么我写了301重定向规则,改了之后浏览器还是跳到老地址?
- 规则明明写对了,.htaccess里就是不生效,可能是什么原因?
- RewriteCond里的 %1和RewriteRule里的 $1是一回事吗?
- 所有的跳转都用mod_rewrite来做是不是更省事?
- 权威参考资料
很多人把mod_rewrite当成一堆抄来就用的咒语,规则一复杂就抓瞎。保哥这篇不教你背配方,而是把引擎拆开讲:重写和重定向到底差在哪、RewriteRule的正则怎么读、L和R和QSA这些标志各管什么、RewriteCond怎么加前提、.htaccess为什么会陷入死循环、出了问题怎么开日志看引擎一步步怎么走。把这套机制吃透,再复杂的规则你也能自己写、自己调,而不是去网上碰运气。
做网站运维和SEO的,绕不开Apache的mod_rewrite。强制HTTPS、去掉www、伪静态、防盗链、旧链接301到新链接——背后都是它在干活。可保哥发现一个普遍现象:大多数人用mod_rewrite的方式是上网搜一段规则,复制粘贴,能跑就谢天谢地,不能跑就再换一段。规则一旦稍微复杂,或者几条规则一打架,立马两眼一抹黑。
根子在于没把引擎本身搞懂。mod_rewrite不是一堆魔法咒语,它是一台有明确执行逻辑的状态机。这篇就专门拆这台机器。它跟保哥之前写的htaccess做SEO的那几层配置清单是互补的:那篇告诉你"为了SEO该配哪些规则",这篇告诉你"这些规则底下的引擎到底怎么运转"。懂了机制,那些清单你才知其所以然。
mod_rewrite到底在Apache请求里的哪一步动手?
先把一个最容易混的概念掰开:重写(rewrite)和重定向(redirect)不是一回事。
重写是内部的,浏览器毫不知情。用户访问 /product/123,服务器内部偷偷把它映射成 /index.php?id=123 去处理,但浏览器地址栏始终显示 /product/123,用户看到的、收藏的、分享的都是那个干净的地址。伪静态就是这么来的。
重定向是外部的,浏览器要重新发一次请求。服务器回一个301或302状态码,外加一个新地址,浏览器收到后地址栏会变成新地址,再发起一次访问。强制HTTPS、去www用的就是这个——你得让浏览器真的换到新URL上去。
这俩的区别直接决定你写规则时加不加 R 标志。不加R就是内部重写,加了 R=301 就是外部永久重定向。搞混了,要么该变的地址没变,要么不该暴露的内部路径全抖给了用户。
那mod_rewrite在请求处理流程的哪一步插手呢?它主要工作在URL到文件名的映射阶段。请求进来,Apache要决定这个URL对应服务器上哪个文件,mod_rewrite就在这个节骨眼上拦截,按你的规则改写目标。开关是 RewriteEngine On——这一句不写,下面的规则全是摆设,引擎压根没启动。这是新手第一大坑:规则写了一堆,忘了开引擎。反过来,遇到规则集体不生效,第一件事就该确认这一句在不在、有没有被前面的注释或条件块挡掉,往往一分钟就能定位到问题,省得对着规则正则一行行死抠。
RewriteRule的正则到底怎么读?
RewriteRule是整个模块的主力,格式就三段:
RewriteRule Pattern Substitution [flags]Pattern是一个正则表达式,用来匹配URL;Substitution是匹配上之后要替换成的目标;方括号里是可选的标志位。一条规则的执行逻辑是:拿当前URL去套Pattern,套上了就改写成Substitution,没套上就跳过这条看下一条。
正则部分有几个符号必须会读。^ 是开头锚点,$ 是结尾锚点,^foo/(.*)$ 意思是"以foo/ 开头、后面跟任意字符直到结尾"。括号 ( ) 是捕获组,被它框住的部分会被记下来,在Substitution里用 $1、$2 按顺序取回。举个最常见的例子:
RewriteRule ^article/([0-9]+)\.html$ /index.php?id=$1 [L]这条把 /article/123.html 改写成 /index.php?id=123,其中 ([0-9]+) 捕获了数字123,到Substitution里用 $1 取回来塞进参数。这就是伪静态最核心的一招。
正则这里有两个新手高频陷阱。一是点号要转义:正则里 . 默认匹配任意字符,你想匹配真正的小数点或文件扩展名前的点,得写成 \.,比如 \.html$,不转义的话 xhtml、ahtml 都会被误匹配。二是贪婪匹配:(.*) 会尽可能多地吞字符,一行里有多个分隔符时容易匹配过头,必要时用 (.*?) 非贪婪或者把字符类收窄成 ([^/]+)(不含斜杠)来精确控制边界。这两个坑写错了不一定报错,但匹配范围悄悄跑偏,行为就诡异了。
这里有个常被忽略的细节:Pattern匹配的内容,在不同上下文里是不一样的。规则写在主配置(httpd.conf或虚拟主机段)里时,Pattern匹配的是去掉开头斜杠的完整URL路径;写在目录级的 .htaccess 里时,Pattern匹配的是去掉了当前目录前缀的那部分。这个差异不搞清楚,同一条规则从httpd.conf搬到 .htaccess里就可能匹配不上。后面讲RewriteBase时还会回到这个点。
还有个特殊写法:Substitution写成一个减号 -,表示"不改写URL,只是给它打上标志位或设环境变量"。比如你想对某类请求直接返回403,就用 RewriteRule pattern - [F],URL不动,只是触发禁止。
标志位L、R、QSA、NC这些到底各管什么?
方括号里的标志位是mod_rewrite最精微也最容易用错的地方。官方文档列了二三十个,日常高频的就那么几个,保哥挑最该懂的讲:
| 标志 | 全称 | 作用 |
|---|---|---|
L | last | 匹配成功就停止往下处理本轮规则 |
R[=code] | redirect | 做外部重定向,R=301 永久、默认是302临时 |
QSA | qsappend | 把原URL的查询串拼到新URL后面,而不是覆盖 |
NC | nocase | 匹配时忽略大小写 |
NE | noescape | 重定向时不对特殊字符做URL转义 |
END | — | 彻底终止重写,连后续的目录级重新处理都不再走 |
F / G | forbidden / gone | 直接返回403 / 410 |
P | proxy | 把请求交给mod_proxy走反向代理 |
几个特别要拎清的点。R默认是302临时重定向,不是301。你做永久跳转一定要显式写 R=301,否则搜索引擎不会把权重转过去——这是SEO上的大事,写错了等于白跳。
L和END的区别很微妙。L只是停止当前这一轮规则处理,但在 .htaccess这种目录上下文里,URL被改写后还会被引擎重新拿去从头过一遍规则集(这正是死循环的温床)。END更狠,它直接宣布"重写到此为止,别再重来",专门用来斩断这种循环。能用END解决的循环问题,比堆一堆条件去防要干净。
QSA管的是查询串拼接。用户访问 /list?page=2,你的规则把 /list 改写成 /index.php?mod=list,不加QSA的话原来那个 page=2 就丢了;加上QSA,引擎会帮你拼成 /index.php?mod=list&page=2。凡是要保留原始查询参数的,别忘了它。
剩下几个次常用但关键时刻能救命的标志也顺一遍。NE(noescape)在重定向时阻止引擎对URL里的特殊字符做编码,比如目标地址里本来就带 # 锚点或者已经编码好的参数,不加NE会被二次转义成乱码。C(chain)把当前规则和下一条链在一起,前一条不匹配就整条链跳过,适合表达"必须同时满足好几步"的逻辑。S=N(skip)匹配成功就跳过紧跟其后的N条规则,常用来做简单的分支。T(type)强制设定响应的MIME类型。这些不必死记,知道有它们,遇到对应需求回去翻标志表就行。
把标志和捕获组拼起来,看一条去www规范化的实例——这是SEO上的标配动作:
RewriteCond %{HTTP_HOST} ^www\.(.+)$ [NC]
RewriteRule ^(.*)$ https://%1/$1 [R=301,L]读法:如果域名以www. 开头,就把www. 后面那段主域名用括号捕获下来(到规则行用 %1 取,注意是百分号不是美元号,因为它来自RewriteCond),再把原始路径用 $1 取回来,拼成不带www的地址做301。一条规则里 %1 和 $1 同时出现、各取各的,是mod_rewrite的经典手筋。
RewriteCond怎么给规则加前提条件?
RewriteRule自己只能看URL路径,可现实中你常要根据别的东西做判断:是不是HTTPS、域名是不是带www、请求的文件存不存在。这时就要请出RewriteCond,它给紧跟其后的RewriteRule加前提条件。格式是:
RewriteCond TestString CondPattern [flags]TestString里能用一大票服务器变量,包在 %{ } 里。最常用的几个:%{HTTP_HOST} 是请求的域名,%{HTTPS} 是否走了HTTPS,%{REQUEST_URI} 请求路径,%{QUERY_STRING} 查询串,还有极其重要的 %{REQUEST_FILENAME}——它代表请求映射到的真实文件路径。
配合文件检测符就能干大事:-f 判断是不是一个存在的文件,-d 判断是不是存在的目录,-l 判断是不是符号链接。现代框架的入口写法,核心就这三行:
RewriteCond %{REQUEST_FILENAME} !-f
RewriteCond %{REQUEST_FILENAME} !-d
RewriteRule ^ /index.php [L]意思是:如果请求的既不是一个真实存在的文件(!-f),也不是一个真实存在的目录(!-d),就统统交给 index.php 处理。这样真实存在的图片、CSS照常直接访问,找不到对应文件的"美化URL"才走前端控制器。WordPress、Laravel的伪静态本质都是这套。
多条RewriteCond默认是"与"的关系,全部满足才执行规则;想改成"或",给前面那条加 [OR] 标志。还有个容易混的:Rule里用 $1 取的是RewriteRule自己捕获组的值,而 %1 取的是上一条RewriteCond捕获组的值,两套反向引用别张冠李戴。
来看一条强制HTTPS的完整规则,把这些拼起来:
RewriteCond %{HTTPS} off
RewriteRule ^(.*)$ https://%{HTTP_HOST}/$1 [R=301,L]读法是:如果当前不是HTTPS(%{HTTPS} off),就把整个路径捕获下来,拼成 https:// 加原域名加原路径,做301永久重定向。这条配上前面HTTPS与证书的处理,就是SEO层面那套HTTPS与规范化的底层实现。
服务器变量这块值得多备几个常用的在手边。除了前面提的几个,%{HTTP_USER_AGENT} 是浏览器/爬虫标识,%{HTTP_REFERER} 是来源页,%{REMOTE_ADDR} 是访客IP,%{REQUEST_METHOD} 是请求方法(GET/POST),%{TIME_HOUR} 之类能拿到当前时间,%{ENV:变量名} 取环境变量。掌握这些变量,你能判断的维度就从单纯的URL扩展到了请求的方方面面。
举个防盗链的实战,靠的就是Referer变量:
RewriteCond %{HTTP_REFERER} !^$
RewriteCond %{HTTP_REFERER} !^https?://(www\.)?zhangwenbao\.com [NC]
RewriteRule \.(jpg|png|webp)$ - [F]读法:如果来源页不为空(排除直接访问),且来源页不是自己的域名,那么对图片请求直接返回403禁止。这样别的网站想盗用你的图片直接热链,就只能拿到一个被拒的响应。注意第一条 !^$ 是放行空Referer,否则浏览器直接打开图片或某些隐私设置会被误伤。
为什么 .htaccess里的重写会陷入死循环?
这是mod_rewrite最经典的翻车现场:浏览器报500,或者直接转圈到超时,日志里一堆"Request exceeded the limit of 10 internal redirects"。罪魁祸首几乎都是死循环。
原理前面埋过伏笔:在 .htaccess 这种目录上下文里,URL被一条规则改写之后,引擎会拿改写后的新URL 重新从头再走一遍整个规则集。如果你的规则改写完,新URL仍然满足同一条规则的匹配条件,那它就会被一遍遍改写,永远停不下来,直到撞上内部重定向次数上限报500。
举个典型:你想把所有请求都导到index.php,写了 RewriteRule ^(.*)$ /index.php?path=$1 [L]。问题是 /index.php 本身也匹配 ^(.*)$,于是它被改写成 /index.php?path=index.php,再匹配再改写……循环就这么起来了。
破法有几种,按推荐顺序:
- 加排除条件。用
RewriteCond %{REQUEST_FILENAME} !-f先把真实存在的index.php排除掉,它就不会再被卷进来。这是最常用、最干净的办法。 - 用END代替L。END标志直接终止整个重写过程,不给引擎重来的机会,从根上掐断循环。
- 精确化Pattern。别用过于宽泛的
^(.*)$,把匹配范围收窄到真正需要改写的那类URL。
顺带说 RewriteBase。在目录上下文里,因为Pattern匹配的是去掉目录前缀的相对路径,Substitution如果写相对路径,引擎需要知道"基准目录"在哪才能拼出正确的最终路径,RewriteBase就是告诉它这个基准。站点装在子目录、或者重定向后路径莫名其妙少了一段时,十有八九是RewriteBase没设对。
想看rewrite到底怎么走的,怎么调试?
规则不生效或者行为诡异,靠瞪着配置文件干想是最低效的。mod_rewrite自带详细的日志,能把引擎每一步的决策摊给你看。
这里有个版本变化要注意:Apache 2.2时代用的是 RewriteLog 和 RewriteLogLevel 两个指令,到了2.4它俩被废弃了,改成统一的日志框架,用 LogLevel 控制:
LogLevel alert rewrite:trace3把rewrite模块的日志级别开到trace3(想看更细可以到trace8),重启后访问出问题的URL,去错误日志里就能看到引擎怎么逐条匹配、每一步把URL改成了什么、哪条规则命中了、哪条被跳过。这比盲猜强一百倍。调试完记得把级别调回去,trace日志很费磁盘。
日志读起来其实有套路。trace级别下,每个请求你会看到一连串带缩进的行,记录引擎拿到的初始URL、尝试匹配每条规则的Pattern、命中或未命中、改写后的中间结果,以及最终交出去的目标。重点盯三处:初始URL是不是你以为的那个(很多坑是因为带了你没注意的查询串或斜杠)、哪条规则先命中了(顺序问题就在这暴露)、改写后的URL又被拿去重走了几轮(轮次过多就是循环的前兆)。把这几行连起来看,引擎的"心路历程"清清楚楚。
除了日志,还有几个高频"假性失效"要排查。第一,AllowOverride没开:如果主配置里对应目录没有 AllowOverride All(或至少FileInfo),.htaccess 里的重写规则会被整个无视,规则写得再对也白搭。第二,规则顺序:mod_rewrite从上往下执行,一条带L的规则提前命中就停了,后面的规则永远轮不到。第三,浏览器缓存了301:301是永久重定向,浏览器会狠狠缓存,你改了规则却发现还跳老地址,多半是缓存在作怪,换无痕窗口或清缓存再测。
这里顺带提一个很多人没意识到的性能账:.htaccess是有代价的。Apache处理每一个请求时,都要沿着目录路径一层层去查找有没有 .htaccess文件并实时解析,这个开销是逐请求发生的。把规则写在主配置(httpd.conf或虚拟主机段)里,Apache启动时加载一次就常驻内存,性能明显更好。
.htaccess的好处是改了不用重启、适合虚拟主机场景下没有主配置权限的用户,但如果你有服务器的完整控制权又追求性能,把规则挪进主配置、再用 AllowOverride None 关掉 .htaccess查找,是一笔划算的优化。这跟保哥讲过的Apache性能调优是一个思路——把能在启动时做完的事,别拖到每个请求里反复做。
mod_rewrite、mod_alias的Redirect、和反代该怎么分工?
最后聊聊边界——不是所有跳转都该上mod_rewrite,杀鸡别用牛刀。
简单的整段重定向,用mod_alias更合适。如果你只是要把一个旧地址永久跳到新地址,没有任何条件判断,Redirect 301 /old-page /new-page 一行搞定,比起mod_rewrite那套正则更轻、更好读。需要简单的模式匹配就用 RedirectMatch。只有当跳转要依赖条件(域名、HTTPS、文件是否存在、查询串内容)时,才轮到mod_rewrite出场。能用简单工具就别上复杂的,这是运维的基本修养。
要做反向代理,靠的是 P 标志。RewriteRule ^api/(.*)$ http://backend:8080/$1 [P] 会把匹配的请求通过mod_proxy转发给后端服务,前台地址不变,后台悄悄换了执行者。这块跟保哥讲过的Apache反向代理是连着的,P标志是rewrite和proxy两个模块的接合部。
大批量映射用RewriteMap。如果你有成千上万条旧链接到新链接的对应关系(比如一次大改版),把它们一条条写成RewriteRule既臃肿又慢。RewriteMap允许你把映射关系放进一个外部文件或数据库,规则里做一次键查找,性能和可维护性都好得多。
它支持好几种类型:txt 是最简单的纯文本键值对文件,rnd 能随机返回(做简单负载均衡),dbm 把文本编译成哈希数据库查得更快(大表必备),prg 调用外部程序、dbd 直接查数据库则适合映射关系动态变化的场景。改版重定向这种几万条静态映射,txt配合dbm编译是性价比最高的组合。
保哥分享一个真实排错。有个客户改版后大批旧链接301到新结构,规则看着没毛病,可就是有一小撮URL跳错地方。开了rewrite:trace日志一看,原来是规则顺序的问题——有一条宽泛的兜底规则排在了几条精确规则前面,带着L提前命中,那几条精确规则根本没机会执行。把精确规则提到兜底规则前面,问题立刻消失。这个案例再次印证:mod_rewrite出诡异问题,十有八九不是某条规则写错,而是规则之间的顺序和相互影响。日志永远是你最可靠的眼睛。
顺便提一句安全:mod_rewrite也常被用来做访问控制,比如根据User-Agent拦垃圾爬虫、根据Referer防盗链。不过这类拦截如果是为了加固服务器,保哥更推荐结合Apache安全加固里那套权限与访问控制一起做,rewrite拦截只是其中一环,别指望它包打天下。
再给几条规则维护上的实操建议。其一,给每条或每组规则写注释(行首加 #),半年后回头看你会感谢自己,否则那一堆正则跟天书没区别。其二,规则按"从特殊到一般"排序,精确匹配的放前面、宽泛兜底的放最后,避免兜底规则带着L把后面的精确规则挡死,前面那个排错案例就是反面教材。其三,上线前在测试环境用真实URL跑一遍、对照日志确认行为,别在生产上拿用户当小白鼠。其四,重大改动前备份当前配置,rewrite规则改崩了能一键回滚。
把这套机制理顺,你再看任何一段mod_rewrite规则,都能逐符号读懂它在干嘛、会不会有副作用,而不是当成一段不敢动的祖传咒语。从"复制粘贴碰运气"到"逐符号读懂引擎",中间隔的不是更多的配方,而是对这台状态机运转逻辑的理解。这一步迈过去,mod_rewrite就从玄学变成了你手里趁手的工具。
常见问题解答
RewriteRule里的L标志和END标志到底有什么区别?
两个都是"停止",但停的程度不同。L(last)只停止当前这一轮的规则处理,问题在于:在 .htaccess目录上下文里,URL被改写后引擎会拿新URL重新从头再走一遍整个规则集,L拦不住这种"重来",这正是很多死循环的来源。END则彻底得多,它宣布整个重写过程到此结束,连后续的目录级重新处理都不再触发,专门用来斩断循环。日常单条规则用L足够;遇到改写后会再次匹配、容易循环的场景,END更稳妥。需要注意END是Apache 2.4才引入的,老版本只能靠加排除条件防循环。
为什么我写了301重定向规则,改了之后浏览器还是跳到老地址?
大概率是浏览器缓存了旧的301。301是永久重定向,浏览器和某些中间缓存会非常激进地缓存它——既然你说"永久",它就记牢了,下次根本不再请求服务器直接跳。所以你改了规则,自己测却发现还跳老地方,多半不是规则没生效,而是缓存在作怪。验证办法:用无痕/隐私窗口访问,或者彻底清掉浏览器缓存,或者换个没访问过的设备测。也正因为301这么"黏",上线永久重定向前一定要确认目标地址正确,一旦放出去缓存到用户那里,再改就很被动。调试阶段可以先用302临时重定向,确认无误再换成301。
规则明明写对了,.htaccess里就是不生效,可能是什么原因?
先查三件事。第一也是最常见的,主配置里对应目录的AllowOverride没开,或者没包含FileInfo——这种情况下Apache会直接忽略 .htaccess里的所有重写指令,规则写得再对也等于没写。第二,RewriteEngine On这句忘了写,引擎压根没启动。第三,规则顺序问题,前面有一条带L的规则提前命中并停止了,导致你这条根本没机会执行。排查的终极武器是打开重写日志(LogLevel设成rewrite:trace3),它会把引擎对每个请求的处理过程完整记录下来,是不是匹配、为什么没匹配,一目了然,别再对着配置文件干瞪眼。
RewriteCond里的 %1和RewriteRule里的 $1是一回事吗?
不是,这是新手最容易搞混的反向引用。$1、$2取的是RewriteRule自己那个Pattern里捕获组的内容,按括号顺序编号;%1、%2取的是最近一条RewriteCond的CondPattern里捕获组的内容。两套引用来源完全不同,一个来自规则行,一个来自条件行。举例:你用RewriteCond匹配域名并捕获其中一段,要在RewriteRule的目标里引用这段,必须用 %1而不是 $1,写错了取到的就是空值或者别的东西,规则行为完全跑偏。记住口诀:百分号配条件,美元符配规则。
所有的跳转都用mod_rewrite来做是不是更省事?
不建议,应该按复杂度选工具。如果只是把一个旧URL永久跳到新URL,没有任何条件判断,用mod_alias的Redirect指令一行就够,比mod_rewrite的正则更轻量、更易读、性能也更好;需要简单模式匹配就用RedirectMatch。只有当跳转要根据条件来决定时——比如判断是否HTTPS、域名带不带www、请求的文件是否存在、查询串是什么——这些mod_alias做不到的判断,才该上mod_rewrite。把简单跳转也硬塞进mod_rewrite,规则文件会越来越臃肿难维护,还容易因为规则之间相互影响埋下死循环的雷。工具用对地方,配置才清爽。
权威参考资料
FAQPage + Article AI 引用友好版
用mod_rewrite老是复制粘贴别人的规则、一复杂就抓瞎?保哥拆开引擎讲:重写与重定向之分、RewriteRule正则怎么读、L与R与QSA标志、RewriteCond条件、死循环成因与调试日志。
- mod_rewrite
- URL重写
- Apache
- 运维
title: Apache mod_rewrite重写规则到底怎么写才不绕晕? author: 张文保 (Paul Zhang) — PatPat SEO 经理 url: https://zhangwenbao.com/apache-mod-rewrite-rewriterule-rewritecond-flags-engine-guide.html published: 2026-03-23 modified: 2026-03-23 source-type: First-hand expert commentary language: zh-CN license: CC BY-NC-SA 4.0 (要求保留原文链接与作者归属)
本文标题:《Apache mod_rewrite重写规则到底怎么写才不绕晕?》
本文链接:https://zhangwenbao.com/apache-mod-rewrite-rewriterule-rewritecond-flags-engine-guide.html
版权声明:本文原创,转载请注明出处和链接。许可协议: CC BY-NC-SA 4.0