Apache Shiroの紹介

Apache Shiroの概要

1. 概要

この記事では、用途の広いJavaセキュリティフレームワークであるApache Shiroについて説明します。

このフレームワークは、認証、承認、暗号化、およびセッション管理を提供するため、高度にカスタマイズ可能でモジュール化されています。

2. 依存

Apache Shiroには多くのmodulesがあります。 ただし、このチュートリアルでは、shiro-coreアーティファクトのみを使用します。

それをpom.xmlに追加しましょう:


    org.apache.shiro
    shiro-core
    1.4.0

Apache Shiroモジュールの最新バージョンはon Maven Central.にあります。

3. SecurityManagerの構成

SecurityManagerは、ApacheShiroのフレームワークの中心的な部分です。 通常、アプリケーションでは、単一のインスタンスが実行されます。

このチュートリアルでは、デスクトップ環境でフレームワークを探ります。 フレームワークを構成するには、リソースフォルダーに次の内容のshiro.iniファイルを作成する必要があります。

[users]
user = password, admin
user2 = password2, editor
user3 = password3, author

[roles]
admin = *
editor = articles:*
author = articles:compose,articles:save

shiro.ini構成ファイルの[users]セクションは、SecurityManagerによって認識されるユーザー資格情報を定義します。 形式はprincipal (username) = password, role1, role2, …, roleです。

ロールとそれに関連する権限は、[roles]セクションで宣言されています。 adminロールには、アプリケーションのすべての部分へのアクセス許可とアクセス権が付与されます。 これは、ワイルドカードの(*)記号で示されます。

editorロールには、articlesに関連付けられたすべての権限がありますが、authorロールには、記事のcomposesaveしかありません。

SecurityManagerは、SecurityUtilsクラスを構成するために使用されます。 SecurityUtilsから、システムと対話している現在のユーザーを取得し、認証および承認操作を実行できます。

IniRealmを使用してshiro.iniファイルからユーザーとロールの定義をロードし、それを使用してDefaultSecurityManagerオブジェクトを構成しましょう。

IniRealm iniRealm = new IniRealm("classpath:shiro.ini");
SecurityManager securityManager = new DefaultSecurityManager(iniRealm);

SecurityUtils.setSecurityManager(securityManager);
Subject currentUser = SecurityUtils.getSubject();

shiro.iniファイルで定義されたユーザー資格情報とロールを認識するSecurityManagerができたので、ユーザー認証と承認に進みましょう。

4. 認証

Apache Shiroの用語では、Subjectはシステムと対話するエンティティです。 それは、人間、スクリプト、またはRESTクライアントのいずれかです。

SecurityUtils.getSubject()を呼び出すと、現在のSubjectのインスタンス、つまりcurrentUserが返されます。

currentUserオブジェクトができたので、指定された資格情報に対して認証を実行できます。

if (!currentUser.isAuthenticated()) {
  UsernamePasswordToken token
    = new UsernamePasswordToken("user", "password");
  token.setRememberMe(true);
  try {
      currentUser.login(token);
  } catch (UnknownAccountException uae) {
      log.error("Username Not Found!", uae);
  } catch (IncorrectCredentialsException ice) {
      log.error("Invalid Credentials!", ice);
  } catch (LockedAccountException lae) {
      log.error("Your Account is Locked!", lae);
  } catch (AuthenticationException ae) {
      log.error("Unexpected Error!", ae);
  }
}

まず、現在のユーザーがまだ認証されていないかどうかを確認します。 次に、ユーザーのプリンシパル(username)とクレデンシャル(password).を使用して認証トークンを作成します

次に、トークンを使用してログインを試みます。 提供された資格情報が正しい場合、すべて正常に動作するはずです。

ケースごとに異なる例外があります。 アプリケーションの要件により適したカスタム例外をスローすることもできます。 これは、AccountExceptionクラスをサブクラス化することで実行できます。

5. 承認

認証はユーザーのIDを検証しようとしている一方で、承認はシステム内の特定のリソースへのアクセスを制御しようとしています。

