1. 序章

アクセス制御リスト ACL)は、オブジェクトに関連付けられているアクセス許可のリストです。 ACL は、特定のオブジェクトに対してどのIDにどの操作を許可するかを指定します。

SpringSecurityアクセス制御リストドメインオブジェクトセキュリティをサポートするSpringコンポーネント。 簡単に言えば、Spring ACLは、一般的な操作レベルで、全面的にではなく、単一のドメインオブジェクトに対する特定のユーザー/ロールのアクセス許可を定義するのに役立ちます。

たとえば、管理者の役割を持つユーザーは、中央通知ボックスのすべてのメッセージを表示(読み取り)および編集(書き込み)できます。 、ただし、通常のユーザーはメッセージのみを表示し、メッセージに関連付けて編集することはできません。 一方、編集者の役割を持つ他のユーザーは、特定のメッセージを表示および編集できます。

したがって、ユーザー/ロールが異なれば、特定のオブジェクトごとに異なる権限が与えられます。 この場合、 SpringACLはタスクを実行できます。 この記事では、 SpringACLを使用して基本的なアクセス許可チェックを設定する方法について説明します。

2. 構成

2.1. ACLデータベース

Spring Security ACL を使用するには、データベースに4つの必須テーブルを作成する必要があります。

最初のテーブルはACL_CLASSで、ドメインオブジェクトのクラス名を格納します。列には次のものが含まれます。

  • ID
  • CLASS:保護されたドメインオブジェクトのクラス名。例: com.baeldung.acl.persistence.entity.NoticeMessage

次に、 ACL_SID テーブルが必要です。これにより、システム内の原則または権限を普遍的に識別できます。 テーブルに必要なもの:

  • ID
  • SID:これはユーザー名またはロール名です。 SIDSecurityIdentityの略です
  • PRINCIPAL: 0または1、対応する SID がプリンシパル( mary、mikeなどのユーザー)であることを示します、ジャック…)または権限( ROLE_ADMIN、ROLE_USER、ROLE_EDITOR…などの役割)

次のテーブルはACL_OBJECT_IDENTITY、で、一意のドメインオブジェクトごとに情報が格納されます。

  • ID
  • OBJECT_ID_CLASS:はドメインオブジェクトクラスを定義し、ACL_CLASSテーブルにリンクします
  • OBJECT_ID_IDENTITY:ドメインオブジェクトは、クラスに応じて多くのテーブルに格納できます。 したがって、このフィールドにはターゲットオブジェクトの主キーが格納されます
  • PARENT_OBJECT:このテーブル内のこの ObjectIdentityの親を指定してください
  • OWNER_SID:オブジェクト所有者の ID ACL_SIDテーブルへのリンク
  • ENTRIES_INHERITING:このオブジェクトの ACLエントリが親オブジェクトから継承するかどうか(ACLエントリACL_ENTRY テーブルで定義されています)

最後に、 ACL_ENTRY ストアの個別のアクセス許可は、オブジェクトIDの各SIDに割り当てられます。

  • ID
  • ACL_OBJECT_IDENTITY:はオブジェクトIDを指定し、ACL_OBJECT_IDENTITYテーブルにリンクします
  • ACE_ORDER:対応するオブジェクトIDACLエントリリスト内の現在のエントリの順序
  • SID:許可が付与または拒否されるターゲット SID は、ACL_SIDテーブルにリンクします
  • MASK:許可または拒否されている実際のアクセス許可を表す整数ビットマスク
  • 許可:値1は許可を意味し、値0は拒否を意味します
  • AUDIT_SUCCESSおよびAUDIT_FAILURE:監査目的

2.2. 依存

プロジェクトでSpringACL を使用できるようにするには、最初に依存関係を定義しましょう。

<dependency>
    <groupId>org.springframework.security</groupId>
    <artifactId>spring-security-acl</artifactId>
</dependency>
<dependency>
    <groupId>org.springframework.security</groupId>
    <artifactId>spring-security-config</artifactId>
</dependency>
<dependency>
    <groupId>org.springframework</groupId>
    <artifactId>spring-context-support</artifactId>
</dependency>
<dependency>
    <groupId>net.sf.ehcache</groupId>
    <artifactId>ehcache-core</artifactId>
    <version>2.6.11</version>
</dependency>

