Pengantar Spring Security ACL

1. Perkenalan

Access Control List ( ACL) adalah daftar izin yang dilampirkan ke suatu objek. Sebuah ACL menspesifikasikan identitas yang diberikan yang operasi pada objek tertentu.

Spring Security Access Control List adalah sebuah semi komponen yang mendukung Domain Object Keamanan. Sederhananya, Spring ACL membantu dalam menentukan izin untuk pengguna / peran tertentu pada objek domain tunggal - bukan di seluruh papan, pada tingkat per operasi yang khas.

Misalnya, pengguna dengan peran Admin dapat melihat ( BACA) dan mengedit ( MENULIS) semua pesan di Kotak Pemberitahuan Pusat , tetapi pengguna biasa hanya dapat melihat pesan, berhubungan dengan mereka dan tidak dapat mengedit. Sementara itu, pengguna lain dengan peran Editor dapat melihat dan mengedit beberapa pesan tertentu.

Karenanya, pengguna / peran yang berbeda memiliki izin yang berbeda untuk setiap objek tertentu. Dalam hal ini, Spring ACL mampu mencapai tugas tersebut. Kami akan menjelajahi cara menyiapkan pemeriksaan izin dasar dengan Spring ACL di artikel ini.

2. Konfigurasi

2.1. Basis Data ACL

Untuk menggunakan Spring Security ACL , kita perlu membuat empat tabel wajib dalam database kita.

Tabel pertama adalah ACL_CLASS , yang menyimpan nama kelas dari objek domain, kolomnya meliputi:

  • Indo
  • KELAS: nama kelas objek domain aman, misalnya: com.baeldung.acl.persistence.entity.NoticeMessage

Kedua, kita memerlukan tabel ACL_SID yang memungkinkan kita mengidentifikasi prinsip atau otoritas apa pun secara universal dalam sistem. Tabel membutuhkan:

  • Indo
  • SID: yang merupakan nama pengguna atau nama peran. SID adalah singkatan dari Security Identity
  • PRINCIPAL: 0 atau 1 , untuk menunjukkan bahwa SID terkait adalah principal (pengguna, seperti mary, mike, jack… ) atau otoritas (peran, seperti ROLE_ADMIN, ROLE_USER, ROLE_EDITOR… )

Tabel berikutnya adalah ACL_OBJECT_IDENTITY, yang menyimpan informasi untuk setiap objek domain unik:

  • Indo
  • OBJECT_ID_CLASS: mendefinisikan kelas objek domain,tautan ke tabel ACL_CLASS
  • OBJECT_ID_IDENTITY: objek domain dapat disimpan di banyak tabel tergantung pada kelasnya. Oleh karena itu, bidang ini menyimpan kunci utama objek target
  • PARENT_OBJECT: tentukan induk dari Identitas Objek dalam tabel ini
  • OWNER_SID: ID pemilik objek, ditautkan ke tabel ACL_SID
  • ENTRIES_INHERITTING: apakah Entri ACL objek ini mewarisi dari objek induk ( Entri ACL ditentukan dalam tabel ACL_ENTRY )

Akhirnya, ACL_ENTRY menyimpan izin individu yang diberikan ke setiap SID pada Identitas Objek :

  • Indo
  • ACL_OBJECT_IDENTITY: tentukan identitas objek, tautan ke tabel ACL_OBJECT_IDENTITY
  • ACE_ORDER: urutan entri saat ini dalam daftar entri ACL dari Identitas Objek yang sesuai
  • SID: target SID yang izin diberikan atau ditolak dari, link ke ACL_SID meja
  • MASK: topeng bit integer yang mewakili izin sebenarnya yang diberikan atau ditolak
  • PEMBERIAN: nilai 1 berarti memberi, nilai 0 berarti menolak
  • AUDIT_SUCCESS dan AUDIT_FAILURE : untuk tujuan audit

2.2. Ketergantungan

Untuk dapat menggunakan Spring ACL dalam proyek kita, pertama-tama tentukan dependensi kita:

 org.springframework.security spring-security-acl   org.springframework.security spring-security-config   org.springframework spring-context-support   net.sf.ehcache ehcache-core 2.6.11 

Spring ACL membutuhkan cache untuk menyimpan entri Object Identity dan ACL , jadi kita akan menggunakan Ehcache di sini. Dan, untuk mendukung Ehcache di Spring, kami juga membutuhkan dukungan konteks pegas.

Saat tidak bekerja dengan Spring Boot, kita perlu menambahkan versi secara eksplisit. Itu bisa diperiksa di Maven Central: spring-security-acl, spring-security-config, spring-context-support, ehcache-core.

2.3. Konfigurasi Terkait ACL

Kita perlu mengamankan semua metode yang mengembalikan objek domain aman, atau membuat perubahan pada objek, dengan mengaktifkan Keamanan Metode Global:

