なんか作ってみよう

ということで、勉強しているだけではつまらないので、何か作ってみることにしました。

といっても、まだノーアイデアなのでとりあえずログイン・ログアウトできるだけのページを作ってみます。

とりあえずの目標として、2段階のログイン、排他的なログインと制限付きの非排他的ログインができることを目指します。

ユーザ情報はデータベースを使用して、ユーザ名、パスワード、ログイン状態フラグを保存することにします。

ユーザ名とパスワードはとりあえず平文で。パスワードの平文が気持ち悪ければApache Commonsにlibcのcrypt (3)互換コードがあるようなのでそれを使えばいいのかなと思っています。

ログイン状態フラグは排他的ログインが既に行われているか否かを示します。ログイン状態フラグがtrueの場合には排他的ログインが既に行われているため、他の排他的ログインは拒絶されます。

これを実現するために、UserManagerおよびUserインタフェースを作成し、以下のメソッドを用意します。

UserManagerインタフェースのメソッド

User.findUser(String username, String password)

ユーザ名(username)とパスワード(password)に一致するユーザが存在する場合にはそのユーザを示すUserオブジェクトを返し、存在しない場合はnullを返します。

この時点では排他的ログイン状態になっていないので、返されたUserオブジェクトを使用して制限されたアクセスが可能です。

Userインタフェースのメソッド

boolean login()

排他的ログインを指示します。排他的ログインに成功すればtrueを返し、Userオブジェクトを排他的ログイン状態に移行させ、すでに排他的ログインが行われている場合にはUserオブジェクトを制限されたアクセス状態のまま維持します。

void logout()

ログアウトを指示します。Userオブジェクトが排他的ログイン状態にある場合、排他的ログイン状態を解消して、Userオブジェクトを制限されたアクセス状態に移行します。Userオブジェクトが制限されたアクセス状態の場合には制限されたアクセス状態が維持されます。

String getUsername()

Userオブジェクトのユーザ名を取得します。

boolean getLoggedin()

Userオブジェクトの排他的ログイン状態を取得します。制限されたアクセス状態で実行できない操作はUserオブジェクトから排他的ログイン状態を取得し、trueであることを確認する必要があります。

ところで、Userオブジェクトがインタフェースではなく、クラスであればfindUserはスタティックメソッドとして実装することが可能で、その方がわかりやすいと思います。

しかし、Userオブジェクトをクラスとして実装するとバックエンドにあるデータベースとアプリケーションが密結合してしまい、いろいろと齟齬が発生する可能性があるので、ここではインタフェースとして実装することとして、UserManagerオブジェクトを別に定義することとしました。

JDBCのようにUserManagerを汎用クラスとして用意し、リフレクションを使用してUserインタフェースを実装したクラスを検索するUserManagerに登録されたUserインタフェースを実装するクラスを検索するといった形式の実装もありですが、さすがにそこまでやるのは大変なので後々の課題とし、今回はUserManagerをバックエンドに依存したクラスとして用意することとしました。

※ ロードされているクラスを検索するリフレクションの機能が見つからなかったので、JDBCのDriverManagerについて調べたところ、Class.forNameでドライバクラスをロードする際に実行されるスタティック初期化メソッドでドライバクラスがDriverManagerに自身を登録しているらしいことがわかりましたので記述を訂正しました。

簡単なコードですが、以下にこれらのインタフェースのコードを示します。

UserManagerインタフェース

package com.example.sample;


public interface UserManager {
  public User findUser(String username, String Password);

}

Userインタフェース

package com.example.sample;


public interface Uesr {
  public boolean login();

  public void logout();

  public String getUsername();

  public String getLoggedin();

  public String toString();

}

toStringメソッドは別になくてもかまわないと思うのですが、とりあえずユーザ名とログイン状態を返すくらいで実装するのがいいかなと思っています。

ということで、インタフェースの実装を行っていきます。

実装にはデータベースとしてとりあえずMySQLを使用し、Userインタフェースを実装するクラスはUserManagerクラスのインナークラスとして実装することにします。

構成の大枠を示すとこんな感じです。

public final class UserManagerMySQL implements UserManager {
  // findUserを実装する
  public final class UserMySQL implements User {

    // login, logout, ...を実装する

  }

}

Userインタフェースを実装するクラスは、外からはUserManagerMySQL.UserMySQLという形で見えるのですが、利用者側ではUserオブジェクトとして扱うので、実クラスの名前が多少鬱陶しくても問題ないと考えます。

まず、データベースのアクセスに必要な定数をUserManagerMySQLに定義しておきます。インナークラスからもこれらの定数が見えるので、タイプミスによる不具合の軽減になると思います。

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";

さて、findUserはデータベースにアクセスしてユーザ名とパスワードの検証を行います。検証はユーザ名とパスワードの両方をキーとしてデータベースを検索し、結果が得られれば検証成功としています。

そして検証が成功したらUserMySQLクラスのインスタンスを生成して返しています。

