Apache Shiro セキュリティフレームワーク入門:認証アーキテクチャと Realm の構成

セキュリティマネージャーの設定

Apache Shiro では、INI 形式の設定ファイルを使用して SecurityManager を構築することが可能です。INI は可読性が高く、セットアップが容易であるため、多くのアプリケーションで採用されています。設定ファイルはファイルシステムやクラスパス、URL から読み込むことができ、それぞれ file:, classpath:, url: というプレフィックスで指定します。

ここでは、resources ディレクトリ下にある設定ファイルをクラスパスから読み取る例を示します。INI 設定に基づいて IniSecurityManagerFactory をインスタンス化し、生成された SecurityManagerSecurityUtils に登録します。

import org.apache.shiro.SecurityUtils;
import org.apache.shiro.util.Factory;
import org.apache.shiro.mgt.SecurityManager;
import org.apache.shiro.config.IniSecurityManagerFactory;

// ...
public class ConfigExample {
    public static void main(String[] args) {
        Factory factory = new IniSecurityManagerFactory("classpath:shiro.ini");
        SecurityManager securityManager = factory.getInstance();
        SecurityUtils.setSecurityManager(securityManager);
    }
}

トークンの作成と認証処理

ユーザーの認証情報を収集したあとは、そのデータを UsernamePasswordToken オブジェクトに格納します。このトークンはユーザー名とパスワードを束ねてシステムに渡すための一般的な方法です。

  • UsernamePasswordToken: ユーザー名とパスワードを組み合わせた最も一般的な認証用トークンです。Web フォーム、HTTP ヘッダー、コマンドラインなど、様々な入力ソースからのデータ受け付けに対応しています。
  • setRememberMe(): ユーザーのセッション維持機能を有効にするメソッドです。
UsernamePasswordToken userToken = new UsernamePasswordToken("testUser", "SecurePass123");
userToken.setRememberMe(true);

AdminToken adminToken = new AdminToken("adminSystem", "RootAccess");
adminToken.setRememberMe(false);

収集されたトークンを元に、実際の認証処理を実行するために Subject オブジェクトを利用します。以下のコードは認証のライフサイクルを示しています。

Subject subject = SecurityUtils.getSubject();
try {
    // ログイン試行
    subject.login(userToken);
    System.out.println("ユーザー認証成功");
    
    // 同様に他のユーザーも試行可能
    subject.logout(); 
    subject.login(adminToken);
    System.out.println("管理者認証成功");
} catch (AuthenticationException e) {
    System.err.println("認証エラーが発生しました:" + e.getMessage());
} finally {
    subject.logout();
}

各ステップの詳細解説

  1. Subject の取得: SecurityUtils.getSubject() は、現在のスレッドに関連付けられている「主体」を取得します。Shiro においては、各スレッドごとに一意な Subject インスタンスが存在し、これにより匿名状態から認証済み状態へ遷移する管理が行われます。
  2. Login メソッドの呼び出し: Subject#login(token) を実行することで、トークンに含まれる ID とパスワードを用いて認証アルゴリズムが起動します。成功すれば、セッション情報が確立され、それ以降のアクセス権限付与が可能になります。
  3. Logout メソッドの呼び出し: アクション完了後に subject.logout() を呼び出すことで、セッションを破棄し、関連付けられた身份信息をクリアします。

複数の Realm を持つ場合の認証戦略

システムに複数の Realm が登録されている場合、どの順序で問い合わせを行い、どれをもって成功とするかは問題となります。これらを管理するのが AuthenticationStrategy です。

Shiro の認証プロセスは概ね以下の 5 段階で進行します:

  1. アプリケーション側から AuthenticationToken を発行する。

  2. SubjectSecurityManager#login を経由して認可を試みる。

  3. ModularRealmAuthenticator が内部的にAuthenticateタスクを受け付ける。

  4. 設定された AuthenticationStrategy に従って、複数の Realm を順次または並列でクエリを実行する。

  5. Realm#supports() でトークンタイプが対応しているか確認し、該当する Realm から getAuthenticationInfo を呼んで結果を統合する。

内部ロジックの一例として、マルチリーマール認証を行うメソッドの流れを確認します。

protected AuthenticationInfo doMultiRealmAuthentication(Collection realms, AuthenticationToken token) {
    AuthenticationStrategy strategy = getAuthenticationStrategy();
    AuthenticationInfo aggregate = strategy.beforeAllAttempts(realms, token);

    for (Realm realm : realms) {
        try {
            aggregate = strategy.beforeAttempt(realm, token, aggregate);
        } catch (ShortCircuitIterationException e) {
            break;
        }

        if (realm.supports(token)) {
            Throwable t = null;
            try {
                AuthenticationInfo info = realm.getAuthenticationInfo(token);
                aggregate = strategy.afterAttempt(realm, token, info, aggregate, t);
            } catch (Throwable throwable) {
                t = throwable;
            }
        }
    }
    return strategy.afterAllAttempts(token, aggregate);
}

主要な認証戦略の実装