shiro.iniファイルで作成した各ユーザーに1つ以上のロールを割り当てることを思い出してください。 さらに、ロールセクションでは、ロールごとに異なるアクセス許可またはアクセスレベルを定義します。

それでは、アプリケーションでこれを使用してユーザーアクセス制御を適用する方法を見てみましょう。

shiro.iniファイルでは、管理者にシステムのすべての部分への完全なアクセス権を与えます。

編集者はarticlesに関するすべてのリソース/操作に完全にアクセスでき、作成者はarticlesのみを作成して保存するように制限されています。

役割に基づいて現在のユーザーを歓迎しましょう。

if (currentUser.hasRole("admin")) {
    log.info("Welcome Admin");
} else if(currentUser.hasRole("editor")) {
    log.info("Welcome, Editor!");
} else if(currentUser.hasRole("author")) {
    log.info("Welcome, Author");
} else {
    log.info("Welcome, Guest");
}

それでは、現在のユーザーがシステムで何をすることが許可されているかを見てみましょう。

if(currentUser.isPermitted("articles:compose")) {
    log.info("You can compose an article");
} else {
    log.info("You are not permitted to compose an article!");
}

if(currentUser.isPermitted("articles:save")) {
    log.info("You can save articles");
} else {
    log.info("You can not save articles");
}

if(currentUser.isPermitted("articles:publish")) {
    log.info("You can publish articles");
} else {
    log.info("You can not publish articles");
}

6. レルム構成

実際のアプリケーションでは、shiro.iniファイルからではなくデータベースからユーザー資格情報を取得する方法が必要になります。 これは、レルムの概念が作用する場所です。

Apache Shiroの用語では、Realmは、認証と承認に必要なユーザー資格情報のストアを指すDAOです。

レルムを作成するには、Realmインターフェースを実装するだけで済みます。 それは退屈です。ただし、フレームワークには、サブクラス化できるデフォルトの実装が付属しています。 これらの実装の1つはJdbcRealmです。

JdbcRealmクラスを拡張し、次のメソッドをオーバーライドするカスタムレルム実装を作成します:doGetAuthenticationInfo()doGetAuthorizationInfo()getRoleNamesForUser()、およびgetPermissions()

JdbcRealmクラスをサブクラス化してレルムを作成しましょう。

public class MyCustomRealm extends JdbcRealm {
    //...
}

簡単にするために、java.util.Mapを使用してデータベースをシミュレートします。

private Map credentials = new HashMap<>();
private Map> roles = new HashMap<>();
private Map> perm = new HashMap<>();

{
    credentials.put("user", "password");
    credentials.put("user2", "password2");
    credentials.put("user3", "password3");

    roles.put("user", new HashSet<>(Arrays.asList("admin")));
    roles.put("user2", new HashSet<>(Arrays.asList("editor")));
    roles.put("user3", new HashSet<>(Arrays.asList("author")));

    perm.put("admin", new HashSet<>(Arrays.asList("*")));
    perm.put("editor", new HashSet<>(Arrays.asList("articles:*")));
    perm.put("author",
      new HashSet<>(Arrays.asList("articles:compose",
      "articles:save")));
}

先に進み、doGetAuthenticationInfo()をオーバーライドしましょう。

protected AuthenticationInfo
  doGetAuthenticationInfo(AuthenticationToken token)
  throws AuthenticationException {

    UsernamePasswordToken uToken = (UsernamePasswordToken) token;

    if(uToken.getUsername() == null
      || uToken.getUsername().isEmpty()
      || !credentials.containsKey(uToken.getUsername())) {
          throw new UnknownAccountException("username not found!");
    }

    return new SimpleAuthenticationInfo(
      uToken.getUsername(),
      credentials.get(uToken.getUsername()),
      getName());
}

まず、提供されたAuthenticationTokenUsernamePasswordTokenにキャストします。 uTokenから、ユーザー名(uToken.getUsername())を抽出し、それを使用してデータベースからユーザー資格情報(パスワード)を取得します。

