Apache Shiro 認証・認可フレームワーク完全ガイド

1. アクセス権限管理とは

1.1 アクセス権限管理の概要

ユーザーが関与するシステムのほとんどでは、アクセス権限管理が必要です。これはシステムセキュリティの範疇に属し、ユーザーのシステムアクセスを制御し、セキュリティルールやセキュリティポリシーに従って、認可されたリソースのみにアクセスできるようにすることを指します。

アクセス権限管理は、ユーザー認証認可の2つの部分から構成され、これらをまとめて認証・認可と呼びます。アクセス制御が必要なリソースに対しては、ユーザーはまず認証を受け、認証に成功した後、そのリソースへのアクセス権限を持っている場合にのみアクセスが許可されます。

1.2 ユーザー認証とは

ユーザー認証とは、ユーザーが正当なユーザーかどうかを判断するプロセスです。最も一般的なシンプルな認証方法は、システムがユーザーが入力したユーザー名とパスワードを、システムに保存されているそのユーザーの情報と照合して、ユーザー身份が正しいかどうかを判断するものです。

ログインAPIを呼び出すと、CustomRealmdoGetAuthenticationInfo() メソッドで、ユーザー名を使用してデータベースからパスワードを検索し、ログインAPIで渡されたパスワードと比較します。一致すれば認証成功となり、Cookie が返されます。

1.3 アクセス認可とは

認可、すなわちアクセス制御は、誰がどのリソースにアクセスできるかを制御します。主体は認証後に権限を割り当てられ、システムリソースにアクセスできるようになります。権限のないリソースにはアクセスできません。

権限アノテーションが付与されたインターフェース(例: /test)を呼び出すと、CustomRealmdoGetAuthorizationInfo() メソッドで、ユーザー名を使用してデータベースからユーザーの権限を取得し、現在必要な権限と比較します。一致すれば認可成功となり、アクセスを続行できます。

2. Shiro とは

Apache Shiro は、認証、認可、暗号化、セッション管理を実行する、強力で使いやすいJavaセキュリティフレームワークです。Shiroの理解しやすいAPIを使用することで、最小のモバイルアプリケーションから最大のWebおよびエンタープライズアプリケーションまで、あらゆるアプリケーションを迅速かつ容易に保護できます。

Shiroは、Apache傘下のオープンソースフレームワークで、ソフトウェアシステムのセキュリティ認証関連の機能を抽出し、ユーザー認証、権限認可、暗号化、セッション管理などの機能を実現し、汎用的なセキュリティ認証フレームワークを構成します。

3. Shiro のコア構造

3.1 Subject

Subject(主体)は、外部アプリケーションと対話する主体です。Subjectは現在操作しているユーザーを記録し、ユーザーの概念を現在の操作主体として理解します。これはブラウザ経由でリクエストするユーザーかもしれませんし、実行中のプログラムかもしれません。SubjectはShiro内のインターフェースであり、認証や認可に関連する多くのメソッドを定義しています。外部プログラムはSubjectを介して認証・認可を行いますが、実際の認証・認可はSecurityManagerを介して行われます。

3.2 SecurityManager

SecurityManager(セキュリティマネージャー)は、全てのSubjectをセキュリティ管理します。これはShiroの核心であり、全てのSubjectのセキュリティ管理を担当します。SecurityManagerを介してSubjectの認証、認可などを行うことができます。実質的に、SecurityManagerはAuthenticator(認証器)を介して認証を行い、Authorizer(認可器)を介して認可を行い、SessionManagerを介してセッション管理などを行います。SecurityManagerは AuthenticatorAuthorizerSessionManager の3つのインターフェースを継承したインターフェースです。

3.3 Authenticator

Authenticator(認証器)は、ユーザーの身份を認証します。Authenticatorはインターフェースであり、Shiroは ModularRealmAuthenticator という実装クラスを提供しており、これでほとんどのニーズを満たせます。また、独自の認証器をカスタマイズすることも可能です。

3.4 Authorizer

Authorizer(認可器)は、認証器で認証が通ったユーザーが、機能にアクセスする際に、その機能の操作権限を持っているかどうかを判断します。

3.5 Realm

