Pengenalan kepada Spring Security ACL

1. Pengenalan

Access Control List ( ACL) adalah senarai kebenaran yang dilampirkan pada objek. An ACL Menentukan identiti diberikan mana operasi pada objek yang diberikan.

Spring Security Access Senarai Control adalah sebuah Spring komponen yang menyokong Domain Objek Keselamatan. Ringkasnya, Spring ACL membantu dalam menentukan kebenaran untuk pengguna / peranan tertentu pada objek domain tunggal - dan bukan secara keseluruhan, pada tahap per operasi biasa.

Sebagai contoh, pengguna dengan peranan Admin dapat melihat ( BACA) dan mengedit ( MENULIS) semua mesej pada Kotak Pemberitahuan Pusat , tetapi pengguna biasa hanya dapat melihat mesej, berhubungan dengan mereka dan tidak dapat mengedit. Sementara itu, pengguna lain dengan peranan Editor dapat melihat dan menyunting beberapa mesej tertentu.

Oleh itu, pengguna / peranan yang berbeza mempunyai kebenaran yang berbeza untuk setiap objek tertentu. Dalam kes ini, Spring ACL mampu mencapai tugas. Kami akan meneroka cara mengatur pemeriksaan kebenaran asas dengan Spring ACL dalam artikel ini.

2. Konfigurasi

2.1. Pangkalan Data ACL

Untuk menggunakan Spring Security ACL , kita perlu membuat empat jadual wajib dalam pangkalan data kita.

Jadual pertama ialah ACL_CLASS , yang menyimpan nama kelas objek domain, lajur merangkumi:

  • ID
  • KELAS: nama kelas objek domain selamat, contohnya: com.baeldung.acl.persistence.entity.NoticeMessage

Kedua, kita memerlukan jadual ACL_SID yang membolehkan kita mengenal pasti semua prinsip atau autoriti dalam sistem. Jadual memerlukan:

  • ID
  • SID: yang merupakan nama pengguna atau nama peranan. SID bermaksud Identiti Keselamatan
  • PRINSIP: 0 atau 1 , untuk menunjukkan bahawa SID yang sesuai adalah prinsipal (pengguna, seperti mary, mike, jack… ) atau pihak berkuasa (peranan, seperti ROLE_ADMIN, ROLE_USER, ROLE_EDITOR… )

Jadual seterusnya adalah ACL_OBJECT_IDENTITY, yang menyimpan maklumat untuk setiap objek domain unik:

  • ID
  • OBJECT_ID_CLASS: tentukan kelas objek domain,pautan ke jadual ACL_CLASS
  • OBJECT_ID_IDENTITY: objek domain boleh disimpan dalam banyak jadual bergantung pada kelas. Oleh itu, bidang ini menyimpan kunci utama objek sasaran
  • PARENT_OBJECT: tentukan induk Identiti Objek ini dalam jadual ini
  • OWNER_SID: ID pemilik objek, pautan ke jadual ACL_SID
  • ENTRIES_INHERITTING: adakah Entri ACL objek ini mewarisi dari objek induk ( Entri ACL ditentukan dalam jadual ACL_ENTRY )

Akhirnya, ACL_ENTRY menyimpan kebenaran individu yang diberikan kepada setiap SID pada Objek Identiti :

  • ID
  • ACL_OBJECT_IDENTITY: tentukan identiti objek, pautan ke jadual ACL_OBJECT_IDENTITY
  • ACE_ORDER: urutan kemasukan semasa dalam senarai entri ACL yang sesuai dengan Identiti Objek
  • SID: sasaran SID kebenaran itu diberikan kepada atau ditolak daripada, pautan ke ACL_SID Rajah
  • MASK: topeng bit integer yang mewakili kebenaran sebenarnya yang diberikan atau ditolak
  • PEMBERIAN: nilai 1 bermaksud pemberian, nilai 0 bermaksud penolakan
  • AUDIT_SUCCESS dan AUDIT_FAILURE : untuk tujuan pengauditan

2.2. Ketergantungan

Untuk dapat menggunakan Spring ACL dalam projek kami, mari tentukan terlebih dahulu kebergantungan kami:

 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 memerlukan cache untuk menyimpan Objek Identity dan entri ACL , jadi kami akan menggunakan Ehcache di sini. Dan, untuk menyokong Ehcache pada musim bunga, kami juga memerlukan sokongan konteks musim bunga.

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

2.3. Konfigurasi Berkaitan ACL

Kita perlu mengamankan semua kaedah yang mengembalikan objek domain selamat, atau membuat perubahan pada objek, dengan mengaktifkan Keselamatan Kaedah Global:

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

Mari juga aktifkan Akses Akses Berasaskan Ekspresi dengan menetapkan prePostEnabled ke true untuk menggunakan Bahasa Ekspresi Musim Semi (SpEL) . Selain itu , kami memerlukan pengendali ekspresi dengan sokongan ACL :

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

Oleh itu , kami menetapkan AclPermissionEvaluator kepada DefaultMethodSecurityExpressionHandler . Penilai memerlukan MutableAclService untuk memuat tetapan kebenaran dan definisi objek domain dari pangkalan data.

Untuk kesederhanaan, kami menggunakan JdbcMutableAclService yang disediakan :

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

Seperti namanya, JdbcMutableAclService menggunakan JDBCTemplate untuk mempermudah akses pangkalan data. Ia memerlukan sumber data ( untuk JDBCTemplate) , LookupStrategy (menyediakan dioptimumkan lookup apabila pertanyaan pangkalan data) dan AclCache ( cache ACL Penyertaan dan Objek Identity) .

Sekali lagi, bagi memudahkan, kita menggunakan disediakan BasicLookupStrategy dan 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() ); } 

Di sini, AclAuthorizationStrategy bertanggungjawab untuk membuat kesimpulan sama ada pengguna semasa mempunyai semua kebenaran yang diperlukan pada objek tertentu atau tidak.

Ia memerlukan sokongan PermissionGrantingStrategy, yang menentukan logik untuk menentukan sama ada kebenaran diberikan kepada SID tertentu .

3. Kaedah Keselamatan Dengan Spring ACL

Setakat ini, kami telah melakukan semua konfigurasi yang diperlukan . Sekarang kita boleh meletakkan peraturan periksa yang diperlukan pada kaedah selamat kita.

Secara lalai, Spring ACL merujuk kepada kelas BasePermission untuk semua kebenaran yang ada. Pada asasnya, kami mempunyai kebenaran BACA, TULIS, BUAT, HAPUS dan PENTADBIRAN .

Mari cuba tentukan beberapa peraturan keselamatan:

@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 melalui konfigurasi asas dan penggunaan Spring ACL dalam artikel ini.

Seperti yang kita ketahui, Spring ACL memerlukan jadual khusus untuk menguruskan objek, prinsip / wewenang, dan penetapan izin. Semua interaksi dengan jadual tersebut, terutamanya tindakan mengemas kini, mesti melalui AclService. Kami akan meneroka perkhidmatan ini untuk tindakan CRUD asas dalam artikel akan datang.

Secara lalai, kami terhad kepada kebenaran yang telah ditentukan di kelas BasePermissio n.

Akhirnya, pelaksanaan tutorial ini boleh didapati di Github.