2019/03/15

Nginxのlocationとproxy_passの末尾スラッシュによる挙動の違いを理解する

Nginxでリバースプロキシの設定をするとき、いつもlocationとproxy_passの末尾スラッシュ(Trailing Slash、トレイリングスラッシュ)による挙動の違いを忘れてしまう。ということで、locationとproxy_passのURIに末尾スラッシュをつけた場合、つけない場合でどのような挙動になるか実験した。その結果についてまとめる。

Nginxのリバースプロキシ設定は以下の4パターンになる。
パターンlocationproxy_pass
1/path/reverse-path
2/path/reverse-path/
3/path//reverse-path
4/path//reverse-path/


これに対し、4パターンのリクエストを投げて実験を行った。
  1. http://example.com/path
  2. http://example.com/path/
  3. http://example.com/path-test
  4. http://example.com/path/test

末尾スラッシュ(Trailing Slash)による挙動の違い


結論から先にまとめる。

location

locationの末尾スラッシュがない場合は前方一致ある場合はURIの完全一致になる。

結果は以下のとおり。
  • location /path
    • http://example.com/path → OK
    • http://example.com/path/ → OK
    • http://example.com/path-test → OK
  • location /path/
    • http://example.com/path → 301 Moved Permanently
    • http://example.com/path/ → OK
    • http://example.com/path-test → 404 Not Found

proxy_pass

proxy_passの末尾スラッシュがない場合はプロキシ先のURIにスラッシュがつかない、ある場合はスラッシュがつく

結果は以下のとおり。
  • proxy_pass /reverse-path;
    • http://example.com/path/ → /reverse-path
    • http://example.com/path/test → /reverse-pathtest
  • proxy_path /reverse-path/;
    • http://example.com/path/ → /reverse-path/
    • http://example.com/path/test → /reverse-path/test

末尾スラッシュの挙動まとめ

  • location末尾スラッシュなし、proxy_pass末尾スラッシュなし
    • GET /path → GET /reverse-path
    • GET /path/ → GET /reverse-path/
    • GET /path-test → GET /reverse-path-test
    • GET /path/test → GET /reverse-path/test
  • location末尾スラッシュなし、proxy_pass末尾スラッシュあり
    • GET /path → GET /reverse-path/
    • GET /path/ → GET /reverse-path//
    • GET /path-test → GET /reverse-path/-test
    • GET /path/test → GET /reverse-path//test
  • location末尾スラッシュあり、proxy_pass末尾スラッシュなし
    • GET /path → 301 Moved Permanently
    • GET /path/ → GET /reverse-path/
    • GET /path-test → 404 Not Found
    • GET /path/test → GET /reverse-pathtest
  • location末尾スラッシュあり、proxy_pass末尾スラッシュあり
    • GET /path → 301 Moved Permanently
    • GET /path/ → GET /reverse-path/
    • GET /path-test → 404 Not Found
    • GET /path/test → GET /reverse-path/test

結論: locationとproxy_passの末尾スラッシュはつけておいたほうが良い

locationとproxy_passの末尾スラッシュの有無がどのように影響するかわかっていただけたと思う。意図しないリバースプロキシを避けるために、locationとproxy_passの末尾スラッシュはつけておいたほうが安全だろう。




以降は、私がリバースプロキシの挙動を理解するために行った実験の手順をまとめる。


リバースプロキシの挙動を確認するための準備


Nginxをインストールする

Nginxの公式イメージを使って、DockerでNginxを起動する。もちろんローカルにインストールしても問題ない。
# Nginxのコンテナを起動する
$ docker run --name learn-nginx -d -p 8080:80 nginx

使っているオプションの意味は以下のとおり。
  • --name: コンテナに名前をつける
  • -d: デタッチモード(バックグラウンド実行)
  • -p: ポートフォワード

Nginxのconfigファイルを編集する

起動中のNginxのコンテナにログインする。
$ docker ps
CONTAINER ID        IMAGE               COMMAND                  CREATED             STATUS              PORTS                  NAMES
fa71f41f9ad0        nginx               "nginx -g 'daemon of…"   4 seconds ago       Up 3 seconds        0.0.0.0:8080->80/tcp   learn-nginx

# dockerにログインする
$ docker exec -it learn-nginx /bin/bash
root@fa71f41f9ad0:/#

Nginxのデバッグ用のログ出力と、リバースプロキシの設定を行う。

と、その前にNginxのイメージにはエディタが入っていないので、vimをインストールする。(nanoでもviでもemacsでも可)
container$ apt-get update
container$ apt-get install vim

