185
第
第
1
1
3
3
章
章
.
.
To
T
om
mc
ca
at
t
を
を
用
用
い
い
た
た
ユ
ユ
ー
ー
ザ
ザ
認
認
証
証
【学習のねらい】
① データベースに登録されたユーザのみにアクセスを許可するユーザ認証の仕組みを、 Tomcat の機能を用いて学習する。<前回の復習>
講義で示された【基礎課題13-1】に解答して下さい。13-1.はじめに
本学の情報ポータルなど、個人情報を管理しているページは、セキュリティのため本人 以外がアクセスできないように、ユーザID とパスワードでアクセスを管理しています。こ の仕組みをユーザ認証と言います。本章では、以下の 13-2~13-4 節の説明にしたがって、 Tomcat が用意している機能を用いてユーザ認証機能がついたページを作成しましょう。13-2.閲覧ページ(サーブレット)の作成
まず、基になる閲覧ページを作成します。作成するのは次のようなページです。 「Page1Servlet.java」というサーブレットに接続すると次のように表示される。 「Page2Servlet.java」というサーブレットに接続すると次のように表示される。 そして、両者はリンクで結びついている。 ユーザ認証は後に回して、この2 つのサーブレットを次のように作成してください。186 ① Tomcat プロジェクトを「Auth」という名前で 新規作成します。その中にパッケージ名「auth」、 クラス名「Page1Servlet.java」というクラスを 作成してください。続いて、同じパッケージ内 に「Page2Servlet.java」というクラスを作成し ます。 ② 「Page1Servlet.java」を次のように記述してください。この時点では新しい内容は含 まれていないので理解に問題はないと思います。 ③ 同様に「Page2Servlet.java」を記述します。次ページの下線部のようにページ番号1 と2が入れ替わるだけです。 package auth; import java.io.IOException; import java.io.PrintWriter; import javax.servlet.ServletException; import javax.servlet.http.HttpServlet; import javax.servlet.http.HttpServletRequest; import javax.servlet.http.HttpServletResponse; public class Page1Servlet extends HttpServlet{
public void doGet(HttpServletRequest request, HttpServletResponse response) throws ServletException, IOException {
request.setCharacterEncoding("Windows-31J"); response.setContentType("text/html;charset=Windows-31J"); PrintWriter out=response.getWriter(); out.println("<html>"); out.println("<head>"); out.println("<title>ユーザー認証のテスト</title>"); out.println("</head>"); out.println("<body>"); out.println("<p>ようこそページ 1 へ</p>"); out.println("<p><a href=¥"/Auth/Page2Servlet¥"> ページ 2 へ</a></p>"); out.println("</body>"); out.println("</html>"); } } <Page1Servlet.java> 「“」を””で囲まれた文字列中に指 定する場合は、「¥”」と指定します。
187 ④ 上の2つのサーブレットの URL を登録するため、web.xml を次ページのように記述し ます。 package auth; import java.io.IOException; import java.io.PrintWriter; import javax.servlet.ServletException; import javax.servlet.http.HttpServlet; import javax.servlet.http.HttpServletRequest; import javax.servlet.http.HttpServletResponse; public class Page2Servlet extends HttpServlet{
public void doGet(HttpServletRequest request, HttpServletResponse response) throws ServletException, IOException {
request.setCharacterEncoding("Windows-31J"); response.setContentType("text/html;charset=Windows-31J"); PrintWriter out=response.getWriter(); out.println("<html>"); out.println("<head>"); out.println("<title>ユーザー認証のテスト</title>"); out.println("</head>"); out.println("<body>"); out.println("<p>ようこそページ 2 へ</p>"); out.println("<p><a href=¥"/Auth/Page1Servlet¥"> ページ 1 へ</a></p>"); out.println("</body>"); out.println("</html>"); } } <Page2Servlet.java> WEB-INF 内に 新規作成
188
【応用課題
13-A】
作成したら動作を確認してください。その上で、「閲覧対象となる二つのページを作成し ました。」き記述して提出してください。13-3.ユーザ情報データベースの作成
次に、ユーザ認証を行う際に参照する、ユーザ名とパスワードを保管したデータベース を作成します。作成するテーブルは、次の「user_table」と「role_table」の2つです。user pass Name
S_Kaneda pass1 金田正太郎 N_Date pass2 伊達直人 user role S_Kaneda user1 N_Date user1 <web-app> <servlet> <servlet-name>Page1Servlet</servlet-name> <servlet-class>auth.Page1Servlet</servlet-class> </servlet> <servlet> <servlet-name>Page2Servlet</servlet-name> <servlet-class>auth.Page2Servlet</servlet-class> </servlet> <servlet-mapping> <servlet-name>Page1Servlet</servlet-name> <url-pattern>/Page1Servlet</url-pattern> </servlet-mapping> <servlet-mapping> <servlet-name>Page2Servlet</servlet-name> <url-pattern>/Page2Servlet</url-pattern> </servlet-mapping> </web-app> <web.xml> user:ユーザ名 pass:パスワード Name:ユーザ氏名 user:ユーザ名 role:ユーザの承認レベル。例えば一般ユーザや管理 者などを区別する際に用いる。ここでは、同一 のロールにしてある(role 名は任意)。 <user_table> <role_table>
189 上の二つのテーブルを、次の手順にしたがって作成してください。 ① MySQL を起動し、下のようにデータベース「auth」を作成します(「認証」は英語で 「authentication」というので、そこから命名)。下線部がコマンド入力部分。 ② 操作対象を「auth」にします。 ③ 次のように「user_table」を作成し、2 名分のデータを登録します。 ④ 上の作成後、テーブルの全データを取り出すと、次のように表示されるはずです。 ⑤ 続いて同じ要領で、テーブル「role_table」を作成し、2 名分のデータを登録します。 user_table の作成 データの追加1 データの追加2 role_table の作成
190 ⑥ 上の作成後、テーブルの全データを取り出すと、次のように表示されるはずです。
【応用課題
13-B】
上の④と⑥の表示画面を確認したら、「データベース「auth」の中に user_table と role_table を作成しました。」と記述して提出してください。13-4.レルム(Realm)の指定 -JDBC レルムの指定
本節ではレルム(Realm)を指定します。レルムとは聞き慣れない用語だと思いますが、 ユーザ認証を行う際に必要となるユーザ ID やパスワードの管理の仕方を指します。Tomcat ではデフォルトで UserDatabase レルムというレルムが設定されています。これは、ユーザ ID、パスワード、ロールを「tomcat-users.xml」というファイルに記述し、それを参照す ることで認証を行う方式です。これは、簡便で便利な反面、ユーザ数が増えた場合には実 用的ではありません。そこで、ここでは、ユーザ ID、パスワード、ロールをデータベース に格納し、JDBC 経由で認証を行う JDBC レルムを用いることにします。つまり、前節で作成 したデータベースを認証に用いる訳です。以下の手順にしたがって、JDBC レルムを用いる ための設定を行ってください。① Tomcat のルートディレクトリ(C:\Program Files\Apache Software Foundation\Tomcat 8.0)にある「conf」というフォルダを開いてください。この中に「server.xml」があ ります。これを編集するので、秀丸エディタなど適当なエディタを使って開いてくだ さい。 ② 130 行目付近にある、「<Realm className=・・・」という部分は、UserDatabase レル ムの設定を行っているところです。今は、これを使わず JDBC レルムを使用するので、 データの追加
191 この部分を下のようにコメントにしてください。そして、その下に点線枠で示した JDBC レルム設定部分を新たに記述します。 【解説】 JDBC レルムタグでは、次の要素を指定しています。 1. className:このレルムを動作させるために Tomcat が用意しているクラス名。 2. driverName:JDBC ドライバ名。MySQL の場合は"com.mysql.jdbc.Driver"。 3. connectionURL:接続するデータベースの URL。最後の auth がデータベース名。 4. connectionName、connectionPassword:当該データベースに接続できるユーザ 名とパスワード。ここではユーザ名として root を用いる。ただし、パスワードは各自 のものを指定すること。 5. userTable:ユーザ名とパスワードが格納されているテーブル名。 6. userNameCol:当該テーブルに於いて、ユーザ名が入っているカラム名(列名)。 7. userCredCol:当該テーブルに於いて、パスワードが入っているカラム名(列名)。 8. userRoleTable:ユーザ名とロール名が格納されているテーブル名。 9. roleNameCol:当該テーブルに於いて、ロール名が入っているカラム名(列名)。 ③ JDBC レルムを使用する場合、使用するデータベースの JDBC ドライバを所定の場所 にコピーしておく必要があります。Tomcat のルートディレクトリ(C:\Program Files\Apache Software Foundation\Tomcat 8.0)にある「lib」というフォルダ内に、 MySQL の JDBC ドライバ「mysql-connector-java-5.1.37-bin.jar」をコピーしてくださ い。JDBC ドライバは 10-3 節(p.149)で確認したはずです。 ・・・ <!-- <Realm className="org.apache.catalina.realm.UserDatabaseRealm" resourceName="UserDatabase"/> --> <Realm className="org.apache.catalina.realm.JDBCRealm" driverName="com.mysql.jdbc.Driver" connectionURL="jdbc:mysql://localhost/auth" connectionName="root" connectionPassword="*******"
userTable="user_table" userNameCol="user" userCredCol="pass" userRoleTable="role_table" roleNameCol="role" /> ・・・ これをつけてコメントにする。 <server.xml> JDBC レルム設定部分 MySQL のパスワード を各自記述すること
192
【応用課題
13-C】
上の処理を行ったら「server.xml に JDBC レルムを使用する設定を行いました。」と記述 して提出してください。13-5.認証方法の指定 -FORM 認証の指定
JavaEE では、次の 4 種類の認証方法が用意されています。 1. BASIC 認証:HTTP の BASIC 認証を使ってユーザ名とパスワードを照合する方式。 パスワードもそのままサーバに送信される。ログインはHTTP で用意されているダイ アログボックスを用いる。 2. DIGEST 認証:上と同様だが、パスワードを簡易的に暗号化してサーバに送信する点 が異なる。 3. FORM 認証:BASIC 認証との違いは、ユーザ名やパスワード入力時に HTTP で用意 されているダイアログボックスではなく、独自のログイン画面を用いる点。 4. CLIENT-CERT 認証:クライアント証明書を使ってユーザ認証を行う、最も安全な認 証方式。この方式では、SSL (Secure Sockets Layer) プロトコルを使用して認証を行 う必要がある。 ここでは、標準的に用いられている FORM 認証を用います。次の手順にしたがって、 FORM 認証の設定を行ってください。 ① web.xml に次ページのように<security-constraint>、<login-config>そして <security-role>の3つのタグを追加してください。 ② 続いて、ログイン時に表示されるページ「Login.html」を下のように Auth プロジェ クトのルートディレクトリに作成し p.194 の様に記述してください。 ③ 最後に、ログイン失敗時に表示されるページ「Error.html」を Auth プロジェクト のルートディレクトリ内に作成し p.194 の様に 記述してください。193 <web-app> ・・・ <security-constraint> <web-resource-collection> <web-resource-name>Form Auth</web-resource-name> <url-pattern>/*</url-pattern> </web-resource-collection> <auth-constraint> <role-name>user1</role-name> </auth-constraint> </security-constraint> <login-config> <auth-method>FORM</auth-method> <form-login-config> <form-login-page>/Login.html</form-login-page> <form-error-page>/Error.html</form-error-page> </form-login-config> </login-config> <security-role> <role-name>user1</role-name> </security-role> </web-app> <web.xml> 認証方法をFORM 認証に指定 セキュリティの対象となる URL:ここで はWeb アプリケーション内の全ページ アクセス制限をする Web リソースに名前 をつける(名前は任意) アクセスを許可するロール名 (使用する)ロール名の定義 ログイン用ページの指定 ログイン失敗時のページの指定
194
【応用課題
13-D】
作成したら、次の動作を確認してください。 ① 「http://localhost:8080/Auth/Page1Servlet」に接続してください。すると、次ページ のログイン画面が現れます。ここで、13-3 節で作成したデータベースに登録されてい るユーザ名とパスワードを入力してください。 <html lang="ja"> <head> <title>ログインエラー画面</title> </head> <body> <h1>ログインエラー</h1> <p>ログインに失敗しました。ユーザ名あるいはパスワードが正しくありません。</p> </body> </html> <html lang="ja"> <head> <title>ログイン画面</title> </head> <body> <h1>ユーザ認証画面</h1> <p>ユーザ名とパスワードを入力して[送信]ボタンをクリックしてください。</p><form method="POST" action="j_security_check" name="loginform"> ユーザ名 <input type="text" name="j_username" ><br>
パスワード <input type="password" name="j_password" ><br> <input type="submit" value="送信">
<input type="reset" value="リセット"> </form> </body> </html> <Login.html> <Error.html> アクション名:j_security_check ユーザ名:j_username パスワード:j_password の各名称は固定されている。
195 ② すると、下のようにページ 1 にアクセスできます。 ③ 上の状態で「ページ 2 へ」をクリックすると、ページ 2 へジャンプします。このよう に、いったんユーザ認証をクリアすると、当該Web アプリケーション内のページに(認 証なしで)移動することができます。 ④ 一方①のログイン画面で、不正なユーザ名あるいはパスワードを入力すると、次のよ うにログインエラー画面に移動します。 上の動作を確認したら、「ユーザ認証機能が動作することを確認しました。」と記述して 提出してください。
196
13-6.ユーザ認証の改良
【応用課題
13-E】
-ログアウト処理の追加
FORM 認証を用いた場合、いったん認証が通ると、セッションが有効な間(つまりブラ ウザを閉じるまで)はログインした状態が続きます。そこで、任意のタイミングでログイ ン状態を終了できるよう、ログアウトの処理を付加してみましょう。次の手順にしたがっ て、ログアウトの処理を加えてください。 ① ログアウト時に表示されるページ「Logut.jsp」を Auth プロジェクトのルートディレクトリに作成 し、下のように記述してください。 下線部の invalidate()メソッドはセッション を無効にするメソッドです。つまり、この命令で セッションは終了し、したがってログイン状態も 終了することになります。 ② 次に、「Page1Servlet.java」および「Page2Servlet.java」を次ページのように修正し てください。下線部が追加部分です。追加したのは、上の「Logout.jsp」へのリンクを つけた部分です。<%@ page contentType="text/html; charset=Windows-31J" %> <html> <body> ログアウトしました <% session.invalidate(); %> </body> </html> <Logout.jsp>
197 package auth;
・・・
public class Page2Servlet extends HttpServlet{ public void doGet(HttpServletRequest request,
HttpServletResponse response) throws ServletException, IOException {
request.setCharacterEncoding("Windows-31J"); response.setContentType("text/html;charset=Windows-31J"); ・・・ out.println("<p><a href=¥"/Auth/Page1Servlet¥"> ページ 1 へ</a></p>"); out.println("<p><a href=¥"/Auth/Logout.jsp¥"> ログアウト</a></p>"); out.println("</body>"); out.println("</html>"); } } package auth; ・・・
public class Page1Servlet extends HttpServlet{ public void doGet(HttpServletRequest request,
HttpServletResponse response) throws ServletException, IOException {
request.setCharacterEncoding("Windows-31J"); response.setContentType("text/html;charset=Windows-31J"); ・・・ out.println("<p><a href=¥"/Auth/Page2Servlet¥"> ページ 2 へ</a></p>"); out.println("<p><a href=¥"/Auth/Logout.jsp¥"> ログアウト</a></p>"); out.println("</body>"); out.println("</html>"); } } <Page1Servlet.java> <Page2Servlet.java> 「¥」は使用フォントによっては「」と表示されます。
198 作成したら、次のように動作を確認してください。 ① Page1Servlet へ接続してください。認証を経た後、今度は次のように「ログアウト」 ページのリンクが表示されます。 ② このログアウトへのリンクをクリックすると、次のように「Lgout.jsp」へジャンプし ます。つまりログアウトします。 ③ 再び、Page1Servlet に接続しようとすると、ログイン画面が現れ、ユーザ認証が必要 なことを確認してください。 上の動作を確認したら「ログアウト処理を確認しました。」と記述して提出してください。
【応用課題
13-F】
-認証情報の取得
例えばユーザ認証画面で、ユーザ名として「S_Kaneda」を入力した場合、接続先の Page1Servlet で次のようにユーザ名が表示されるように改良してみましょう。次ページの 様にPage1Servlet.java および Page2Servlet.java を修正してください。下線部が修正箇所 です。199 package auth;
・・・
public class Page1Servlet extends HttpServlet{ public void doGet(HttpServletRequest request,
HttpServletResponse response) throws ServletException, IOException {
request.setCharacterEncoding("Windows-31J"); response.setContentType("text/html;charset=Windows-31J"); PrintWriter out=response.getWriter(); String userID=request.getRemoteUser(); out.println("<html>"); out.println("<head>"); out.println("<title>ユーザー認証のテスト</title>"); out.println("</head>"); out.println("<body>"); out.println("<p>ようこそ"+userID+"さん、ページ 1 へ</p>"); ・・・ } } package auth; ・・・
public class Page2Servlet extends HttpServlet{ public void doGet(HttpServletRequest request,
HttpServletResponse response) throws ServletException, IOException {
request.setCharacterEncoding("Windows-31J"); response.setContentType("text/html;charset=Windows-31J"); PrintWriter out=response.getWriter(); String userID=request.getRemoteUser(); out.println("<html>"); out.println("<head>"); out.println("<title>ユーザー認証のテスト</title>"); out.println("</head>"); out.println("<body>"); out.println("<p>ようこそ"+userID+"さん、ページ 2 へ</p>"); ・・・ } } <Page1Servlet.java> <Page2Servlet.java> getRemoteUser()メソッドでユーザ名を取 得できる
200 作成したら Page1Servlet に接続し、ログイン画面で入力したユーザ名が p.198 のように 表示されることを確認してください。確認したら、「request.GetRemoteUser()によっ てログイン画面で入力したユーザ名を取得できることを確認しました。」と記述して提出し てください。
【応用課題
13-G】
-認証情報の取得(改良)
【応用課題 13-F】を改良して、今度は認証後に、入力したユーザ名の氏名が表示されるよ うに改良してみましょう。どのようにすれば良いか分かるでしょうか? 13-3 節で作成したテーブル「user_table」には Name というカラム(列)があり、そこ にユーザの氏名が入っていました。そこで、認証後にこのテーブルを検索して該当するユ ーザ名のレコード(行)の Name カラムの値を取り出せば良いことが分かります。これを プログラミングしましょう。次の手順にしたがって作成してください。 ① サーブレットからデータベースへ接続するので、 11-2 節で作成したのと同様な 「DBManager.java」を作成し、次ページのように記述します(DBManager.java をコ ピーして修正しても結構です)。 ② 次に、p.202 のように「Page1Servlet.java」を修正します。プログラムの形式は 12-1 節と同様です。また、「Page2Servlet.java」についても同様に修正して下さい。201 作成後、動作を確認して下さい。2 名のユーザ(金田正太郎、伊達直人)について、p.200 のようにユーザ名に対応する氏名が表示されることを確認したら、「user_table からユーザ 名に対応する氏名を取り出しそれを表示する事ができました。」と記述して提出してくださ い。 package auth; import java.sql.Connection; import java.sql.DriverManager;
public class DBManager {
public static Connection getConnection() { try{ Class.forName("com.mysql.jdbc.Driver"); Connection con=DriverManager.getConnection( "jdbc:mysql://localhost/auth","root","*******"); return con; } catch (Exception e) {
throw new IllegalStateException(e); } } } <DBManager.java> ユーザ名 パスワード MySQL に接続する際のユーザ名とパスワ ードは、各自の値を用いる事。
202 package auth; ・・・ import java.sql.Connection; import java.sql.ResultSet; import java.sql.SQLException; import java.sql.Statement;
public class Page1Servlet extends HttpServlet{ public void doGet(HttpServletRequest request,
HttpServletResponse response) throws ServletException, IOException {
request.setCharacterEncoding("Windows-31J");
response.setContentType("text/html;charset=Windows-31J"); PrintWriter out=response.getWriter();
String userID=request.getRemoteUser();
String sql="select * from user_table where user='"+userID+"'"; Connection con=null; Statement smt=null; try{ con=DBManager.getConnection(); smt=con.createStatement(); ResultSet rs=smt.executeQuery(sql); rs.next(); String Name=rs.getString("Name"); out.println("<html>"); out.println("<head>"); out.println("<title>ユーザー認証のテスト</title>"); out.println("</head>"); out.println("<body>"); out.println("<p>ようこそ"+Name+"さん、ページ 1 へ</p>"); ・・・ out.println("</html>"); } catch(SQLException e) {
throw new ServletException(e); } finally { if(smt!=null) {try{smt.close();} catch(SQLException ignore) {} } if(con!=null) {try{con.close();} catch(SQLException ignore) {} } } } } <Page1Servlet.java>