Realm(領域)はデータソースに相当します。SecurityManagerはセキュリティ認証を行うために、Realmを介してユーザーの権限データを取得します。例えば、ユーザー身份データがデータベースにある場合、Realmはデータベースからその情報を取得する必要があります。

注意: Realmを単にデータソースからデータを取得するものと理解しないでください。Realm内には認証・認可のチェックに関するコードも含まれています。

3.6 SessionManager

SessionManager(セッションマネージャー)は、Shiroフレームワークが定義した一連のセッション管理機能です。Webコンテナのセッションに依存しないため、Shiroは非Webアプリケーションでも使用でき、分散アプリケーションのセッションを一箇所に集中管理することも可能です。この特性により、シングルサインオン(SSO)を実現できます。

3.7 SessionDao

SessionDao(セッションDAO)は、セッションを操作するための一連のインターフェースです。例えば、セッションをデータベースに保存する場合、JDBCを使用してデータベースに保存できます。

3.8 CacheManager

CacheManager(キャッシュマネージャー)は、ユーザーの権限データをメモリに保存し、パフォーマンスを向上させます。

3.9 Cryptography

Cryptography(暗号管理)は、Shiroが提供する暗号化・復号化のコンポーネント群で、開発を容易にします。例えば、一般的なハッシュ関数や暗号化・復号化機能などを提供します。

4. Shiro における認証

4.1 認証の定義

ユーザー身份認証とは、ユーザーが正当なユーザーかどうかを判断するプロセスです。最も一般的なシンプルな認証方法は、システムがユーザーが入力したユーザー名とパスワードを、システムに保存されているそのユーザーの情報と照合するものです。

4.2 認証の主要オブジェクト

  • Subject(主体): システムにアクセスするユーザー。主体はユーザーやプログラムなど、認証を受ける全てのものを指します。
  • Principal(身份情報): 主体が認証を受けるための識別子。ユーザー名、電話番号、メールアドレスなど、一意性が保証される必要があります。一つの主体が複数の身份を持つことはできますが、主身份(Primary Principal)が一つ存在する必要があります。
  • Credential(資格情報): 主体のみが知る安全な情報です。例:パスワード、証明書。

4.3 認証フロー

Shiro認証フロー図

4.4 認証の実装

  1. プロジェクトの作成と依存関係の追加

    <dependency>
        <groupId>org.apache.shiro</groupId>
        <artifactId>shiro-core</artifactId>
        <version>1.5.3</version>
    </dependency>
    
  2. Shiro設定ファイルの作成(例:shiro.ini)

    [users]
    zhangsan=123456
    
  3. テストコード

    public class AuthenticationTest {
        public static void main(String[] args) {
            // 1. セキュリティマネージャーを作成
            DefaultSecurityManager securityManager = new DefaultSecurityManager();
    
            // 2. セキュリティマネージャーにRealmを設定(ここではiniファイルから読み込むIniRealm)
            securityManager.setRealm(new IniRealm("classpath:shiro.ini"));
    
            // 3. グローバルセキュリティユーティリティクラスに設定
            SecurityUtils.setSecurityManager(securityManager);
    
            // 4. 現在の主体(Subject)を取得
            Subject subject = SecurityUtils.getSubject();
    
            // 5. ユーザー名とパスワードのトークンを作成
            UsernamePasswordToken token = new UsernamePasswordToken("zhangsan", "123456");
    
            try {
                System.out.println("認証状態: " + subject.isAuthenticated());
                subject.login(token);
                System.out.println("認証状態: " + subject.isAuthenticated());
            } catch (UnknownAccountException e) {
                System.out.println("認証失敗: ユーザー名が存在しません");
            } catch (IncorrectCredentialsException e) {
                System.out.println("認証失敗: パスワードが間違っています");
            }
        }
    }
    

4.5 カスタムRealm

