もう間違えないmod_rewrite

2019/03/06更新

目次

序論

URLを内部的に書き換えるためのApacheモジュール「mod_rewrite」は、黒魔術と呼ばれるほど機能が複雑で、仕様や書き方をなかなか覚えられず、目的の処理を書くことが難しい。書けたとしても、副作用によるよく分からない挙動に悩まされることもしばしばである。

そこで、そのようなmod_rewriteを徹底理解するべく、ここにまとめておくことにする。

基本

使用のための準備

mod_rewriteは、Apacheのモジュールであるため、httpd.conf(またはそこからロードされる*.confファイル)内で以下の記述によりロードされている必要がある。

LoadModule rewrite_module modules/mod_rewrite.so

加えて、.htaccessでmod_rewriteを使用可能にするためには、以下の設定が対象のディレクトリで有効となっている必要がある。

# .htaccessでの上書き設定を許可する
AllowOverride All

# mod_rewriteの使用に必要なオプション
Options FollowSymLinks

# mod_rewriteを有効にする
RewriteEngine on

コンテキストとは

Apacheの設定には、サーバーコンテキストディレクトリコンテキストというものがある。サーバーコンテキストは、httpd.confなどに直に書いた設定を指し、ディレクトリコンテキストは、httpd.conf内でも<Directory>ディレクティブで囲った中や、各ディレクトリの.htaccessに書いた設定を指す。

mod_rewriteの設定は、いずれのコンテキストにも記述できるが、サーバーコンテキストとディレクトリコンテキストでは一部挙動が異なる。

RewriteRule

基本

RewriteRuleで、書き換えのルールを記述する。以下の書式で、URLが置換条件の正規表現にマッチした場合、全体を置換文字列に置き換える。

RewriteRule 置換条件 置換文字列

まれに勘違いされる場合があるが、「置換条件にマッチした部分を置換文字列に置き換える」のではないので注意する。正しくは「置換条件にマッチしたら、全体を置換文字列に置き換える」である。すなわち、「if 置換条件 then 置換文字列」であって、「s/置換条件/置換文字列/」ではない。

# /hoge/で始まっていたら/fuga/index.htmlを表示する。
RewriteRule ^/hoge/ /fuga/index.html

# 「s/置換条件/置換文字列/」ではないため、以下の書き方は冗長。
RewriteRule ^/hoge/.+$ /fuga/index.html

置換条件

RewriteRuleの書式において、置換条件には、置換条件を正規表現で記述する。「.」などのエスケープや、先頭「^」末尾「$」等の指定を忘れたり間違えたりしないように注意する。また、日本語などASCII以外の文字は、UTF-8の16進表記で書く。例えば、「」にマッチさせたいのであれば「\xE3\x81\x82」と書く。

置換条件に渡される文字列は、サーバーコンテキストでは/から始まるパス全体/hoge/fuga/~」が渡される。一方、ディレクトリコンテキストでは、対象のディレクトリ(.htaccessであれば.htaccessが置かれたディレクトリ)からの相対パス/以外の文字から始まる)が渡される。例えば、「/hoge/fuga/index.html」にアクセスした場合、/hoge/.htaccessには「fuga/index.html」だけが渡される。

なお、「?」に続くクエリ文字列は渡されない。これは後述のRewriteCondで拾う必要がある。

# /hoge/index.htmlにマッチさせたい場合

# httpd.confに記述する場合
RewriteRule ^/hoge/index\.html$ ~

# /.htaccessに記述する場合
RewriteRule ^hoge/index\.html$ ~

# /hoge/.htaccessに記述する場合
RewriteRule ^index\.html$ ~

置換文字列とRewriteBase

RewriteRuleの書式において、置換文字列には、書き換え後のパス全体(/から始まる)を記述する。ディレクトリコンテキストでは、相対パスで書くこともでき、その場合その対象のディレクトリが起点となる。日本語等のURL上安全でない文字は、URLエンコード(パーセントエンコード)で書く。ただし、「%」は後述の後方参照を意味する特殊文字なので、「\%」にエスケープして書く必要がある。

# /hoge/.htaccessでの例

# /hoge/AAA.htmlで/hoge/BBB.htmlを表示
RewriteRule ^AAA\.html$ /hoge/BBB.html

# 以下のように書いても同じ
RewriteRule ^AAA\.html$ BBB.html

ディレクトリコンテキストで相対パスの起点を変更するには、RewriteBaseを使用する。RewriteBaseに指定するパスは/から書き始める。末尾の/はあっても無くてもよい。なお、RewriteBaseを設定した場合でも、置換文字列を/から始めると必ずルートからのパスとなる。

