はじめに
本記事は、OpenAM/OpenIGを使用したシングルサインオン環境の構築という記事から続く関連記事の一部です
本記事では、OpenIGのルートファイルについて各種説明を行う。
その他OpenIG関連記事を見て分かる通り、OpenIGの要はルートファイルである。
これがまたやれることが非常にたくさんあるので、ルートファイルを作ろうと思ったとき、なかなかどうやって作っていけば良いかわからない。
そのため、ルートファイルを作るにあたってどういった構成物があるのか、それぞれどういった意味があるのか、どう組合せていけばよいのか、そういったことを全てではないが、ざざざっと紹介していきたい。
ルートファイルの構成物は非常に多彩であり、私も全てを使ったことがあるわけでも、理解しているわけでもないため、少なくとも自分が使ったことが有る範囲内で出来る限りの説明をしていきたいと思う。
前提
- 既にOpenAMおよびOpenIGはインストール済であること
- 検証目的の構成を行う
環境
-
OpenAM側
- サーバOS: CentOS7.3
- ホスト名: sso.test.local
- IPアドレス: 172.16.1.130
- JDK Ver: 1.8.0_111
- Tomcat Ver: 8.0.39
- OpenAM Ver: 13.0.0
-
OpenIG側
- サーバOS: CentOS7.3
- ホスト名: openig.test.local
- JDK Ver: 1.8.0_111
- Jetty Ver: 8.1.21
- OpenIG ver: 4.0.0
ルートファイルとは
ルートファイルは、OpenIGが動作しているユーザーのホームディレクトリ配下、具体的には[homedir]/.openig/config/route/
配下に配置されるファイルであり、OpenIGは自動的にこの配下のファイルを読み込み、自身(OpenIG自体)に取り込む。
そもそもルートファイルという呼び方が正しいのかどうかもわからないが・・・routeというフォルダ配下に置くファイルなので、一先ず本記事内ではこういった呼び方をさせてもらう。
ルートファイルはjson形式で記述されていて、何なのかと言うと、簡単に言えばリバースプロキシ先の定義でありどう振る舞うかの設定である。
ルートファイル1つ1つがリバースプロキシ先、つまりはバックエンドのWebアプリ、もっと具体的に本記事の例で言うならSSO対象の社内システムに該当する。
つまり、SSO対象の社内システムが5個あるなら、ルートファイルも5個作ることになる。
それぞれのルートファイルには後述するHandlerやFilterが複数定義され、それらはリバースプロキシ先のWebアプリ固有の設定内容になる。
例えば、どういったURIに対して、どのような内容のPOSTを行ってログイン代行を行うのか、というのは、全てルートファイルで定義される内容になる。
そういった意味で、ルートファイルはOpenIGの要であり、OpenIGの構築すなわちルートファイルの作成といって過言ではないと思う。
ルートファイルの構成物
ルートファイルの構成物として以下がある。
絶対全てを使うというわけでもないのだが、何だかんだでほぼ全て使っている。
-
Heap Objects
- ルートファイルの基礎となるオブジェクトであり、ルートファイルを作るとなったら、まずはHeap Objectsを作ることになる。とはいえ実際にはルートファイルというより、config.jsonで定義することがほとんどなので、あまり見る機会も触る機会も実際には少ない。
-
Handlers
- 名前の如く、HTTPリクエスト/レスポンスのハンドリングを行う。多くの場合1つのルートファイルの中には複数のHandlerを定義する。
-
Filters
- HTTPリクエスト/レスポンスをインターセプトして様々なフィルタリングを行える。正直な所ルートファイルを作るときはHeapやHandlerはほぼ固定化されていてFilterをどう組み合わせるかというやり方になる。
-
Decorators
- Heapのデコレートを行う。例えばログ設定だったり、HTTPリクエスト/レスポンスのキャプチャ設定を追加・変更できる。
-
Expressions/Functions
- OpenIGには評価に使う様々な演算子や関数が存在する。例えばand, or, ==, !=, <, > あるいはmatches, toString, splitなどなど。三項演算子も使える。
-
Request, Response Objects
- OpenIGの中でHTTPリクエスト/レスポンスは、Request/Responseという名のオブジェクトとして存在する。これらのオブジェクトは、ルートファイル内全ての場所で変数的に扱える。例えば、request.uri.path となれば、HTTPリクエストのパスが格納されている。
ルートファイルの構成例
現実的なルートファイルの中身としては、大きな単位でHandlerが1つあって、その中に複数のFilter、最後にもう1つかHandlerがある・・・という形が王道だと思う。
例として、別記事のOpenIGとOpenAMを連携させた記事内で紹介した基本的なルートファイルを使って少し説明を。
{
"handler": { <== ここからHandlerの定義ですよ、という宣言。単一のHandlerのみ定義する。
"type": "Chain", <== まずはChain Handlerを定義、複数のFilterともう1つHandlerを定義できるようにする。
"config": {
"filters": [ <== ここから配列形式で複数のFilterを定義する。
{
"type": "PasswordReplayFilter", <== ログイン代行処理を行うFilter
"config": {
"loginPage": "${true}",
"headerDecryption": {
"algorithm": "DES/ECB/NoPadding",
"key": "DESKEY",
"keyType": "DES",
"charSet": "utf-8",
"headers": [
"password"
]
},
"request": {
"method": "POST",
"uri": "http://openig.test.local:8081",
"form": {
"username": [
"${request.headers['username'][0]}"
],
"password": [
"${request.headers['password'][0]}"
]
}
}
}
},
{
"type": "HeaderFilter", <== 2個目のFilter定義。OpenAMから渡ってきたpasswordとusernameというHTTPヘッダを削除する。
"config": {
"messageType": "REQUEST",
"remove": [
"password",
"username"
]
}
} <== この例では2個のFilterだがさらにもっと追加することもできる
],
"handler": "ClientHandler" <== ClientHandlerを定義することでHTTPリクエスト/レスポンスを処理できるようにする。おまじないみたいなもの。
}
},
"condition": "${matches(request.uri.path, '^/replay')}" <== conditionによってこのルートファイルが該当するHTTPパケットの条件を定義する。
}
最初の説明で、各SSO対象のWebアプリに対して、1つのルートファイルが結びつく、という話をしたが、それは最後のconditionで定義されている。
上の例でいうならば、http://hogehoge.com/replayというURLに対してのアクセスの場合、conditonのmatches関数によってリクエストURIのパスを判断し、このルートファイルが使用される。
conditionはtrueになったとき、この定義を使用するというものであり、matchesは第一引数の文字列に第二引数の正規表現がマッチした場合、trueを返す。
つまり、http://hogehoge.com/replayに対してのアクセスの場合、このルートファイルが使用される。
最にHandlerの定義があり、Chainがあり、PasswordReplayFilterがあり、conditionで条件設定がある・・・というパターンは非常によく使われるというか、ほぼこの形式しか使わないというぐらいのテンプレ的な構成になる。
もちろん、実際にはこのような単純な定義だけではSSO実現は難しいので、その他HandlerやFilterがいくつかさらに合体してくることにはなるのだが、よくよく紐解けば、上記のようなパターンになっている。
と、まぁ、ここまで読むと、/replayへのアクセスで上記ルートファイルが使用されるのはわかったけど、ログイン時以外はどのURLへリバプロ先するの?と思うかもしれない。
実際ルートファイル上には、ログイン代行処理に使用するPasswordReplayFilterの中にしかURLは存在せず、しかもそれはPOST送信先URLであって、ログイン以外はどこに行くんだ、となる。
このときどこにいくのかと言うと、config.jsonのbaseURIである。
下記は別記事でOpenIG構築時、最初に作ったconfig.jsonであるが、実はconfig.jsonというのはOpenIGにおけるグローバル設定であり、もしOpenIGに対するHTTPパケットの中でルートファイルに該当するものがない、あるいは、ルートファイル内に記述されていない設定は、基本的にconfig.jsonの設定が採用される。
config.jsonの中にも存在しない設定値の場合、これはそのHandlerかFilterのデフォルト値が使用される。
下記config.jsonでは、冒頭でbaseURI: http://sso.test.local:8080という設定がある。
ルートファイル内にbaseURIがないのであれば、このconfig.jsonのbaseURIが使用される。
つまり、上記ルートファイルでログイン時以外のHTTPパケットの宛先はhttp://sso.test.localになる。
{
"handler": {
"type": "Router",
"audit": "global",
"baseURI": "http://sso.test.local:8080",
"capture": "all"
},
"heap": [
{
"name": "LogSink",
"type": "ConsoleLogSink",
"config": {
"level": "DEBUG"
}
},
{
"name": "JwtSession",
"type": "JwtSession"
},
{
"name": "capture",
"type": "CaptureDecorator",
"config": {
"captureEntity": true,
"_captureContext": true
}
}
]
}
別記事の内容では、サンプル的に作ったものでありリバプロ先が1つしかなかったため、これで動作として問題なかったが、実際にSSO環境を作るとしたら当然SSO対象、つまりリバプロ先は1つではない。
そのため、各ルートファイルにはbaseURIを書く必要がある。もちろんconditionも。
conditionによって、OpenIGに到着したHTTPパケットを振り分け判定し、baseURIによってリバプロ先(HTTPパケットの送信先)を決定する。
では、どうやってbaseURIを定義するのかというと、いくつか方法はあるが最も簡単なのはconditionと同じような位置にド直球に書いてしまう。
{
"handler": {
"type": "Chain",
"config": {
"filters": [
{
※略
},
"baseURI": "http://sso.test.local:8080", <== コレ
"condition": "${matches(request.uri.path, '^/replay')}"
}
私の場合、こういったやり方を知らなくて、下記のようにDispatchHandlerで何とかしていたこともある。
Chainの最後にDispatchHandlerをつけてその中でbaseURIを定義しているのだが、これをするとClientHandlerを定義できなくなるので、上にheapを定義してその中でClientHandlerを定義していた。
正直言ってかなりの力技だと思う・・・ちなみにこれでも全然動作する。
ちなみにDispathHandlerとかClientHandlerとか何だ、と思われると思うが、あとの方でHandlerの説明はするのでご安心を。
{
"heap": [
{
"name": "ClientHandler",
"type": "ClientHandler",
"config": {}
}
],
"handler": {
"type": "Chain",
"config": {
"filters": [
{
※略
],
"handler": {
"type": "DispatchHandler",
"config": {
"bindings": [
{
"handler": "ClientHandler",
"baseURI": "http://sso.test.local:8080"
}
]
}
}
}
},
"condition": "${matches(request.uri.path, '^/replay')}"
}
Objectについて
これよりルートファイルを構成するための各Objectの説明を行う。
Objectは正直かなりたくさんの種類があり、ここには書ききれないので、主要なもの/個人的によく使うものだけ説明する。
内容は正直な所、「openig-4-refernce.pdf」を日本語で自分なりの言葉で表したものになる。
ここで確認したあと実際に使用する前には必ず「openig-4-refernce.pdf」の該当ページを確認して欲しい。
各Objectが非常に詳細に載っている。
基本的な構造として、Description - Usage - Properies - Example という流れで全てのObjectの説明が書かれている。
Exampleをベースにして、UsageでObjectの書き方を確認して、Properiesにて必要な値を確認する・・・といった感じで読み進める。
注意としては、Propertiesにてrequired
と書かれているものに関しては、読んで字の如く絶対に書く必要がある。
書いてないとそのルートファイルを読んだときに怒られる。
ちなみに、各Objectに共通するのは、nameは必須ではない。
場所というかObjectをどこに書くかによってはnameも書いたりするが・・・正直、nameはなくても動く。
Heap Objects について
openig-4-refernce.pdf:P27
ルートファイルを作るにあたっての基礎となるオブジェクト。
とはいえ、Heap Objectsはイニシャライズに必要なものであるので、基本的にはconfig.jsonで定義されている。
内部的には、このHeap Objectsにその後作るルートファイルのHandlerやFilterが注入・定義されているらしい。
とりあえず、絶対使うとても大事なものだと思えば良い。
正直一回テンプレ的な感じで作ってしまったら、後から弄ったりすることはほぼない。
もちろんルートファイルの中で定義することも可能だが、あまり意味がないというか、よほどのことがないとそういうことはしないと思う。
Handlerについて
Chain
openig-4-refernce.pdf:P33
概要
少し説明が難しいが、ルートファイルを作るにあたって基本中の基本のHandlerになる。
名前の如く、複数のFilterを鎖のようにつなげることで1つのルートファイルの中で複数の処理を行えるようになる。
普通にルートファイルを書くと(書き方によるが)1つのFilterしか使えず、非常に柔軟性に乏しいルートファイルになってしまうが、Chainを使うことで非常に多彩なルートファイルを作れるようになる。
そのため、よっぽど単純なルートファイルでない限り、まず間違いなくChainは使用することになる。
Chainの最後ではもう1つHandlerを定義できる。リファレンスガイドではこれをfinally handlerと書いている。
Filterの鎖の最後に別のHandlerを定義できるということである。
用途
上で説明してしまったが、複数のFilterを組合せたいときに使用する。
使い方
- filtersに配列として、複数のFilterを指定する。
- handlerは1つだけ
{
"name": string,
"type": "Chain",
"config": {
"filters": [ Filter reference, ... ],
"handler": Handler reference
}
}
ClientHandler
openig-4-refernce.pdf:P35-39
概要
HTTPリクエストを送信するHandler
・・・なのだが、個人的には、HTTPリクエスト送信の制御を行うHandlerだと思う。
用途
HTTPリクエストを送信する、つまりOpenIGの主要な目的であるログイン代行処理を実現するための要のHandlerとなるため、一番使われるHandlerの1つ。
上のChainと併せてどのルートファイルでも使うHandlerになる。
使い方
正直言ってClientHandlerに関してはただ書いておくだけで、具体的な設定はしないことも多い。
設定するとしたら個人的によく設定するのは、soTimeoutとconnectipnTimeoutの2つ。
OpenIGはバックエンドサーバへHTTPリクエストを送信し、10秒待ってもレスポンスがない場合、タイムアウトと判断する。
上記2つを変更することでその辺の制御が行える。
あとは、バックエンドサーバがSSLを使っていた場合は、keyManagerとtrustManagerを使用しなければいけない場合が多い。
例えばバックエンドサーバが自己証明書を使っていた場合、その証明書をOpenIG用のkeystoreに入れてあげて、ここで指定してあげる。
{
"name": string,
"type": "ClientHandler",
"config": {
"connections": number,
"disableReuseConnection": boolean,
"disableRetries": boolean,
"hostnameVerifier": string,
"soTimeout": duration string,
"connectionTimeout": duration string,
"numberOfWorkers": number,
"sslCipherSuites": array,
"sslContextAlgorithm": string,
"sslEnabledProtocols": array,
"keyManager": KeyManager reference(s),
"trustManager": TrustManager reference(s),
}
}
DesKeyGenHandler
openig-4-refernce.pdf:P41
概要
DES鍵を生成するためのHandler
用途
OpenIGとOpenAMを連携させる際、ユーザーのパスワード情報をDESで暗号化させてやりとりしたりするのだが、そのときに使用する共通鍵をこのHandlerで生成する。
使い方
設定変更内容はない。そのまま書くだけでok
{
"name": string,
"type": "DesKeyGenHandler"
}
DispatchHandler
openig-4-refernce.pdf:P43-44
概要
特定の状況に対するHandlerを設定できるHandler
用途
何言ってんだって思えるかもしれないが、いや本当にそうで、conditionに適合するHTTPパケットに対して、baseURIとhandlerを割り当てる動きをする。
どんな用途があるのだと言われるととても難しいが、複数のHandlerをスイッチする用途であろうか・・
個人的にはbaseURIを切り替えるために使うことがある。
DispatchHandlerのconditionはrequiredではないので、baseURIとhandlerとしてClientHandlerを指定すれば、その時点で強制的にDispathHandlerが適用され、DispatchHandler内で指定したbaseURIに切り替えることが出来る。
もちろん、conditionと組み合わせて単一ルートファイル内で状況にわけてbaseURIを切り替えたりもできる。
(それをやるメリットはなかなかない気がするが)
使い方
{
"name": string,
"type": "DispatchHandler",
"config": {
"bindings": [
{
"condition": expression,
"handler": Handler reference,
"baseURI": string,
}, ...
]
}
}
Filterについて
正直な所、HandlerよりもFilterのほうが遥かに使用頻度が多く、考えさせられることが多い。
HandlerはこのときにはこのHandlerを使うといったパタンーンが割とわかりやすく固まっているため、一度パターンが出来てしまえば、あまり悩むことはないのだが、Filterはその都度そのWebアプリごとに考え、組合せなければならないため非常に大変。
AssignmentFilter
openig-4-refernce.pdf:P71-72
概要
HTTPリクエストあるいはレスポンスの際に様々な条件に則り、変数に値を代入できるFilter
用途
個人的にとても好きなFilterなのだが、HTTPヘッダの値を変更したりする用途で使うことが多い。
またApacheやNginxでいうところのrewriteのようなことも実現できる。
使い方
conditionに適合するとき、targetに対してvalueが代入される。
conditionはrequiredではないが、conditionがないと毎度targetにvalueが代入されてしまうので、まぁまずconditionも毎回書くことになると思う。
targetやvalueには後述するRequestやresponse Objectが使える。
なので、HTTPリクエストのパスやクエリを書き換えたり、レスポンスヘッダの内容を書き換えたり・・・といったことも出来てしまう。
onRequestはHTTPリクエストに対する定義、onResponseはHTTPレスンポンスに対する定義を行う。
そのため、当然ながらonRequestの中で、response Objectは使えない(存在しない)し、逆もまた然り。
{
"name": string,
"type": "AssignmentFilter",
"config": {
"onRequest": [
{
"condition": expression,
"target": lvalue-expression,
"value": expression
}, ...
],
"onResponse": [
{
"condition": expression,
"target": lvalue-expression,
"value": expression
}, ...
]
}
}
CookieFilter
openig-4-refernce.pdf:P73-74
概要
Cookieを扱えるようになるFilter
用途
リバプロ先がCookieを使うWebアプリである場合に使用する。
つまり、OpenIGはCookieFilterがない場合、Cookieを考慮しない。
使い方
CookieFilterには動作モードとして"MANAGE","RELAY","SUPPRESS"の3つが存在する。
デフォルトではMANAGEで動作している。
- MANAGE: CookieをOpenIGが管理する。Webアプリから送られてきたCookie(Set-Cookie)をOpenIGが管理し、ユーザー側へは渡さない。管理されたCookieはHTTPリクエストの度にOpenIGがWebアプリに対して自動的にくっつけて送信してくれる。
- RELAY: CookieをOpenIGが一切関与せず、ユーザー側へリレーする。
- SUPPRESS: Webアプリ側へCookieの送信をしない。特定のCookieは渡したくない、という場合に使用する。
特に理由がない限りはMANAGEを使用するパターンで全く問題ない。
そのため、configは書かないパターンが多い。(デフォルト動作がMANAGEであるため)
ただし、別記事でまとめる多段プロキシパターンの場合、RELAYを使用する。
SUPPRESSは・・・必要になったことがない。個人的には。
ちなみにdefaultActionにはバグがあり、正常に動作しないことがある
(別記事で詳細を書く予定だが、defaultActionでRELAYを指定しているのに、正しくリレーされてこない、ユーザーに渡ってこない・・・)
{
"name": string,
"type": "CookieFilter",
"config": {
"managed": [ string, ... ],
"suppressed": [ string, ... ],
"relayed": [ string, ... ],
"defaultAction": string
}
}
注意点
よくある問題が、MANAGEの場合CookieはOpenIGが管理するため、ユーザー側へは送信されないという点である。
例えばWebアプリがSet-Cookie:hogehoge=foobarというのを送ってきたとして、ユーザーのWebブラウザ上ではhogehoge=foobarというCookieは付与されていない状態になる。
OpenIGがうまく動作しなくてデバッグをするときにWebブラウザの開発者ツールを見て、あれ?Cookieないじゃん!これが原因だ!などと思ってはいけない。
ユーザー側にCookieがないのは全くもって正常な動作。
その辺を確認したい場合は、OpenIGが動作しているサーバ上でtcpdumpとかのパケットキャプチャをして、OpenIGからWebアプリへの直接的な通信を見る必要がある。
この通信でCookieが存在しないのであれば、それは明らかにおかしい状態といえる。
CryptoHeaderFilter
openig-4-refernce.pdf:P75-76
概要
HTTPヘッダの暗号化/復号化を行うFilter
用途
暗号化/復号化どちらもできるのだが、専ら複合用途で使われる。
OpenIGをOpenAMと連携させている場合、OpenAMからユーザーのパスワード情報が何らかのアルゴリズムで暗号化されたHTTPヘッダとして渡ってくる。
(リファレンスガイドに則って構築した場合DES)
この暗号化されたHTTPヘッダを復号化し、変数に代入できる。
変数はそのあと別のFilterなどで使用できる。
別に紹介するPasswordReplayFilterで同様の機能があるので(というか内部的にこいつを呼び出している)、個別に使う機会は少ないかもしれない。
ただし、こいつもバグがあるようでPasswordReplayFilterではなく、こいつをあえて使う・・・といったことを以前やったことがある。これもきっと別記事にまとめる。たぶん。
使い方
何というHTTPヘッダに対して、どんなアルゴリズムで暗号化/復号化させるかということを設定する。
{
"name": string,
"type": "CryptoHeaderFilter",
"config": {
"messageType": string,
"operation": string,
"key": expression,
"algorithm": string,
"keyType": string,
"headers": [ string, ... ]
}
}
algrorithmやketTypeがデフォルトではAESになっているのでDESで使う場合は全てのconfigを書く必要がある。
とはいえ、書くことは決まりきっているので、事実上変更する必要があるのはkeyとheadersぐらい。
下の例の場合、replaypasswordというのがDESで暗号化されているので、それを共通鍵oqdP3DJdE1Q=で復号化している。
{
"name": "DecryptReplayPasswordFilter",
"type": "CryptoHeaderFilter",
"config": {
"messageType": "REQUEST",
"operation": "DECRYPT",
"algorithm": "DES/ECB/NoPadding",
"keyType": "DES",
"key": "oqdP3DJdE1Q=",
"headers": [
"replaypassword"
]
}
}
EntityExtractFilter
openig-4-refernce.pdf:P77-79
概要
HTTPレスポンスボディのHTMLから正規表現で値を抜き変数に格納するFilter
用途
ログインする際のPOSTでログインページ上のhiddenの値も一緒に投げないといけなかったり、CSRF対策で動的に変わるnonceなどを送らないといけない場合に、動的にHTMLから値を抜き出すために使用する。
使い方
{
"name": string,
"type": "EntityExtractFilter",
"config": {
"messageType": string,
"charset": string,
"target": lvalue-expression,
"bindings": [
{
"key": string,
"pattern": pattern,
"template": pattern-template
}, ...
]
}
}
下記の例では、正規表現wpLoginToken"\s.value="(.)"によってwpLoginTokenのvalueを抜き出し、変数${attributes.extract.wpLoginToken}に格納している。
templateの$1はpatternの正規表現の()に対応している。
正規表現のマッチにあたる。今回の場合1つだけなので必須ではないと思う。
当然ながら変数は別のFilterで使用できる。これも同等機能がPasswordReplayFilterに存在するため、単体で使うことは少ないかもしれない。
{
"name": "WikiNoncePageExtract",
"type": "EntityExtractFilter",
"config": {
"messageType": "response",
"target": "${attributes.extract}",
"bindings": [
{
"key": "wpLoginToken",
"pattern": "wpLoginToken\"\s.*value=\"(.*)\"",
"template": "$1"
}
]
}
}
HeaderFilter
openig-4-refernce.pdf:P83-84
概要
HTTPヘッダの追加あるいは削除を行うFilter
用途
上の説明の通り、HTTPヘッダの追加あるいは削除を行いたい場合に使用する。
AssignmentFilterと似たようなものに感じるが、こちらはHTTPヘッダ専門で追加or削除のみしかできない。
逆に言えばそれだけの用途ならこちらのほうが簡単に書ける。
OpenAMとOpenIGを連携させる場合、ユーザー名やパスワードをHTTPヘッダにくっつけてやり取りしたりするが、ずっとくっつけておくとセキュリティ的にどうなん?となるので、バックエンドのWebアプリへのログインが終わったらこのFilterで該当HTTPヘッダを削除させてあげる、ということによく使われる。
使い方
{
"name": string,
"type": "HeaderFilter",
"config": {
"messageType": string,
"remove": [ string, ... ],
"add": {
name: [ string, ... ], ...
}
}
}
この場合バックエンドのWebアプリへのHTTPリクエストの際にhost: myhost.comというヘッダを追加する。
{
"name": "ReplaceHostFilter",
"type": "HeaderFilter",
"config": {
"messageType": "REQUEST",
"remove": [ "host" ],
"add": {
"host": [ "myhost.com" ]
}
}
}
レスポンスヘッダもいじれるので、Set-Cookieをつけることも出来る。
ただこれをやらなければならないパターンが思いつかないが・・・まぁこういったこともできるということで。
{
"name": "SetCookieFilter",
"type": "HeaderFilter",
"config": {
"messageType": "RESPONSE",
"add": {
"Set-Cookie": [ "mysession=12345" ]
}
}
}
StaticRequestFilter
openig-4-refernce.pdf:P115-116
概要
HTTPリクエストを生成するFilter
用途
こいつを単体で使うことはまずないが、後述のPasswordReplayFilterの中身として使われる関係で一応説明。
使い方
{
"name": string,
"type": "StaticRequestFilter",
"config": {
"method": string,
"uri": string,
"version": string,
"headers": {
name: [ expression, ... ], ...
},
"form": {
param: [ expression, ... ], ...
},
"entity": expression
}
}
どのURIに対して、どのメソッドでどんなヘッダーでどんなformで投げるかを指定する。
よっぽどの理由がないとこれを単体では使わないと思う。
PasswordReplayFilter
openig-4-refernce.pdf:P101-104
概要
今まで紹介したCryptoHeaderFilter/EntityExtractFilter/StaticRequestFilterを全て合体させたFilter
OpenAMと連携しパスワードを復号化してHTMLをパースしてログイン代行を行うという本来であれば3つのFilterを組合せながら行う処理を単一のFilterで実現できる。
OpenIGの要となる処理であり、非常に需要が多いため、恐らくこういった合体verのFilterが用意されたんだと思う。非常に便利。
用途
概要の通り、ログイン代行を行うFilterであり、必要な機能が全て揃っている。
基本的には全てのルートファイルでこのFilterを使うことになるはず。
ただしバグがあってどうにも回避できず、あえて別Filterに機能を分離させた経験がある・・・
使い方
{
"name": string,
"type": "PasswordReplayFilter",
"config": {
"request": request configuration object,
"loginPage": expression,
"loginPageContentMarker": pattern,
"credentials": Filter reference,
"headerDecryption": crypto configuration object,
"loginPageExtractions": [ extract configuration object, ... ]
}
}
headerDecryptionについて
中身はCryptoHeaderFilterのDecryptになる。
loginPageExtactionsについて
中身は、EntityExtractFilterになる。
loginPageとloginPageContentMarkerについて
PasswordReplayFilterにはloginPageとloginPageContentMarkerというものがあり、これはどういった場合にログイン代行処理を行うのか?という条件を設定する。
この2つは排他であり、2つとも設定することはできない。
どう使い分けるかというと、
- loginPage: Cookieを使用せず、ログインページとログイン後ページのパスがわかれている場合
- loginPageContentMarker: loginPageで認識できないその他のパターン全て
例えば、下記のようなURL構造のWebアプリだった場合、
- ログイン画面URL: http://hogehoge.com/top
- ログイン処理URL: http://hogehoge.com/login
- ログイン後URL: http://hogehoge.com/dashboard
下記のようなFilterになる。
"type": "PasswordReplayFilter",
"config": {
"loginPage": "${request.uri.path == '/top'}",
"request": {
"method": "POST",
"uri": "http://hogehoge.com/login",
"form": {
"username": [
"MY_USERNAME"
],
"password": [
"MY_PASSWORD"
]
}
}
}
/topというパスに対するアクセスだった場合、これはログインページであると判断し、PasswordReplayFilterのログイン処理代行機能が発動。
/loginに対してPOSTを投げている。この場合は非常にわかりやすい。
一方で、下記のようなURL構造のWebアプリだった場合、
- ログイン画面URL: http://foobar.com/app
- ログイン処理URL: http://foobar.com/app/login
- ログイン後URL: http://foobar.com/app
上記のようなログイン画面もログイン後画面もCookieやSessionによる内部的な判断で返すHTMLを変えているだけのような場合、loginPageでの判断が困難になる。
一口に/appといっても、それがログイン画面なのかその他なのか判断がつかないためである。
※まぁこれはこれでrefererを見るとか、queryがあるかないかとかで、判断できたりもするが・・・
こういった場合はloginPageContentMarkerを使う必要がある。
"type": "PasswordReplayFilter",
"config": {
"loginPageContentMarker": "<form.name=\"login\".action=\"/app/login\"",
"request": {
"method": "POST",
"uri": "http://foobar.com/app/login",
"form": {
"username": [
"MY_USERNAME"
],
"password": [
"MY_PASSWORD"
]
}
}
}
上記はHTMLの中に正規表現で<form name="login" action="/app/login"
という文字列が存在した場合、ログインページだと判断し、POSTを行うという処理になる。
※\sが使えないようなので.で代用
要はログインページだと判断できるような文字列を見つけられたら良い。
多くの場合、ログインページだとformでそういったコードが書かれているはずなので、それを引っ掛ければ良いと思う。
これならばパスが同じ/appだとしても、中身を見てログインページか否かを判断できる。
また他の用途としては、Cookieを利用するWebアプリのときも多くの場合loginPageContentMarkerを必要とする。
というのも、loginPageの場合、ユーザーからOpenIGへアクセスする際にURIを見てログインページかどうかを判断しているので、Webアプリへ一度もアクセスせず最初にログイン代行処理を行う。
もし、ログインの際に何らかのCookieを必要としている場合、これではログインが失敗してしまう。
なぜなら初回のアクセスであるためCookieが存在しないから。
この点、loginPageContentMarkerは、一度Webアプリへアクセスし、そのレスポンスのHTMLを見てログインページか否かを判断するため、この時点でSet-CookieによりCookieを得ることが出来る。
それをもってログイン代行処理を行うため、結果的にログイン処理が成功する。
ただしloginPageでも何とかならないわけではない。例えば上述のloginPageの説明におけるWebアプリにプラスして、
- ログイン後機能AのURL: http://hogehoge.com/func
であり、未ログイン状態でここにアクセスすると自動的にログイン処理URL: http://hogehoge.com/loginにリダイレクトされる、という仕様ならば、loginPageでも何とかなる。
自動的にリダイレクトされることでCookieを得た上でログインページに自動的にアクセスされるためそれをloginPageでひっかけられる。
変数・演算子・関数について
ここまでの説明でかなりたくさん出てきているので、あまり説明しなくてもわかるかもしれないが、一応・・・
ルートファイルの中では様々な変数や演算子、Javaの関数などが使える。
これもまた非常に多くの種類があるので、主要なものだけ紹介する。
全体的に共通するのは、変数や関数を使用する際は、${}で囲む必要があるということである。
例えば、下記の場合、matchesという関数を使用するために${}で囲まれている。
${}で囲まれていれば、無条件に関数や変数が何個でも使えるので、1つ1つに${}が必要なわけではない。
"condition": "${matches(request.uri.path, '^/replay')}"
requestというオブジェクトには該当ルートファイルに到達した時点でのHTTPリクエストの内容が全て入力されている。
詳しくはopenig-4-refernce.pdfを確認して欲しいが、例えば、リクエストパスにアクセスしたいなら、request.uri.pathになるし、クエリにアクセスしたいならrequest.uri.queryになる。
cookieにアクセスもできるため、request.cookies['hogehoge'][0].valueといった形で取得できる。
また、Boolenとしてtrueを表現したい場合、以下になる。
"condition": "${true}"
単に"true"では文字列としてのtrueになってしまう。
openig-4-refernce.pdfに非常に参考になるExamplesが載っているので引用させてもらう。詳しいことは是非そちらを確認して欲しい。
"${request.uri.path == '/wordpress/wp-login.php' and request.form['action'][0] != 'logout'}"
"${request.uri.host == 'wiki.example.com'}"
"${request.cookies[keyMatch(request.cookies,'^SESS.*')][0].value}"
"${toString(request.uri)}"
"${request.method == 'POST' and request.uri.path == '/wordpress/wp-login.php'}"
"${request.method != 'GET'}"
"${request.headers['cookie'][0]}"
"${request.uri.scheme == 'http'}"
"${not (response.status.code == 302 and not empty session.gotoURL)}"
"${response.headers['Set-Cookie'][0]}"
"${request.headers['host'][0]}"
"${not empty system['OPENIG_BASE'] ? system['OPENIG_BASE'] : '/path/to'}/logs/gateway.log"
演算子
openig-4-refernce.pdf:P213-216
OpenIGのHandlerやFilterでは、下記演算子が使える。
プログラミングをやっていれば、とても馴染み深いものだと思うので、詳細は割愛。
下記はopenig-4-refernce.pdhのP214からの転載。
Universal Expression Language supports arbitrarily complex arithmetic, logical,
relational and conditional operations. They are, in order of precedence:
• Index property value: [], .
• Change precedence of operation: ()
• Unary negative: -
• Logical operations: not, !, empty
• Arithmetic operations: *, /, div, %, mod
• Binary arithmetic operations: +, -
• Relational operations: <, >, <=, >=, lt, gt, le, ge, ==, !=, eq, ne
• Logical operations: &&, and, ||, or
• Conditional operations: ?, :
関数
openig-4-refernce.pdf:P217-228
OpenIGでは、多彩な関数が用意されていて、各HandlerやFilterで利用できる。
個人的によく使うものだけ紹介。
正直言って、Javaに同名のものが存在して動作も基本的に同じなようなので、あまり説明する必要はないと思うが・・・
decodeBase64
base64エンコードされた文字列をデコードする。
書いておいて何だが、あまり利用する機会がないかも。
encodeBaase64
平文の文字列をbase64エンコードする。
POSTする際のformの中でbase64エンコードした文字列を送信しなければならないときがあったりするので、そういうときに使う。
今まであったのは、パスワードをbase64エンコードして送信する必要があったWebアプリを見たことがある。
join
配列を文字列として連結させる。まぁ各プログラミング言語でよくある関数だと思う。
Cookieの中で特定の文字列があった場合ごにょごにょする・・・といった場合、OpenIGではCookieがそれぞれ小分けにして配列に格納されているので、それをjoinしてcontainsで探す・・・といったことをしたことがある。
matches
一番使う関数。リファレンスガイドでも一番よく出てくる関数な気がする。
正規表現で文字列の中に特定の文字列が存在するかどうかを調べる関数。conditionとの組合せでよく見る。
split
文字列の分割に使う。rewriteっぽく使うときによく使う。
toLowerCase
文字を強制的に小文字化する。
例えばOpenAMでログインしたときのユーザーIDがHOGEHOGEだったとして、とあるWebアプリではhogehogeでなければ認証成功しない、というのであれば、こいつで強制的に小文字化できる。
ユーザーデータストアがADの場合、ユーザーアカウントは小文字でも大文字でもログイン成功してしまうため、こういった問題が起こりうる。
toUpperCase
文字を強制的に大文字化する。
利用パターンとしては、上記のtoLowerCaseと同じ。
ルートのサンプル
今までの説明でそれぞれのObjectの使い方など、ルートファイルの基礎的な部分は大分おさえられたと思う。
ここからは実際のルートファイルのサンプルを提示して、どういう動きをするのか、どういった意図があるのかの説明を行う。
想定する環境は以下となる。
想定環境
サンプル1:
- OpenIG URI: http://openig.test.local:8080/app1
- バックエンドサーバ URI: http://hogehoge.com/keihi
サンプル2:
- OpenIG URI: http://openig.test.local:8080/app2
- バックエンドサーバ URI: http://foobar.com/moodle
SSO連携対象のバックエンドサーバとして、hogehoge.comとfoobar.comが存在する状況を想定し、単一のOpenIGからリバプロ方式のSSOを行うことを想定する。
OpenIGのURIの第一パスを切り替えることで、複数のSSO連携対象を識別することとする。
つまり、OpenIGに対して、/app1であれば、hogehoge.comだし、/app2であればfoobar.comにリバプロする。
もちろんリバプロ先が分かれるわけだから、ルートファイルとしても2個作る。
今回はそのルートファイル2個をサンプルとして説明を行う。
こういった第一パスによって1つのOpenIGで効果的にSSO連携対象(リバプロ先)を分ける手法は一般的なやり方な気がする。
(一般的といっても他の例を知らないので全く自信はないのだが・・・)
1. ログインがPOSTではなくGETで行う場合
ログインにPOSTではなくGETを使うパターンのときのルートファイルのサンプル。
今時そんなWebアプリ存在しているのか!?と思うが、社内の業務システムだと、どれだけ前に作ったかもわからないレガシーシステムがたくさんあるので、こういったものも全然生き残っている。
早い話がクエリとして渡せばいいだけなので、実はPOSTパターンのルートファイルよりも簡単になる。
ただ、こんな簡単な話で良いと知らないと、えーどうすりゃいいんだろ・・・となったりするので(私が最初そうだった)、参考程度にサンプルを示します。
やっていること
- http://openig.test.local:8080/app1 => http://hogehoge.com/keihi へのリバプロ
- /app1/keihi/ => /keihi となるように書換(Apacheやnginxのrewriteっぽい処理)
- HTTPリクエスト時にrefererを書換
- HTTPレスポンスヘッダのlocationを書換
- /keihi/login.aspのときログインページへのアクセスのため、GETクエリとしてユーザー名/パスワードを渡してログイン代行処理
{
"handler": {
"type": "Chain",
"config": {
"filters": [
{
"type": "AssignmentFilter",
"config": {
"onRequest": [
{
"target": "${request.uri.path}",
"value": "${split(request.uri.path, '/app1')[1]}"
},
{
"target": "${request.headers['referer']}",
"value": "http://hogehoge.com/"
}
],
"onResponse": [
{
"condition": "${matches(response.headers['location'][0], '^/')}",
"target": "${response.headers['location']}",
"value": "/app1${response.headers['location'][0]}"
},
{
"condition": "${matches(response.headers['location'][0], '^../')}",
"target": "${response.headers['location']}",
"value": "/app1/${split(response.headers['location'][0], '../')[1]}"
}
]
}
},
{
"type": "PasswordReplayFilter",
"config": {
"loginPage": "${request.uri.path == '/keihi/login.asp'}",
"headerDecryption": {
"algorithm": "DES/ECB/NoPadding",
"key": "hogehoge",
"keyType": "DES",
"charSet": "utf-8",
"headers": [
"password"
]
},
"request": {
"method": "GET",
"uri": "http://hogehoge.com/keihi/login.asp?id=${request.headers['username'][0]}&passwd=${request.headers['password'][0]}"
}
}
},
{
"type": "HeaderFilter",
"config": {
"messageType": "REQUEST",
"remove": [
"password",
"username"
]
}
}
],
"handler": "ClientHandler"
}
},
"baseURI": "http://hogehoge.com"
"condition": "${matches(request.uri.path, '^/app1')}"
}
ユーザーからするとアクセスとしては、/app1/keihi/logiin.aspとなるわけだが、実際のリバプロ時は/app1/という部分は必要ない。
http://hogehoge.com/app1/keihi/login.asp なんてURIにアクセスしたところで404になるだけ。
というわけでsplit関数を使って、/app1を消してあげる。
split関数で、第一引数の文字列を第二引数の文字列で分割して配列として返すので、消したい文字列を第二引数に指定してあげて、配列の2個目を変数に代入してあげれば結果的にそれは文字列を消せるというわけ。
もっと良いやり方がある気がするが・・・私が考えだした苦肉の策。
ちなみに、jettyにはrewrite moduleがあるので、これを使ってみるという手もある(試したことはない)
"onRequest": [
{
"target": "${request.uri.path}",
"value": "${split(request.uri.path, '/app1')[1]}"
},
これは単純でHTTPリクエストヘッダのRefererの値を書き換えている。
Refererを見て、これは不正なアクセスだ!ということで認証処理を強制的に失敗させるシステムがあったりする場合はこういったことをする必要がある。
{
"target": "${request.headers['referer']}",
"value": "http://hogehoge.com/"
}
Webアプリ上で戻るボタンを押したときの挙動として、HTTPステータスコードの301や302とlocatinヘッダを使って移動させている場合、このような書換が必要になる。
これがないと、http://openig.test.local:8080/app1/keihi/func1にアクセスしているとき、トップへ戻るボタンを押して、locationヘッダとして"/keihi/top"が返ったきた場合、ユーザーのWebブラウザはhttp://openig.test.local:8080/keihi/topへ移動することになる。
まぁ何とかしようと思えば出来るのだが(conditionでkeihiをひっかける)、hogehoge.comは識別子としてapp1を使いたいんだ!となる場合、これではまずい。
というわけでlocationヘッダを書き換える。
ちなみにこれはLocationHeaderFilterでも可能。ていうかそっちを使うほうが正解だと思う。
"onResponse": [
{
"condition": "${matches(response.headers['location'][0], '^/')}",
"target": "${response.headers['location']}",
"value": "/app1${response.headers['location'][0]}"
},
{
"condition": "${matches(response.headers['location'][0], '^../')}",
"target": "${response.headers['location']}",
"value": "/app1/${split(response.headers['location'][0], '../')[1]}"
}
]
LocationHeaderFilterの場合、下記のように書ける。
{
"type": "LocationHeaderFilter",
"config": {
"baseURI": "http://openig.test.local:8080"
}
}
nginxでいうとproxy_redirectみたいなものでこれでlocationヘッダをbaseURIに合うように一手に書き換えてくれる。
すげー簡単じゃん!これで良いじゃん!と思う・・・のだが、例えばlocationヘッダでapp
というのが返ってきた場合、
LocationHeaderFilterはこれを、http://openig.test.local:8080app
と書き換えてしまう。
当然これではDNSエラーになる。
こういったlocationヘッダが返ってこないならLocationHeaderFilterで良いのだが・・・個人的にはどんなパターンでも汎用的につかえてしまうAssignmentFilterでのやり方を好んで使っている。
本サンプルのキモはここだろうと思う。
methodとしてGETを指定して、クエリ丸ごとuriを作ってあげる。
この例の場合、クエリとしてidとpasswdを必要としている。
またOpenIGはusernameとpasswordというヘッダにユーザー名とパスワードを格納している、
上述の説明の通り、変数やObjectは${}で囲むのでそれぞれ囲んであげて、文字列を組み立ててあげる。
わかってしまえば意外とシンプルなことをやっている。
"request": {
"method": "GET",
"uri": "http://hogehoge.com/keihi/login.asp?id=${request.headers['username'][0]}&passwd=${request.headers['password'][0]}"
}
2. ヘッダ復号処理の分離/Cookieを使用する場合
OpenAMと連携する場合暗号化されたHTTPヘッダを復号化するのは必須の処理だが(OpenAMの設定次第ではヘッダ以外の場所にも格納できるが)、
その処理を別Filterとして分離させたパターン。
また、Cookieを使用するWebアプリであるためCookieFilter1を使用している。
やっていること
- http://openig.test.local:8080/app2 => http://foobar.com/moodle へのリバプロ
- /app2/moodle/ => /moodle となるように書換(Apacheやnginxのrewriteっぽい処理)
- HTTPレスポンスヘッダのlocationを書換
- HTTPヘッダの複合処理を分離
- Cookieを使用
- ClientHandlerのタイムアウトを大きくなるように変更
- conditionをor表現
{
"handler": {
"type": "Chain",
"config": {
"filters": [
{
"type": "AssignmentFilter",
"config": {
"onRequest": [
{
"condition": "${matches(request.uri.path, '^/app2')}",
"target": "${request.uri.path}",
"value": "${split(request.uri.path, '/app2')[1]}"
}
],
"onResponse": [
{
"condition": "${matches(response.headers['location'][0], 'http://foobar.com')}",
"target": "${response.headers['location']}",
"value": "/app2${split(response.headers['location'][0], 'http://foobar.com')[1]}"
}
]
}
},
{
"type": "CryptoHeaderFilter",
"config": {
"messageType": "REQUEST",
"operation": "DECRYPT",
"algorithm": "DES/ECB/NoPadding",
"keyType": "DES",
"key": "hogehoge",
"headers": [
"password"
]
}
},
{
"type": "PasswordReplayFilter",
"config": {
"loginPageContentMarker": "あなたはログインしていません",
"request": {
"method": "POST",
"uri": "http://foobar.com/moodle/login/index.php",
"form": {
"anchor": [
""
],
"username": [
"${request.headers['username'][0]}"
],
"password": [
"${request.headers['password'][0]}"
]
}
}
}
},
{
"type": "CookieFilter"
}
],
"handler": {
"type": "ClientHandler",
"config": {
"connectionTimeout": "30 seconds",
"soTimeout": "30 seconds"
}
}
}
},
"baseURI": "http://foobar.com"
"condition": "${matches(request.uri.path, '^/app2') or matches(request.uri.path, '^/moodle')}"
}
CryptoHeaderFilterを使ってHTTPヘッダの複合処理をPasswordReplayFilterから分離させる。
HTTPヘッダのpasswordがDESで暗号化された状態なのでそれを復号化している。
このFilterを通ったあと、passwordヘッダは復号化されたパスワードの状態なので、それをすぐ後のPasswordReplayFilterのPOSTで投げている。
{
"type": "CryptoHeaderFilter",
"config": {
"messageType": "REQUEST",
"operation": "DECRYPT",
"algorithm": "DES/ECB/NoPadding",
"keyType": "DES",
"key": "hogehoge",
"headers": [
"password"
]
}
},
今回はPasswordReplayFilterでloginPageContentMarkerを使用してログインページの判定処理を行っている。
今回の場合、ログインページのhtmlには日本語であなたはログインしていません
という文字列が毎回存在するため、これをひっかけている。
これは特殊なパターンだと思う。普通はhtmlのタグ(formとかの)で引っ掛けるパターンのほうが多いと思う。
{
"type": "PasswordReplayFilter",
"config": {
"loginPageContentMarker": "あなたはログインしていません",
こう書くだけでOpenIGはCookieを考慮した通信を行ってくれる。
configを何も書いていないので、MANANGEとして動作する。
先述した通り、ユーザー側にはCookieは渡らない。
{
"type": "CookieFilter"
}
Webアプリの中で処理に時間がかかりレスポンスが返ってくるのに20秒ほど必要であると想定した場合、下記のように書くことでタイムアウトを伸ばすことができる。
こうしないと、場合によってはOpenIG上でタイムアウトエラーが発生する。詳細は最後のトラブルシュートにて。
"handler": {
"type": "ClientHandler",
"config": {
"connectionTimeout": "30 seconds",
"soTimeout": "30 seconds"
}
}
少し注意点としては、このサンプル2では、サンプル1のときには存在したHeaderFilterのremoveが存在しない。
{
"type": "HeaderFilter",
"config": {
"messageType": "REQUEST",
"remove": [
"password",
"username"
]
}
}
先述した通り、OpenAMから渡ってきたパスワード情報はPasswordReplayFilter経由後(正確にはCryptoHeaderFilter)は復号化されHTTPヘッダに格納されている。
それはセキュリティ的にどうなの?ってことで(まぁならそもそもHTTPS使えよとはなるんだが)、HeaderFilterでそういった情報を削除してしまうのが一般的なんだと思う。
実際、「openig-4-refernce.pdf」上のサンプル的に載っているルートファイルもそういった書かれ方をしている場合が多い。
ただし、これはloginPageContentMarkerを使うとき問題になる。
なぜかと言うと、loginPageContentMarkerは一度バックエンドのWebアプリにアクセスしてそのレスポンスに対して反応するものなので、この一度目のアクセスのときにHeaderFilterが動作して、せっかくのユーザー名/パスワード情報が削除されてしまう。
つまりサンプルでいえば、username/passwordが空の状態になる。
そのため、loginPageContentMarkerが反応してログイン代行のPOSTを送ったとしても認証失敗になってしまう。
以上のことからloginPageContentMarkerを使用するパターンの場合HeaderFilterでの削除は行わないほうが無難だと思っている。
無難というかやっちゃいけないというか。
まぁそれならどうするんだ・・・となるんだが・・・個人的にこの問題に対する解を持っていない。
というのもルートファイルの中でログインを行ったかどうかを判定する手段がない。
それを判定できれば、ログインを行う前はHeaderFilterは動作させず、ログイン後HeaderFilterによって削除させる、といったことが出来るかと思うのだが・・・
やるとしたら、HTTPヘッダかCookieにフラグみたいなものを持たせて、その値で管理するようなやり方だろうか・・・
頑張れば出来ると思うが、今のところやったことはない。
トラブルシュート
ルートファイルを作った。定義したconditionに適用するようにWebブラウザからアクセスしたら、no handler to dispatch to
と表示されて意図したWebページが表示されない。
conditionの内容が間違っている可能性が高い。
conditionが間違っているせいで届いたHTTPパケットの適用先が見つからず、上記のエラーメッセージが表示されていると思う。
あるいは、ルートファイル自体がOpenIGに読み込まれていない可能性もある。
ログファイル(ex: /opt/jetty/logs/)を見てみると、The route defined in file '/home/jetty/.openig/config/routes/hogehoge.json' cannot be added
といったメッセージが載っていないだろうか。
これはつまりルートファイルの中に何らかの間違い(記述ミスや設定の矛盾など)がある関係でルートファイルの読み込みに失敗している。
どんな間違いがあるのかは、同じログファイル内に書かれている。Javaのプログラミングに慣れている人なら簡単にわかると思う。
ルートファイルが読み込まれていないということは当然その中のconditionも定義されていないわけで、これもまたno handler to dispatch to
になる。
ルートファイルがうまく動いているが、特定の検索ボタンを押したら、Failed to obtain response for http://hogehoge.com
と表示された
恐らくだがタイムアウトしている。
ClientHandlerのsoTimeoutやconnectionTimeoutの値を調整してみると改善する可能性が高い。(特にsoTimeout)
デフォルトは10secなので、検索ボタンなどWebアプリ内部で処理に時間がかかり、レスポンスが返ってくるのが10秒以上かかる場合、上記のエラーメッセージが表示される。