Einführung in Spring Security ACL

1. Einleitung

Access Control List ( ACL) ist eine Liste von Berechtigungen, die an ein Objekt angehängt sind. Eine ACL gibt an, welche Identitäten welchen Operationen für ein bestimmtes Objekt gewährt werden.

Die Spring Security- Zugriffssteuerungsliste ist eine Spring- Komponente, die die Domänenobjektsicherheit unterstützt . Einfach ausgedrückt, hilft Spring ACL beim Definieren von Berechtigungen für bestimmte Benutzer / Rollen für ein einzelnes Domänenobjekt - anstatt auf der typischen Ebene pro Operation auf der ganzen Linie.

Zum Beispiel kann ein Benutzer mit der Rolle Admin sehen ( READ) und Bearbeiten ( WRITE) alle Nachrichten auf einem Zentral Hinweis Box , aber die normalen Benutzer können Nachrichten nur sehen, beziehen sich auf sie und können nicht bearbeiten. Inzwischen andere Benutzer mit der Rolle Editor können einige spezifische Nachrichten sehen und zu bearbeiten.

Daher haben unterschiedliche Benutzer / Rollen unterschiedliche Berechtigungen für jedes bestimmte Objekt. In diesem Fall kann Spring ACL die Aufgabe erfüllen. In diesem Artikel erfahren Sie, wie Sie die grundlegende Berechtigungsprüfung mit Spring ACL einrichten .

2. Konfiguration

2.1. ACL-Datenbank

Um Spring Security ACL verwenden zu können , müssen vier obligatorische Tabellen in unserer Datenbank erstellt werden.

Die erste Tabelle ist ACL_CLASS , in der der Klassenname des Domänenobjekts gespeichert wird. Zu den Spalten gehören:

  • ICH WÜRDE
  • CLASS: Der Klassenname gesicherter Domänenobjekte , z. B. com.baeldung.acl.persistence.entity.NoticeMessage

Zweitens benötigen wir die Tabelle ACL_SID , mit der wir jedes Prinzip oder jede Autorität im System universell identifizieren können. Der Tisch braucht:

  • ICH WÜRDE
  • SID: Dies ist der Benutzername oder Rollenname. SID steht für Security Identity
  • PRINCIPAL: 0 oder 1 , um anzuzeigen, dass die entsprechende SID ein Principal (Benutzer wie Mary, Mike, Jack… ) oder eine Autorität (Rolle wie ROLE_ADMIN, ROLE_USER, ROLE_EDITOR… ) ist.

Die nächste Tabelle ist ACL_OBJECT_IDENTITY, in der Informationen für jedes eindeutige Domänenobjekt gespeichert werden:

  • ICH WÜRDE
  • OBJECT_ID_CLASS: Definieren Sie die Domänenobjektklasse.Links zur Tabelle ACL_CLASS
  • OBJECT_ID_IDENTITY: Domänenobjekte können je nach Klasse in vielen Tabellen gespeichert werden. In diesem Feld wird daher der Primärschlüssel des Zielobjekts gespeichert
  • PARENT_OBJECT: angeben Eltern dieser Objektidentität in dieser Tabelle
  • OWNER_SID: ID des Objektbesitzers, Links zur Tabelle ACL_SID
  • ENTRIES_INHERITTING: Gibt an, ob ACL-Einträge dieses Objekts vom übergeordneten Objekt erben ( ACL-Einträge sind in der Tabelle ACL_ENTRY definiert.)

Schließlich wird die ACL_ENTRY speichern einzelne Erlaubnis ordnet jeden SID auf einer Objektidentität :

  • ICH WÜRDE
  • ACL_OBJECT_IDENTITY: Geben Sie die Objektidentität an und verknüpfen Sie sie mit der Tabelle ACL_OBJECT_IDENTITY
  • ACE_ORDER: die Reihenfolge des aktuellen Eintrags in der ACL - Einträgen Liste der entsprechenden Objektidentität
  • SID: Die Ziel- SID, der die Berechtigung erteilt oder verweigert wird, ist mit der Tabelle ACL_SID verknüpft
  • MASKE: Die Ganzzahl-Bitmaske, die die tatsächliche Berechtigung darstellt, die erteilt oder verweigert wird
  • GEWÄHRUNG: Wert 1 bedeutet Gewährung, Wert 0 bedeutet Ablehnung
  • AUDIT_SUCCESS und AUDIT_FAILURE : zu Prüfungszwecken

