カスタムセキュリティの実装

前のセクションで説明したデフォルトの JRun セキュリティメカニズムは、サイトによっては適用できない場合があります。たとえば、JDBC、LDAP、および Windows ログインモジュールの入力パラメータは、既存のセキュリティシステムでは機能しない場合があります。

このような場合、1 つ以上のカスタマイズされたログインモジュールを作成することによって、JRun セキュリティを必要に応じてカスタマイズして拡張できます。

ログインモジュール

JRun セキュリティをカスタマイズする前に、ログインモジュールが JRun でどのように使用されるかについて理解しておくことが必要です。デフォルトの JAAS 実装では、ログインモジュールは認証のみに使用されます。しかし、JRun では次に示すように認証および承認の両方でログインモジュールを使用します。

実行する処理は、auth.config ファイルで指定されている mode 属性の USER または ROLE を渡して指定します。この属性の処理については、サンプルコードを参照してください。

次のサンプルコードは、ログインモジュールがユーザー名およびパスワードにアクセスする方法を示しています。

...
NameCallback n = new NameCallback("User Name - ", "Guest");
PasswordCallback p = new PasswordCallback("Password - ", false);
Callback[] callbacks = {n, p};

try {
  cbHandler.handle(callbacks);
  username = n.getName().trim();
  char[] tmpPassword = p.getPassword();
...

次のサンプルコードは、ユーザー名および許可されるロールにアクセスします。

...
boolean userRoleFound = false;RolesCallback rcb = new RolesCallback();Callback[] callbacks = {rcb};try {  // cbHandler は CallbackHandler オブジェクトです。  cbHandler.handle(callbacks);  Principal p = rcb.getPrincipal();  username = p.getName().trim();
  Collection rolesToCheck = rcb.getRoles();
...

JDBC を使用するカスタマイズされたログインモジュールの定義

多くのサイトでは、デフォルトの JRun ログインモジュールを使用して認証および承認を行うことができます。ただし、ユーザーのサイトでデフォルトの JRun の JDBC ログインモジュールを使用できない場合は、カスタマイズされたログインモジュールを作成して、ユーザーのサイト固有のリレーショナルデータベースのユーザーおよびロールにアクセスできます。

デフォルトの JDBC ログインモジュールの詳細については、「JDBC ベースのセキュリティ実装の使用」を参照してください。

カスタマイズされた JDBC ログインモジュールを使用するには、次の手順を実行します。

次の例は、カスタマイズされた JDBC ログインモジュールを示しています。

...
// 必要なインポート
import javax.security.auth.spi.LoginModule;
import javax.security.auth.Subject;
import javax.security.auth.login.LoginException;
import javax.security.auth.login.FailedLoginException;
import javax.security.auth.callback.*;
import java.security.Principal;
// ログインモジュール特有の機能に使用されるインポートです。
// ログインモジュール特有 (JDBC) の機能に使用されるインポートです。
import javax.naming.InitialContext;
import javax.naming.NamingException;
import javax.sql.DataSource;
import java.util.Map;
import java.util.Collection;
import java.util.Iterator;
import java.util.ArrayList;
import java.sql.Connection;
import java.sql.PreparedStatement;
import java.sql.ResultSet;
import java.sql.SQLException;

// JRun 特有のインポート
import jrun.security.SimplePrincipal;
import jrun.security.RolesCallback;
// リソースバンドル処理に使用されます。
import jrunx.util.RB;
// ロギングに使用されます。
import jrunx.kernel.JRun;

public class SampleJDBCLoginModule implements LoginModule {
    private Subject subject;
    private CallbackHandler cbHandler;
    private Map sharedState;
    private Map options;

    /**
     * mode = "USER" の
     * SQL ステートメントは、特定のユーザー名のパスワードを照会します。
     * 例:"select PasswordColumn from UsersTable where UserName=?"
     *
     * mode = "ROLE" の
     * SQL ステートメントは、特定のユーザー名のロールを照会します。
     * 例:"select RoleColumn from RolesTable where UserName=?"
     */
    private String queryString=null;

    /**
     * ユーザーは、ユーザー名と、ユーザー名を入力として扱うパスワードを
     * 取り出す SQL ステートメントを指定するか、テーブル名、ユーザー名、
     * 列、およびパスワード列を指定できます。
     * auth.config でパラメータとして指定されます。
     */
    private String userColumn=null;
    private String passwordColumn=null;
    private String tableName=null;
    private String roleColumn=null;
    /**
     * ユーザーセキュリティ情報が含まれるデータベースにアクセスするユーザー ID とパスワードです。
     * auth.config でパラメータとして指定されます。
     */
    private String dbUserId=null;
    private String dbPassword=null;

    private String username=null;    private char[] password=null;
    /**
     * データベースにアクセスするために JNDI 内で検索する JDBC データソースです。
     * これは、JRun データソースとなり得ます。
     * auth.config でパラメータとして指定されます。
     */
    String dsJndiName=null;

    // 認証ステータス
    private boolean succeeded = false;
    private boolean commitSucceeded = false;

    //認証された principal オブジェクト
    private SimplePrincipal userPrincipal;

    // ログインモードを "USER" または "ROLE" にして、ユーザー証明を認証するか、
    // ユーザーロールを承認するかを指定します。 
    protected String loginMode = "USER";

    /**
     * <p>JRun セキュリティマネージャは、認証または
     * 承認リクエストの開始時に initialize メソッドを
     * 呼び出します。
     */
    public void initialize(Subject subj, CallbackHandler cbh, Map sharedState, 
Map options) {
        this.subject = subj;
        this.cbHandler = cbh;
        this.sharedState = sharedState;
        this.options = options;

        loginMode = (String) options.get("mode");
        dbUserId = (String) options.get("databaseUserId");
        dbPassword = (String) options.get("databasePassword");
        dsJndiName = (String) options.get("dataSourceJNDIName");
        queryString = (String) options.get("queryString");
        tableName = (String) options.get("tableName");
        userColumn = (String) options.get("userNameColumn");

        if(loginMode.equals("USER") )
            // ユーザー認証に特有のパラメータ
            passwordColumn = (String) options.get("passwordColumn");
        else // ユーザーロール承認に特有のパラメータ
            roleColumn = (String) options.get("roleColumn");
        JRun.getLogger().logInfo("*** SampleJDBCLoginModule:initialize");
    }

    /**
     * <p>JRun セキュリティマネージャは、認証または承認リクエスト
     * に対して initialize メソッドを呼び出した後、login メソッドを
     * 呼び出します。
     */
    public boolean login() throws LoginException {

        if(cbHandler == null) {            throw new LoginException(RB.getString(SampleJDBCLoginModule.class,
"SampleJDBCLoginModule.noCallBackHandlerAvailable") );
        }

        JRun.getLogger().logInfo("*** SampleJDBCLoginModule:ログイン。モード:" + 
loginMode);
        if (loginMode.equals("ROLE") ) {
            // 承認のためにこのメソッドを呼び出します。
            return validateRole();
        }
        else {
           // デフォルト
           // 認証のためにこのメソッドを呼び出します。
           return loginUser();
        }
    }

    /**
     * <p> mode=USER のときに呼び出されます。
     */
    protected boolean loginUser() throws LoginException {
        NameCallback n = new NameCallback("User Name - ", "Guest");
        PasswordCallback p = new PasswordCallback("Password - ", false);
        Callback[] callbacks = {n, p};

        try {
            cbHandler.handle(callbacks);
            // 渡されたユーザー名 NameCallback を取得します。
            username = n.getName().trim();
            // デバッグの補助
            JRun.getLogger().logInfo("*** loginUser():ログインしているユーザー:" + 
username);
            // PasswordCallback から渡されたパスワードを取得します。
            char[] tmpPassword = p.getPassword();
            if( tmpPassword != null ) {
                password = new char[tmpPassword.length];
                System.arraycopy(tmpPassword, 0, password, 0, tmpPassword.length);
                p.clearPassword();
            }

        }
        catch(java.io.IOException e) {
            throw new LoginException(e.toString());
        }
        catch(UnsupportedCallbackException uce) {
            // 新規 LoginException("サポートされていないコールバック:" + 
uce.getCallback());
            throw new LoginException(RB.getString(SampleJDBCLoginModule.class, 
"SampleJDBCLoginModule.unsupportedCallBack"));
        }

        // コールバックからのパスワードとデータベースのパスワードを検証します。
        String passwordFromDB = getUserPassword().trim();
        JRun.getLogger().logInfo("*** loginUser():データベースのパスワード :" + 
passwordFromDB);
        succeeded = true;
        char[] pw = passwordFromDB.toCharArray() ;
        if(password.length == pw.length) {
            for(int i = 0; i < pw.length; i++) {
                if (password[i] != pw[i]) {
                    succeeded = false;
                    break;
                } // 内側 if を終了
            } // for を終了
        }
        else
            succeeded = false; // 同じ長さではない
        return succeeded;
    }

    /**
     * <p> mode=ROLE のときに呼び出されます。
     */
    protected boolean validateRole() throws LoginException {
        boolean userRoleFound = false;

        JRun.getLogger().logInfo("*** validateRole()");
        RolesCallback rcb = new RolesCallback();
        Callback[] callbacks = {rcb};
        try {
            cbHandler.handle(callbacks);
            // RolesCallback からユーザー名を取得します。
            Principal p = rcb.getPrincipal();
            username = p.getName().trim();

            // 承認されたロールを RolesCallback から取得します。
            Collection rolesToCheck = rcb.getRoles();
            // 割り当てられているロールをデータベースから取得します。
            ArrayList rolesFromDatabase = getUserRoles();

            //rolesToCheck を繰り返し、rolesFromDataBase からユーザー名を持つものを検索し
ます。
            Iterator i = rolesToCheck.iterator();
            while(i.hasNext() && !userRoleFound) {
                String thisRoleName = (String) i.next();
                int numberOfRolesFromDB = rolesFromDatabase.size();
                for(int index = 0; index < numberOfRolesFromDB; index++) {
                    String dbRoleName = (String) rolesFromDatabase.get(index);
                    if(thisRoleName.equals(dbRoleName.trim()) ) {
                        succeeded = true;
                        userRoleFound = true;
                    } // 内側 if を終了
                } // for を終了
            } // while を終了
        }
        catch(java.io.IOException e) {
            throw new LoginException(e.toString());
        }
        catch(UnsupportedCallbackException uce) {
            throw new LoginException(RB.getString(SampleJDBCLoginModule.class, 
"SampleJDBCLoginModule.unsupportedCallBack") );
        }
        catch(Exception e) {
            JRun.getLogger().logError(RB.getString(SampleJDBCLoginModule.class, 
"SampleJDBCLoginModule.errorValidatingRole"), e);
        }
        return userRoleFound;
    }

    /* 
     * <p>データベースを呼び出して、ユーザー名のパスワードを取り出します。
     */
    protected String getUserPassword() throws LoginException {
        Connection conn = null;
        PreparedStatement ps = null;
        String passwordFromResult=null;

        try {
            InitialContext ctx = new InitialContext();
            DataSource ds = (DataSource) ctx.lookup(dsJndiName);
            if(dbUserId != null)
                ds.getConnection(dbUserId, dbPassword);
            else
                conn = ds.getConnection();

             // SQL クエリ文字列か、テーブル名、パスワード、およびユーザー名の列を提供できます。
             if(queryString != null) {
                ps = conn.prepareStatement(queryString);
                ps.setString(1, username);
             }
             else {
                 queryString = "select " + passwordColumn + " from " +  tableName 
+ " where " + userColumn + "=" + username;
                 ps = conn.prepareStatement(queryString);
             }
             ResultSet rs = ps.executeQuery();
             if( rs.next() == false )
                throw new 
FailedLoginException(RB.getString(SampleJDBCLoginModule.class,
                   "SampleJDBCLoginModule.userNameNotFound") );

             passwordFromResult = rs.getString(1);
             rs.close();
        }
        catch(NamingException ex) {
            throw new LoginException(ex.toString(true));
        }
        catch(SQLException ex) {
             throw new LoginException(ex.toString());
        }
        finally {
            connCleanup(ps, conn);
        }
        return passwordFromResult;
   }

    /**
     * ユーザー名に割り当てられてたロールを取り出すためにデータベースを呼び出します。
     */
    protected ArrayList getUserRoles() throws LoginException {
        Connection conn = null;
        PreparedStatement ps = null;
        ArrayList rolesFromDB= new ArrayList();
        try {
            InitialContext ctx = new InitialContext();
            DataSource ds = (DataSource) ctx.lookup(dsJndiName);
            if(dbUserId != null)
                ds.getConnection(dbUserId, dbPassword);
            else
                conn = ds.getConnection();

            // auth.config ファイルでは、SQL クエリ文字列、または
            // テーブル名、ロール名、およびユーザー名の列を提供できます。
            if(queryString != null) {
                ps = conn.prepareStatement(queryString);
                ps.setString(1, username);
            }
            else {
                 queryString = "select " + roleColumn + " from " +  tableName + " 
where " + userColumn + "=" + username;
                 ps = conn.prepareStatement(queryString);
            }
            ResultSet results = ps.executeQuery();
            String dbRolename=null;
            if(results.getFetchSize() > 0) {
                while(results.next() ) {
                    dbRolename = results.getString(1);
                    rolesFromDB.add(dbRolename);
                } // while を終了
            } // if を終了
            else
                throw new 
FailedLoginException(RB.getString(SampleJDBCLoginModule.class,
                     "SampleJDBCLoginModule.userNameNotFound") );

            results.close();        } // try を終了
        catch(NamingException ex) {
            throw new LoginException(ex.toString(true));
        }
        catch(SQLException ex) {
             throw new LoginException(ex.toString());
        }
        finally {
            connCleanup(ps, conn);
        }
        return rolesFromDB;
    }

    protected void connCleanup(PreparedStatement ps, Connection conn) {
        if( ps != null ) {
            try {
               ps.close();
            }
            catch(SQLException e)
            {}
         }
         if( conn != null ) {
            try {
               conn.close();
            }
            catch (SQLException ex)
            {}
         }
    }

    /**
     * <p>認証および承認が成功したかどうかを示します。
     */
    public boolean commit() throws LoginException {
        if (succeeded == false) {
            return false;
        }
        else {
            // Principal (認証済みの ID) を
            // Subject に追加します。

            // 認証されるユーザーは SimplePrincipal とします。
            userPrincipal = new SimplePrincipal(username);
            if (!subject.getPrincipals().contains(userPrincipal))
            subject.getPrincipals().add(userPrincipal);

            // いずれの場合も、ステートをクリーンアップします。            username = null;            // for (int i = 0; i < password.length; i++)            //password[i] = ' ';            password = null;
            commitSucceeded = true;
            return true;
        }
    }

    /**
     * <p> このメソッドは LoginContext の
     * すべての認証が失敗した場合に呼び出されます。
     * (関連する REQUIRED、REQUISITE、SUFFICIENT、および OPTIONAL LoginModules
     * は成功していません。)
     *
     * <p> この LoginModule の認証が成功すると (<code>login</code> および
     * <code>commit</code> メソッドで保存されている プライベートステートを
     *取得することによって確認できます)、このメソッドははじめから
     * 保存されているすべてのステートをクリーンアップします。
     *
     * <p>
     *
     * abort が失敗した場合は例外 LoginException です。
     *
     * この LoginModule のログインまたはコミットが失敗した場合は false が返され、
     * それ以外は true が返されます。
     */
    public boolean abort() throws LoginException {
        if (succeeded == false) {
            return false;
        }
        else if (succeeded == true && commitSucceeded == false) {
            // ログインには成功しましたが、全体の認証には失敗しました。
            succeeded = false;
            username = null;
            if (password != null) {
            password = null;
            }
            userPrincipal = null;
        }
        else {
            // 全体の認証およびコミットは成功しましたが、
            // 他のユーザーのコミットには失敗しました。
            logout();
        }
        return true;
    }

    /**     * ユーザーをログアウトします。
     *
     * <p> このメソッドは、<code>SamplePrincipal</code> を削除します。
     * これは、<code>commit</code> メソッドによって追加されたものです。
     *
     * <p>
     *
     * logout に失敗した場合は例外 LoginException です。
     *
     * この <code>LoginModule</code> は無視できないので
     * すべてのケースで true が返されます。
     */
    public boolean logout() throws LoginException {
        subject.getPrincipals().remove(userPrincipal);
        username = null;
        if (password != null) {
            for (int i = 0; i < password.length; i++)
            password[i] = ' ';
            password = null;
        }
        userPrincipal = null;
        return true;
    }
} // クラスを終了します。