上記のプログラムはShiro標準の IniRealm を使用しており、ini設定ファイルからユーザー情報を読み取ります。多くの場合、システムのデータベースからユーザー情報を読み取る必要があるため、カスタムRealmが必要です。

  1. カスタムRealmの作成

    public class CustomRealm extends AuthorizingRealm {
    
        // 認可
        @Override
        protected AuthorizationInfo doGetAuthorizationInfo(PrincipalCollection principals) {
            return null;
        }
    
        // 認証
        @Override
        protected AuthenticationInfo doGetAuthenticationInfo(AuthenticationToken token) throws AuthenticationException {
            String principal = (String) token.getPrincipal(); // ユーザー名
    
            // ここでJDBCやMyBatisを使ってデータベースを検索
            if ("zhangsan".equals(principal)) {
                // データベースから見つかったユーザー名とパスワードをSimpleAuthenticationInfoにカプセル化して返す
                return new SimpleAuthenticationInfo(principal, "123456", this.getName());
            }
            return null;
        }
    }
    
  2. カスタムRealmを使用した認証

    public class CustomRealmAuthenticatorTest {
        public static void main(String[] args) {
            DefaultSecurityManager securityManager = new DefaultSecurityManager();
            securityManager.setRealm(new CustomRealm());
            SecurityUtils.setSecurityManager(securityManager);
            Subject subject = SecurityUtils.getSubject();
            UsernamePasswordToken token = new UsernamePasswordToken("zhangsan", "123456");
    
            try {
                subject.login(token);
                System.out.println("ログイン成功");
            } catch (UnknownAccountException e) {
                System.out.println("ユーザー名エラー");
            } catch (IncorrectCredentialsException e) {
                System.out.println("パスワードエラー");
            }
        }
    }
    

4.6 MD5 と Salt の使用

  • MD5暗号化: 一般的に暗号化または署名(チェックサム)に使用されます。MD5アルゴリズムは不可逆であり、内容が同じであれば何回実行しても結果は同じです。例:"123" は常に32文字の16進数文字列になります。
  • カスタムRealmでのMD5+Saltの使用例:
public class CustomMd5Realm extends AuthorizingRealm {
    @Override
    protected AuthorizationInfo doGetAuthorizationInfo(PrincipalCollection principals) {
        return null;
    }

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

        if ("zhangsan".equals(principal)) {
            // パラメータ: ユーザー名, データベースのハッシュ化されたパスワード, ランダムソルト, Realm名
            return new SimpleAuthenticationInfo(principal,
                    "14b9f6ecccb925f15cf92c44ff9326ce", // 1024回ハッシュ化された値
                    ByteSource.Util.bytes("x0*7ps"), // ランダムソルト
                    this.getName());
        }
        return null;
    }
}
  • テストコード(HashedCredentialsMatcherを使用):
public class CustomMd5RealmTest {
    public static void main(String[] args) {
        DefaultSecurityManager securityManager = new DefaultSecurityManager();
        CustomMd5Realm realm = new CustomMd5Realm();

        HashedCredentialsMatcher matcher = new HashedCredentialsMatcher();
        matcher.setHashAlgorithmName("md5");
        matcher.setHashIterations(1024);
        realm.setCredentialsMatcher(matcher);
        securityManager.setRealm(realm);
        SecurityUtils.setSecurityManager(securityManager);

        Subject subject = SecurityUtils.getSubject();
        UsernamePasswordToken token = new UsernamePasswordToken("zhangsan", "123456");

        try {
            subject.login(token);
            System.out.println("ログイン成功");
        } catch (UnknownAccountException e) {
            System.out.println("ユーザー名が存在しません");
        } catch (IncorrectCredentialsException e) {
            System.out.println("パスワードエラー");
        }
    }
}

5. Shiro における認可

5.1 認可の定義

認可、すなわちアクセス制御は、誰がどのリソースにアクセスできるかを制御します。主体は認証後に権限を割り当てられ、システムリソースにアクセスできるようになります。権限のないリソースにはアクセスできません。

5.2 主要オブジェクト

認可は、誰が (Who) 何を (What/Which) どのように (How) 操作するか、と単純に理解できます。

  • Who: 主体(Subject)、システム内のリソースにアクセスする必要がある主体。
  • What(リソース): システムメニュー、ページ、ボタン、クラスメソッド、システム商品情報など。リソースにはリソースタイプリソースインスタンスがあり、例えば商品情報がリソースタイプで、タイプがt01の商品がリソースインスタンス、番号001の商品情報もリソースインスタンスに含まれます。
  • How(権限/許可): 主体のリソースに対する操作許可を規定します。権限はリソースから切り離して意味を持ちません。例:ユーザー照会権限、ユーザー追加権限、特定のクラスメソッドの呼び出し権限、番号001のユーザーの変更権限など。権限を通じて、主体がどのリソースに対してどのような操作許可を持っているかがわかります。