2.2. Abhängigkeit

Um Spring ACL in unserem Projekt verwenden zu können, definieren wir zunächst unsere Abhängigkeiten:

 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 benötigt einen Cache zum Speichern von Objektidentitäts- und ACL-Einträgen , daher verwenden wir hier Ehcache . Und um Ehcache im Frühjahr zu unterstützen, benötigen wir auch die Unterstützung des Frühlingskontexts.

Wenn Sie nicht mit Spring Boot arbeiten, müssen Sie explizit Versionen hinzufügen. Diese können in Maven Central überprüft werden: spring-security-acl, spring-security-config, spring-context-support, ehcache-core.

2.3. ACL-bezogene Konfiguration

Wir müssen alle Methoden sichern, die gesicherte Domänenobjekte zurückgeben oder Änderungen am Objekt vornehmen, indem wir die globale Methodensicherheit aktivieren :

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

Aktivieren Sie auch die ausdrucksbasierte Zugriffssteuerung, indem Sie prePostEnabled auf true setzen , um Spring Expression Language (SpEL) zu verwenden . Darüber hinaus , müssen wir einen Ausdruck Handler mit ACL - Unterstützung:

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

Daher , wir weisen Sie AclPermissionEvaluator zum DefaultMethodSecurityExpressionHandler . Der Evaluator benötigt einen MutableAclService , um Berechtigungseinstellungen und Domänenobjektdefinitionen aus der Datenbank zu laden.

Der Einfachheit halber verwenden wir den bereitgestellten JdbcMutableAclService :

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

Der Name JdbcMutableAclService verwendet JDBCTemplate , um den Datenbankzugriff zu vereinfachen. Es benötigt eine DataSource ( für JDBCTemplate) , LookupStrategy (bietet eine optimierte Suche beim Abfragen der Datenbank) und einen AclCache ( Zwischenspeichern von ACL- Einträgen und Objektidentität ) .

Der Einfachheit halber verwenden wir wieder BasicLookupStrategy und 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() ); } 

Hier ist die AclAuthorizationStrategy dafür verantwortlich zu entscheiden, ob ein aktueller Benutzer alle erforderlichen Berechtigungen für bestimmte Objekte besitzt oder nicht.

Es benötigt die Unterstützung von PermissionGrantingStrategy, die die Logik zum Bestimmen definiert, ob einer bestimmten SID eine Berechtigung erteilt wird .

3. Methodensicherheit mit Spring ACL

Bisher haben wir alle erforderlichen Konfigurationen vorgenommen . Jetzt können wir unsere gesicherten Methoden mit den erforderlichen Überprüfungsregeln versehen.

Standardmäßig bezieht sich Spring ACL für alle verfügbaren Berechtigungen auf die BasePermission- Klasse. Grundsätzlich haben wir die Berechtigung READ, WRITE, CREATE, DELETE und ADMINISTRATION .

Versuchen wir einige Sicherheitsregeln zu definieren:

@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

In diesem Artikel haben wir die grundlegende Konfiguration und Verwendung von Spring ACL erläutert.

Wie wir wissen, erforderte Spring ACL bestimmte Tabellen zum Verwalten von Objekt-, Prinzip- / Berechtigungs- und Berechtigungseinstellungen. Alle Interaktionen mit diesen Tabellen, insbesondere das Aktualisieren von Aktionen, müssen über AclService erfolgen. Wir werden diesen Service in einem zukünftigen Artikel auf grundlegende CRUD- Aktionen untersuchen.

Standardmäßig sind wir auf vordefinierte Berechtigungen in der BasePermissio n-Klasse beschränkt.

Die Implementierung dieses Tutorials finden Sie auf Github.