Apache Tomcat(以下Tomcat) にはさまざまな機能が用意されています。主にserver.xmlやweb.xmlを変更することによって利用できる機能を「逆引き辞典」的に紹介します。Tomcatを使いなれているが、もう一段階上の使い方を学びたい方の参考になれば良いと思っています。またここに記述されている機能はTomcatでしか利用できない機能ではないため、他のサーブレットコンテナでも利用できるものもあります。
また文中に「TOMCATホーム」が登場しますが、これはTomcatをインストールしたディレクトリを指します。必要に応じ読み替えて下さい。文中に Tomcat5 といった記述があります。こちらは「Tomcatのバージョン5で動作確認した」という意になります。
Tomcatに自身の作成したアプリケーションを登録するには以下のように行います。
以下の例ではルートディレクトリ直下にある「hogedirディレクトリ」をhogeという名称で登録しています。つまり「http://サーバ名:8080/hoge/index.jsp」は「/hogedir/index.jsp」を指し示すことになります。
<?xml version="1.0" encoding="UTF-8"?>
<Server port="8005" shutdown="SHUTDOWN">
<Service name="Catalina">
<Engine name="Catalina" defaultHost="localhost">
<Host name="localhost" appBase="webapps">
<Context path="hoge" docBase="/hogedir"></Context>
</Host>
</Engine>
</Service>
</Server>
上記と異なりContextタグを別ファイルにすることができます。別ファイルにするには「pathの値がファイル名になる」というようにファイル化します。つまりファイル名がpath属性を表すためpath属性は削除して下さい。
このファイルは通常「TOMCATホーム/conf/Catalina/localhost/」の中に配置して下さい。
<Context path="hoge" docBase="/hogedir"></Context>
このサイトでは、ここで作成したhoge.xmlをContextファイルと呼ぶことにします。
表題の通りpathに階層をもたせる場合、ファイル名に/(スラッシュ)を含めることができないためファイル化できません。上記の場合「foo#bar.xml」とすることで別ファイルにすることができます。
JDBCを用いて接続する場合、接続に時間が掛かる(可能性がある)ため、コネクションプールを利用します。以下の設定で接続することを想定し設定してみます。また文中にResourceタグのname属性の値は自由に命名することができます。今回は「jdbc/ds」としました。
No. | 項目名 | 値 |
---|---|---|
1 | JDBCドライバ | com.mysql.jdbc.Driver |
2 | 接続URL | jdbc:mysql://localhost:3306/データベース名 |
3 | 接続ユーザ名 | yuzamei |
4 | 接続パスワード | pasuwado |
<Context>
<Resource
name="jdbc/ds"
auth="Container"
type="javax.sql.DataSource"
driverClassName="com.mysql.jdbc.Driver"
url="jdbc:mysql://localhost:3306/データベース名"
username="yuzamei"
password="pasuwado"
validationQuery="select 1"
maxActive="20"
maxIdle="10"
/>
</Context>
<%@ page import="java.sql.Connection" %>
<%@ page import="java.sql.SQLException" %>
<%@ page import="javax.naming.Context" %>
<%@ page import="javax.naming.InitialContext" %>
<%@ page import="javax.sql.DataSource" %>
<html>
<body>
<%
Connection conn = null;
try {
Context ctx = new InitialContext();
DataSource ds = (DataSource) ctx
.lookup("java:comp/env/jdbc/ds");
conn = ds.getConnection();
// 何かしらのSQL処理
} catch (Exception e) {
// 適切な処理を記述すること
} finally {
if (conn != null) {
try {
conn.close();
} catch (SQLException e) {
// 適切な処理を記述すること
}
}
}
%>
</body>
</html>
validationQuery="select 1"とありますが、これはなんでしょうか。コネクションプールではデータベースと接続状態を維持し続けます。接続し続けるたいとTomcat側が思っても、データベースによっては長時間の接続を許さないものもあります。するとTomcatは接続し続けているつもりでも実際に切断されてしまうことがあります。
このようなときに接続切れかどうか判断するためのSQLをvalidationQueryに記述します。ですがvalidationQueryはコネクションプールからコネクションを貸し出す度に実行されるのでデータベースにとって最小限の負担にしたいのです。よって"select 1"が一般的な設定値になります。
またContextファイルでなくserver.xmlに設定することもできます。以下はserver.xmlへの設定例です。
<?xml version="1.0" encoding="utf-8"?>
<Server port="8005" shutdown="SHUTDOWN">
<GlobalNamingResources>
<!-- Resourceの詳細設定は省略します(詳細はコチラ) -->
<Resource name="jdbc/ds" />
</GlobalNamingResources>
<Service name="Catalina">
</Service>
</Server>
<?xml version="1.0" encoding="UTF-8"?>
<Server port="8005" shutdown="SHUTDOWN">
<Service name="Catalina">
<Engine name="Catalina" defaultHost="localhost">
<Host name="localhost" appBase="webapps">
<Context path="hoge" docBase="/hogedir">
<!-- Resourceの詳細設定は省略します(詳細はコチラ) -->
<Resource name="jdbc/ds" />
</Context>
</Host>
</Engine>
</Service>
</Server>
Resourceタグはどこに記述するのが正しいのでしょうか。筆者が頻繁に見かける箇所をピックアップしてみます。
- Server > GlobalNamingResources > Resource
- Server > Service > Engine > Host > Resource
- Server > Service > Engine > Host > Context > Resource
まずは理解しやすい下2つを比較してみます。これはResourceタグの設定がHostタグ配下で利用できるのかContextタグ内のみで利用できるのかの違いがあります。
下記を参照下さい。Hostタグの内側にContextが2つ設定されており、それぞれのContextタグ内にResourceタグが別々に設定されています。またContextタグと同一レベルでResourceタグ(jdbc/cmn)が設定されています。
このような設定の際、hogeアプリケーションでは「jdbc/cmn」「jdbc/hoge」が利用可能、foobarアプリケーションでは「jdbc/cmn」「jdbc/foobar」が利用可能です。このことを考えると「XXXに記述するのが正しい」ということはなく「アプリケーションの使い方による」ということが結論になります。
<?xml version="1.0" encoding="UTF-8"?>
<Server port="8005" shutdown="SHUTDOWN">
<Service name="Catalina">
<Engine name="Catalina" defaultHost="localhost">
<Host name="localhost" appBase="webapps">
<Resource name="jdbc/cmn" />
<Context path="hoge">
<Resource name="jdbc/hoge" />
</Context>
<Context path="foobar">
<Resource name="jdbc/foobar" />
</Context>
</Host>
</Engine>
</Service>
</Server>
Tomcatではブラウザと直接やりとりしたり、Apache等のWEBサーバと連携することができます。この機能を担っているのがコネクタになります。
ポート番号8080を省略したい実装中などポート番号の入力を面倒に思ったことはありませんか。このようなときにはTomcatで標準的に動作しているコネクタのポート番号を変更します。
下記はインストール直後の設定なのでポート番号が8080になっています。この番号を80に変更することで今後はポート番号を省略することができます。
<?xml version="1.0" encoding="UTF-8"?>
<Server port="8005" shutdown="SHUTDOWN">
<Service name="Catalina">
<Connector port="8080" protocol="HTTP/1.1" connectionTimeout="20000" redirectPort="8443" />
</Service>
</Server>
上記の「ポート番号8080を省略したい」にある「protocol="HTTP/1.1"」はHTTP/1.1という方式で送信されたものを処理するコネクタという意味になります。つまり外部のブラウザとやりとりをするコネクタということの意です。
Apacheとやりとりするということは、Apacheとの専用コネクタを設け、そのコネクタでやりとりすることになります。
Apacheとやりとりするには一般的にAJPを利用します。下記例ではAJP/1.3を利用することを意味します。つまりAJP/1.3でTomcatにアクセスするとポート番号8009を使うという意になります。Apache側の設定は別の機会に紹介します。
<?xml version="1.0" encoding="UTF-8"?>
<Server port="8005" shutdown="SHUTDOWN">
<Service name="Catalina">
<Connector port="8009" protocol="AJP/1.3" redirectPort="8443" />
</Service>
</Server>
ここでは「HTTP/1.1」「AJP/1.3」用の2つのコネクタの設定を学びました。ではどちらのコネクタを利用するのがよいのでしょうか。
筆者推奨では実装時(開発時)はApacheと連携するのが手間になるので「HTTP/1.1」で80ポートで利用、本番や検証環境など実稼働に近い場合は「AJP/1.3」を利用します。
つまり「使用していないコネクタはコメント化し利用しない」ということです。Apacheと連携するのにブラウザと直接やりとりする「HTTP/1.1」のコネクタが動作していてはApacheと連携する意味がありません。微妙ではありますが省メモリでもあります。不要なものは起動しないよう設定しましょう。
文字化け問題は頻繁に耳にします。よくある対応として以下のコードを見かけます。
request.setCharacterEncoding("UTF-8");
しかし上記だけでは不足している場合があります。コネクタに以下の様な設定を加えて下さい。
<?xml version="1.0" encoding="UTF-8"?>
<Server port="8005" shutdown="SHUTDOWN">
<Service name="Catalina">
<Connector port="8080" protocol="HTTP/1.1" useBodyEncodingForURI="true" />
</Service>
</Server>
これはTomcat5系以降、FORMのGETメソッドでパラメータを送信した場合、setCharacterEncodingメソッドを無視するようになったためです(POSTはこの値に関係なく有効)。
TomcatにはRealmという認証で利用できる機能があります。こちらを利用してみます。一般的に認証というと「認証に必要な情報を入力する」「入力された情報を利用し実際の認証を行う」の2つに分類されると思います。
簡単な認証ここでは「認証に必要な情報を入力する」は「ブラウザが用意している認証画面」、「入力された情報を利用し実際の認証を行う(Realmと呼びます)」は「ユーザ名、パスワードが記載されているファイル」を利用します。
ブラウザが用意している認証画面「ブラウザが用意している認証画面」はBASIC認証と呼ばれています(少なくともココではそのように呼びます)。BASIC認証を行うためにはweb.xmlに以下の変更を加えます。
<?xml version="1.0" encoding="UTF-8"?>
<web-app>
<login-config>
<auth-method>BASIC</auth-method>
</login-config>
</web-app>
こちらはTomcatをインストールすると既に用意されているファイル(TOMCATホーム/conf/tomcat-users.xml)を利用します。
<?xml version="1.0" encoding="UTF-8"?>
<tomcat-users version="1.0">
<role rolename="tomcat"/>
<role rolename="admin"/>
<user username="foo" password="tomcat1" roles="tomcat"/>
<user username="bar" password="tomcat2" roles="admin"/>
<user username="both" password="tomcat3" roles="tomcat,admin"/>
</tomcat-users>
ここでは2つのことが定義されています。1つ目は権限名です。roleタグがこれに該当します。ここでいう権限名は自由に決めることができます。ここでは「tomcat権限」「admin権限」の2つを定義したことになります。
2つ目はユーザとパスワードです。userタグがこれに該当します。username属性とpassword属性がこれに当たります。最後にあるroles属性がこのユーザが所有する権利を表します。
roles属性にはroleタグで定義した権限名を記述することができます。rolesという名称から想像できる通り複数の権限をユーザに与えることができます。つまりbothユーザはtomcat権限とadmin権限の双方を与えられたことを意味します。
次に上記ファイルを利用する設定になります。server.xmlを以下のように設定して下さい(通常は設定せずともアンコメントされているはずです)。
<?xml version="1.0" encoding="UTF-8"?>
<Server port="8005" shutdown="SHUTDOWN">
<GlobalNamingResources>
<Resource name="UserDatabase" auth="Container"
type="org.apache.catalina.UserDatabase"
description="User database that can be updated and saved"
factory="org.apache.catalina.users.MemoryUserDatabaseFactory"
pathname="conf/tomcat-users.xml" />
</GlobalNamingResources>
<Service name="Catalina">
<Engine name="Catalina" defaultHost="localhost">
<Realm className="org.apache.catalina.realm.UserDatabaseRealm"
resourceName="UserDatabase"/>
<Host name="localhost" appBase="webapps"></Host>
</Engine>
</Service>
</Server>
RealmもResourceと同様に各要素の中に含めることができます。記述箇所により有効範囲(利用することができる範囲)が決まります。アプリケーションの設計により、正しい箇所に記述しましょう。
今回は以下の認証ルールを設定します。
- URLに「http://サーバ名/アプリ名/admin/」が含まれていたら、admin権限を必要とする。
- URLに「http://サーバ名/アプリ名/auth/」が含まれていたら、権限は必要ないが認証のみ必要とする。
- URLに「http://サーバ名/アプリ名/guest/」といったように、上記に該当しない場合は、権限も認証も必要ないものとする。
まずはserver.xmlの設定です。権限なし(認証のみ)の設定を行わない場合は、この設定は不要です。権限なしを利用するにはRealmタグのallRolesMode属性にauthOnlyを設定します。
次にweb.xmlの設定です。まずはこのweb.xml内で利用する権限を定義します。これは「web-app > security-role > role-name」に記述します。今回はadmin権限を定義するので<role-name>admin</role-name>とします。
最後にURLと必要な権限を設定します。「web-app > security-constraint」に記述します。URLは「web-app > security-constraint > web-resource-collection > url-pattern」に記述します。*(アスタリスク)は利用できますが先頭もしくは末尾にしか利用できません。このURLに必要な権限の設定は「web-app > security-constraint > auth-constraint > role-name」に記述します。
「url-pattern」と「role-name」は複数記述することが可能です。role-nameに*(アスタリスク)が設定されていますが、こちらは権限なし(認証のみ)の場合の設定です。上記でふれましたが*(アスタリスク)を利用する場合はserver.xmlのRealmに追加が必要です。
<Realm className="org.apache.catalina.realm.UserDatabaseRealm"
resourceName="UserDatabase" allRolesMode="authOnly"/>
<?xml version="1.0" encoding="UTF-8"?>
<web-app>
<security-constraint>
<web-resource-collection>
<url-pattern>/admin/*</url-pattern>
</web-resource-collection>
<auth-constraint>
<role-name>admin</role-name>
</auth-constraint>
</security-constraint>
<security-constraint>
<web-resource-collection>
<url-pattern>/auth/*</url-pattern>
</web-resource-collection>
<auth-constraint>
<role-name>*</role-name>
</auth-constraint>
</security-constraint>
<security-role>
<role-name>admin</role-name>
</security-role>
</web-app>
Realm機能によってログイン時に使用したユーザIDを取得したい場合は以下のコードで取得できます。
request.getRemoteUser();
他にもログインユーザ関連のメソッドを紹介します。
No. | シグネチャ | 戻り値 | 概要 |
---|---|---|---|
1 | request.getRemoteUser(); | String | ログインに使用したユーザIDを取得 |
2 | request.isUserInRole(権限名) | boolean | パラメータで渡した権限を所有しているか判定 |
3 | request.getUserPrincipal(); | Principal | ログインユーザに関する詳細情報(Principal)を取得 |
上記の「簡単な認証」ではなく自作のHTML画面を認証に用いることができます。FORM認証と呼ばれています(少なくともココではそのように呼びます)。
FORM認証を行うためには、上記のBASIC認証の箇所を以下のように変更します。認証情報を入力するwelcome.jsp、認証失敗時に表示するfail.jspとし、以下のように定義しました。
<login-config>
<auth-method>FORM</auth-method>
<form-login-config>
<form-login-page>/WEB-INF/welcome.jsp</form-login-page>
<form-error-page>/WEB-INF/fail.jsp</form-error-page>
</form-login-config>
</login-config>
自作HTMLには以下のルールで記述します。
- 送信先(formタグのaction属性)はj_security_checkとする。
- ユーザIDはj_usernameというnameで送信する。
- パスワードはj_passwordというnameで送信する。
<html>
<body>
<form method="post" action="j_security_check">
ID:<input type="text" name="j_username"><br/>
PW:<input type="password" name="j_password"><br/>
<input type="submit">
</form>
</body>
</html>
上記のfail.jspをwelcome.jspにしてしまうと認証失敗理由等が表示できません。このような場合、筆者は以下のようにしています。
<login-config>
<auth-method>FORM</auth-method>
<form-login-config>
<form-login-page>/WEB-INF/welcome.jsp</form-login-page>
<form-error-page>/WEB-INF/welcome.jsp?message=401</form-error-page>
</form-login-config>
</login-config>
<html>
<body>
<% if ("401".equals(request.getParameter("message"))) { %><li>ID または PW が違います。</li><% } %>
<form method="post" action="j_security_check">
ID:<input type="text" name="j_username"><br/>
PW:<input type="password" name="j_password"><br/>
<input type="submit">
</form>
</body>
</html>
ファイルに認証情報を記述するのは、いささか不便(筆者の感想です)であり、データベースで管理したいと思うのは自然の流れかと思います。
データベースで認証情報を管理する方法はいくつかあるのですが、今回は「DataSourceレルム」を利用します。名称のとおりDataSourceを用いた認証ということになります。
DataSourceレルムではテーブルを利用します。ユーザIDとパスワード、ユーザIDと権限を保存するテーブルを用意します。実際のデータは上記の「tomcat-users.xml」と同一データをテーブルに登録してみます。
カラム名 | 型 | その他 |
---|---|---|
id | varchar(20) | ユーザID,PK |
pass | varchar(20) | パスワード |
No. | id | pass |
---|---|---|
1 | foo | tomcat1 |
2 | bar | tomcat2 |
3 | both | tomcat3 |
カラム名 | 型 | その他 |
---|---|---|
id | varchar(20) | ユーザID,PK |
role | varchar(20) | 権限,PK |
No. | id | role |
---|---|---|
1 | foo | tomcat |
2 | bar | admin |
3 | both | tomcat |
4 | both | admin |
上記テーブルと実データを用いてDataSourceレルムを利用します。
<?xml version="1.0" encoding="utf-8"?>
<Server port="8005" shutdown="SHUTDOWN">
<GlobalNamingResources>
<!-- Resourceの詳細設定は省略します(詳細はコチラ) -->
<Resource name="jdbc/realm" />
</GlobalNamingResources>
<Service name="Catalina">
<Engine name="Catalina" defaultHost="localhost">
<Host name="localhost" appBase="webapps">
<Realm className="org.apache.catalina.realm.DataSourceRealm"
allRolesMode="authOnly" dataSourceName="jdbc/realm"
userTable="auth_tbl"
userNameCol="id"
userCredCol="pass"
userRoleTable="role_tbl"
roleNameCol="role"
>
</Host>
</Engine>
</Service>
</Server>
属性名 | 今回の具体値 | 解説 |
---|---|---|
userTable | auth_tbl | ユーザID,パスワードを含んでいるテーブル名 |
userNameCol | id | ユーザIDのカラム名 |
userCredCol | pass | パスワードのカラム名 |
userRoleTable | role_tbl | ユーザID,権限を含んでいるテーブル名 |
roleNameCol | both | 権限のカラム名 |
ここで注意点として、userRoleTable属性のテーブル内のユーザIDカラム名は指定できないことです。userNameCol属性で指定したカラム名になります。
このクラスを利用する限り現時点では指定できないようです。では仮に権限テーブルのカラム名が以下の様な場合はどのように対処しましょう。
カラム名 | 型 | その他 |
---|---|---|
user | varchar(20) | ユーザID,PK |
role | varchar(20) | 権限,PK |
容易な対処法としてはデータベースでVIEWを利用することです。VIEWでuserカラムの名称をidに変えてしまえばよいのです。他に以下の様なこともできるようです。
userRoleTable="(select user as id, role from role_tbl) tmp"
ただしこの設定例は現在の実装で動作することは確認していますが、将来的な実装がどのようになるかはわかりません。VIEWで対応するのがスマートです。
全てのユーザIDに対しauthed権限を持たせることにしましょう。すると全てのユーザと同じ件数だけ権限テーブルに権限authedのレコードが必要になります。
このような場合、上記「権限テーブルのユーザIDカラム名」ような発想で乗り越えましょう。
userRoleTable="(select id, role from role_tbl
union all select id, 'authed' from auth_tbl) tmp"
(実際には1行)
あるサイトで「参照」「更新」「印刷」という権限があるとします。この場合上記テーブルには1ユーザあたり最大3レコードINSERTしなければなりません。
いわゆるスーパーユーザ(機能制限されることのないユーザの意)を登録する場合、現在は3レコードで済みますが、権限を拡張されると難しいです。どのように対応しましょう。以下のテーブル構造で対応できそうです。
カラム名 | 型 | その他 |
---|---|---|
id | varchar(20) | ユーザID,PK |
pass | varchar(20) | パスワード |
role_id | varchar(20) | 権限ID |
カラム名 | 型 | その他 |
---|---|---|
role_id | varchar(20) | 権限ID,PK |
func_id | varchar(20) | 機能ID,PK |
No. | id | pass | role_id |
---|---|---|---|
1 | foo | tomcat1 | view |
2 | bar | tomcat2 | edit |
3 | both | tomcat3 | super |
No. | role_id | func_id |
---|---|---|
1 | view | view |
2 | edit | edit |
3 | super | view |
4 | super | edit |
5 | super |
userRoleTable="(select id, func_id as role from auth_table a
join func_tbl f on a.role_id = f.role_id) tmp"
(実際には1行)
1つのTomcat内で複数のWEBアプリが動作しており、全てのアプリでRealmでの認証を行っている場合の、アプリ間でSingle Sign Onする設定。
ただし同一のTomcat内であってもバーチャルドメイン等、別ドメインのものは再認証となる。
<?xml version="1.0" encoding="UTF-8"?>
<Server port="8005" shutdown="SHUTDOWN">
<Service name="Catalina">
<Engine name="Catalina" defaultHost="localhost">
<Host name="localhost" appBase="webapps">
<Valve className="org.apache.catalina.authenticator.SingleSignOn" />
</Host>
</Engine>
</Service>
</Server>
Realmを用いる際に以下のようにdigest属性を用いることで、画面にて入力されたパスワードを暗号化し認証させることができる。
利用できる暗号方法は「MD5」「SHA」の2つ。ただし「BASIC」「DIGEST」「FORM」「CLIENT-CERT」のうち「DIGEST」では期待通りの動作をしないようだ。
<Realm
className="org.apache.catalina.realm.UserDatabaseRealm"
resourceName="UserDatabase"
digest="MD5"
/>
<Realm className="org.apache.catalina.realm.DataSourceRealm"
dataSourceName="jdbc/XXXX"
userTable="users" userNameCol="id" userCredCol="password"
userRoleTable="roles" roleNameCol="role"
digest="SHA"
/>
TOMCAT_HOME/lib/catalina.jarに暗号化できるツールが存在します。UserDatabaseRealmの際、暗号化する方法がない場合、以下を参考に利用して下さい。(要ダウンロード:tomcat-juli.jarの「JULI log4j jar」)
Microsoft Windows [Version 6.3.9600]
(c) 2013 Microsoft Corporation. All rights reserved.
C:\Users\username>cd %TOMCAT_HOME%\lib
C:\TOMCAT_HOME\lib>java -cp tomcat-util.jar;
tomcat-juli.jar;
tomcat-util-scan.jar;
servlet-api.jar;
catalina.jar
org.apache.catalina.realm.RealmBase -a MD5 abc (実際のコマンドは1行)
abc:900150983cd24fb0d6963f7d28e17f72
C:\TOMCAT_HOME\lib>
どのバージョンからこの機能が有効かはわかりかねます。上記にあるものは確認がとれたバージョンになります。
「自作HTMLを認証情報入力画面にする」ではFORMを用いて自作HTMLを利用しました。自作HTMLでは「j_username」「j_password」と2つのデータしか送信できませんでした。
たとえば「会社ID」「社員ID」「パスワード」のように送信する値を3つに変更。といった場合に認証情報を取得するクラスを変更することができます。ここではクラスを自作せずクラスを変更する方法を紹介します。
まずはカスタムクラスを作成します。今回は「CustomAuthenticator」というクラス名にしました。このクラスの親クラスは「catalina.jar」をクラスパスに追加することでコンパイルできるようになります。
package jp.co.mclnet.tomcat8ex;
import org.apache.catalina.authenticator.FormAuthenticator;
public class CustomAuthenticator extends FormAuthenticator {
}
つぎに「Authenticators.properties」ファイルを新規作成します。このファイルは「TOMCATホーム/lib/org/apache/catalina/startup/Authenticators.properties」になるよう配置します。
ファイルの内容ですが、今回は「CUSTOM」という名称で追加します。ファイルの内容は以下を参考にしてください。
CUSTOM=jp.co.mclnet.tomcat8ex.CustomAuthenticator
最後に「web.xml」ファイルを設定します。
<?xml version="1.0" encoding="UTF-8"?>
<web-app>
<login-config>
<auth-method>CUSTOM</auth-method>
</login-config>
</web-app>
-Dorg.apache.catalina.session.StandardSession.ACTIVITY_CHECK=true"
CREATE TABLE session_table (
session_id VARCHAR(100) NOT NULL,
session_valid CHAR(1) NOT NULL,
session_max_inactive INT NOT NULL,
session_last_accessed BIGINT NOT NULL,
session_app VARCHAR(255) DEFAULT NULL,
session_data MEDIUMBLOB,
PRIMARY KEY (session_id),
KEY kapp_name (session_app)
);
<Context>
<Manager
className="org.apache.catalina.session.PersistentManager"
maxIdleBackup="10"
>
<Store
className="org.apache.catalina.session.JDBCStore"
connectionName="username"
connectionPassword="password"
connectionURL="jdbc:mysql://localhost:3306/databasename"
driverName="com.mysql.jdbc.Driver"
sessionTable="session_table"
sessionIdCol="session_id"
sessionValidCol="session_valid"
sessionMaxInactiveCol="session_max_inactive"
sessionLastAccessedCol="session_last_accessed"
sessionAppCol="session_app"
sessionDataCol="session_data"
/>
</Manager>
</Context>
session_id | 6AFCADC23D18C9DE36975B6EF5B4EF63 |
---|---|
session_valid | 1 |
session_max_inactive | 1800 |
session_last_accessed | 1445395062842 |
session_app | /Catalina/localhost/appname |
session_data | Binary |
どのバージョンからこの機能が有効かはわかりかねます。上記にあるものは確認がとれたバージョンになります。
MySQLで動作確認しました(必要に応じ他のデータベースでお試し下さい)。テーブルを作成し、Tomcatの環境変数を変更、Contextの設定と主に3箇所対応しました。JDBCStoreの他にFileStoreというものもあるそうです。
どのバージョンからこの機能が有効かはわかりかねます。上記にあるものは確認がとれたバージョンになります。
JDBCにセッション情報を保存するとデータベースに負荷がとてもかかるため、KeyValueStoreにデータを保存したいと思います。AWS DynamoDB に保存してみたいと思います。
JARのダウンロード「AWS SDK」と「Tomcat向けDynamoDBセッション管理」をダウンロードします。ダウンロードしたファイルは「TOMCATホーム/lib/」に保存します。
コンテキストの設定を行います。必要に応じ設定値を変更して下さい。この設定にて動作させるとDynamoDB内にTomcat_SessionStateテーブルが自動で作成されます。テーブル名を指定する場合はtable属性で自由に名称を設定下さい。
<?xml version="1.0" encoding="UTF-8"?>
<Context backgroundProcessorDelay="1">
<WatchedResource>WEB-INF/web.xml</WatchedResource>
<Manager className="com.amazonaws.services.dynamodb.sessionmanager.DynamoDBSessionManager"
regionId="ap-northeast-1"
endpoint="dynamodb.ap-northeast-1.amazonaws.com"
createIfNotExist="true"
saveOnRestart="true"
processExpiresFrequency="1"
awsAccessKey="YourAWSAccessKey"
awsSecretKey="YourAWSSecretKey"
/>
</Context>
DynamoDBSessionManagerクラスはTomcatのPersistentManagerBaseを継承しています。#setMaxIdleBackupやprocessExpiresFrequencyを読む限り、どんなに小さな値に設定しても、最小でも1秒後にセッションの永続化をするようです(上記のJDBCStoreも同様と思われる)。
この方法ではラウンドロビンの状態で連打されると対応できません。そのような意味ではセッションの管理はスティッキーセッションにし、被害を最小限にする必要があるようです。
上記の1秒に関係なく、saveOnRestartをtrueに設定することによって、ロードバランサー配下のインスタンスに障害が発生した場合、セッションのフェイルオーバーが行われるようです。
endpointの指定ができるため「http://localhost:8000」を指定することで利用できます。
<?xml version="1.0" encoding="UTF-8"?>
<web-app>
<error-page>
<error-code>403</error-code>
<location>/WEB-INF/jsp/error.jsp?error=403</location>
</error-page>
<error-page>
<exception-type>com.hogehoge.ForbiddenException</exception-type>
<location>/WEB-INF/jsp/error.jsp?error=403</location>
</error-page>
</web-app>
HTTPレスポンスコードが200(Successful)以外であったり、例外がthrowされServlet内でcatchされずTomcatまで投げられた場合、error-pageタグを用いてエラーページに遷移させることができます。
HTTPレスポンスコードによってエラーページに遷移させるにはerror-codeタグ、例外によってエラーページに遷移させるにはexception-typeタグを利用します。エラーページの遷移先はlocationタグに記述します。
またerror-codeタグとexception-typeタグは両立できませんので、別々に定義する必要があります。
上記ではHTTPレスポンスコードが403の場合、error.jspを表示する設定になっています。この場合のHTMLのタグとして正しく(htmlタグやbodyタグを書くという意)記述して下さい。ブラウザによってはレスポンスボディが期待通り表示されないことがあるようです。
<html>
<head>
<title>OK</title>
</head>
<body>
エラーが発生しました。
</body>
</html>
エラーが発生しました。
上記設定ですと「com.hogehoge.ForbiddenException」がthrowされたら「/WEB-INF/jsp/error.jsp?error=403」が表示されます。しかしこのような例外をerror.jspに遷移させる設定が複数存在する場合、どの例外がthrowされたか判別できないと、その後のハンドリングができません。
このような場合「request.getAttribute("javax.servlet.error.exception")」にてthrowされた例外を取得することができます。
<%@ page contentType="text/html; charset=UTF-8" %>
<?xml version="1.0" encoding="UTF-8"?>
<web-app>
<jsp-config>
<jsp-property-group>
<url-pattern>*.jsp</url-pattern>
<url-pattern>*.html</url-pattern>
<page-encoding>UTF-8</page-encoding>
</jsp-property-group>
</jsp-config>
</web-app>
たいていのサイトではJSPに利用する文字コードをページ毎にわけたりせず、同一文字コードを利用していると思います。このような場合、各ページに「JSPページの文字コード指定の例」のようなコードを書くと思います。しかしこの方法では労力もかかりますし、漏れも起こりえます。
このような場合、「全ページに文字コード指定」を行うことで、該当ページ全てに上記と同様のことを設定したことになります。今回例ではjspとhtmlファイルの文字コードを指定しました。
url-patternタグは複数記述することができます。url-patternタグの値は*(アスタリスク)は利用できますが先頭もしくは末尾にしか利用できません。
<%@ taglib prefix="html" uri="/tags/struts-html.tld" %>
<?xml version="1.0" encoding="UTF-8"?>
<web-app>
<jsp-config>
<jsp-property-group>
<url-pattern>/WEB-INF/jsp/body/*</url-pattern>
<include-prelude>/WEB-INF/jsp/tablib.jsp</include-prelude>
<include-prelude>/WEB-INF/jsp/header.jsp</include-prelude>
<include-coda>/WEB-INF/jsp/footer.jsp</include-coda>
</jsp-property-group>
</jsp-config>
</web-app>
たいていのサイトではWEBページにヘッダやフッタが存在します。また直接表示されませんがタグライブラリの指定など、必ず読み込ませたいファイルがあると思います。
このような場合、以下の対応をすることで該当ページ全てにインクルードしたことになります。今回例では「/WEB-INF/jsp/body/*」に該当するファイル全ての先頭にtablib.jsp,header.jspの両ファイルを、末尾にfooter.jspファイルをincludeしています。
include-preludeタグ、include-codaタグは複数記述することができます。記述順にincludeされるようです。
<html>
<head>
<meta http-equiv="Content-Type" content="text/html; charset=Shift_JIS">
<meta http-equiv="Content-Style-Type" content="text/css">
<meta http-equiv="Pragma" content="no-cache">
<meta http-equiv="Cache-Control" content="no-cache">
<meta http-equiv="Cache-Control" content="no-store">
<meta http-equiv="Cache-Control" content="max-age=0">
<meta http-equiv="Expires" content="0">
<title>XXX</title>
<link rel="stylesheet" href="<%= application.getContextPath() %>/all.css" type="text/css" />
</head>
<body>
<div style="float: left;">ID: <%= request.getRemoteUser() %> 氏名: XXXX</div>
<div style="float: right;"><a href="logout">ログアウト</a></div>
<hr style="clear: both;">
<noscript>
<p>このページはJavaScriptを使用しています。JavaScriptを有効にし再度表示して下さい。</p>
</noscript>
<div id="main" style="display:none;">
</div>
<hr style="clear: both;">
<div style="float: left;">
<html:link action="menu">メニュー</html:link>
<html:link action="logout">ログアウト</html:link>
</div>
<address style="float: right; font-size: 0.8em;">copyRight</address>
<script type="text/javascript">
<!--
document.getElementById("main").style.display='block';
//-->
</script>
</body>
</html>
<%@ taglib prefix="html" uri="/tags/struts-html.tld" %><-- taglib -->
hogehoge
<-- taglib --><html>
<head>
<meta http-equiv="Content-Type" content="text/html; charset=Shift_JIS">
<meta http-equiv="Content-Style-Type" content="text/css">
<meta http-equiv="Pragma" content="no-cache">
<meta http-equiv="Cache-Control" content="no-cache">
<meta http-equiv="Cache-Control" content="no-store">
<meta http-equiv="Cache-Control" content="max-age=0">
<meta http-equiv="Expires" content="0">
<title>XXX</title>
<link rel="stylesheet" href="アプリケーション名/all.css" type="text/css" />
</head>
<body>
<div style="float: left;">ID: xxxx 氏名: XXXX</div>
<div style="float: right;"><a href="logout">ログアウト</a></div>
<hr style="clear: both;">
<noscript>
<p>このページはJavaScriptを使用しています。JavaScriptを有効にし再度表示して下さい。</p>
</noscript>
<div id="main" style="display:none;">
hogehoge
</div>
<hr style="clear: both;">
<div style="float: left;">
<html:link action="menu">メニュー</html:link>
<html:link action="logout">ログアウト</html:link>
</div>
<address style="float: right; font-size: 0.8em;">copyRight</address>
<script type="text/javascript">
<!--
document.getElementById("main").style.display='block';
//-->
</script>
</body>
</html>
Tomcat本体のログ出力は「Apache Commons Logging」を利用しています。これをLog4Jに変更します。
- TomcatのダウンロードページよりExtraパッケージをダウンロードする。ファイルは「tomcat-juli.jar」と「tomcat-juli-adapters.jar」の2つ。
- Log4Jのダウンロードページより「log4j.jar」をダウンロードする。
- 「tomcat-juli.jar」を「TOMCATホーム/bin/」に配置する。既存ファイルを上書きする。
- 「tomcat-juli-adapters.jar」と「log4j.jar」を「TOMCATホーム/lib/」に配置する。
- 「TOMCATホーム/conf/logging.properties」を削除する。
- 「TOMCATホーム/lib/」にLog4Jの設定ファイル(log4j.propertiesもしくはlog4j.xml)を配置する。
以下はシンプルなlog4j.xmlです。必要に応じ変更して下さい。
<?xml version="1.0" encoding="UTF-8" ?>
<!DOCTYPE log4j:configuration SYSTEM "log4j.dtd">
<log4j:configuration>
<appender name="CONSOLE" class="org.apache.log4j.DailyRollingFileAppender">
<param name="File" value="${catalina.base}/logs/stdout" />
<param name="DatePattern" value="'.'yyyy-MM-dd'.log'" />
<param name="Append" value="true" />
<layout class="org.apache.log4j.PatternLayout">
<param name="ConversionPattern" value="%d [%t] %-5p %c- %m%n" />
</layout>
</appender>
<appender name="CATALINA" class="org.apache.log4j.DailyRollingFileAppender">
<param name="File" value="${catalina.base}/logs/catalina" />
<param name="DatePattern" value="'.'yyyy-MM-dd'.log'" />
<param name="Append" value="true" />
<layout class="org.apache.log4j.PatternLayout">
<param name="ConversionPattern" value="%d [%t] %-5p %c- %m%n" />
</layout>
</appender>
<appender name="LOCALHOST" class="org.apache.log4j.DailyRollingFileAppender">
<param name="File" value="${catalina.base}/logs/localhost" />
<param name="DatePattern" value="'.'yyyy-MM-dd'.log'" />
<param name="Append" value="true" />
<layout class="org.apache.log4j.PatternLayout">
<param name="ConversionPattern" value="%d [%t] %-5p %c- %m%n" />
</layout>
</appender>
<logger name="org.apache.catalina.core.ContainerBase.[Catalina].[localhost]">
<level value="info" />
<appender-ref ref="LOCALHOST" />
</logger>
<root>
<level value="info" />
<appender-ref ref="CONSOLE" />
<appender-ref ref="CATALINA" />
</root>
</log4j:configuration>
Tomcat8で動作確認を行いましたが、Tomcat6,7でも同様の方法でLog4Jに変更できると思います。
/var/log/catalina.out {
missingok
copytruncate
daily
sharedscripts
postrotate
EXT=`date +%Y%m%d`
for f in $1;
do mv $f.1 $f.$EXT;
done
endscript
}
Linux環境などにTomcatをインストールし運用すると、問題となるのがcatalina.out。何の対策もとらないと肥大化してしまいます。
ここでは「logrotateコマンド」を用いたローテーションの設定を紹介します。似たようなものに「rotatelogs」がありますが、apache付属なのでtomcat単体では使えません。
一般に「/etc/logrotate.conf」にログローテーションの設定を記述するのですが、「/etc/logrotate.d」ディレクトリに新規にファイルを作り記載するのがよいでしょう。
ローテーションさせたいファイルが「/var/log/catalina.out」である場合を例に書きます。「/etc/logrotate.d/hoge(任意の名称でOK)」を新規作成します。ファイルの内容はこちらのソースを参考にして下さい。
書式エラーを発見するために「/usr/sbin/logrotate -d /etc/logrotate.d/hoge」を実行します。誤りがある場合表示されるでしょう。
次に強制実行します。「/usr/sbin/logrotate -f /etc/logrotate.d/hoge」を実行することで、ローテーションするはずです。実際にファイルがローテーションしたか確認して下さい。
最後に日時を待ちます・・・。実際にファイルがローテーションしたか確認して下さい。この作業を怠るとよくない結果が待っていますのでご注意下さい。
ブラウザにて次のAPI(http://サーバ名/アプリ名/aaa.jsp)を呼び出したとします。その際、aaa.jspはbbb.jspにforwardしているとします。このときHttpServletRequest#getRequestURI()は以下の値を返します。
Tomcat | getRequestURI |
---|---|
4.1 | /アプリ名/aaa.jsp |
5.5 | /アプリ名/bbb.jsp |
4.1系のようなクライアントから直接アクセスされたURIを取得したい場合は以下のコードで取得できます。
(String) request.getAttribute("javax.servlet.forward.request_uri")
と書きましたが情報として誤りのようでした。申し訳ありません。
Tomcat8で確認したのですが、bbb.jspの中で「HttpServletRequest#getRequestURI()」を行うと上記のようになるようです。
解釈としては「aaa.jsp」から「bbb.jsp」へリクエストを行ったといったところでしょうか。
a.jarがb.jar内のクラスを使うとき、TOMCATホーム/lib/a.jarを配置するとb.jarがない。といったメッセージが表示されます。以下を参考に設定して下さい。
<Context>
<JarScanner scanManifest="false"/>
</Context>