まずはリバースプロキシのログを出力するために、/etc/nginx/nginx.confを修正する。
container$ cd /etc/nginx
container$ vim nginx.conf
container$ cat nginx.conf
# /etc/nginx/nginx.conf

# 省略
http {
    # 省略
    
    log_format  main  '$remote_addr - $remote_user [$time_local] "$request" '
                      '$status $body_bytes_sent "$http_referer" '
                      '"$http_user_agent" "$http_x_forwarded_for"';
    access_log  /var/log/nginx/access.log  main;

    # reverse-proxy用のログを追加
    log_format upstreamlog '[$time_local] $remote_addr $host $upstream_addr $request';
    access_log /var/log/nginx/upstream.log upstreamlog;

    # 省略
}
# Confファイルのチェック
container$ nginx -t

# 設定ファイルを反映させる
container$ nginx -s reload

# ログを表示する(tail -fでファイルが更新されたら自動的に表示される)
container$ cd /var/log/nginx
container$ tail -f upstream.log

# ローカルマシンからcurlしてみる
$ curl localhost:8080

# upstream.logのログ
[08/Mar/2019:09:27:33 +0000] 172.17.0.1 localhost - GET / HTTP/1.1

次にリバースプロキシの設定を行う。
container$ cd /etc/nginx/conf.d
container$ vim reverse-proxy.conf
container$ cat reverse-proxy.conf
# /etc/nginx/conf.d/reverse-proxy.conf
server {
  listen 80;
  server_name localhost 127.0.0.1;

  location /path1 {
    resolver 127.0.0.1;
    proxy_pass http://host.docker.internal:3000/reverse-path1;
  }

  location /path2 {
    resolver 127.0.0.1;
    proxy_pass http://host.docker.internal:3000/reverse-path2/;
  }

  location /path3/ {
    resolver 127.0.0.1;
    proxy_pass http://host.docker.internal:3000/reverse-path3;
  }

  location /path4/ {
    resolver 127.0.0.1;
    proxy_pass http://host.docker.internal:3000/reverse-path4/;
  }
}
# confファイルのチェックをする
container$ nginx -t

# もし以下のようなserver nameがコンフリクトしているという警告が表示されたら
container$ nginx -t
2019/03/11 01:51:13 [warn] 3512#3512: conflicting server name "localhost" on 0.0.0.0:80, ignored
nginx: [warn] conflicting server name "localhost" on 0.0.0.0:80, ignored
nginx: the configuration file /etc/nginx/nginx.conf syntax is ok
nginx: configuration file /etc/nginx/nginx.conf test is successful

# default.confのserver_nameをコメントアウトしておく
container$ vim default.conf

これでNginxの設定は終わり。


ローカルにリバースプロキシ先のサーバを立てる

想定通りのパスにプロキシされるか確認するために、Node.jsで簡易APIサーバを立てる。
# json-serverのインストール
$ npm i json-server

# json-serverを起動する (api.jsonは実行時に自動生成されるので作成は不要)
$ npx json-server --watch api.json

  \{^_^}/ hi!

  Loading api.json
  Oops, api.json doesn't seem to exist
  Creating api.json with some default data

  Done

  Resources
  http://localhost:3000/posts
  http://localhost:3000/comments
  http://localhost:3000/profile

  Home
  http://localhost:3000

  Type s + enter at any time to create a snapshot of the database
  Watching...

これでhttp://localhost:3000にリクエストすることでログが表示される。

json-serverの詳しい使い方は、以下の記事を参照してください。

末尾スラッシュによりリバースプロキシがどのように動作するか確認する


以下の4つのリバースプロキシに対して、4つのリクエストを投げて確認した。

リバースプロキシの設定
パターンlocationproxy_pass
1/path/reverse-path
2/path/reverse-path/
3/path//reverse-path
4/path//reverse-path/

確認用リクエスト
  • http://localhost:8080/path
  • http://localhost:8080/path/
  • http://localhost:8080/path-test
  • http://localhost:8080/path/test


パターン1: location末尾スラッシュなし、proxy_pass末尾スラッシュなし

# GET /path1
$ curl localhost:8080/path1
# nginxログ
[11/Mar/2019:04:33:48 +0000] 172.17.0.1 localhost 192.168.65.2:3000 GET /path1 HTTP/1.1
# プロキシ先
GET /reverse-path1 404 4.613 ms

