1. 概要

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

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

2. 依存

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

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

<dependency>
    <groupId>org.apache.shiro</groupId>
    <artifactId>shiro-core</artifactId>
    <version>1.4.0</version>
</dependency>

Apache Shiroモジュールの最新バージョンは、MavenCentralにあります。

3. SecurityManagerの構成

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

このチュートリアルでは、デスクトップ環境でのフレームワークについて説明します。 フレームワークを構成するには、リソースフォルダーに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によって認識されるユーザー資格情報を定義します。 形式は次のとおりです。p rincipal(username)= password、role1、role2、…、role

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

編集者ロールには記事に関連付けられたすべての権限がありますが、作成者ロールは作成および記事の保存のみが可能です。

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);           
  }                                                 
}

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

次に、トークンを使用してログインを試みます。 提供された資格情報が正しければ、すべてがうまくいくはずです。

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

5. 承認

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

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

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

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

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

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

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()[X211X ]およびgetPermissions()

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

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

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

private Map<String, String> credentials = new HashMap<>();
private Map<String, Set<String>> roles = new HashMap<>();
private Map<String, Set<String>> 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構成は、ここにあります。

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の基本構造の概要を説明し、デスクトップ環境に実装しました。 フレームワークをSpring Bootアプリケーションに統合することから始めましょう。

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

9.1. 依存関係

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

<parent>
    <groupId>org.springframework.boot</groupId>
    <artifactId>spring-boot-starter-parent</artifactId>
    <version>2.6.1</version>
</parent>

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

<dependency>
    <groupId>org.springframework.boot</groupId>
    <artifactId>spring-boot-starter-web</artifactId>
</dependency>
<dependency>
    <groupId>org.springframework.boot</groupId>
    <artifactId>spring-boot-starter-freemarker</artifactId>
</dependency>
<dependency>
    <groupId>org.apache.shiro</groupId>
    <artifactId>shiro-spring-boot-web-starter</artifactId>
    <version>${apache-shiro-core-version}</version>
</dependency>

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アプリケーションにデフォルトで付属しています。 他のデフォルトのフィルターはここにあります。

Realm Beanを定義しなかった場合、 ShiroAutoConfiguration は、デフォルトで、shiro.iniを検出することを期待するIniRealm実装を提供します。 ] src / main /resourcesまたはsrc/ main / resources/META-INF。のファイル

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

これで完了です。これで、ApacheShiroをSpring Bootアプリケーションに統合できます。

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

10. JEE統合

Apache ShiroをJEEアプリケーションに統合するには、web.xmlファイルを構成するだけです。 通常どおり、構成ではshiro.iniがクラスパスに含まれている必要があります。 詳細な構成例は、こちらで入手できます。 また、JSPタグはここにあります。

11. 結論

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

いつものように、完全なソースコードはGitHubから入手できます。