レコードが見つからない場合–UnknownAccountExceptionをスローします。それ以外の場合は、資格情報とユーザー名を使用して、メソッドから返されるSimpleAuthenticatioInfoオブジェクトを作成します。

ユーザー資格情報がソルトでハッシュされている場合、関連するソルトとともにSimpleAuthenticationInfoを返す必要があります。

return new SimpleAuthenticationInfo(
  uToken.getUsername(),
  credentials.get(uToken.getUsername()),
  ByteSource.Util.bytes("salt"),
  getName()
);

また、doGetAuthorizationInfo()、およびgetRoleNamesForUser()getPermissions()をオーバーライドする必要があります。

最後に、カスタムレルムをsecurityManagerに接続しましょう。 上記のIniRealmをカスタムレルムに置き換え、それをDefaultSecurityManagerのコンストラクターに渡すだけです。

Realm realm = new MyCustomRealm();
SecurityManager securityManager = new DefaultSecurityManager(realm);

コードの他の部分はすべて以前と同じです。 カスタムレルムを使用してsecurityManagerを適切に構成するために必要なのはこれだけです。

問題は、フレームワークが資格情報とどのように一致するかです。

デフォルトでは、JdbcRealmSimpleCredentialsMatcherを使用します。これは、AuthenticationTokenAuthenticationInfoの資格情報を比較することによって同等性をチェックするだけです。

パスワードをハッシュする場合は、代わりにHashedCredentialsMatcherを使用するようにフレームワークに通知する必要があります。 ハッシュ化されたパスワードを持つレルムのINI構成は、hereにあります。

7. ログアウト

ユーザーの認証が完了したので、次はログアウトを実装します。 これは、単一のメソッドを呼び出すだけで実行できます。これにより、ユーザーセッションが無効になり、ユーザーがログアウトされます。

currentUser.logout();

8. セッション管理

フレームワークには、セッション管理システムが付属しています。 Web環境で使用する場合、デフォルトでHttpSession実装になります。

スタンドアロンアプリケーションの場合、エンタープライズセッション管理システムを使用します。 利点は、デスクトップ環境でも、通常のWeb環境で行うようにセッションオブジェクトを使用できることです。

簡単な例を見て、現在のユーザーのセッションを操作してみましょう。

Session session = currentUser.getSession();
session.setAttribute("key", "value");
String value = (String) session.getAttribute("key");
if (value.equals("value")) {
    log.info("Retrieved the correct value! [" + value + "]");
}

9. Springを使用したWebアプリケーションのShiro

これまで、Apache Shiroの基本構造の概要を説明し、デスクトップ環境に実装しました。 フレームワークをSpringBootアプリケーションに統合することから始めましょう。

ここでの主な焦点はShiroであり、Springアプリケーションではないことに注意してください。これは、単純なサンプルアプリを強化するためにのみ使用します。

9.1. 依存関係

まず、Spring Bootの親依存関係をpom.xmlに追加する必要があります。


    org.springframework.boot
    spring-boot-starter-parent
    1.5.2.RELEASE

次に、同じpom.xmlファイルに次の依存関係を追加する必要があります。


    org.springframework.boot
    spring-boot-starter-web


    org.springframework.boot
    spring-boot-starter-freemarker


    org.apache.shiro
    shiro-spring-boot-web-starter
    ${apache-shiro-core-version}

9.2. 設定

shiro-spring-boot-web-starter依存関係をpom.xmlに追加すると、デフォルトで、SecurityManagerなどのApacheShiroアプリケーションの一部の機能が構成されます。

ただし、RealmとShiroセキュリティフィルターを構成する必要があります。 上記で定義したのと同じカスタムレルムを使用します。

したがって、Spring Bootアプリケーションが実行されるメインクラスに、次のBean定義を追加しましょう。

@Bean
public Realm realm() {
    return new MyCustomRealm();
}

