問題の概要
OAuth2 Clientを統合したサービスにおいて、ローカル環境やアプリケーションに直接アクセスする限り問題ありませんでしたが、本番環境でNginxを経由した転送を行うと、OAuthログインが常に失敗するようになりました。
具体的には、認証が必要なAPIエンドポイント https://blog.95id.com:4005/user_attr にアクセスすると、認証サーバー(例: GitHub)へのリダイレクトが期待されますが、実際には http://blog.95id.com/login にリダイレクトされてしまいます。
原因の特定と段階的な解決
複雑なNginx構成(例: 2層のNginx)が原因で問題の切り分けが難しかったため、単純なシナリオから段階的に検証を行いました。
シナリオ1: Nginx (ポート80) が OAuth Client (ポート8082) をプロキシ
最も単純な構成です。この場合、リダイレクトは正常に動作します。
アプリケーション設定 (application.properties):
server.port=8082
Nginx設定:
server {
listen 80;
server_name blog.95id.com;
port_in_redirect off;
location / {
add_header Cache-Control no-store;
proxy_pass http://127.0.0.1:8082;
proxy_set_header Host $host;
proxy_set_header X-Real-IP $remote_addr;
proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;
}
}
シナリオ2: Nginx (非80ポート: 4005) が OAuth Client (ポート8082) をプロキシ
ポート4005でリクエストを受け付け、バックエンドに転送します。この場合、リダイレクト先からポート情報が失われる問題が発生します。
問題の動作:
- リクエスト:
http://blog.95id.com:4005/user_attr - リダイレクト先:
http://blog.95id.com/login(ポート4005が欠落)
原因: Spring SecurityのOAuth2 Clientライブラリが、プロキシされたリクエストから正しいポート番号を取得できないためです。
解決策: Nginx設定で Host ヘッダーにポート番号を明示的に含めます。
proxy_set_header Host $host:$server_port;
この設定により、アプリケーションはリクエストURLからポート4005を正しく認識できるようになります。
シナリオ3: Nginx (HTTPS ポート443) が OAuth Client (ポート8082) をプロキシ
HTTPS経由でアクセスすると、リダイレクト先のプロトコルがHTTPに変わってしまう問題が発生します。
問題の動作:
- リクエスト:
https://blog.95id.com/user_attr - リダイレクト先:
http://blog.95id.com/login(HTTPSがHTTPに)
解決策: Nginxからアプリケーションに、元のリクエストプロトコル (HTTPS) を伝える必要があります。
Nginx設定:
proxy_set_header X-Forwarded-Proto $scheme;
アプリケーション設定 (application.properties):
server.tomcat.remote-ip-header=x-forwarded-for
server.tomcat.protocol-header=X-Forwarded-Proto
server.use-forward-headers=true
シナリオ4: Nginx (HTTPS 非443ポート: 4005) が OAuth Client (ポート8082) をプロキシ
HTTPSと非標準ポートの両方に対応する必要がある場合です。このシナリオでは、ポート情報が再び失われる問題が発生します。
問題の動作:
- リクエスト:
https://blog.95id.com:4005/user_attr - リダイレクト先:
https://blog.95id.com/login(ポート4005が欠落)
原因: Spring Bootのデフォルト設定では、X-Forwarded-Port ヘッダーを参照しないためです。
解決策: X-Forwarded-Port ヘッダーを使用してポート情報を明示的に渡します。
Nginx設定:
proxy_set_header X-Forwarded-Port $server_port;
アプリケーション設定 (application.properties):
server.tomcat.port-header=X-Forwarded-Port
シナリオ5: 2層のNginx リバースプロキシ
複数層のNginxを経由する場合、プロトコルとポート情報が適切に伝搬されない問題が発生します。
アーキテクチャ: クライアント → Nginx1 (ポート4005, SSL終端) → Nginx2 (ポート4005) → OAuth Client (ポート8082)
問題: Nginx1がHTTPSをHTTPに変換してNginx2に転送するため、Nginx2は元のプロトコルがHTTPSであることを認識できません。
解決策: プロトコル情報をNginx1からNginx2へ伝搬する必要があります。
Nginx1 (最外部) の設定:
proxy_set_header Host $host;
proxy_set_header X-Real-IP $remote_addr;
proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;
proxy_set_header X-Forwarded-Proto $scheme;
proxy_set_header X-Forwarded-Port $server_port;
Nginx2 (内部) の設定: 受信した X-Forwarded-Proto ヘッダーをそのまま転送する必要があります。以下の2つの方法があります。
方法1: プロトコルを強制的にHTTPSに固定する (全てのトラフィックがHTTPSである場合)
proxy_set_header X-Forwarded-Proto https;
方法2: map ディレクティブを使用して動的に設定する (HTTPとHTTPSが混在する可能性がある場合)
http コンテキスト:
map $http_x_forwarded_proto $thescheme {
default $http_x_forwarded_proto;
'' $scheme;
}
server または location コンテキスト:
proxy_set_header X-Forwarded-Proto $thescheme;
最終的な解決策とベストプラクティス
上記のすべてのシナリオを考慮した、汎用的な設定を以下に示します。この設定は、標準ポート、HTTPS、非標準ポート、多層プロキシのいずれの環境でも機能します。
1. アプリケーション設定 (application.properties):
server.tomcat.remote-ip-header=x-forwarded-for
server.tomcat.protocol-header=X-Forwarded-Proto
server.use-forward-headers=true
server.tomcat.port-header=X-Forwarded-Port
2. Nginx設定:
http コンテキスト:
map $http_x_forwarded_proto $thescheme {
default $http_x_forwarded_proto;
'' $scheme;
}
各 server コンテキスト (プロキシ設定):
server {
listen 4005; # 例: リスニングポート
server_name example.com;
# ... SSL設定など ...
location / {
proxy_pass http://127.0.0.1:8082; # バックエンドアプリケーション
proxy_set_header Host $host;
proxy_set_header X-Real-IP $remote_addr;
proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;
proxy_set_header X-Forwarded-Proto $thescheme;
proxy_set_header X-Forwarded-Port $server_port;
}
}
外部ロードバランサー/CDNを使用する場合: もしSSL終端が外部で行われており、Nginxが常にHTTPリクエストを受信する場合は、$scheme 変数が常に http になります。この場合、プロトコルを強制的にHTTPSに設定します。
proxy_set_header X-Forwarded-Proto "https";
Spring Boot以外のTomcatを使用する場合: 内蔵Tomcatを使用しない場合は、server.xml に RemoteIpValve を追加します。
<Valve className="org.apache.catalina.valves.RemoteIpValve"
remoteIpHeader="X-Forwarded-For"
protocolHeader="X-Forwarded-Proto"
portHeader="X-Forwarded-Port"/>
補足: 関連するSpring Boot / Nginx設定の解説
Spring Boot プロパティ
server.tomcat.port-header: ポート情報を上書きするために使用するHTTPヘッダー名を指定します (例:X-Forwarded-Port)。server.tomcat.protocol-header: プロトコル情報を含むHTTPヘッダー名を指定します (例:X-Forwarded-Proto)。server.tomcat.protocol-header-https-value: プロトコルヘッダーの値がHTTPSであることを示す文字列を指定します (デフォルト:https)。server.tomcat.remote-ip-header: クライアントのリモートIPアドレスを含むHTTPヘッダー名を指定します (例:X-Forwarded-For)。server.use-forward-headers=true: この設定により、TomcatはHttpServletRequestオブジェクトから直接取得する代わりに、上記で指定したヘッダーからプロトコル、ポート、リモートIP情報を取得するようになります。
Nginx ディレクティブ
proxy_set_header: バックエンドサーバーに送信されるリクエストヘッダーを設定または上書きします。$scheme: リクエストのプロトコル (http または https) を表す変数です。$server_port: サーバーがリクエストを受信したポート番号を表す変数です。$host: リクエストのHostヘッダーの値を表す変数です。$http_x_forwarded_proto: 受信したリクエストのX-Forwarded-Protoヘッダーの値を表す変数です。map: 変数の値を他の値にマッピングするためのディレクティブです。ここでは、受信したX-Forwarded-Protoヘッダーが存在する場合はその値を、存在しない場合は$scheme変数の値を使用するために使用しています。