Spring ACL には、 Object IdentityおよびACLエントリを格納するためのキャッシュが必要なので、ここではEhcacheを使用します。 また、SpringでEhcache をサポートするには、spring-context-supportも必要です。

Spring Bootを使用していない場合は、バージョンを明示的に追加する必要があります。 これらはMavenCentralで確認できます: spring-security-acl spring-security-config spring-context-support ehcache-core

2.3. ACL関連の構成

グローバルメソッドセキュリティ:を有効にして、保護されたドメインオブジェクトを返す、またはオブジェクトに変更を加えるすべてのメソッドを保護する必要があります。

@Configuration
@EnableGlobalMethodSecurity(prePostEnabled = true, securedEnabled = true)
public class AclMethodSecurityConfiguration 
  extends GlobalMethodSecurityConfiguration {

    @Autowired
    MethodSecurityExpressionHandler 
      defaultMethodSecurityExpressionHandler;

    @Override
    protected MethodSecurityExpressionHandler createExpressionHandler() {
        return defaultMethodSecurityExpressionHandler;
    }
}

また、有効にしましょう式ベースのアクセス制御設定することにより prePostEnabled真実使用する Spring式言語(SpEL)さらに 式ハンドラーが必要です ACL サポート:

@Bean
public MethodSecurityExpressionHandler 
  defaultMethodSecurityExpressionHandler() {
    DefaultMethodSecurityExpressionHandler expressionHandler
      = new DefaultMethodSecurityExpressionHandler();
    AclPermissionEvaluator permissionEvaluator 
      = new AclPermissionEvaluator(aclService());
    expressionHandler.setPermissionEvaluator(permissionEvaluator);
    return expressionHandler;
}

したがって、 AclPermissionEvaluatorDefaultMethodSecurityExpressionHandlerに割り当てます。 評価者は、データベースから権限設定とドメインオブジェクトの定義をロードするために、MutableAclServiceを必要とします。

簡単にするために、提供されているJdbcMutableAclServiceを使用します。

@Bean 
public JdbcMutableAclService aclService() { 
    return new JdbcMutableAclService(
      dataSource, lookupStrategy(), aclCache()); 
}

その名前として、JdbcMutableAclServiceJDBCTemplateを使用してデータベースアクセスを簡素化します。 DataSource( for JDBCTemplate) LookupStrategy (データベースのクエリ時に最適化されたルックアップを提供)、および AclCache(キャッシング)が必要です。 ACL エントリおよびオブジェクトID)

ここでも、簡単にするために、提供されているBasicLookupStrategyおよびEhCacheBasedAclCacheを使用します。

@Autowired
DataSource dataSource;

@Bean
public AclAuthorizationStrategy aclAuthorizationStrategy() {
    return new AclAuthorizationStrategyImpl(
      new SimpleGrantedAuthority("ROLE_ADMIN"));
}

@Bean
public PermissionGrantingStrategy permissionGrantingStrategy() {
    return new DefaultPermissionGrantingStrategy(
      new ConsoleAuditLogger());
}

@Bean
public EhCacheBasedAclCache aclCache() {
    return new EhCacheBasedAclCache(
      aclEhCacheFactoryBean().getObject(), 
      permissionGrantingStrategy(), 
      aclAuthorizationStrategy()
    );
}

@Bean
public EhCacheFactoryBean aclEhCacheFactoryBean() {
    EhCacheFactoryBean ehCacheFactoryBean = new EhCacheFactoryBean();
    ehCacheFactoryBean.setCacheManager(aclCacheManager().getObject());
    ehCacheFactoryBean.setCacheName("aclCache");
    return ehCacheFactoryBean;
}

@Bean
public EhCacheManagerFactoryBean aclCacheManager() {
    return new EhCacheManagerFactoryBean();
}

@Bean 
public LookupStrategy lookupStrategy() { 
    return new BasicLookupStrategy(
      dataSource, 
      aclCache(), 
      aclAuthorizationStrategy(), 
      new ConsoleAuditLogger()
    ); 
}

ここで、 AclAuthorizationStrategy は、現在のユーザーが特定のオブジェクトに対して必要なすべての権限を持っているかどうかを判断する役割を果たします。

特定のSIDにアクセス許可を付与するかどうかを決定するロジックを定義するPermissionGrantingStrategyのサポートが必要です。

3. SpringACLによるメソッドセキュリティ

これまでに、必要なすべての構成を完了しましたこれで、セキュリティで保護されたメソッドに必要なチェックルールを設定できます。

