なんか作ってみよう (2) - ログイン・ログアウトの処理
ということで前回の続きです。
前回はログイン状態を管理するUserオブジェクトとUserManagerオブジェクトの概要を決め、UserManagerオブジェクトを実装するクラスのfindUserメソッドを作りました。
findUserメソッドはユーザ名、パスワードを受け取って、データベースを検索し、ユーザ名、パスワードが一致するユーザが登録されていればUserオブジェクトを返します。
Userインターフェースを実装するクラスはUserManagerインタフェースを実装するクラスのインナークラスとして実装することにしましたので、こんな感じになっています。
public final class UserManagerMySQL implements UserManager {
// データベースアクセスに必要な文字列定数
private static final String JDBC_DRIVER = "com.mysql.cj.jdbc.Driver";private static final String JDBC_URL = "jdbc:mysql:/localhost::3306/sample_db";
private static final String JDBC_USER = "sampleUser";
private static final String JDBC_PASSWORD = "SU_pass";
public User findUser(String username, String password) {
// 途中は省略します
user = new UserMySQL(username);
// 残りも省略します
}
public final class UserMySQL implements User {
// この中身は以下で考えていきます。
}
}
さて、Userインタフェースを実装するUserMySQLのコンストラクタにはユーザ名しか渡していません。
findUserメソッド中でパスワードが正しいことは検証済みですので、以降の処理では必要ないと判断し、渡さないこととしました。
findUserメソッドが終了した時点で(これらのオブジェクトでは)パスワード文字列を持たないので、安全になっているのではないかと思います。もちろんデータベースを掘り返されたアウトなので、そちらの対策も必要ですけど。
それではUserインタフェースを実装するUserMySQLクラスについて考えます。
public class UserMySQL implements User {
private String username;
private boolean loggedin;public UserMySQL(String username) {
this.username = username;
loggedin = false;
}
public String getUsername() {
return username;
}
public boolean getLoggedin() {
return loggedin;
}
// 以下略
}
Userインタフェースはユーザ名usernameと排他的ログイン状態loggedinを取得するゲッターメソッドが定義されていますので、これらをプライベートのオブジェクト変数として宣言します。
次にコンストラクタです。
findUserメソッドからはUserMySQL(String username)の形式で呼ばれるので、オブジェクト変数usernameには引数で与えられてユーザ名を設定し、初期状態ではUserオブジェクトは排他的ログイン状態にはないのでloggedinにはfalseを設定します。
usernameとloggedinのゲッターメソッドはオブジェクト変数の値を返すだけです。
ということでUser.login()メソッドをの実装を考えます。
このメソッドはユーザ名でデータベースを引いて、ログイン済みかどうかを検査します。
ユーザがすでに排他的ログイン状態でログイン済みではない場合にはデータベースを更新してログイン済みとし、Userオブジェクトのログイン状態をtrueに設定したうえでtrueを返します。
ユーザがすでに排他的ログイン状態でログイン済みであった場合にはfalseを返して、排他的ログイン状態になれなかったことを示します。
さて、このメソッドはデータベースの検索と更新が必要なため、同じユーザへのログインが同時に発生した場合には同時に実行されたログインプロセスすべてが排他的ログイン状態に移行できると判断できて、複数のアクセスが同時に排他的ログイン状態になってしまう可能性があります。
こういった競合を避けるため、今回はMySQLの行ロックの機能を使用して、アップデートが終わるまでは同じユーザのユーザ情報アクセスは待たされる構成とします(もう少し細かいアクセス制御があるようですけど)。
MySQLをJDBCから使用する場合、Connectionを取得した時点ではinsertあるいはupdateを実行した時点でデータベースの変更がされるモードとなっていて、このモードでは行ロックをかけることができません。そのためsetAutoCommit(false)を呼んでモードを変更する必要があります。
次に対象となる行を検索するselect文にfor update句を追加することで検索された行をロックします。
ロックの解除はcommitにより行われるので、行の変更の有無にかかわらずcommitを実行する必要があります。
実際のコードは以下のようになります。
public boolean login() {
Connection conn = null;
PreparedStatement pstmt = null;try {
Class.forName(JDBC_DRIVER);
conn = DriverManager.getConnection(JDBC_URL, JDBC_USER, JDBC_PASSWORD);
conn.setAutoCommit(false); // 行ロックを有効にする
pstmt = conn.prepareString("SELECT * FROM user" +
" WHERE username=? FOR UPDATE"); // for update句を追加して、行ロックをかけています
pstmt.setString(1, username); // usernameはユーザ名を保持するクラス変数です
ResultSet rset = pstmt.executeQuery();
if (rset.next()) { // 通常は必ずtrueですが、念のため検査しています
int loggedin = rset.getInt("loggedin");
if (loggedin == 0) {
this.loggedin = true; // 排他的ログイン状態になったことを示します
pstmt.close(); // PreparedStatementの使用しているリソースを破棄します
pstmt = conn.prepareStatement("UPDATE user SET loggedin=1" +
" WHERE username=?");
pstmt.setString(1, username);
pstmt.executeUpdate(); // updateを実行します
}
}
conn.commit(); // 変更の有無にかかわらずコミットを行って行ロックを解除します
} catch (Exception ex) {
System.out.println("login: " + ex.toString()); // ほんとはもっとちゃんと例外処理をしないと
try {
if (conn != null) conn.rollback(); // 行ロックを解除するため、可能であればロールバックします
} catch (SQLException se) {}
} finally {
try {
if (pstmt != null) pstmt.close();
if (conn != null) conn.close();
} catch (SQLException se) {}
}
return loggedin;
}
catch節以降がちょっと長くなって読みにくいのですが、まずは前半部分。
データベース接続に使用するConnectionとPreparedStatementは例外が発生した場合にも適切にクローズできるようtry節の外側で宣言しています。
Class.forNameとDriverManager.getConnectionはJDBCのお約束なのでそのまま書いています。データベースアクセスに必要な定数はアウタークラスで定義したスタティッククラス変数がそのまま使えるのでそのまま使っています。
次のsetAutoCommit(false)が行ロックを使用するための儀式で、autoCommitにfalseを設定すると明示的にコミットを行わないとデータベースに反映されないモードに変わります。
次のprepareStatementでSELECT文を定義し、setStringで1番目の「?」にオブジェクト変数として記録されているユーザ名を設定しています。前回も書きましたが、ユーザ名に不適切な文字が含まれていてもsetStringが良きに計らってくれるので、StatementではなくPreparedStatementを使用しています。
SELECT文の最後にfor update句を入れることでユーザ名が一致した行にロックがかかるようになっています。
executeQueryによって取得したResultSetには検索結果が保存されています。ResultSetには必ず1レコードが記録されているはずですが、念のためnext()の結果がtrueであることを確認しています(findUserで検証を行った直後にユーザが削除されている可能性が否定できないので)。そして(ユーザ登録時点でユーザ名の重複を排除しているはずなので)最初の結果だけを確認すればよいため、whileではなくifで検査しています。
データベースのloggedinカラムには排他的ログイン状態が記録されていて、0以外が排他的ログイン状態と決めているので、getIntで値を取得し、値が0であれば排他的ログイン状態への移行を行います。
まず、オブジェクト変数のloggedinをtrue (排他的ログイン状態)に設定します。続いて検索に用いたPreparedStatementをクローズして更新用のSQL文を設定しています。
ここでPreparedStatementをクローズするのは直前のデータベースアクセスで使用されたリソースを破棄してリソースリークを回避するためです。
ここで注意しなければいけないのは、PreparedStatement (およびStatement)のクローズではResultSetも同時にクローズされてしまうという点です。今回はif文で1回検査するだけですので問題ありませんが、while文ですべての結果にアクセスする場合には更新用のStatementを別途用意する必要があります。
データベースの更新が終了したらcommitを実行してデータベースに反映するのですが、先に書いたようにcommitは行ロックの解除も同時に行うので、データベース更新の直後ではなく大外のif文の外側、executeQueryを実行したブロックで行うことでデータベースを更新しなかった場合でもcommitするようにしています。
さて、例外処理です。
例外が発生した場合、Connectionが有効であればrolbackを呼び出してデータベース更新のキャンセルと行ロックの解除を行っています。
そのあと、finally節でPreparedStatementとConnectionのクローズを実行することで例外が発生した場合でもこれらがリソースを破棄するようにしています。
最後にオブジェクト変数のloggedinを返すことで排他的ログイン状態に移行できたかを返しています。
ログアウトを最後に考えます。
logoutメソッドは排他的ログイン状態を解消するメソッドで以下のようなコードになります。
public void logout() {
if (loggedin) {
Connection conn = null;PreparedStatement pstmt = null;
try {
Class.forName(JDBC_DRIVER);
conn = DriverManager.getConnection(JDBC_URL, JDBC_USER, JDBC_PASSWORD);
pstmt = conn.prepareStatement("UPDATE user SET loggedin=0" +
" WHERE username=?");
pstmt.setString(1, username);
pstmt.executeUpdate();
loggedin = false;
} catch (Exception ex) {
System.out.println("logout: " + ex.toString()); // 例外処理はもう少し真面目に
} finally {
try {
if (pstmt != null) pstmt.close();
if (conn != null) conn.close();
} catch (SQLException se) {}
}
}
}
当然ですが排他的ログイン状態にあるUserオブジェクトのみがログアウトできるので、最初に排他的ログイン状態の検査を行っています。また、データベースのloggedinを0に戻す操作は無条件に行うことができるので、行ロックは特に行っていません。
また、ログアウトは排他的ログイン状態から抜けるだけでUserオブジェクトは制限されたログイン状態で引き続き利用できる仕様としています。
以上でデータベースを使用したログイン・ログアウト管理は大体できました。
次はWebアプリケーションからのアクセスについて考えていこうと思っています。
0コメント