Introduksjon til Spring Security ACL

1. Introduksjon

Tilgangskontrolliste (ACL) er en liste over tillatelser knyttet til et objekt. An ACL spesifiserer hvilke identiteter som blir gitt hvilke operasjoner på et gitt objekt.

Vårsikkerhet Tilgangskontrollisteer en Vår komponent som støtter Domain Object Security. Enkelt sagt, Spring ACL hjelper med å definere tillatelser for spesifikk bruker / rolle på et enkelt domeneobjekt - i stedet for over hele linjen, på det typiske per operasjonsnivået.

For eksempel en bruker med rollen Administrator kan se (LESE) og rediger (SKRIVE) alle meldinger på en Sentral varselboks, men den normale brukeren kan bare se meldinger, forholde seg til dem og kan ikke redigere. I mellomtiden bruker andre rollen Redaktør kan se og redigere noen spesifikke meldinger.

Derfor har forskjellige brukere / roller forskjellige tillatelser for hvert enkelt objekt. I dette tilfellet, Vår ACL er i stand til å oppnå oppgaven. Vi vil undersøke hvordan du setter opp grunnleggende tillatelseskontroll med Vår ACL i denne artikkelen.

2. Konfigurasjon

2.1. ACL-database

Å bruke Spring Security ACL, må vi lage fire obligatoriske tabeller i databasen vår.

Første bord er ACL_CLASS, som lagrer klassenavn for domenen, inneholder kolonnene:

  • ID
  • KLASSE: klassenavnet på sikrede domeneobjekter, for eksempel:com.baeldung.acl.persistence.entity.NoticeMessage

For det andre trenger vi ACL_SID tabell som lar oss universelt identifisere ethvert prinsipp eller autoritet i systemet. Bordet trenger:

  • ID
  • SID: som er brukernavnet eller rollenavnet. SID står for Sikkerhetsidentitet
  • REKTOR: 0 eller 1, for å indikere at den tilsvarende SID er rektor (bruker, for eksempel Mary, Mike, Jack ...) eller en autoritet (rolle, for eksempel ROLE_ADMIN, ROLE_USER, ROLE_EDITOR ...)

Neste tabell er ACL_OBJECT_IDENTITY, som lagrer informasjon for hvert unike domeneobjekt:

  • ID
  • OBJECT_ID_CLASS: definere domeneobjektklassen,lenker til ACL_CLASS bord
  • OBJECT_ID_IDENTITY: domeneobjekter kan lagres i mange tabeller, avhengig av klasse. Derfor lagrer dette feltet primærnøkkelen for målobjektet
  • PARENT_OBJECT: spesifiser foreldre til dette Objektidentitet innenfor denne tabellen
  • OWNER_SID: ID av objekteieren, lenker til ACL_SID bord
  • ENTRIES_INHERITTING: om ACL-oppføringer av dette objektet arver fra det overordnede objektet (ACL-oppføringer er definert i ACL_ENTRY bord)

Til slutt, ACL_ENTRY lagre individuelle tillatelser tildeles hver SID på en Objektidentitet:

  • ID
  • ACL_OBJECT_IDENTITY: spesifiser objektets identitet, lenker til ACL_OBJECT_IDENTITY bord
  • ACE_ORDER: rekkefølgen for gjeldende oppføring i ACL-oppføringer liste over tilsvarende Objektidentitet
  • SID: målet SID som tillatelsen gis til eller nektes fra, lenker til ACL_SID bord
  • MASKE: heltallsbitmasken som representerer den faktiske tillatelsen som gis eller nektes
  • TILDELING: verdi 1 betyr tildeling, verdi 0 betyr å fornekte
  • AUDIT_SUCCESS og AUDIT_FAILURE: for revisjonsformål

2.2. Avhengighet

Å kunne bruke Vår ACL La oss først definere våre avhengigheter i prosjektet vårt:

 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 

Vår ACL krever hurtigbuffer for å lagre Objektidentitet og ACL-oppføringer, så vi bruker Ehcache her. Og for å støtte Ehcache i Vår, vi trenger også vår-kontekst-støtte.

Når vi ikke jobber med Spring Boot, må vi legge til versjoner eksplisitt. Disse kan sjekkes på Maven Central: spring-security-acl, spring-security-config, spring-context-support, ehcache-core.

2.3. ACL-relatert konfigurasjon

Vi må sikre alle metodene som returnerer sikre domeneobjekter, eller gjøre endringer i objektet, ved å aktivere Global metodesikkerhet:

@Configuration @EnableGlobalMethodSecurity (prePostEnabled = true, secureEnabled = true) offentlig klasse AclMethodSecurityConfiguration utvider GlobalMethodSecurityConfiguration {@Autowired MethodSecurityExpressionHandler defaultMethodSecurityExpressionHandler; @ Override-beskyttet MethodSecurityExpressionHandler createExpressionHandler () {return defaultMethodSecurityExpressionHandler; }}

La oss også aktivere Uttrykkbasert tilgangskontroll ved å sette prePostEnabled til ekte å bruke Våruttrykk (SpEL). Dessuten, vi trenger en uttrykksbehandler med ACL Brukerstøtte:

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

Derfor, vi tildeler AclPermissionEvaluator til DefaultMethodSecurityExpressionHandler. Evaluatoren trenger en MutableAclService for å laste inn tillatelsesinnstillinger og domeneobjektets definisjoner fra databasen.

For enkelhets skyld bruker vi den medfølgende JdbcMutableAclService:

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

Som navnet sitt, JdbcMutableAclService bruker JDBCTemplate for å forenkle tilgangen til databasen. Det trenger en Datakilde (til JDBCTemplate), Oppslagstrategi (gir en optimalisert oppslag når du spør databasen), og en AclCache (caching ACLInnganger og Objektidentitet).

Igjen, for enkelhets skyld, bruker vi gitt BasicLookupStrategy og EhCacheBasedAclCache.

@Autowired DataSource dataSource; @Bean public AclAuthorizationStrategy aclAuthorizationStrategy () {return new AclAuthorizationStrategyImpl (new SimpleGrantedAuthority ("ROLE_ADMIN")); } @Bean public PermissionGrantingStrategy permissionGrantingStrategy () {returner nytt DefaultPermissionGrantingStrategy (ny ConsoleAuditLogger ()); } @Bean public EhCacheBasedAclCache aclCache () {return new EhCacheBasedAclCache (aclEhCacheFactoryBean (). GetObject (), permissionGrantingStrategy (), aclAuthorizationStrategy ()); } @Bean public EhCacheFactoryBean aclEhCacheFactoryBean () {EhCacheFactoryBean ehCacheFactoryBean = ny EhCacheFactoryBean (); ehCacheFactoryBean.setCacheManager (aclCacheManager (). getObject ()); ehCacheFactoryBean.setCacheName ("aclCache"); returner ehCacheFactoryBean; } @Bean public EhCacheManagerFactoryBean aclCacheManager () {returner nye EhCacheManagerFactoryBean (); } @Bean public LookupStrategy lookupStrategy () {return new BasicLookupStrategy (dataSource, aclCache (), aclAuthorizationStrategy (), new ConsoleAuditLogger ()); } 

Her, den AclAuthorizationStrategy har ansvaret for å konkludere med om en nåværende bruker har alle nødvendige tillatelser til bestemte objekter eller ikke.

Det trenger støtte fra TillatelseGivendeStrategi, som definerer logikken for å bestemme om en tillatelse gis til et bestemt SID.

3. Metodesikkerhet med ACL på våren

Så langt har vi gjort alle nødvendige konfigurasjoner. Nå kan vi sette nødvendige kontrollregler på våre sikrede metoder.

Som standard, Vår ACL refererer til BasePermission klasse for alle tilgjengelige tillatelser. I utgangspunktet har vi en LES, SKRIV, OPPRETT, SLETT og ADMINISTRASJON tillatelse.

La oss prøve å definere noen sikkerhetsregler:

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

Etter utførelsen av findAll () metode, @PostFilter vil bli utløst. Den nødvendige regelen hasPermission (filterObject, ‘LES’), betyr å returnere bare de Merknad Melding hvilken nåværende bruker som har LESE tillatelse på.

På samme måte, @PostAuthorize utløses etter utførelsen av findById () må du sørge for at du bare returnerer Merknad Melding objektet hvis den nåværende brukeren har LESE tillatelse på det. Hvis ikke, vil systemet kaste et AccessDeniedException.

På den andre siden utløser systemet @PreAuthorize kommentar før du påberoper deg lagre() metode. Den vil bestemme hvor den tilsvarende metoden får utføre eller ikke. Hvis ikke, AccessDeniedException vil bli kastet.

4. I aksjon

Nå skal vi teste alle disse konfigurasjonene ved hjelp av JUnit. Vi bruker H2 database for å holde konfigurasjonen så enkel som mulig.

Vi må legge til:

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

4.1. Scenarioet

I dette scenariet har vi to brukere (manager, hr) og en brukerrolle (ROLE_EDITOR), så vår acl_sid vil være:

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