デフォルトでは、 Spring ACL は、使用可能なすべてのアクセス許可についてBasePermissionクラスを参照します。 基本的に、 READ、WRITE、CREATE、DELETE 、およびADMINISTRATIONの権限があります。

いくつかのセキュリティルールを定義してみましょう。

@PostFilter("hasPermission(filterObject, 'READ')")
List<NoticeMessage> findAll();
    
@PostAuthorize("hasPermission(returnObject, 'READ')")
NoticeMessage findById(Integer id);
    
@PreAuthorize("hasPermission(#noticeMessage, 'WRITE')")
NoticeMessage save(@Param("noticeMessage")NoticeMessage noticeMessage);

findAll()メソッドの実行後、@PostFilterがトリガーされます。 必要なルールhasPermission(filterObject、’READ’)、は、現在のユーザーがREAD権限を持っているNoticeMessageのみを返すことを意味します。

同様に、 @PostAuthorize は、 findById()メソッドの実行後にトリガーされます。現在のユーザーがREAD[を持っている場合にのみNoticeMessageオブジェクトを返すようにしてください。 X187X]許可。 そうでない場合、システムはAccessDeniedExceptionをスローします。

一方、システムは、 save()メソッドを呼び出す前に、@PreAuthorizeアノテーションをトリガーします。 対応するメソッドの実行を許可する場所を決定します。 そうでない場合、AccessDeniedExceptionがスローされます。

4. 動作中

次に、JUnitを使用してこれらすべての構成をテストします。 H2 データベースを使用して、構成を可能な限り単純にします。

追加する必要があります:

<dependency>
  <groupId>com.h2database</groupId>
  <artifactId>h2</artifactId>
</dependency>

<dependency>
  <groupId>org.springframework</groupId>
  <artifactId>spring-test</artifactId>
  <scope>test</scope>
</dependency>

<dependency>
  <groupId>org.springframework.security</groupId>
  <artifactId>spring-security-test</artifactId>
  <scope>test</scope>
</dependency>

4.1. シナリオ

このシナリオでは、2人のユーザー( manager、hr)と1人のユーザーロール( ROLE_EDITOR)、があるため、acl_sidは次のようになります。

INSERT INTO acl_sid (id, principal, sid) VALUES
  (1, 1, 'manager'),
  (2, 1, 'hr'),
  (3, 0, 'ROLE_EDITOR');

次に、acl_classNoticeMessageクラスを宣言する必要があります。 そしての3つのインスタンス NoticeMessage クラスが挿入されます system_message。

さらに、これら3つのインスタンスに対応するレコードは、acl_object_identityで宣言する必要があります。

INSERT INTO acl_class (id, class) VALUES
  (1, 'com.baeldung.acl.persistence.entity.NoticeMessage');

INSERT INTO system_message(id,content) VALUES 
  (1,'First Level Message'),
  (2,'Second Level Message'),
  (3,'Third Level Message');

INSERT INTO acl_object_identity 
  (id, object_id_class, object_id_identity, 
  parent_object, owner_sid, entries_inheriting) 
  VALUES
  (1, 1, 1, NULL, 3, 0),
  (2, 1, 2, NULL, 3, 0),
  (3, 1, 3, NULL, 3, 0);

最初に、最初のオブジェクト( id = 1 )に対するREADおよびWRITE権限をユーザーmanagerに付与します。 一方、 ROLE_EDITOR を持つユーザーは、3つのオブジェクトすべてに対して READ 権限を持ちますが、3番目のオブジェクトに対しては WRITE 権限しか持ちません( id = 3 [X174X ])。 さらに、ユーザー hr は、2番目のオブジェクトに対するREAD権限のみを持ちます。

ここでは、権限チェックにデフォルトの Spring ACL BasePermission クラスを使用しているため、 READ パーミッションのマスク値は1になり、のマスク値は1になります。 ]WRITE許可は2になります。 acl_entryのデータは次のようになります。

INSERT INTO acl_entry 
  (id, acl_object_identity, ace_order, 
  sid, mask, granting, audit_success, audit_failure) 
  VALUES
  (1, 1, 1, 1, 1, 1, 1, 1),
  (2, 1, 2, 1, 2, 1, 1, 1),
  (3, 1, 3, 3, 1, 1, 1, 1),
  (4, 2, 1, 2, 1, 1, 1, 1),
  (5, 2, 2, 3, 1, 1, 1, 1),
  (6, 3, 1, 3, 1, 1, 1, 1),
  (7, 3, 2, 3, 2, 1, 1, 1);