public User findUser(String username, String password) {
  Connection conn = null;
  PreparedStatement pstmt = null;

  User user = null;

  try {

    Class.forName(JDBC_DRIVER);

    conn = DriverManager.getConnection(JDBC_URL, JDBC_USER, JDBC_PASSWORD);

    pstmt = conn.prepareStatement("SELECT * FROM user WHERE" + 

      " username=? AND password=?");

    pstmt.setString(1, username);

    pstmt.setString(2, password);

    ResultSet rset = pstmt.executeQuery();

    if (rset.next()) {

      user = new UserMySQL(username);

    }

  } catch (Exception ex) {

    System.out.println("findUser" + ex.toString());

  } finally {

    try {

      if (pstmt != null) {

        pstmt.close();

      }

      if (conn != null) {

        conn.close();

      }

    } catch (SQLException se) {

    }

  }

  return user;

}

上から順に説明していきます。

Connection conn = null;
PreparedStatement pstmt = null;
User user = null;

Connection (java.sql)とPreparedStatement (java.sql)はclose()を明示的に呼ばないとリソースリークが発生する可能性があるため、例外発生時にもclose()を呼べるようにtry節の外側で変数を定義して、finally節でclose()を呼ぶ構成としています。

また、戻り値userは検証に失敗した場合にnullを返す仕様なので、nullで初期化しておき、検証に成功した場合にオブジェクトを作成して返すつくりにしています。

次にtry節の中のデータベースアクセスです。

Class.forName(JDBC_DRIVER);
conn = DriverManager.getConnection(JDBC_URL, JDBC_USER, JDBC_PASSWORD);
pstmt = conn.prepareStatement("SELECT * FROM user WHERE" +

  " username=? AND password=?");

pstmt.setString(1, username);

pstmt.setString(2, password);

ResultSet rset = pstmt.executeQuery();

最初の2行、Class.forNameとDriverManager.getConnectionはJDBCのお約束です。

次のprepareStatementですが、これはSQL文に前処理を行ってデータベース処理の高速化を図るものです。ここでは検索高速化の効果はほとんどないのですが、次に説明するsetStringが、文字列中にSQL文で使用できない文字が含まれていた場合、適切な処理をしてくれるので、今回でいえばユーザ名やパスワードの文字列に不正な文字列が含まれている場合にも正しく処理することができます。

次の2文はPreparedStatementに設定されたSQL文のパラメータ置き換えのためのコードです。

pstmt.setString(1, username);
pstmt.setString(2, password);

prepareStatementに渡されたSQL文には2個の「?」が含まれています。これはprepareStatement呼び出し時には確定していない可変パラメータの位置を指定するものです。

setString(1, username)は1個目の「?」をusername引数の文字列に置き換えるということを示しています。この時、SQL文の文字列では使用できない文字は適切にエスケープされるので、SQLインジェクション対策になるそうです。もちろんこれだけ十分ではないと思いますが。

そしてexecuteQueryを呼び出すとパラメータ置き換えが行われたSQL文に基づいてデータベースの検索が行われ、その結果が戻り値として返されます。

ここでSQL文も説明しておきます。

「SELECT * FROM user WHERE username='ユーザ名' AND password='パスワード'」が今回の検索で使用されるSQL文になります (ユーザ名、パスワードはそれぞれ引数で渡された文字列です)。

「SELECT * FROM user」でデータベース上の「user」テーブルにあるすべての項目を行ごとに読み出すことを指定しています。

その後ろに「WHERE username='...' AND password='...'」が指定されているので、読みだす行の制約としてusername項目及びpassword項目の両方が指定の値でなければいけないことを指定しています。

今回、コードの見やすさを改善する目的でSQL文を2行に分けています。この時、行頭、もしくは行末に空白が入っていないと行末の文字列と行頭の文字列が連結して不正なSQL文になるので、文字列を分割して記述する際には行頭、もしくは行末に空白を入れる決めを行っておいたほうがいいかもしれません。

以下が検証結果の判定部になります。

if (rset.next()) {
  user = new UserMySQL(username);
}

一般論としてデータベースの検索結果は複数になる場合があります。このため、JDBCの検索メソッド(executeQuery)の戻り値ResultSetは複数の検索結果を順次取得できる仕様になっています。

ResultSetは検索結果列の先頭の一つ前を指した状態でexecuteQueryから返されます。そしてnext()が呼び出されるたびに検索結果列を指す位置を1個ずつ進めて、その位置に検索結果があればnext()の戻り値としてtrueを返します。

ここでは該当するユーザが一人でもいれば検証は終了なので (二人以上いないことは登録時点で保障されるべきで、検索で気にする事項ではありません)、「if」で最初の検索結果があるかどうかだけを判断しています。

ここではUserMySQLのインスタンス作成に際して、パスワードは渡していません。パスワードは検証において使用済みで、検証済みが保証されていれば再検証する必要はないので引数で与えられたパスワードは保存しないという判断としました。セキュリティ面でも必要のないものは持ち歩かないというのが正解ですから。

あとは例外関連の処理とPreparedStatementとConnectionのクローズです。

例外処理は超安易に作っていますが、最低でもログに残すくらいはやるべきだと思います。

だいぶ長くなってきたので、続きであるUserインタフェースの実装については後程。


0コメント

  • 1000 / 1000