Einführung in die Spring Security ACL

1. Einführung

Access Control List ( ACL) ist eine Liste von Berechtigungen, die einem Objekt zugeordnet sind. Ein ACL gibt an, welchen Identitäten welche Operationen an einem bestimmten Objekt gewährt werden.

Spring-Sicherheit Access-Steuerelementliste _ ist eine Spring -Komponente, die Domain-Objektsicherheit unterstützt ._ Spring-ACL vereinfacht die Definition von Berechtigungen für einen bestimmten Benutzer/eine bestimmte Rolle für ein einzelnes Domänenobjekt Niveau.

Zum Beispiel kann ein Benutzer mit der Rolle Admin ( READ) und editieren ( WRITE) alle Nachrichten in einer Central Notice Box sehen, der normale Benutzer kann jedoch nur Nachrichten sehen, sich auf sie beziehen und sie nicht bearbeiten. In der Zwischenzeit können andere Benutzer mit der Rolle Editor einige bestimmte Nachrichten sehen und 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. Aufbau

2.1. ACL-Datenbank

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

Die erste Tabelle ist ACL CLASS__, die den Klassennamen des Domänenobjekts speichert. Die Spalten enthalten:

  • ICH WÜRDE

  • CLASS: der Klassenname der gesicherten Domänenobjekte, zum Beispiel:

_org.baeldung.acl.persistence.entity.NoticeMessage _

Zweitens benötigen wir die ACL SID__-Tabelle, die es uns ermöglicht, alle Prinzipien oder Befugnisse im System universell zu identifizieren. Der Tisch braucht:

  • ICH WÜRDE

  • SID: Dies ist der Benutzername oder der Name der Rolle. SID steht für __Security

Identität ** PRINCIPAL: 0 oder 1 , um anzuzeigen, dass die entsprechende SID__ a ist

Principal (Benutzer wie mary, mike, jack …​ ) oder eine Autorität (Rolle wie ROLE ADMIN, ROLE USER, ROLE EDITOR …​__)

Die nächste Tabelle ist ACL OBJECT IDENTITY, , die Informationen für jedes eindeutige Domänenobjekt speichert:

  • ICH WÜRDE

  • OBJECT ID CLASS: Definiert die Domänenobjektklasse, __ verlinkt zu

ACL CLASS -Tabelle ** OBJECT ID IDENTITY: __ Domänenobjekte können in vielen Tabellen gespeichert werden

abhängig von der Klasse. Daher speichert dieses Feld das Zielobjekt Primärschlüssel ** PARENT OBJECT: legt das übergeordnete Element dieser Object-Identität in diesem fest

Tabelle ** OWNER SID: ID des Objektbesitzers, verknüpft mit der ACL SID -Tabelle

  • ENTRIES INHERITTING: ob ACL-Einträge__ dieses Objekts erben

aus dem übergeordneten Objekt ( ACL Entries werden in der Tabelle ACL ENTRY__ definiert)

Schließlich weist der ACL ENTRY Store-Speicher eine individuelle Berechtigung für jeden SID in einer Object-Identität zu:

  • ICH WÜRDE

  • ACL OBJECT IDENTITY: Geben Sie die Objektidentität an, zu der eine Verknüpfung besteht

ACL OBJECT IDENTITY -Tabelle ** ACL ORDER: die Reihenfolge des aktuellen Eintrags in der ACL-Einträge__ Liste von

entsprechende Object-Identität ** SID: das Ziel SID , dem die Berechtigung erteilt oder verweigert wird

Von, Links zu ACL SID -Tabelle ** MASK: __ Die Ganzzahl-Bitmaske, die die tatsächliche Berechtigung darstellt

gewährt oder abgelehnt werden ** GRANTING: Wert 1 bedeutet Gewährung, Wert 0 bedeutet Ablehnen

  • AUDIT SUCCESS und AUDIT FAILURE : zu Prüfzwecken

2.2. Abhängigkeit

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

<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 erfordert einen Cache zum Speichern von Object Identity - und ACL-Einträgen , sodass wir hier Ehcache verwenden. Um Ehcache in Spring zu unterstützen, benötigen wir auch den spring-context-support.

Wenn Sie nicht mit Spring Boot arbeiten, müssen Sie Versionen explizit hinzufügen.

Diese können auf Maven Central überprüft werden:

2.3. ACL-bezogene Konfiguration

Wir müssen alle Methoden sichern, die gesicherte Domänenobjekte zurückgeben oder Änderungen am Objekt vornehmen, indem Sie Global Method Security aktivieren:

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

    @Autowired
    MethodSecurityExpressionHandler
      defaultMethodSecurityExpressionHandler;

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

Lassen Sie uns auch Expression-Based Access Control aktivieren, indem Sie prePostEnabled auf true setzen, um __Spring Expression Language (SpEL) zu verwenden.

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

_, weisen wir AclPermissionEvaluator dem DefaultMethodSecurityExpressionHandler zu. Der Evaluator benötigt einen MutableAclService_ , um Berechtigungseinstellungen und Definitionen von Domänenobjekten aus der Datenbank zu laden.

Zur Vereinfachung 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 bei der Abfrage der Datenbank) und einen AclCache ( caching ACL Entries und Object Identity) _ .