5.3 認可フロー

Shiro認証・認可フロー図

5.4 認可方式

  • ロールベースアクセス制御 (RBAC)

    • ロールを中心にアクセス制御を行います。
    • 例: subject.hasRole("admin")
  • リソースベースアクセス制御 (RBAC)

    • リソースを中心にアクセス制御を行います。
    • 例: subject.isPermitted("user:create:*") (全てのユーザー作成権限)、subject.isPermitted("user:update:01") (特定のユーザーの更新権限)

5.5 権限文字列

権限文字列のルールは リソース識別子:操作:リソースインスタンス識別子 です。これは「どのリソースのどのインスタンスにどの操作をするか」を意味します。 : はリソース/操作/インスタンスの区切り文字であり、ワイルドカード * を使用することもできます。

例:
- ユーザー作成権限: user:create
- インスタンス001のユーザー変更権限: user:update:001
- ユーザーインスタンス001の全権限: user:*:001

5.6 認可の実装方法

  • プログラム式

    Subject subject = SecurityUtils.getSubject();
    if (subject.hasRole("admin")) {
        // 権限あり
    } else {
        // 権限なし
    }
    
  • アノテーション式

    @RequiresRoles("admin")
    public void hello() {
        // 権限あり
    }
    
  • タグ式(例:JSP)

    <shiro:hasRole name="admin">
        <!-- 権限あり -->
    </shiro:hasRole>
    

5.7 認可の実装例

  1. Realm での実装(doGetAuthorizationInfo メソッド内):

    public class CustomMd5Realm extends AuthorizingRealm {
        @Override
        protected AuthorizationInfo doGetAuthorizationInfo(PrincipalCollection principals) {
            String primaryPrincipal = (String) principals.getPrimaryPrincipal();
            System.out.println("身份情報: " + primaryPrincipal);
    
            SimpleAuthorizationInfo info = new SimpleAuthorizationInfo();
            // ロール情報を追加
            info.addRole("admin");
            info.addRole("user");
    
            // 権限情報を追加
            info.addStringPermission("user:*:01");
            info.addStringPermission("product:create");
    
            return info;
        }
        //... doGetAuthenticationInfo は省略
    }
    
  2. テストコード(認証後):

    if (subject.isAuthenticated()) {
        // ロールベース
        // System.out.println(subject.hasRole("admin"));
    
        // 権限文字列ベース
        // System.out.println(subject.isPermitted("user:update:01"));
        // System.out.println(subject.isPermittedAll("user:*:01", "product:create"));
    }
    

6. Spring Boot との統合

6.1 Shiro と Spring Boot の統合アーキテクチャ

Shiro Spring Boot 統合図

上図のリクエストは、制限されたリソースにアクセスする場合にのみ ShiroFilter を通過します。公開リソースへのアクセス時は通過しません。

6.2 依存関係の追加

<!-- Shiro Spring Boot Starter -->
<dependency>
    <groupId>org.apache.shiro</groupId>
    <artifactId>shiro-spring-boot-starter</artifactId>
    <version>1.5.3</version>
</dependency>

6.3 Shiro 設定クラス

@Configuration
public class ShiroConfig {

    // カスタムRealm
    @Bean
    public Realm getRealm() {
        CustomerRealm realm = new CustomerRealm();
        // 必要に応じて認証情報マッチャーなどを設定
        HashedCredentialsMatcher matcher = new HashedCredentialsMatcher();
        matcher.setHashAlgorithmName("MD5");
        matcher.setHashIterations(1024);
        realm.setCredentialsMatcher(matcher);
        return realm;
    }

    // Webセキュリティマネージャー
    @Bean
    public DefaultWebSecurityManager getDefaultWebSecurityManager(Realm realm) {
        DefaultWebSecurityManager securityManager = new DefaultWebSecurityManager();
        securityManager.setRealm(realm);
        return securityManager;
    }