# GET /path1/
$ curl localhost:8080/path1/
# nginxログ
[13/Mar/2019:06:10:25 +0000] 172.17.0.1 localhost 192.168.65.2:3000 GET /path1/ HTTP/1.1
# プロキシ先
GET /reverse-path1/ 404 1.363 ms

# GET /path1-test
$ curl localhost:8080/path1-test
# nginxログ
[13/Mar/2019:05:17:11 +0000] 172.17.0.1 localhost 192.168.65.2:3000 GET /path1-test HTTP/1.1
# プロキシ先
GET /reverse-path1-test 404 1.576 ms

# GET /path1/test
$ curl localhost:8080/path1/test
# nginxログ
[11/Mar/2019:04:33:56 +0000] 172.17.0.1 localhost 192.168.65.2:3000 GET /path1/test HTTP/1.1
# プロキシ先
GET /reverse-path1/test 404 1.820 ms


パターン2: location末尾スラッシュなし、proxy_pass末尾スラッシュあり

# GET /path2
$ curl localhost:8080/path2
# nginxログ
[11/Mar/2019:04:34:14 +0000] 172.17.0.1 localhost 192.168.65.2:3000 GET /path2 HTTP/1.1
# プロキシ先
GET /reverse-path2/ 404 1.883 ms

# GET /path2/
$ curl localhost:8080/path2/
# nginxログ
[13/Mar/2019:06:11:28 +0000] 172.17.0.1 localhost 192.168.65.2:3000 GET /path2/ HTTP/1.1
# プロキシ先
GET /reverse-path2// 404 1.363 ms

# GET /path2-test
$ curl localhost:8080/path2-test
# nginxログ
[13/Mar/2019:05:17:58 +0000] 172.17.0.1 localhost 192.168.65.2:3000 GET /path2-test HTTP/1.1
# プロキシ先
GET /reverse-path2/-test 404 1.357 ms

# GET /path2/test
$ curl localhost:8080/path2/test
# nginxログ
[11/Mar/2019:04:34:18 +0000] 172.17.0.1 localhost 192.168.65.2:3000 GET /path2/test HTTP/1.1
# プロキシ先
GET /reverse-path2//test 404 1.785 ms


パターン3: location末尾スラッシュあり、proxy_pass末尾スラッシュなし

# GET /path3
$ curl localhost:8080/path3
301 Moved Permanently
# nginxログ
[11/Mar/2019:04:34:21 +0000] 172.17.0.1 localhost - GET /path3 HTTP/1.1

# GET /path3/
$ curl localhost:8080/path3/
# nginxログ
[13/Mar/2019:06:12:01 +0000] 172.17.0.1 localhost 192.168.65.2:3000 GET /path3/ HTTP/1.1
# プロキシ先
GET /reverse-path3 404 1.467 ms

# GET /path3-test
$ curl localhost:8080/path3-test
404 Not Found
# nginxログ
[13/Mar/2019:05:18:43 +0000] 172.17.0.1 localhost - GET /path3-test HTTP/1.1

# GET /path3/test
$ curl localhost:8080/path3/test
# nginxログ
[11/Mar/2019:04:34:30 +0000] 172.17.0.1 localhost 192.168.65.2:3000 GET /path3/test HTTP/1.1
# プロキシ先
GET /reverse-path3test 404 1.626 ms


パターン4: location末尾スラッシュあり、proxy_pass末尾スラッシュあり

# GET /path4
$ curl localhost:8080/path4
301 Moved Permanently
# nginxログ
[11/Mar/2019:04:34:37 +0000] 172.17.0.1 localhost - GET /path4 HTTP/1.1

# GET /path4/
$ curl localhost:8080/path4/
# nginxログ
[13/Mar/2019:06:12:45 +0000] 172.17.0.1 localhost 192.168.65.2:3000 GET /path4/ HTTP/1.1
# プロキシ先
GET /reverse-path4/ 404 1.404 ms

# GET /path4-test
$ curl localhost:8080/path4-test
404 Not Found
# nginxログ
[13/Mar/2019:05:19:33 +0000] 172.17.0.1 localhost - GET /path4-test HTTP/1.1

# GET /path4/test
$ curl localhost:8080/path4/test
# nginxログ
[11/Mar/2019:04:34:41 +0000] 172.17.0.1 localhost 192.168.65.2:3000 GET /path4/test HTTP/1.1
# プロキシ先
GET /reverse-path4/test



参考サイト






以上

written by @bc_rikko

0 件のコメント :

コメントを投稿