Så må vi erklære Merknad Melding klasse i acl_class. Og tre forekomster av Merknad Melding klasse vil bli satt inn i systemmelding.

Videre må tilsvarende poster for de tre forekomstene deklareres i 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 IN 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);

I utgangspunktet gir vi LESE og SKRIVE tillatelser for det første objektet (id = 1) til brukeren sjef. I mellomtiden, enhver bruker med ROLE_EDITOR vil ha LESE tillatelse på alle tre objektene, men har bare SKRIVE tillatelse til det tredje objektet (id = 3). Dessuten bruker hr vil bare ha LESE tillatelse på det andre objektet.

Her, fordi vi bruker standard Vår ACLBasePermission klasse for tillatelseskontroll, maskeverdien til LESE tillatelse vil være 1, og maskeverdien på SKRIVE tillatelse vil være 2. Våre data i acl_entry vil være:

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, 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. Testforsøk

Først av alt prøver vi å kalle finn alle metode.

Som vår konfigurasjon returnerer metoden bare de Merknad Melding som brukeren har LESE tillatelse.

Derfor forventer vi at resultatlisten bare inneholder den første meldingen:

@Test @WithMockUser (brukernavn = "manager") offentlig ugyldig gittUserManager_whenFindAllMessage_thenReturnFirstMessage () {Listedetaljer = repo.findAll (); assertNotNull (detaljer); assertEquals (1, details.size ()); assertEquals (FIRST_MESSAGE_ID, details.get (0) .getId ()); }

Så prøver vi å kalle den samme metoden med enhver bruker som har rollen - ROLE_EDITOR. Merk at i dette tilfellet har disse brukerne LESE tillatelse på alle tre objektene.

Derfor forventer vi at resultatlisten vil inneholde alle tre meldingene:

@Test @WithMockUser (roller = {"EDITOR"}) offentlig ugyldig givenRoleEditor_whenFindAllMessage_thenReturn3Message () {Listedetaljer = repo.findAll (); assertNotNull (detaljer); assertEquals (3, details.size ()); }

Deretter bruker du sjef bruker, vil vi prøve å få den første meldingen etter id og oppdatere innholdet - som alle skal fungere bra:

@Test @WithMockUser (brukernavn = "manager") offentlig ugyldig gittUserManager_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 (redigertFirstMessage); assertEquals (FIRST_MESSAGE_ID, editedFirstMessage.getId ()); assertEquals (EDITTED_CONTENT, editedFirstMessage.getContent ()); }

Men hvis noen brukere med ROLE_EDITOR rolle oppdaterer innholdet i den første meldingen - systemet vårt vil kaste et AccessDeniedException:

@Test (forventet = AccessDeniedException.class) @WithMockUser (roller = {"EDITOR"}) offentlig ugyldig givenRoleEditor_whenFind1stMessageByIdAndUpdateContent_thenFail () {NoticeMessage firstMessage = repo.findByIdAGE (FIRST_M) assertNotNull (firstMessage); assertEquals (FIRST_MESSAGE_ID, firstMessage.getId ()); firstMessage.setContent (EDITTED_CONTENT); repo.save (firstMessage); }

Tilsvarende hr brukeren kan finne den andre meldingen etter id, men klarer ikke å oppdatere den:

@Test @WithMockUser (brukernavn = "hr") offentlig ugyldig gittUsernameHr_whenFindMessageById2_thenOK () {NoticeMessage secondMessage = repo.findById (SECOND_MESSAGE_ID); assertNotNull (secondMessage); assertEquals (SECOND_MESSAGE_ID, secondMessage.getId ()); } @Test (forventet = AccessDeniedException.class) @WithMockUser (brukernavn = "hr") offentlig ugyldig gittUsernameHr_whenUpdateMessageWithId2_thenFail () {NoticeMessage secondMessage = new NoticeMessage (); secondMessage.setId (SECOND_MESSAGE_ID); secondMessage.setContent (EDITTED_CONTENT); repo.save (secondMessage); }

5. Konklusjon

Vi har gått gjennom grunnleggende konfigurasjon og bruk av Vår ACL i denne artikkelen.

Som vi vet, Vår ACL krevde spesifikke tabeller for å administrere objekt, prinsipp / autoritet og tillatelsesinnstilling. Alle interaksjoner med disse tabellene, spesielt oppdateringshandlinger, må gå gjennom AclService. Vi vil utforske denne tjenesten for grunnleggende CRUD handlinger i en fremtidig artikkel.

Som standard er vi begrenset til forhåndsdefinerte tillatelser i BasePermission klasse.

Endelig kan implementeringen av denne opplæringen finnes på Github.