    // Shiroフィルターファクトリービーン
    @Bean
    public ShiroFilterFactoryBean getShiroFilterFactoryBean(DefaultWebSecurityManager securityManager) {
        ShiroFilterFactoryBean factoryBean = new ShiroFilterFactoryBean();
        factoryBean.setSecurityManager(securityManager);

        // フィルターチェーンの定義
        Map<String, String> filterChainMap = new LinkedHashMap<>();
        // 公開リソース
        filterChainMap.put("/login.jsp", "anon");
        filterChainMap.put("/user/login", "anon");
        // 認証が必要なリソース
        filterChainMap.put("/**", "authc");

        factoryBean.setFilterChainDefinitionMap(filterChainMap);

        // ログインページの設定
        factoryBean.setLoginUrl("/login.jsp");
        return factoryBean;
    }
}

6.4 カスタムRealm(データベース認証)

データベースからユーザー情報を取得するためのカスタムRealmです。Spring の依存性注入を利用して Service を取得します。

public class CustomerRealm extends AuthorizingRealm {

    @Override
    protected AuthorizationInfo doGetAuthorizationInfo(PrincipalCollection principals) {
        // 認可ロジック (必要に応じてデータベースからロールと権限を取得)
        return null;
    }

    @Override
    protected AuthenticationInfo doGetAuthenticationInfo(AuthenticationToken token) throws AuthenticationException {
        String username = (String) token.getPrincipal();

        UserService userService = (UserService) ApplicationContextUtils.getBean("userServiceImpl");
        User user = userService.findByUserName(username);

        if (user != null) {
            return new SimpleAuthenticationInfo(user.getUsername(),
                    user.getPassword(),
                    ByteSource.Util.bytes(user.getSalt()),
                    this.getName());
        }
        return null;
    }
}

6.5 ログインとログアウト

Controller の例:

@Controller
@RequestMapping("/user")
public class UserController {

    @RequestMapping("/login")
    public String login(String username, String password, HttpSession session) {
        Subject subject = SecurityUtils.getSubject();
        try {
            subject.login(new UsernamePasswordToken(username, password));
            return "redirect:/index.jsp";
        } catch (UnknownAccountException e) {
            System.out.println("ユーザー名エラー");
        } catch (IncorrectCredentialsException e) {
            System.out.println("パスワードエラー");
        }
        return "redirect:/login.jsp";
    }

    @RequestMapping("/logout")
    public String logout() {
        Subject subject = SecurityUtils.getSubject();
        subject.logout();
        return "redirect:/login.jsp";
    }
}

6.6 キャッシュ管理

Shiro ではキャッシュを使用して認証・認可情報を保存し、データベースへの負荷を軽減できます。

  • EhCache の使用: shiro-ehcache 依存関係を追加し、Realm に setCacheManager(new EhCacheManager()) を設定します。
  • Redis の使用: カスタム RedisCacheManagerRedisCache を実装します。その際、シリアライズの問題に対処する必要があります(例:カスタム ByteSource のシリアライズ対応)。

7. Thymeleaf との統合

7.1 依存関係の追加

<!-- Spring Boot Thymeleaf Starter -->
<dependency>
    <groupId>org.springframework.boot</groupId>
    <artifactId>spring-boot-starter-thymeleaf</artifactId>
</dependency>

7.2 名前空間の追加

HTML テンプレートの <html> タグに Shiro の名前空間を追加します。

<html lang="en" xmlns:th="http://www.thymeleaf.org"
      xmlns:shiro="http://www.pollix.at/thymeleaf/shiro">

7.3 Shiro ダイアレクトの設定

Spring Boot 設定クラスなどで、Thymeleaf に Shiro ダイアレクトを追加します。

@Bean
public ShiroDialect shiroDialect() {
    return new ShiroDialect();
}

7.4 テンプレートでの権限制御例

<!-- ロールベース -->
<div shiro:hasRole="admin">
    <p>管理者のみ表示</p>
</div>

<!-- 権限ベース -->
<div shiro:hasPermission="user:create">
    <p>ユーザー作成権限がある場合のみ表示</p>
</div>

<!-- 認証状態 -->
<p shiro:authenticated>
    ユーザー <span shiro:principal></span> でログイン中
</p>

タグ: Shiro Java セキュリティフレームワーク 認証 認可

7月22日 05:34 投稿