AuthenticationStrategy は以下のような条件で判定を行います:

  • AtLeastOneSuccessfulStrategy: 少なくとも一つの Realm で認証が成功すれば全体を成功とみなす。これがデフォルト設定です。
  • FirstSuccessfulStrategy: 最初に成功した Realm の結果のみを採用し、以降の Realm は無視する。
  • AllSuccessfulStrategy: 全 Realm が正常に認証を行う必要がある。一つでも失敗すると全体は失敗となる。

これらの戦略を変更するには、shiro.iniauthStrategy プロパティを設定します。

[main]
authStrategy = org.apache.shiro.authc.pam.FirstSuccessfulStrategy
securityManager.authenticator.authenticationStrategy = $authStrategy

Realm の実行順序

認証時の Realm 呼び出し順序は、INI ファイル内での定義順序(暗黙的)または realms 属性での明示的なリスト指定(明示的)によって制御できます。

  • 明示的順序設定:
myRealm1 = com.example.Realm1
myRealm2 = com.example.Realm2

securityManager.realms = $myRealm2, $myRealm1

上記のように指定した場合、まず myRealm2 が処理され、次に myRealm1 が処理されます。

認証トークンの適合性と検証

各 Realm は supports(AuthenticationToken) メソッドを通じて、自身で処理可能なトークンタイプを宣言します。もしトークン型が一致しない場合、UnsupportedTokenException が発生し、該当 Realm はスキップされます。

これを修正する場合は、以下のいずれかの手法をとります:

  1. supports メソッドをオーバーライドし、特定のトークン判定ロジックを加える。
  2. getAuthenticationTokenClass() メソッドをオーバーライドし、対象とするトークンクラスのクラスオブジェクトを返却する。
@Override
public Class getAuthenticationTokenClass() {
    return MyCustomToken.class;
}

クレデンシャルマッチャーとハッシュ処理

認証成功判定のためには、ユーザーが入力したパスワードとデータベースに登録済みの情報を比較する必要があります。単純な平文比較ではなく、安全のためハッシュ処理を行うことが推奨されます。

SimpleCredentialsMatcher

平文と比較する場合に使用される基本的なマッチャーです。入力がバイト配列に変換可能であれば適用可能です。

HashedCredentialsMatcher

より高いセキュリティを提供するために利用されます。ユーザーの入力値をハッシュ化し、データベース上の保存済みハッシュ値と照合します。

  1. ログイン時に提供されたパスワードに対してハッシュ演算を行う。
  2. ハッシュ化後の値と、認証情報 (AuthenticationInfo) に格納された保存値を比較する。

この際、サルト(Salt)を併用し、複数回の反復ハッシュ処理を行うことで、ランダム性の確保と計算コストの増加によりブルートフォース攻撃への耐性を高めます。

カスタム Realm 実装サンプル

ここでは、SHA-256 アルゴリズムを用いた加塩ハッシュ処理を行うカスタム Realm の実装例を示します。これは AuthenticatingRealm を継承して作成されます。

import org.apache.shiro.authc.*;
import org.apache.shiro.crypto.SecureRandomNumberGenerator;
import org.apache.shiro.crypto.hash.SimpleHash;
import org.apache.shiro.realm.AuthenticatingRealm;
import org.apache.shiro.util.ByteSource;

import java.util.HashMap;
import java.util.Map;

public class SecureAuthRealm extends AuthenticatingRealm {
    
    private Map accountStore = new HashMap<>();

    @Override
    protected AuthenticationInfo doGetAuthenticationInfo(AuthenticationToken token) throws AuthenticationException {
        String principal = (String) token.getPrincipal();
        UserAccount user = accountStore.get(principal);

        if (user == null) {
            throw new UnknownAccountException("指定されたアカウントが見つかりません");
        }

        // 認証情報の整合性を確認後、キャッシュ等の処理を行う
        return new SimpleAuthenticationInfo(
            principal, 
            user.getPasswordHash(), 
            ByteSource.Util.bytes(user.getSalt())
        );
    }
    
    // アカウント登録ロジック(参考)
    public void registerUser(String username, String plainPassword) {
        SecureRandomNumberGenerator rng = new SecureRandomNumberGenerator();
        String salt = rng.nextBytes().toHex();
        
        int iterations = 1024;
        String hashAlgorithmName = "sha256";
        String passwordHash = new SimpleHash(hashAlgorithmName, plainPassword, ByteSource.Util.bytes(salt), iterations).toHex();
        
        accountStore.put(username, new UserAccount(username, salt, passwordHash));
    }
}

上記のロジックに合わせて shiro.ini を設定します。

[main]
credentialsMatcher = org.apache.shiro.authc.credential.HashedCredentialsMatcher
credentialsMatcher.hashAlgorithmName = sha256
credentialsMatcher.hashIterations = 1024
credentialsMatcher.storedCredentialsHexEncoded = true

mySecureRealm = com.example.SecureAuthRealm
mySecureRealm.credentialsMatcher = $credentialsMatcher

securityManager.realms = $mySecureRealm

タグ: apache-shiro java-security authentication realm-implementation credentials-matcher

7月22日 20:11 投稿