@Configuration @EnableGlobalMethodSecurity(prePostEnabled = true, securedEnabled = true) public class AclMethodSecurityConfiguration extends GlobalMethodSecurityConfiguration { @Autowired MethodSecurityExpressionHandler defaultMethodSecurityExpressionHandler; @Override protected MethodSecurityExpressionHandler createExpressionHandler() { return defaultMethodSecurityExpressionHandler; } }

Mari kita juga mengaktifkan Kontrol Akses Berbasis Ekspresi dengan menyetel prePostEnabled ke true untuk menggunakan Spring Expression Language (SpEL) . Selain itu , kami membutuhkan penangan ekspresi dengan dukungan ACL :

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

Oleh karena itu , kami menetapkan AclPermissionEvaluator ke DefaultMethodSecurityExpressionHandler . Penilai membutuhkan MutableAclService untuk memuat pengaturan izin dan definisi objek domain dari database.

Untuk mempermudah, kami menggunakan JdbcMutableAclService yang disediakan :

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

Seperti namanya, JdbcMutableAclService menggunakan template JDBCT untuk menyederhanakan akses database. Ini membutuhkan Sumber Data ( untuk template JDBCT) , LookupStrategy (menyediakan pencarian yang dioptimalkan saat membuat kueri database), dan AclCache ( menyimpan Entri ACL dan Identitas Objek dalam cache ) .

Sekali lagi, untuk kesederhanaan, kami menggunakan BasicLookupStrategy dan EhCacheBasedAclCache yang disediakan .

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

Di sini, AclAuthorizationStrategy bertanggung jawab untuk menyimpulkan apakah pengguna saat ini memiliki semua izin yang diperlukan pada objek tertentu atau tidak.

Ini membutuhkan dukungan PermissionGrantingStrategy, yang mendefinisikan logika untuk menentukan apakah izin diberikan ke SID tertentu .

3. Metode Pengamanan Dengan Spring ACL

Sejauh ini, kami telah melakukan semua konfigurasi yang diperlukan . Sekarang kita dapat menempatkan aturan pemeriksaan yang diperlukan pada metode aman kita.

Secara default, Spring ACL mengacu pada kelas BasePermission untuk semua izin yang tersedia. Pada dasarnya, kami memiliki izin BACA, TULIS, BUAT, HAPUS, dan ADMINISTRASI .

Mari kita coba untuk mendefinisikan beberapa aturan keamanan:

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

After the execution of findAll() method, @PostFilter will be triggered. The required rule hasPermission(filterObject, ‘READ'), means returning only those NoticeMessage which current user has READ permission on.

Similarly, @PostAuthorize is triggered after the execution of findById() method, make sure only return the NoticeMessage object if the current user has READ permission on it. If not, the system will throw an AccessDeniedException.

On the other side, the system triggers the @PreAuthorize annotation before invoking the save() method. It will decide where the corresponding method is allowed to execute or not. If not, AccessDeniedException will be thrown.

4. In Action

Now we gonna test all those configurations using JUnit. We'll use H2 database to keep configuration as simple as possible.

We'll need to add:

 com.h2database h2   org.springframework spring-test test   org.springframework.security spring-security-test test 

4.1. The Scenario

In this scenario, we'll have two users (manager, hr) and a one user role (ROLE_EDITOR), so our acl_sid will be:

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

Then, we need to declare NoticeMessage class in acl_class. And three instances of NoticeMessage class will be inserted in system_message.

Moreover, corresponding records for those 3 instances must be declared in 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);

Initially, we grant READ and WRITE permissions on the first object (id =1) to the user manager. Meanwhile, any user with ROLE_EDITOR will have READ permission on all three objects but only possess WRITE permission on the third object (id=3). Besides, user hr will have only READ permission on the second object.

Here, because we use default Spring ACLBasePermission class for permission checking, the mask value of the READ permission will be 1, and the mask value of WRITE permission will be 2. Our data in acl_entry will be:

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. Test Case

First of all, we try to call the findAll method.

As our configuration, the method returns only those NoticeMessage on which the user has READ permission.

Hence, we expect the result list contains only the first message:

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

Then we try to call the same method with any user which has the role – ROLE_EDITOR. Note that, in this case, these users have the READ permission on all three objects.

Hence, we expect the result list will contain all three messages:

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

Next, using the manager user, we'll try to get the first message by id and update its content – which should all work fine:

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

But if any user with the ROLE_EDITOR role updates the content of the first message – our system will throw an 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); }

Similarly, the hr user can find the second message by id, but will fail to update it:

@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. Conclusion

Kami telah membahas konfigurasi dasar dan penggunaan Spring ACL di artikel ini.

Seperti yang kita ketahui, Spring ACL membutuhkan tabel khusus untuk mengatur objek, prinsip / otoritas, dan pengaturan izin. Semua interaksi dengan tabel tersebut, terutama tindakan pembaruan, harus melalui AclService. Kami akan menjelajahi layanan ini untuk tindakan CRUD dasar di artikel mendatang.

Secara default, kami dibatasi untuk izin yang telah ditentukan di kelas BasePermissio n.

Akhirnya, implementasi tutorial ini dapat ditemukan di Github.