Zur Vereinfachung verwenden wir wiederum 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()
    );
}

Die AclAuthorizationStrategy ist hier für die Entscheidung verantwortlich, 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, ob einer bestimmten SID eine Berechtigung erteilt wird, definiert.

3. Methodensicherheit mit Spring-ACL

Bis jetzt haben wir alle erforderlichen Konfigurationen vorgenommen. Jetzt können wir die erforderlichen Überprüfungsregeln für unsere gesicherten Methoden festlegen.

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

Versuchen wir einige Sicherheitsregeln zu definieren:

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

Nach der Ausführung der findAll () -Methode wird @ PostFilter ausgelöst. Die erforderliche Regel hasPermission (filterObject, 'READ'), bedeutet, dass nur die NoticeMessage zurückgegeben werden, für die der aktuelle Benutzer die Berechtigung READ hat.

Ebenso wird @ PostAuthorize nach der Ausführung der Methode findById () ausgelöst. Stellen Sie sicher, dass das Objekt NoticeMessage nur zurückgegeben wird, wenn der aktuelle Benutzer die Berechtigung READ hat. Wenn nicht, wirft das System eine AccessDeniedException aus.

Auf der anderen Seite löst das System die Annotation @ PreAuthorize aus, bevor die Methode save () aufgerufen wird. Es wird entscheiden, wo die entsprechende Methode ausgeführt werden darf oder nicht. Wenn nicht, wird AccessDeniedException ausgelöst.

4. In Aktion

Jetzt testen wir all diese Konfigurationen mit JUnit . Wir verwenden die H2 -Datenbank, um die Konfiguration so einfach wie möglich zu gestalten.

Wir müssen hinzufügen:

<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. Das Szenario

In diesem Szenario haben wir zwei Benutzer ( manager, hr) und eine Benutzerrolle ( ROLE EDITOR) , sodass acl sid Folgendes ist:

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

Dann müssen wir die NoticeMessage -Klasse in acl class deklarieren. Drei Instanzen der NoticeMessage -Klasse werden in system message. eingefügt.

Darüber hinaus müssen entsprechende Datensätze für diese 3 Instanzen in acl object identity deklariert werden:

INSERT INTO acl__class (id, class) VALUES
  (1, 'org.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);

Anfangs erteilen wir dem Benutzer manager die Berechtigungen READ und WRITE für das erste Objekt ( id = 1 ). In der Zwischenzeit hat jeder Benutzer mit ROLE EDITOR die Berechtigung READ für alle drei Objekte, jedoch nur die Berechtigung WRITE für das dritte Objekt ( id = 3 ). Außerdem hat der Benutzer hr nur die Berechtigung READ__ für das zweite Objekt.

Da wir für die Berechtigungsprüfung die standardmäßige Spring ACL BasePermission -Klasse verwenden, ist der Maskenwert der READ -Berechtigung 1, und der Maskenwert der WRITE -Berechtigung ist 2. Unsere Daten in acl entry__ sind:

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. Testfall

Zunächst versuchen wir, die Methode findAll aufzurufen.

Bei unserer Konfiguration gibt die Methode nur die NoticeMessage zurück, für die der Benutzer die READ -Berechtigung besitzt.

Wir erwarten daher, dass die Ergebnisliste nur die erste Nachricht enthält:

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

Dann versuchen wir, dieselbe Methode mit jedem Benutzer aufzurufen, der die Rolle ROLE EDITOR hat. Beachten Sie, dass diese Benutzer in diesem Fall die Berechtigung READ__ für alle drei Objekte haben.

Daher erwarten wir, dass die Ergebnisliste alle drei Meldungen enthält:

@Test
@WithMockUser(roles = {"EDITOR"})
public void
  givenRoleEditor__whenFindAllMessage__thenReturn3Message(){
    List<NoticeMessage> details = repo.findAll();

    assertNotNull(details);
    assertEquals(3,details.size());
}

Als Nächstes versuchen wir mithilfe des Benutzers manager , die erste Nachricht nach ID abzurufen und den Inhalt zu aktualisieren.

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

Wenn jedoch ein Benutzer mit der Rolle ROLE EDITOR den Inhalt der ersten Nachricht aktualisiert, gibt unser System eine AccessDeniedException__ aus:

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

In ähnlicher Weise kann der Benutzer hr die zweite Nachricht anhand der ID finden, kann diese jedoch nicht aktualisieren:

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

In diesem Artikel haben wir die grundlegende Konfiguration und Verwendung von Spring ACL durchlaufen.

Wie wir wissen, benötigte Spring ACL bestimmte Tabellen zum Verwalten von Objekt, Prinzip/Berechtigung und Berechtigungseinstellungen. Alle Interaktionen mit diesen Tabellen, insbesondere die Aktualisierungsaktion, müssen AclService durchlaufen. Wir werden diesen Dienst in einem zukünftigen Artikel auf grundlegende CRUD__-Aktionen untersuchen.

Standardmäßig sind wir auf vordefinierte Berechtigungen in der Klasse __BasePermissio __n beschränkt.

Die Implementierung dieses Tutorials finden Sie schließlich unter https://github.com/eugenp/tutorials/tree/master/spring-security-acl Über Github].