間違えやすいポイントであるが、RewriteBaseの設定は、置換条件の方には一切関係ないので注意する。

# /.htaccessでの例:

# /hoge/AAA.htmlで/hoge/BBB.htmlを表示
# RewriteBaseによって、置換文字列では/hoge/を省略できるが、置換条件では省略できない点に注意。
RewriteBase /hoge/
RewriteRule ^hoge/AAA.html BBB.html

置換文字列として「-」を指定すると、書き換えを行なわないことを意味する。また、置換文字列中には、後述のRewriteCondで使われる環境変数(%{~})や後方参照($1%1)も使用できる。

後方参照

置換条件の正規表現内では、()によるキャプチャが可能で、置換文字列の中で$1$2、…で参照できる。

# キャプチャしてパスの一部を引き継ぐ例。
RewriteRule ^hoge/(.+)$ /fuga/$1

フラグ

各ルールには、以下のように様々な意味を加えるフラグというものが指定できる。複数のフラグを同時に指定する場合は、「,」で区切る。

# [F]:HTTP 403(Forbidden)を返す(「-」は、書き換えを行なわないの意)
RewriteRule ^hoge\.html$ - [F]

# [R=404]:HTTP 404(Not Found)を返す
RewriteRule ^hoge\.html$ - [R=404,L]

# [G]:HTTP 410(Gone)を返す
RewriteRule ^hoge\.html$ - [G]

# [NC]:置換条件の大文字小文字を区別しない(正規表現のiフラグと同じ)
RewriteRule ^hoge\.html$ fuga.html [NC]

# [NE]:リダイレクト時にクエリ文字列をURLエンコードしない(URLエンコード済みと見なす)
# 以下の2つはいずれも「fuga.cgi?q=%25」にリダイレクトする
RewriteRule ^hoge\.html$ fuga.cgi?q=\% [R]
RewriteRule ^hoge\.html$ fuga.cgi?q=\%25 [R,NE]

# [QSA]:クエリ文字列をマージする。以下のルールで、hoge.html?p=2にアクセスした場合、
# 1つ目は、そのまま引き継がれて「fuga.cgi?p=2」、
# 2つ目は、新しいクエリに置き換えられて「fuga.cgi?q=1」、
# 3つ目は、[QSA]フラグによりマージされて「fuga.cgi?p=2&q=1」となる。
RewriteRule ^hoge\.html$ fuga.cgi
RewriteRule ^hoge\.html$ fuga.cgi?q=1
RewriteRule ^hoge\.html$ fuga.cgi?q=1 [QSA]

# [QSD]:クエリ文字列を削除する(Apache2.4以降。2.3以前では、空のクエリ「?」を指定すればよい)
RewriteRule ^hoge\.html$ fuga.cgi [QSD]
RewriteRule ^hoge\.html$ fuga.cgi?

# [E]:マッチしたら、独自変数「X_HOGE」に値「1」をセットする。
# 別途RewriteCondの%{ENV:~}で独自変数の値を条件として使うことができる。
RewriteRule ^hoge\.html$ fuga.html [E=X_HOGE:1]

よく使われる[L]フラグは、ルール適用をそこで打ち切るものであるが、.htaccess等のディレクトリコンテキストでは、打ち切られても書き換え後のURLに対するルールが存在すれば再び書き換えが行なわれてしまうので注意が必要である。これは、.htaccessでmod_rewriteを使用する際に最も多くの人が勘違いしているポイントと思われる。

# AAA.htmlにアクセスした場合、[L]フラグによって一度は打ち切られても、
# BBB.htmlに対して再び最初からルール適用されるので、結果CCC.htmlになってしまう。
RewriteRule ^AAA\.html$ BBB.html [L]
RewriteRule ^BBB\.html$ CCC.html

それでは[L]フラグの存在価値が無いように思われるが、大量のルールを記述した場合などに、それ以降マッチしないことが確実なURLを早い段階で排除できるので、ディレクトリコンテキストでも使用する意義はある。

# 以下のように記述すると、AAA.htmlは最初にマッチングを終えるので、無駄が減る。
# また、BBB.html以降も、マッチした段階でそれ以降のルールを無視するので、無駄が減る。
# さらに、置換後の○○_XXX.htmlに対しては、再び最初からマッチングが行なわれるが、
# これも冒頭で排除されるので、無駄が減る。
RewriteRule ^AAA\.html$ - [L]
RewriteRule _XXX\.html$ - [L]
RewriteRule ^BBB\.html$ BBB_XXX.html [L]
RewriteRule ^CCC\.html$ CCC_XXX.html [L]
RewriteRule ^DDD\.html$ DDD_XXX.html [L]
  :

