はじめに
本記事は、OpenAM/OpenIGを使用したシングルサインオン環境の構築という記事から続く関連記事の一部です
本記事では、OpenIGの多段プロキシ構成について各種説明を行う。
OpenIGはリバースプロキシサーバーであり、Webブラウザ => OpenIG => バックエンドサーバー という流れが基本的なのだが、特定条件下ではOpenIGの前か後ろに別のプロキシサーバーを配置する構成をとることがとても有力なときがある。
バックエンドサーバー(社内システムのWebアプリ)が大量に存在する、利用ユーザー数が数百~数千規模になるのであれば、この多段プロキシは非常に効果的であるため、一つの記事として多段プロキシ構成について説明をする。
(むしろWebアプリの構造などによってはOpenIGでは対処しきれないため、この構成を必ずとらなければ解決できない・・・といったこともよくある)
注意点としては、この構成は自分で悩みぬき、考え、ひねり出した構成なので、他社やSSO業界でこういう呼び方やこういった構成が一般的かどうかはわからない。
多分、ベストプラクティスではない気がする。気軽にとれる対策としてはかなりアリだとは思っているが・・・
というわけで、間違っている/もっと良い方法があるという可能性は十二分にあるため、そこだけは注意して欲しい。
そしてそういったことがあるなら、是非是非教えてもらえると非常に嬉しいです。
前提
- 既にOpenAMおよびOpenIGはインストール済であること
- 検証目的の構成を行う
環境
-
OpenIG側
- サーバOS: CentOS7.3
- ホスト名: openig.test.local
- JDK Ver: 1.8.0_111
- Jetty Ver: 8.1.21
- OpenIG ver: 4.0.0
-
SSO対象となるバックエンドサーバーのWepアプリのURL構成
- URL: http://hogehoge.test.local/keihi/
- ログインページ: http://hogehoge.test.local/keihi
- ログイン処理ページ: http://hogehoge.test.local/keihi/login
- ログイン後ページ: http://hogehoge.test.local/keihi
多段プロキシ構成とは
上で説明しているので簡単に。
普通、リバースプロキシとしてOpenIGを使用し次にバックエンドサーバーのWebアプリへ接続・・・ という形のはずだが、もう一段前か後ろにプロキシを置く構成のこと
- 本記事の中ではOpenIGの前にリバースプロキシとしてnginxを置く構成を想定
- 他にもOpenIGの次にapacheを置き次に社内フォワードプロキシを置く・・・という構成もやったことがある
- 結局ボツになって検証だけで終わったけど
多段プロキシ構成をとる理由
大きくわけて3つ
- バックエンドサーバーのWebアプリがShift-JIS環境である場合
- 絶対パスでのリンクが存在する場合
- 負荷低減
バックエンドサーバーがShift-JIS環境である場合
別記事でこの辺は詳しく書くつもりなのだけれど、まだ公開していないし、そもそもまだ書いていないしってことで、ここで多少説明を。
バックエンドサーバー、本記事内では社内の業務システム系Webアプリを想定しているが、それらのシステムがShift-JIS環境かつ日本語を使ったパス構造であった場合、非常に大きな問題が発生する。
Shift-JIS環境というのも、何というかどう言えばいいのかわからないが、要はHTMLの中の文字コードであったり、URLがShift-JISで表現されている環境のことを指す。
近年はUTF-8が主流なのでそんなの見るかよ、と思うかもしれないが、レガシー社内システムだとShift-JISなんて全く珍しくない。
では、具体的に何が問題になるのかと言うと、OpenIGとJettyは内部的に文字コードとしてUTF-8しかサポートしていない関係で、リバプロ時にもUTF-8としてHTTPリクエストを投げてしまい、結果Webアプリ側で不正な通信となってしまう。
(ちなみにまずそもそもJettyの文字コードがUTF-8であり、この状態ではエラーが発生してOpenIGにも到達せずサービスが止まってしまう。が、Jettyについては回避方法があるので現実的にはOpenIGのみ問題になる)
つまり、WebブラウザからHTTPリクエストが飛んできたとき、OpenIGではUTF-8として解釈し、そのままWebアプリへリバプロする。
バックエンドサーバではShift-JISとして処理を進める。当然、これではうまくいかない。
ただ、実は単にShift-JISであること自体は問題にならない。
なぜかというと、Shift-JISもUTF-8もASCII互換文字コードであるため、アルファベットや数字であれば、Shift-JISでもUTF-8でも、バイナリは変わらない。
なので、問題にはならない。
しかし、Shift-JISであり、ましてや我々が日本人である以上、日本語が多用される。
日本語の漢字などはShift-JISとUTF-8でバイナリが変わる。
そのため、Webブラウザ-OpenIG-Webアプリ それぞれで齟齬が生じ正常な通信ができなくなってしまう。
まとめると、仮にShift-JIS環境であったとして、下のパターンであればこの問題が発生する。
あるいは、試したことはないが、今はドメイン名として日本語を使えたりするので、その場合でも問題が発生すると思う。
問題ないパターン
問題あるパターン
さぁ、どうしよう。
はっきり言うと上述した通り、OpenIGでは回避する方法がない。
私もリファレンスガイドを読み込み、ネット上の情報などでもかなり探したが、OpenIGの設定で文字コードを制御するような項目は存在しない。
そして現実的にどうしようとなったとき、策としてこの多段プロキシパターンが非常に有効になる。
ログイン時のみOpenIGを使用し、それ以外はapacheやnginxのリバプロを使う。
apacheやnginxの場合、経由するHTTPリクエストやレスポンスの文字コードが何であろうと気にしない、そのままリバプロする。
つまり、今回のようなことが問題になることはない。
ちなみに私がこの対策をとったときは大丈夫であったが、ログイン処理ページのURLやパスが日本語であると・・・うん、無理・・・
非常に申し訳ないが、その場合の回避策は今のところ私は持っていない。
絶対パスでのリンクが存在する場合
これも原則OpenIGでは回避不可能な問題。
WebアプリのHTMLのリンクなどが以下のようなものであった場合、どうなるだろう。
<a href="http://hogehoge.test.local/keihi/seisan">経費精算はこちら</a>
上記のように絶対パスで書かれている場合、OpenIGのURLはopenig.test.localである以上、このリンクに進んでしまうと、OpenIGの配下から外れてしまう。
恐らくはリンク先で、認証されていないのでログイン画面に戻されるとか何かしらのエラーが発生するとか、そういった挙動になるだろう。
つまりOpenIG配下から外れないようなリンクは以下になる。
<a href="/keihi/seisan">経費精算はこちら</a>
他にも書き方はあるが、一般的な形は上記のような形であろう。
上記のような相対パスであれば、実際のリンク先URLは今現在のドメインが使用されるため、http://openig.test.local:8080/keihi/seisanに飛んでくれる。
もちろん絶対パスでもあっても、以下のような形であれば良い。
<a href="http://openig.test.local:8080/keihi/seisan">経費精算はこちら</a>
とはいえ、現実のWebアプリ側でHTMLのコードを書き換えてもらうのは現実的ではない。
もちろん依頼を出して簡単にやってくれるなら多分それが一番早いが、社内システムとかだとそういった対応であっても予算がないから無理とかになってしまう可能性は高いだろう。というかうちの会社がそうだった。
こうなったときSSOシステム側でどうにかしないといけないわけでだが、現実的な案が多段プロキシ構成である。
apacheやnginxであれば、経由するHTML(HTMLだけではないが)を書き換えることが出来る。
そうすれば、Webブラウザに到達したときには、こちらが意図するリンク先(OpenIGから外れないURL)になっているため、問題は発生しない。
いや、そもそもOpenIGで出来ないの?と思うし私もそう思って検証したところ、以下のような結果になった。
- EntityExtractFilterというFilterがあってこれはレスポンスボディにアクセスできるFilterだけど、あくまで正規表現でマッチしたものを変数に突っ込むだけなので、レスポンスボディ自体を書き換える能力があるわけではない
- これはPasswordReplayFilterのloginPageExtractionsも同じ
- Response Objectにentityというプロパティがあって、リファレンスガイドには、「The message entity body」と書かれている
- おぉ!?いけるか?と思うが、「(no accessible properties).」とも書いている
- 実際試してダメだった
仮にOpenIGでレスポンスボディのHTMLを直接書き換えれるにせよ、それをOpenIGにやらせるのは、んーどうだろう?って思う。
ただでさえ、OpenIGで色々やらせていて負荷がかかりやすいのに、こんなことリアルタイムでやったら、さらに負荷をかけることになってしまう。
ましてや経由するレスポンスボディを全て見て特定の文字列を全置換なんて、決して軽い処理ではないわけで。
上記のことから、OpenIGではなく他のやつにやらせましょうよ、ということで多段プロキシ構成をとる。
負荷低減
2.と関連する話なのだが、OpenIGの負荷を下げるという目的で、この構成をとることもかなり有効。
そもそもログイン代行処理が終わったら、その先はOpenIGがリバプロする必要性って実はない。
OpenIGのメインの仕事であり、OpenIGでなければ出来ないことは、あくまでログイン代行処理なのだから。
普通にパススルー的な感じでのリバプロであれば、OpenIG以外のapacheやnginxにだってそういったことは十分出来るし、OpenIGよりも遥かに多く全世界で実用的に使われている。
例えばSSO配下から外れないようにURLを制御したいってことでURLのrewriteやLocationヘッダ制御してるなら、それって別にOpenIGじゃなくてもginxやapacheだってできる。
※計測したことあるわけじゃないが、多分彼らのほうが高速・軽量のはず。つまり、同じリバプロでもOpenIGにやらせるより、apacheやnginxにやらせたほうがより良いだろう、ということ。
なので、リバプロを2つ用意し、ログイン時のみOpenIGを経由、それ以外はnginxやapacheを経由とすれば、OpenIGの仕事を減らすことができ、つまり負荷を下げることができ、OpenIGのみならずシステム全体的な負荷の平滑化を図れる。
※純粋なリバプロ能力としてもOpenIGよりnginxのほうが上だろう。今度、ベンチマークとってみたい。
また、OpenIGを使うとログイン後ページの様々な機能に不具合が生じる・・・といったことがよく発生するが、この構成をとるとそういった影響を抑えることができたりする。
というのも、私も全て原因を究明したわけではないのだが、OpenIGを経由するとどうもHTTPリクエスト/レスポンスに不正な値が含まれてしまったりするのかどうかわからないが、Webアプリ側の機能に不具合が発生してしまうことが多い。
要は社内システムであれば、とある検索ボタンを押したら結果が返ってこない、とか。とある書類のPDF生成がうまくいかない、とか。
この点、apacheやnginxの場合、本当に何もせずパススルーというか透過しているようなリバプロの動きのようで不具合が発生しなくなる・・・といったことが何度もあった。
技術者として原因究明しないのはどうなんだ、とは思うが、とにかく不具合多発しまくって参っていた、ということもあって、具体的に何が悪くてどう改善したのかは調べきれなかった。
まぁとにかくこういったこともあるので、多段プロキシ構成は非常に有効だと個人的には思っている。
フロー図
パターン別に図で表してみる。
1.WebアプリがShift-JIS環境である場合
と3.負荷低減
のときは以下のような流れになる。
2.絶対パスでのリンクが存在する場合
の場合は以下。絶対パスの書換が目的であるため、ログインページか否かというので振り分けはしていない。
1つ上の図では振り分けをしていなかったが、もちろん実際は振り分けをしたって良い。
OpenIG負荷を下げられるならそれに越したことはないわけで。
OpenIG CookieFilter について
多段プロキシ構成をとるときに一番問題になるというか、キモとなるのがCookieの扱いである。
まず別記事(OpenIGルートファイルの作り方)で述べている通り、OpenIGでCookieを扱う必要があるときはCookieFilterを使用する。
CookieFilter自体の詳細については上記の別記事の内容を読んでほしいが、このCookieFilterが多段プロキシ構成ではかなり重要になってくる。
どういった点が重要なのかというと、CookieFilterのデフォルトの動作モードであるMANAGEでは、これはCookieをOpenIG自体で管理するため、ユーザーまで届かない(Webブラウザまで届かない)、
つまりバックエンドサーバーのWebアプリに対し、Cookieとして、HOGE=fooという値がなければ認証状態とは見なされないという場合、WebブラウザでCookieの状態を見たり、あるいはクライアント端末上でパケットキャプチャを行ったとしても、そのHTTPリクエストにはHOGE=fooは付与されていない。
とはいえ、現実的には全く問題ではなく、これらのCookieはOpenIGが管理しているため、リバプロ時にOpenIGが自動的にCookieを付与してHTTPリクエストをバックエンドサーバーのWebアプリに対して送信する。
Webアプリ開発など行ったりしてCookieについてある程度ご存知であれば、もうおわかりになるかと思うが、この動作が非常に問題になる。
まず多段プロキシ構成で、一段目にnginx、二段目にOpenIGを置くとした場合、ログイン後はnginxのみを経由するという構成の場合。
先程の通り、WebブラウザはCookieを持っていないため、当然そこからのHTTPリクエストにもCookieは付与されていない状態になる。
当然、nginxもそのままリバプロしてバックエンドサーバーのWebアプリに送信する。
そうすると、現実的にはログイン後ではあるのだが、Cookieがない以上、当然ながらWebアプリからすれば認証してねーよ!となり、恐らくはログイン画面に戻されたりするだろう。
この問題の対策として、デフォルトのMANAGEではなくRELAYを使用する。
RELAYはその名の如く、Cookieをリレーする。
つまりCookieに対してOpenIGは何も関与せず、HTTPレスポンスのSet-CookieがそのままWebブラウザまで届く。
そして、Set-CookieによりWebブラウザにはCookieが保存されるため、WebブラウザからのHTTPリクエストにもCookieが付与される。
これによって、一段目リバプロ単体経由のときでもログイン後の認証Cookieを持った状態であるため、正常にアクセスすることができる。
そのため、多段プロキシ構成におけるOpenIGはCookieFilterはRELAYを使うのが必須となる。
ただし、このRELAYの挙動にも罠があり、うまくいかないパターンがある・・・はぁ・・・orz
CookieFilterの罠
普通のOpenIGの一段構成の場合、CookieFilterを使うときはMANAGEで、PasswordReplayFilterはloginContentMarkerで使うことでCookieを扱いながらログインが出来ることは別記事(OpenIGルートファイルの作り方)でも述べている。
ただ、実はCookieFIlterをRELAYにすると、この動作が不可能になってしまう。
どういうことかと言うと、CookieFilter:MANAGEの場合、OpenIGがCookieを管理していたため、PasswordReplayFilterによりOpenIG内部でHTTPリクエストが作られ送信されるタイミングで、Cookieが付与された。
何故かと言えば、OpenIGが管理しているし、存在を知っているからである。
だが、RELAYになると、Cookieの管理はユーザー側のWebブラウザに委ねられる。
つまり、OpenIGはCookieを管理していないし、存在を知らない。
そのため、以下の画像のようにPasswordReplayFilterによるHTTPリクエストにはCookieは付与されていない状態になり、結果認証が失敗する。
さてどうするか・・・
例えば、ログイン時に必要なCookieとログイン後に必要なCookieが分かれて存在しているならば、以下のようにログイン時に必要なCookieのみMANAGEにするという回避方法がある。
この方法ならば、原則的にCookieFilterがRELAYで動き、ログイン時に必要CookieであるHOGEHOGEのみMANEGEによってOpenIGに管理させる・・・という動作になり、この問題を回避できるはずだが、そういった構成のWebアプリはそれほど多くないと思う・・・
少なくとも社内システムでそういったものはまだ見たことがない。
あとここまで書いていて何だが、見たことがなく試したこともないので、回避できるかどうかも確かめてはいない。ただ理論的にはいけるはず。
{
"type": "CookieFilter",
"config": {
"managed": ["HOGEHOGE"],
"defaultAction": "RELAY"
}
}
と、ログイン時もログイン後もCookieの値が異なったものにならないパターンのときはどうするか。
もっと他の良いやり方がありそうな気がするが、私が考えた方法を紹介する。
そもそも今回の問題は、CookieをOpenIGが管理していないためOpenIGから送信されるHTTPリクエストにCookieが付与されない。
それならば、自動的にOpenIGが付与してくれないなら、手動で付けてしまえば良い。
まずは一度対象のWebアプリに至って普通にアクセスしてSet-CookieによりCookieを取得する。
ただしこのままではユーザーのWebブラウザには対象Webアプリのログイン画面が表示されていて、全くSSOになっていない。
SSOを実現するためには既にCookieを取得しているこの状態で再度、OpenIG経由でWebアプリにアクセスする必要がある。
SSOだというのにまさかユーザーにもう一度アクセスして下さいとか更新して下さいとか言うわけにもいかず、何とかして自動的にアクセスする必要がある。
解決策として一度目のアクセスの際、一段目リバプロにより、HTTPレスポンスヘッダーにrefresh
を付与する。
"refresh"ヘッダーは簡単に言えば、このヘッダーが存在する場合、ブラウザにページの更新をさせることが出来る。
つまりこの場合で言えば、Cookieを取得した状態での再アクセスを実現することが出来る。
こうした二度目のアクセスの際、そもそものHTTPリクエストには既に必要なCookieが存在するため、OpenIGに到達した時点でも当然そのCookieが存在する。
具体的に言えば、Request Object内にCookieが存在している。
そして、ルートファイルを少々変更し、そのCookieをPasswordReplayFilterのHTTPリクエストに付与されるように明示的に指定する。
これにより、OpenIGのログイン代行処理のPOSTにはCookieが付与され、正しいユーザー名やパスワードが送られるため、結果認証成功、SSOが成功する。
ルートファイルとしては、以下のような感じでPasswordReplayFilterのrequestに対して、headersを定義し、さらにその中でRequest ObjectのCookieを指定する。
具体的には、request.cookies[cookie_name][num].value
でRequest ObjectのCookieを取得できる。
本来であればこのheadersのCookieはOpenIGが勝手にごにょごにょしてくれるが、それはMANEGEの話。
RELAYであればそういったことはやってくれないため、このようにして手動でCookieの付与をしてあげる。
{
"type": "PasswordReplayFilter",
"config": {
"loginPageContentMarker": "あなたはまだログインしていません",
"request": {
"method": "POST",
"URL": "http://hogehoge.test.local/keihi/login",
"headers": {
"Cookie": ["HOGEHOGE=${request.cookies['HOGEHOGE'][0].value}"] <== ここが重要ポイント
},
"form": {
"login_id": [
"${request.headers['username'][0]}"
],
"passwd": [
"${request.headers['password'][0]}"
]
}
}
}
},
正直言ってかなりの力技だし、ユーザー側から見ても少し気持ち悪い動きになってしまうのだが・・・
私が考えた限り、これより良い方法が思い浮かばなかった。
多段プロキシ構成を組むのが難しいパターン
難しい点がありながらも何とか実現は可能で、実現さえすれば良いことづくめの多段プロキシ構成だが、この構成をとりたくてもとれないときもある。
ただ、完璧ではないが対策自体はある。
- ログインページとログイン後ページでURLが変わらない
- 一段目でOpenIGとバックエンドサーバー、どちらに振ればいいか判別できない
- loginPageContentMarkerを使用していて、対象Webアプリのログイン認証処理のHTTPレスポンスにSet-Cookieが含まれている場合
- CookieFilterのrelayの処理の問題で、ログインのためのPOSTのレスポンスでSet-Cookieがくると、それをWebブラウザにリレーしてくれない
前者のパターン1は比較的わかりやすい。
例えば、以下のようなログイン前と後でURL自体は変わらず、認証済かどうかで表示する内容が変わる構造であった場合
- ログインページ: http://hogehoge.test.local/keihi/
- ログイン処理ページ: http://hogehoge.test.local/keihi/login
- ログイン後ページ: http://hogehoge.test.local/keihi/
これでは一段目のリバプロ、例えばnginxなどでは、二段目リバプロのOpenIGに振るべきか、そのままバックエンドサーバーへ向かうべきか判断が出来ない。
これについては対策がある。
nginxでいえば、普通locationとproxy_passディレクティブを使用してリバプロを構成するが、その際Cookieなどログインしているかどうかを判別できる要素を使用して振り分けを行えば良い。
対策の詳細については、後述する。
まずはパターン2の説明を。
後者のパターン2がこれがまたかなりきつい。
CookieをOpenIGが管理しないためPasswordReplayFilterからのHTTPリクエストにCookieが付与されない件については前項にて説明をしたが、一体これはなんだ?と言うと・・・
前項の話は以下の画像のような構造になっている場合。
つまり、ログイン時に必要なCookieとログイン後のCookieが変わらない。
問題になるのは、ログイン時に必要なCookieと、さらにログイン後、正確には認証処理のHTTPレスポンスにてCookieが追加されたり、同名で変更されたりするパターン。
PassowrdReplayFilterのHTTPリクエストのレスポンスに重要なSet-Cookieが含まれている場合、図で言えばHOGE=bar,FUGA=bazのSet-CookieがWebブラウザまで届かないという問題がある。
この問題によりSet-CookieがWebブラウザまで到達しないため、次回アクセスのログイン後ページなどにアクセスすると、本来必要なCookieが存在しない状態であるため、未認証状態に見えてしまい、結果再ログインを求められたりしてしまう。
え?RELAYでしょ?なぜ?と思うだろう。私もそう思う。
これはOpenIGのバグのようで、loginPageContentMarkerを使用している場合に限り、PasswordReplayFilterのHTTPリクエストのレスポンスに対しては、CookieFilterのRELAYが効かないようなのだ。
ちなみにMANAGEなら問題ないし、loginPageならこの問題は発生しない。
私も色々試したが、この問題の根本的な対策は見つけられていない・・・
ただ上述の通り、loginPageを使うならこの問題は発生しないため、何とかしてloginPageContentMarkerではなくloginPageを使用して、引っ掛けられるようにルートファイルを構成するしかない。
元々、Cookiew必要とするWebアプリの関係で否応なしにloginPageContentMarkerを使っていたのであれば、今回の構成では、refreshヘッダーを使ったCookieの取得方法を使うのでそれについては問題ない。
簡単にloginPageで書き直せるなら、そうしてしまおう。
だが、Webアプリの構造的な問題でloginPageContentMarkerを使っていた場合。
例えばログインページとログイン後ページのURLが異なるなら、loginPageでよくやるやり方としてrequest.URL.pathを使って問題なく記述できるであろうが、変化がないときにどうするか。
別記事(OpenIGルートファイルの作り方)で示している通り、loginPageContentMarkerを使って書くのが手っ取り早いし確実な方法となる。
しかし、今回はそれをloginPageで表現したいし、しなければならない。
さて、この対策については、パターン1と2で実は変わらない。
なぜなら。どちらもログインページとログイン後ページでURLが変わらないことが原因だからである。
##ログインページとログイン後ページのURLが変化ない場合の対策
仮に以下のようなURL構成であった場合、loginPageで単にrequest.URL.pathではログインページか否かの判断が付かない。
- ログインページ: http://hogehoge.test.local/keihi/
- ログイン処理ページ: http://hogehoge.test.local/keihi/login
- ログイン後ページ: http://hogehoge.test.local/keihi/
では現実的にどうするか。
多くの場合、nginxであってもOpenIGのloginPageであっても、HTTPリクエストのパスで振り分けを行っている。
そのため、パスが変わらない以上、ログインページか否かの判断がつかない。
ならば、パス以外のその他の要素でログインページか否かの判断をすれば良い。
例えば、上で示した図でいえば、こんなやり方でloginPageで記述できる。
"loginPage": "${matches(request.URL.path,'^/keihi') and length(request.cookies['FUGA'][0].value) == 0}",
上記のloginPageの判定には、パスだけでなく、Cookieが存在するかどうかを見ている。
例で示した図で言えば、ログインが完了して初めてFUGA=bazというCookieが付与される。
そうであるならば、ログインページの時点、つまりユーザーがログインしたいと思った最初のHTTPリクエストのCookieにはFUGA=bazは存在していないはず、と言える。
そのため、loginPageでパスのチェックにandでつなげて、HTTPリクエストのCookieのFUGAの長さをチェックしている。
FUGAが存在しないなら、当然length(文字列長)は0になるはず。
これでFUGAが存在しないことを確認し、かつ、パスが正しいならば、これはログインページであるとの判断をしている。
ちなみにやったことはないがemptyでも同様のことが出来るかと思う。
ただパターン2のOpenIGのloginPageContentMarkerをloginPageに書き直す、という意味ではこれで良いのだが、よくよく考えると、ログインページか否かを判断しているのはOpenIGの前のnginxである。
ましてや、パターン1はそもそもnginxで振り分けが行えないという話であるため、上記のCookieでの判別方法をnginx側で実現してしまえば、パターン1も2もどちらも解決する対策となる。
nginxにおけるCookieは組込変数の$cookie_[cookie_name]
にて参照できる。
今回で言えば、$cookie_FUGA
になる。
ifと組み合わせて、$cookie_FUGAの文字列が空か否かで、proxy_pass、つまりリバプロ先を変更している。
注意点としてはproxy_passの書き方が、ifの中では通常のものとは異なるものになる。
通常は、proxy_pass http://openig.test.local:8080/keihi/といった感じで書いたりするが、ifの中のproxy_passではパスは書けない。
エラーが発生してプロセスの起動に失敗する。
ifの中では、ドメインのみ、つまり、http://openig.test.local:8080としか書けない。
ただ実際の挙動として、下の例でいえば、location /keihi/ ならばproxy_pass http://openig.test.local:8080/keihi/と書いていても、最終的なアクセスとしては、http://openig.test.local:8080/keihi/ になるため、動作としては問題ない。
location /keihi/ {
if ($cookie_FUGA = "") {
proxy_pass http://openig.test.local:8080;
}
if ($cookie_FUGA != "") {
proxy_pass http://hogehoge.test.local;
}
}
こうしてしまえば、そもそもログインまではOpenIGを使用し、ログイン後は一段目のリバプロのみで直接アクセス・・・となるため、先ほどのOpenIG側のルートファイルでCookieを見る処理を入れなくても良い。(もちろん入れても良いが)
だったら最初からnginx側の説明をしろよと思うが、まぁ、OpenIG側でのやり方というのも理解しておいて損はないので、一応ね・・・
こういった手法で、もちろんWebアプリの構造はかなり選んでしまうのだが、対策をとれる。
もちろんCookie以外にも何かしらログイン前と後とで変化する要素があるのであれば、それを使えば良い。
nginxにせよ、OpenIGにせよ、HTTPヘッダー内に存在する要素なら、原則全てにアクセスできるし、ifの要素としては判定できる。
場合によってはCookieの変化がないものもあるだろうから、そういったものはログイン前とログイン後のHTTPヘッダーを見比べて何かしらの差を見つける必要がある。
もし差がないなら・・・諦めるしかない。
構築
それでは、長ったらしい説明が一通り終わったところで、実際の構築についての説明を行う。
ここまで読んでくれた方なら、既にある程度の見当がついているかと思うが、実は多段プロキシ構成は意外と簡単というか設定内容は割と少ない。
- 多段プロキシ対象のルートファイルのCookieFilterをRELAYに変更
- 別の多段用リバプロを構築
まず1.に関してはとても簡単。
例えば以下のようなCookieFilterだとして(というかほぼ間違いなくこうだと思うが)
{
"type": "CookieFilter"
}
以下のようにrelayedで明示的に使用するCookieを指定する。
使用するCookieは対象のWebアプリに対して直接アクセスし、Webブラウザの開発者ツールかパケットキャプチャでもすれば、簡単に判明する。
{
"type": "CookieFilter",
"config": {
"relayed": ["HOGEHOGE"]
}
}
2.ついてはやることは多いが、1つ1つはさほど難しくはない。
まずこの場では、OpenIGとは別のリバプロとしてnginxを使用するものとする。
apacheであってもリバプロとしては動けるので、もちろんそちらを使っても良いが、私として実績があるのはnginxであるので、一先ずこの場ではnginxを用いた説明を行う。
また、nginxはOpenIGと同一サーバー内にインストールするものとする。
この場合、ポート番号80で動作するため、nginxにアクセスするときはopenig.test.local:80、OpenIGにアクセスするときはopenig.test.local:8080にアクセスすることになる。
一段目リバプロとして、nginxを使用するとして、実現したいことは以下である。
- リバースプロキシ機能
- HTMLの書換
- HTTPレスポンスヘッダーの書換
今までの説明の通り、1.は必須として、2.や3.は何故多段プロキシ構成をとるのか、という理由によっては、絶対必要な要件というわけではないが、とりあえず全ての機能を有した構成をとることとする。
1.に関してはnginxはデフォルトの機能としてリバプロ機能が存在するため気にする必要はない。あとはコンフィグの書き方次第である。
2.に関しては、nginxの追加モジュールとしてsub_filterというものを利用する。
sub_filterを利用することでnginxを経由する際、HTMLの書換が行える。
ただし、sub_filterはデフォルト状態ではビルドされておらず、利用することができない。
※CentOS7以降のyumでepel経由で入れるnginxならsub_filterが使えた
そのため、まずはソースコードを持ってきて自前でビルド => インストールという手順を踏む必要がある。
3.に関しても似たような形になる。
nginxはデフォルトの機能として、HTTPリクエストヘッダーの内容を変更することはできるが、レスポンスヘッダーに関しては何も操作できない。
ここで、nginx公式から提供されているわけではないが、ngx_headers_moreというnginx用のモジュールを開発してくれた方がいらっしゃって、このモジュールを使用することで、HTTPレスポンスヘッダーに追加や変更を行うことができる。
つまり、多段プロキシ構成用のnginxを用意しようと思ったら、yumなどによるお手軽インストールではなく、2.と同じくソースコードからのビルド/インストールが必要になるということになる。
というわけで、ざざっと手順を。
まぁやってることはngx_headers_moreのInstallationに書かれている内容に、少しだけコンパイルオプションを増やしているだけ。
[root@openig /usr/local/src/]# wget 'http://nginx.org/download/nginx-1.11.2.tar.gz'
[root@openig /usr/local/src/]# tar -xzvf nginx-1.11.2.tar.gz
[root@openig /usr/local/src/]# cd nginx-1.11.2/
[root@openig /usr/local/src/nginx-1.11.2/]# ./configure --prefix=/opt/nginx \
--add-module=/path/to/headers-more-nginx-module \
--with-http_sub_module
[root@openig /usr/local/src/nginx-1.11.2/]# make
[root@openig /usr/local/src/nginx-1.11.2/]# make install
これで/opt/nginxに、ngx_headers_moreとsub_filterが有効になったnginxがインストールされる。
次にコンフィグを書いていこう。
まず、nginxでリバプロを使う場合は、locationでリバプロ対象のURLを定義して、中でproxy_passによってリバプロ先URLを定義する。
ざっくりした説明だが、これだけやればとても簡単にリバプロを実現できてしまう。
この辺はググればnginxにおけるリバプロの設定のしっかりとした説明が腐るほど出て来ると思うのでこの場では説明しない。
さて、今回の構成における非常にシンプルなリバプロ用コンフィグの基本形は以下となる。
location = /keihi/login {
proxy_pass http://openig.test.local:8080/keihi/login;
}
location /keihi/ {
proxy_pass http://hogehoge.test.local/keihi;
}
まず、対象Webアプリのベースとなるパスが/keihiであるとして、ログイン処理ページが/keihi/loginだったとした場合、上記のようなコンフィグになる。
ログイン時のみ二段目リバプロであるOpenIG側へ移し、それ以外のページに関しては直接Webアプリへアクセスする。
これによりOpenIGはログイン時のみしか仕事をしないため負荷を下げることが出来るし、Shift-JIS + 日本語のパスがあったとしても、nginx側を経由するのであれば全く問題にならないはずだ。
次にHTMLの書換するときのコンフィグ。
とても簡単で、対象のlocationの中で、sub_filter [検索文字列] [置換文字列] と指定する。
下の例では、http://hogehoge.test.local/keihi/seisan を http://openig.test.local:8080/keihi/seisanに書き換えている。
これで本来ならOpenIG配下を外れてしまうなリンク先をOpenIG経由になるように強制的に書き換えている。
また、sub_filter_onceをoffにする。デフォルトではonの状態になっていて、これでは置換が1回しか行われない。
HTML内に存在する全ての絶対パスを書き換える・・・と考えると、そういった絶対パスが複数存在することを想定するべきなので、これはoffにする。
さらに、書き換える対象のMIMEタイプが"text/html"だけでなく、その他も書換えたい場合は、sub_filter_typesも変更する必要がある。
デフォルトは"text/html"になっている。
下の例では"*"を指定してどんなMIMEタイプでも対象にして書換えを行う。絞れるなら絞っておいたほうが良いと思う。
そうでなければ、nginxを経由する全てのHTTP通信に対してsub_filterが働こうとしてしまうため、動作効率的に考えてあまり良いとは言えないだろう。
location /keihi/ {
proxy_pass http://hogehoge.test.local/keihi;
sub_filter "href=\"http://hogehoge.test.local/keihi/seisan" "href=\"http://openig.test.local/keihi/seisan";
sub_filter_types *;
sub_filter_once off;
}
次にrefreshヘッダーを追加するコンフィグ。
現実的なやり方としてこの方法をとることは多いはず。
現在のWebアプリであれば、当たり前のようにCookieを使用するわけで、この方法が多段プロキシ構成における基本パターンと思って良いのかもしれない。
今まではログイン処理とログイン後で2つのlocationを用意していたが、このパターンでは、さらにもう1つ、Cookieを取得するためだけのlocationを用意する。
このCookie取得用のlocationにてHTTPレスポンスにrefreshヘッダーを追加する。
refreshヘッダーは、オプション設定として再アクセスを行うまでの秒数と再アクセス先URLを指定することができる。
秒数としては極力小さい数字を指定し、再アクセス先URLでログイン処理用のlocationに届くようなURLを指定する。
※下記の例ではURLではなくパスだが
location = /keihi/login/dummy {
proxy_pass http://hogehoge.test.local/keihi;
more_set_headers "refresh: 0.1;url=/keihi/login";
}
location = /keihi/login {
proxy_pass http://openig.test.local:8080/keihi/login;
}
location /keihi/ {
proxy_pass http://hogehoge.test.local/keihi;
}
ユーザーが最初にアクセスするのは一番上のCookie取得用のパスになる。つまり、http://openig.test.local/keihi/login/dummyとなる。
ここは結局のところ、オリジナルのSSO対象Webアプリであるhttp://hogehoge.test.local/keihiにリバプロされる。
これにより、Set-Cookieが返ってきてWebブラウザはログインに必要なCookieを手に入れることが出来る。
また、nginxのmore_set_headersによりHTTPレスポンスにrefreshヘッダーを追加する。
refreshヘッダーによって0.1秒後には/keihi/loginに対して再度アクセスが行われる。
/keihi/loginはOpenIG側の同じパスに対してリバプロする。
このとき既にWebブラウザから送信されたHTTPリクエストにはログインに必要なCookieが存在するため、OpenIG側でそのCookieをPasswordReplayFilterに明示的に指定する。
対象Webアプリのルートファイルを開き、下記のように編集する。
既にルートファイルの中にPasswordReplayFIlterは存在すると思うので、ほんの少しの修正で済む。
{
"type": "PasswordReplayFilter",
"config": {
"loginPageContentMarker": "あなたはまだログインしていません",
"request": {
"method": "POST",
"URL": "http://hogehoge.test.local/keihi/login",
"headers": { <== headersを追加
"Cookie": ["HOGEHOGE=${request.cookies['HOGEHOGE'][0].value}"] <== 明示的にCookieを指定
}, <== headersの定義終了
"form": {
"login_id": [
"${request.headers['username'][0]}"
],
"passwd": [
"${request.headers['password'][0]}"
]
}
}
}
},
具体的に言えば、PasswordReplayFilterでheadersを定義することで、PasswordReplayFilterから送信されるHTTPリクエストにHTTPヘッダーを追加することが出来る。
Request Object内に存在するログインに必要なCookieを明示的に指定して、PasswordReplayFilterのheaders内に定義する。
これによって、ログインに必要なCookieが存在するログイン代行用のHTTPリクエストを送信することができる。結果、認証成功するはずだ。
ただし、"多段プロキシ構成を組むのが難しいパターン"に該当する場合は、loginPageContentMarkerをloginPageに書換えたり、nginx側のコンフィグを書換えたりなども作業も必要になる。
どう変更するかについては、詳細を該当箇所で説明しているため、ここでは述べない。
まとめ
ここまで、多段プロキシ構成を組む必要があるパターンと、どのように構築すれば良いかという話の説明を行ったが、多段プロキシ構成は自身で作ってみれば、構築にあたって意外と作業項目が少ないことに気づくと思う。
仮に完全に新規でOpenIGを構築する場合、いきなり多段プロキシ構成を目指すのではなく、個人的にはまずOpenIG単体でのSSOを目指し、それが出来上がったら次に多段プロキシ構成化させるという流れが良いと思っている。
この流れならルートファイルやコンフィグの内容のほとんどをコピペでいけるので、それこそ事前にnginxが用意されていたりするならば、かなり簡単に多段プロキシ構成に変更することが出来るし、その構成を量産できる。
もちろん慣れてきたら、あーこれは多段構成にしないと無理だなーと設計段階で気づくし、最初からそういった構成で構築も出来たりするが、まずは慣れていない時期は単体構成の構築を目指したほうが良いと思う。
多段プロキシ構成は非常にメリットが多いため、対象Webアプリの構造上不可能な場合を除いて、原則的には多段プロキシ構成にすることを目指したほうが良いと考えている。
もちろん対象Webアプリが1つか2つしかないとか、利用者が非常に少ないとかであれば、Shift-JIS問題などで必要に迫られない限りは、無理にしなくても良いかもしれないが。
本記事で述べた通り、OpenIGのCookieFilterは非常に単純なように思えて意外と奥が深い。悪く言えば挙動が怪しい。
実際の構築の中で、おや?あやしいぞ?と思えるようなことがあったら、CookieFilterの処理を行っている箇所のソースコードを読むのが解決の早道となる。
その他の構成
本記事で説明した内容と反対に、OpenIGを一段目、二段目に別のプロキシ・・・といったパターンも考えられる。
何に使うのかと言うと、私が試したことがある経験の中だと、社内NWに置いたOpenIGからインターネット上のサービスへSSOしようとしたとき。
プロキシを通さなければインターネットに出られないという場合、OpenIGにはフォワードプロキシを設定できないため、OpenIGの次にapacheを配置し、さらにその先で社内NWのフォワードプロキシを設定・・・といった形でインターネットに出ていく構成をとったことがある。
何故apacheを使ったのかと言うと、恐らくだがnginxではリバプロ時にフォワードプロキシを設定する方法がないと思う。
※調べた限りではわからなかった
apacheであれば、リバプロしつつも、その際にフォワードプロキシを使うことが出来る。そのため、apacheを採用した。
この手法により、GappsにOpenIGを使ってSSOすることが出来た。
まぁSAMLでSSOしたほうがスマートではあるのだが、色々諸事情があってOpenIGでのリバプロ方式でやるしかなかったので・・・