@Bean
public ShiroFilterChainDefinition shiroFilterChainDefinition() {
    DefaultShiroFilterChainDefinition filter
      = new DefaultShiroFilterChainDefinition();

    filter.addPathDefinition("/secure", "authc");
    filter.addPathDefinition("/**", "anon");

    return filter;
}

ShiroFilterChainDefinitionでは、authcフィルターを/secureパスに適用し、anonフィルターをAntパターンを使用して他のパスに適用しました。

authcフィルターとanonフィルターの両方が、Webアプリケーションにデフォルトで付属しています。 他のデフォルトのフィルターはhereで見つけることができます。

Realm Beanを定義しなかった場合、ShiroAutoConfigurationは、デフォルトで、src/main/resourcesまたは%でshiro.iniファイルを見つけることを期待するIniRealm実装を提供します。 (t5)s

ShiroFilterChainDefinition Beanを定義しない場合、フレームワークはすべてのパスを保護し、ログインURLをlogin.jspとして設定します。

application.propertiesに次のエントリを追加することで、このデフォルトのログインURLとその他のデフォルトを変更できます。

shiro.loginUrl = /login
shiro.successUrl = /secure
shiro.unauthorizedUrl = /login

authcフィルターが/secureに適用されたので、そのルートへのすべての要求にはフォーム認証が必要になります。

9.3. 認証と承認

次のパスマッピングを使用してShiroSpringControllerを作成しましょう:/index/login, /logout/secure.

login()メソッドは、上記のように実際のユーザー認証を実装する場所です。 認証が成功すると、ユーザーはセキュアページにリダイレクトされます。

Subject subject = SecurityUtils.getSubject();

if(!subject.isAuthenticated()) {
    UsernamePasswordToken token = new UsernamePasswordToken(
      cred.getUsername(), cred.getPassword(), cred.isRememberMe());
    try {
        subject.login(token);
    } catch (AuthenticationException ae) {
        ae.printStackTrace();
        attr.addFlashAttribute("error", "Invalid Credentials");
        return "redirect:/login";
    }
}

return "redirect:/secure";

そして、secure()の実装では、currentUserは、SecurityUtils.getSubject().を呼び出すことによって取得されました。ユーザーの役割とアクセス許可は、ユーザーのプリンシパルと同様に、安全なページに渡されます。

Subject currentUser = SecurityUtils.getSubject();
String role = "", permission = "";

if(currentUser.hasRole("admin")) {
    role = role  + "You are an Admin";
} else if(currentUser.hasRole("editor")) {
    role = role + "You are an Editor";
} else if(currentUser.hasRole("author")) {
    role = role + "You are an Author";
}

if(currentUser.isPermitted("articles:compose")) {
    permission = permission + "You can compose an article, ";
} else {
    permission = permission + "You are not permitted to compose an article!, ";
}

if(currentUser.isPermitted("articles:save")) {
    permission = permission + "You can save articles, ";
} else {
    permission = permission + "\nYou can not save articles, ";
}

if(currentUser.isPermitted("articles:publish")) {
    permission = permission  + "\nYou can publish articles";
} else {
    permission = permission + "\nYou can not publish articles";
}

modelMap.addAttribute("username", currentUser.getPrincipal());
modelMap.addAttribute("permission", permission);
modelMap.addAttribute("role", role);

return "secure";

And we’re done.これが、ApacheShiroをSpringBootアプリケーションに統合する方法です。

また、フレームワークは、アプリケーションを保護するためにフィルターチェーン定義と一緒に使用できる追加のannotationsを提供することに注意してください。

10. JEE統合

Apache ShiroをJEEアプリケーションに統合するには、web.xmlファイルを構成するだけです。 いつものように、構成はshiro.iniがクラスパスにあることを想定しています。 詳細な構成例は、hereで入手できます。 また、JSPタグはhereで見つけることができます。

11. 結論

このチュートリアルでは、ApacheShiroの認証および承認メカニズムについて説明しました。 また、カスタムレルムを定義し、それをSecurityManagerにプラグインする方法にも焦点を当てました。

いつものように、完全なソースコードはover on GitHubで利用できます。