このようにいささか直感に反する[L]フラグに対し、Apache2.3以降では、以降の書き換えを完全に停止する[END]フラグというものが新たに導入された。

# AAA.htmlはBBB.htmlとなる。前述の[L]フラグの場合との違いに注意。
RewriteRule ^AAA\.html BBB.html [END]
RewriteRule ^BBB\.html CCC.html

リダイレクト

RewriteRuleにおいて、[R]フラグを付けるとリダイレクトとなり、ブラウザのURLが書き換わる。また、置換文字列http://またはhttps://から始まるURLを記述した場合もリダイレクトとなる。

# 302リダイレクト
RewriteRule ^hoge/ hoge2/ [R,L]
RewriteRule ^fuga/ https://www.example.com/fuga/ [L]

# 301リダイレクト
RewriteRule ^hoge/ hoge2/ [R=301,L]
RewriteRule ^fuga/ https://www.example.com/fuga/ [R=301,L]

RewriteCond

基本

RewriteCondは、ルールを実行するためのさらなる条件を記述する。以下のように、RewriteRuleの直前に0個以上のRewriteCondを並べて書くことで、そのルールに条件を付け加えることができる。複数のルールに条件を付けたい場合は、RewriteRuleごとにRewriteCondを書く必要がある。

# 全てのRewriteCond条件を満たした場合のみ、hoge.html→fuga.htmlの書き換えが行なわれる。
RewriteCond テスト文字列 条件パターン
RewriteCond テスト文字列 条件パターン
  :
RewriteRule ^hoge\.html fuga.html

複数のRewriteCondは通常AND条件となるが、[OR]フラグを使うことでOR条件にすることもできる。[OR]フラグによるOR条件は、何も付けない場合のAND条件よりも優先順位が高い。なお、ORよりANDを優先したい場合は、同じRewriteRuleを複数記述すればよい。

# 以下は、(条件A OR 条件B)AND 条件C の意味。
RewriteCond 条件A [OR]
RewriteCond 条件B
RewriteCond 条件C
RewriteRule ^hoge\.html fuga.html

# 同じRewriteRuleを複数記述することで、事実上、条件A OR(条件B AND 条件C)となる。
RewriteCond 条件A
RewriteRule ^hoge\.html fuga.html
RewriteCond 条件B
RewriteCond 条件C
RewriteRule ^hoge\.html fuga.html

RewriteRuleと同様に、[NC]フラグを付けると、正規表現で大文字小文字を区別しないようにできる。

テスト文字列

RewriteCondの書式において、テスト文字列には、%{変数名}の形でサーバーから渡される環境変数を使用できる。主なものとして、以下のようなものがある。

環境変数

内容

%{HTTP_USER_AGENT}

UserAgent。「Mozilla/5.0 ~」など。

%{HTTP_REFERER}

リファラ。「http://~」「https://~」から始まるURL。

%{HTTP_COOKIE}

クッキー。「hoge=AAA; fuga=BBB」など。

%{HTTP_HOST}

ホスト名。「www.example.com」「localhost」など。

%{REMOTE_ADDR}

接続元IPアドレス。「123.45.67.89」「::1」など。

%{REQUEST_METHOD}

リクエストメソッド。「GET」「POST」など。

%{PATH_INFO}

パス情報。スクリプト名の後の/~の部分。

%{QUERY_STRING}

クエリ文字列。URLの?より後の部分。

%{REQUEST_URI}

リクエストされたURL上のパス。ドメインの後の/から、クエリ文字列の?の前まで。

%{REQUEST_FILENAME}

リクエストされたファイルのサーバー上のパス。「/var/www/html/~」など。

%{THE_REQUEST}

リクエスト文字列全体。「GET /index.html?hoge=fuga HTTP/1.1」など。

%{TIME}

日時。「20181231235959」など。

%{TIME_YEAR}
%{TIME_MON}
%{TIME_DAY}
%{TIME_HOUR}
%{TIME_MIN}
%{TIME_SEC}

%{TIME}の年月日時分秒の個別の値。「2018」「12」「31」など。

%{TIME_WDAY}

曜日。0~6の値。

%{HTTPS}

HTTPSか否か。「on」または「off」。

%{HTTP:X-MY-HEADER}

任意のHTTPヘッダ(左記の場合「X-MY-HEADER」ヘッダの値)。