4.2. テストケース

まず、findAllメソッドを呼び出そうとします。

構成として、このメソッドは、ユーザーがREAD権限を持っているNoticeMessageのみを返します。

したがって、結果リストには最初のメッセージのみが含まれると予想されます。

@Test
@WithMockUser(username = "manager")
public void 
  givenUserManager_whenFindAllMessage_thenReturnFirstMessage(){
    List<NoticeMessage> details = repo.findAll();
 
    assertNotNull(details);
    assertEquals(1,details.size());
    assertEquals(FIRST_MESSAGE_ID,details.get(0).getId());
}

次に、ROLE_EDITORという役割を持つ任意のユーザーで同じメソッドを呼び出そうとします。 この場合、これらのユーザーは3つのオブジェクトすべてに対してREAD権限を持っていることに注意してください。

したがって、結果リストには3つのメッセージすべてが含まれると予想されます。

@Test
@WithMockUser(roles = {"EDITOR"})
public void 
  givenRoleEditor_whenFindAllMessage_thenReturn3Message(){
    List<NoticeMessage> details = repo.findAll();
    
    assertNotNull(details);
    assertEquals(3,details.size());
}

次に、 manager ユーザーを使用して、IDで最初のメッセージを取得し、そのコンテンツを更新しようとします。これはすべて正常に機能するはずです。

@Test
@WithMockUser(username = "manager")
public void 
  givenUserManager_whenFind1stMessageByIdAndUpdateItsContent_thenOK(){
    NoticeMessage firstMessage = repo.findById(FIRST_MESSAGE_ID);
    assertNotNull(firstMessage);
    assertEquals(FIRST_MESSAGE_ID,firstMessage.getId());
        
    firstMessage.setContent(EDITTED_CONTENT);
    repo.save(firstMessage);
        
    NoticeMessage editedFirstMessage = repo.findById(FIRST_MESSAGE_ID);
 
    assertNotNull(editedFirstMessage);
    assertEquals(FIRST_MESSAGE_ID,editedFirstMessage.getId());
    assertEquals(EDITTED_CONTENT,editedFirstMessage.getContent());
}

ただし、 ROLE_EDITOR ロールを持つユーザーが最初のメッセージのコンテンツを更新すると、システムはAccessDeniedExceptionをスローします。

@Test(expected = AccessDeniedException.class)
@WithMockUser(roles = {"EDITOR"})
public void 
  givenRoleEditor_whenFind1stMessageByIdAndUpdateContent_thenFail(){
    NoticeMessage firstMessage = repo.findById(FIRST_MESSAGE_ID);
 
    assertNotNull(firstMessage);
    assertEquals(FIRST_MESSAGE_ID,firstMessage.getId());
 
    firstMessage.setContent(EDITTED_CONTENT);
    repo.save(firstMessage);
}

同様に、 hr ユーザーはidで2番目のメッセージを見つけることができますが、更新に失敗します。

@Test
@WithMockUser(username = "hr")
public void givenUsernameHr_whenFindMessageById2_thenOK(){
    NoticeMessage secondMessage = repo.findById(SECOND_MESSAGE_ID);
    assertNotNull(secondMessage);
    assertEquals(SECOND_MESSAGE_ID,secondMessage.getId());
}

@Test(expected = AccessDeniedException.class)
@WithMockUser(username = "hr")
public void givenUsernameHr_whenUpdateMessageWithId2_thenFail(){
    NoticeMessage secondMessage = new NoticeMessage();
    secondMessage.setId(SECOND_MESSAGE_ID);
    secondMessage.setContent(EDITTED_CONTENT);
    repo.save(secondMessage);
}

5. 結論

この記事では、 SpringACLの基本的な構成と使用法について説明しました。

ご存知のとおり、 Spring ACL には、オブジェクト、原則/権限、および権限設定を管理するための特定のテーブルが必要でした。 これらのテーブルとのすべての対話、特に更新アクションは、 AclServiceを経由する必要があります。このサービスについては、今後の記事で基本的なCRUDアクションについて説明します。

デフォルトでは、 BasePermissionクラスで事前定義された権限に制限されています。

最後に、このチュートリアルの実装は、Githubにあります。