%{ENV:X_HOGE}

[E]フラグで設定した独自変数の値(左記の場合、変数「X_HOGE」の値)。

環境によっては使用できない変数もある。なお、単なる定文字列もテスト文字列として使用できる。

# ファイル「/var/tmp/hoge.txt」が存在していたら。
RewriteCond /var/tmp/hoge.txt -f

# 定文字列と環境変数を連結したり、複数の環境変数を使うこともできる。
RewriteCond /var/tmp%{PATH_INFO} -f
RewriteCond %{TIME_HOUR}%{TIME_MIN} <1200

条件パターン

RewriteCondの書式において、条件パターンには、テスト文字列に対する正規表現や条件式を記述する。

# 通常の正規表現。例:「Mozilla」で始まっていたら。
RewriteCond %{HTTP_USER_AGENT} ^Mozilla

# 正規表現の否定。例:「Mozilla」で始まっていなかったら。
RewriteCond %{HTTP_USER_AGENT} !^Mozilla

# ファイル検査。例:通常ファイルであれば。同様に、-d、-l、-s等が使える。
RewriteCond %{REQUEST_FILENAME} -f

# ファイル検査の否定。例:通常ファイルでなければ。
RewriteCond %{REQUEST_FILENAME} !-f

# 文字列の辞書順比較。例:辞書順で0401より前であれば。同様に、>、=が使える。
RewriteCond %{TIME_MON}%{TIME_DAY} <0401

# 数値比較。例:12未満であれば。同様に、-le、-gt、-ge、-eq、-neが使える。
RewriteCond %{TIME_HOUR} -lt12

後方参照

RewriteCondおよびRewriteRule内では、%1%2、…によってRewriteCond内の()によるキャプチャを参照できる。

# 直後のRewriteCondやRewriteRuleで使用する例。RewriteCondで再び正規表現が使われない限り、値は保存される。
RewriteCond %{REQUEST_URI} ^/hoge/fuga/([a-z]+)/([a-z]+)$
RewriteCond /var/tmp/%1/%2 -f
RewriteRule ^hoge\.html$ fuga.cgi?dir=%1&file=%2

# 次の場合、2行目で正規表現が使われたため、3行目のRewriteRuleでは1行目の(~)の内容は取得できない。
RewriteCond %{REQUEST_URI} ^/hoge/fuga/([a-z]+)/([a-z]+)$
RewriteCond /var/tmp/%1/%2 \.html$
RewriteRule ^hoge\.html$ fuga.cgi?dir=%1&file=%2

競合時の挙動

ルールの競合

条件が同じルールが2つ以上記述されていた場合、一番上に書いてあるもののみが有効である。

RewriteRule ^AAA\.html$ BBB.html
RewriteRule ^AAA\.html$ CCC.html

# AAA.htmlでBBB.htmlが表示される。

コンテキスト間の競合

サーバーコンテキストとディレクトリコンテキストでルールが競合した場合は、サーバーコンテキストのルールが有効となる。つまり、httpd.confに直に書いたルールが最も優先される。ただし、競合していないルールに関しては、ディレクトリコンテキストのルールが引き続き有効となる

最優先したいルールはhttpd.confに記述し、.htaccessでその他のルールを追加していく、という使い方ができる。

# httpd.confの内容
RewriteEngine on
RewriteRule ^/hoge/AAA\.html$ /hoge/BBB.html [L]

# /hoge/.htaccessの内容
RewriteEngine on
RewriteRule ^AAA\.html$ CCC.html [L]
RewriteRule ^DDD\.html$ EEE.html [L]

# この場合、/hoge/AAA.htmlは、httpd.confが優先されて/hoge/BBB.htmlとなるが、
# /hoge/DDD.htmlは、.htaccessが引き続き有効で/hoge/EEE.htmlとなる。

ディレクトリコンテキスト同士の競合

ディレクトリコンテキストのルール同士が競合した場合は、注意が必要である。この場合、優先順位の高い方にRewriteEngine Onの記述があるだけで、たとえルールが一切競合していなくても、優先順位の低い方は全て無効となる。優先順位は、ディレクトリ階層が深い.htaccessが最も高く、ルートディレクトリの.htaccessが.htacessの中では最も低い。そして、<Directory>ディレクティブはそれよりもさらに低い。

以下の例を見て分かる通り、ルートディレクトリの.htaccessでサブディレクトリのルールを記述しても、サブディレクトリの.htaccessでRewriteが使用されると、それらは全て無効となってしまう。これも[L]フラグと同様に多くの人がハマってしまうポイントと思われる。.htaccessでは、そのディレクトリパスに対するルールだけを記述するようにしていくと混乱を招きにくい。

# /.htaccessの内容
RewriteEngine on
RewriteRule ^hoge/AAA\.html$ hoge/BBB.html [L]
RewriteRule ^hoge/CCC\.html$ hoge/DDD.html [L]
RewriteRule ^EEE\.html$ FFF.html [L]

# /hoge/.htaccessの内容
RewriteEngine on
RewriteRule ^AAA\.html$ CCC.html [L]

# この場合、/hoge/AAA.htmlは、より階層の深い方の.htaccessが優先されて/hoge/CCC.htmlとなる。
# また、/hoge/CCC.htmlは競合していないが、/hoge/.htaccessでmod_rewriteが有効になっているため、
# /.htaccess内の/hoge/に関するルールは全て無効となるので、/hoge/DDD.htmlになることはない。
# ただし、/EEE.htmlのルールは/hoge/に関するものではないため、有効である。

連鎖の挙動

httpd.conf内での連鎖

RewriteRuleは上から順に実行されていき、マッチするたびに置換されていく。

RewriteRule ^/AAA\.html$ /BBB.html
RewriteRule ^/BBB\.html$ /CCC.html
RewriteRule ^/DDD\.html$ /EEE.html
RewriteRule ^/CCC\.html$ /DDD.html

# /AAA.htmlで/DDD.htmlが表示される。
# 上から順に評価されるため、/CCC.htmlが/EEE.htmlになることはない。

[L]フラグを付けると、その時点でマッチングを終了する。

RewriteRule ^/AAA\.html$ /BBB.html [L]
RewriteRule ^/BBB\.html$ /CCC.html

# /AAA.htmlで/BBB.htmlが表示される。

[N]フラグを付けると、もう一度先頭からマッチングを行なう。

RewriteRule ^/CCC\.html$ /DDD.html
RewriteRule ^/AAA\.html$ /CCC.html [N]
RewriteRule ^/BBB\.html$ /CCC.html

# /AAA.htmlで/DDD.htmlが表示される。/BBB.htmlは/CCC.htmlとなる。

httpd.confでも<Directory>ディレクティブ内に記述すると、後述の.htaccessと同じ挙動となるので注意。

.htaccess内での連鎖

これまでに述べている通り、.htaccessでは、[L]フラグでマッチングを打ち切っても、URLが変わっていれば、その新しいURLに対応するルールでマッチングがくり返し行なわれる。異なるディレクトリパスへ書き換えられた場合は、新しいパスにおいて有効な.htaccessのルールが適用される。URLが変化しなくなるまで、マッチングはくり返される。

# /.htaccessの内容
RewriteRule ^BBB\.html$ /hoge/AAA.html [L]
RewriteRule ^AAA\.html$ /BBB.html [L]

# /hoge/.htaccessの内容
RewriteRule ^AAA\.html$ /hoge/BBB.html [L]

# /AAA.htmlはまず、/.htaccessのルールに2回マッチして/hoge/AAA.htmlとなり、
# 次いで、/hoge/.htaccessのルールによって/hoge/BBB.htmlとなる。

サブリクエストとは

mod_rewrite以外の処理でURLが書き換わった場合、その新しいURLがmod_rewriteに流れてくることがある。これをサブリクエストという。例えば、DirectoryIndexは、ディレクトリを要求されたときに返すファイルを指定するものであるが、その際にサブリクエストが発生する。また、SSIでファイルをインクルードする際などもサブリクエストが発生する。

# 「~/」にアクセスしたら「~/hoge.html」を返す。
# この際「~/hoge.html」がサブリクエストとしてRewrite処理に流れてくる。
DirectoryIndex hoge.html

# 以下のルールがあると、「~/」で発生したサブリクエスト「~/hoge.html」が
# 「~/fuga.html」に書き換わるため、結局「~/」で「~/fuga.html」が表示されることになる。
RewriteRule ^hoge\.html$ fuga.html

# [NS]フラグを使うと、サブリクエストの場合は実行しない、という条件を付けられる。
# 以下のルールだと、「~/hoge.html」に直接アクセスした場合のみ「~/fuga.html」が表示される。
RewriteRule ^hoge\.html$ fuga.html [NS]

# RewriteCondで%{IS_SUBREQ}という変数も使用できる。
# 以下のルールでは、逆にサブリクエストの場合のみ書き換えを実行する。
RewriteCond %{IS_SUBREQ} true
RewriteRule ^hoge\.html$ fuga.html

外部リンク