Contents

Add the following to your pom.xml (Maven) or build.gradle (Gradle). Spring Boot manages compatible versions automatically when you use the Spring Boot BOM.

<dependency> <groupId>org.springframework.security</groupId> <artifactId>spring-security-acl</artifactId> </dependency> <!-- ACL uses Spring Cache for performance --> <dependency> <groupId>org.springframework.boot</groupId> <artifactId>spring-boot-starter-cache</artifactId> </dependency>

Spring Security ACL requires four tables. Run these DDL statements (PostgreSQL shown; adjust for MySQL/H2):

CREATE TABLE acl_sid ( id BIGSERIAL PRIMARY KEY, principal BOOLEAN NOT NULL, -- true = username, false = role sid VARCHAR(100) NOT NULL, -- username or role name UNIQUE (sid, principal) ); CREATE TABLE acl_class ( id BIGSERIAL PRIMARY KEY, class VARCHAR(255) NOT NULL UNIQUE, -- fully-qualified class name class_id_type VARCHAR(100) -- type of the object identity id ); CREATE TABLE acl_object_identity ( id BIGSERIAL PRIMARY KEY, object_id_class BIGINT NOT NULL REFERENCES acl_class(id), object_id_identity VARCHAR(36) NOT NULL, -- the domain object's ID parent_object BIGINT REFERENCES acl_object_identity(id), owner_sid BIGINT REFERENCES acl_sid(id), entries_inheriting BOOLEAN NOT NULL DEFAULT TRUE, UNIQUE (object_id_class, object_id_identity) ); CREATE TABLE acl_entry ( id BIGSERIAL PRIMARY KEY, acl_object_identity BIGINT NOT NULL REFERENCES acl_object_identity(id), ace_order INT NOT NULL, sid BIGINT NOT NULL REFERENCES acl_sid(id), mask INT NOT NULL, -- bitmask of permissions granting BOOLEAN NOT NULL, -- true = grant, false = deny audit_success BOOLEAN NOT NULL DEFAULT FALSE, audit_failure BOOLEAN NOT NULL DEFAULT FALSE, UNIQUE (acl_object_identity, ace_order) );

The class below shows the implementation. Key points are highlighted in the inline comments.

import org.springframework.cache.CacheManager; import org.springframework.context.annotation.*; import org.springframework.security.access.expression.method.DefaultMethodSecurityExpressionHandler; import org.springframework.security.access.expression.method.MethodSecurityExpressionHandler; import org.springframework.security.acls.AclPermissionEvaluator; import org.springframework.security.acls.domain.*; import org.springframework.security.acls.jdbc.*; import org.springframework.security.acls.model.AclCache; import javax.sql.DataSource; @Configuration @EnableMethodSecurity public class AclConfig { @Bean public AclCache aclCache(CacheManager cacheManager) { return new SpringCacheBasedAclCache( cacheManager.getCache("aclCache"), new DefaultPermissionGrantingStrategy(new ConsoleAuditLogger()), new AclAuthorizationStrategyImpl(new SimpleGrantedAuthority("ROLE_ADMIN"))); } @Bean public LookupStrategy lookupStrategy(DataSource dataSource, AclCache aclCache) { return new BasicLookupStrategy(dataSource, aclCache, new AclAuthorizationStrategyImpl(new SimpleGrantedAuthority("ROLE_ADMIN")), new ConsoleAuditLogger()); } @Bean public JdbcMutableAclService aclService(DataSource dataSource, LookupStrategy ls, AclCache cache) { return new JdbcMutableAclService(dataSource, ls, cache); } @Bean public MethodSecurityExpressionHandler methodSecurityExpressionHandler( JdbcMutableAclService aclService) { DefaultMethodSecurityExpressionHandler handler = new DefaultMethodSecurityExpressionHandler(); handler.setPermissionEvaluator(new AclPermissionEvaluator(aclService)); return handler; } } # application.properties — enable caching for ACL performance spring.cache.type=caffeine spring.cache.cache-names=aclCache spring.cache.caffeine.spec=maximumSize=500,expireAfterWrite=60s

Use MutableAclService to create or update an ACL for a domain object when it is created, shared, or transferred. Call this from your service layer after persisting the object.

import org.springframework.security.acls.domain.*; import org.springframework.security.acls.model.*; import org.springframework.security.core.Authentication; import org.springframework.security.core.context.SecurityContextHolder; import org.springframework.stereotype.Service; import org.springframework.transaction.annotation.Transactional; @Service public class DocumentAclService { private final MutableAclService mutableAclService; public DocumentAclService(MutableAclService mutableAclService) { this.mutableAclService = mutableAclService; } @Transactional public void grantPermission(Long documentId, String username, Permission permission) { ObjectIdentity oi = new ObjectIdentityImpl(Document.class, documentId); MutableAcl acl; try { acl = (MutableAcl) mutableAclService.readAclById(oi); } catch (NotFoundException e) { acl = mutableAclService.createAcl(oi); } Sid sid = new PrincipalSid(username); acl.insertAce(acl.getEntries().size(), permission, sid, true /* granting */); mutableAclService.updateAcl(acl); } @Transactional public void revokePermission(Long documentId, String username, Permission permission) { ObjectIdentity oi = new ObjectIdentityImpl(Document.class, documentId); MutableAcl acl = (MutableAcl) mutableAclService.readAclById(oi); acl.getEntries().stream() .filter(ace -> ace.getSid().equals(new PrincipalSid(username)) && ace.getPermission().equals(permission)) .map(ace -> acl.getEntries().indexOf(ace)) .findFirst() .ifPresent(acl::deleteAce); mutableAclService.updateAcl(acl); } } // In your document creation service: @Transactional public Document createDocument(CreateDocumentRequest req) { Document doc = documentRepository.save(new Document(req.title(), req.content())); // Grant the creator full permissions String owner = SecurityContextHolder.getContext().getAuthentication().getName(); aclService.grantPermission(doc.getId(), owner, BasePermission.READ); aclService.grantPermission(doc.getId(), owner, BasePermission.WRITE); aclService.grantPermission(doc.getId(), owner, BasePermission.DELETE); return doc; }

Once ACLs are stored, use hasPermission() in @PreAuthorize or @PostAuthorize. Spring Security looks up the ACL for the object and evaluates whether the current principal has the required permission.

import org.springframework.security.access.prepost.*; import org.springframework.security.acls.domain.BasePermission; import org.springframework.stereotype.Service; @Service public class DocumentService { // Check by object ID and type name — ACL lookup happens before method runs @PreAuthorize("hasPermission(#id, 'com.example.Document', 'read')") public Document getDocument(Long id) { return documentRepository.findById(id).orElseThrow(); } // Check on the returned object — ACL lookup after method runs @PostAuthorize("hasPermission(returnObject, 'read')") public Document findDocument(Long id) { return documentRepository.findById(id).orElseThrow(); } @PreAuthorize("hasPermission(#id, 'com.example.Document', 'write')") public Document updateDocument(Long id, UpdateRequest req) { Document doc = documentRepository.findById(id).orElseThrow(); doc.setContent(req.content()); return documentRepository.save(doc); } @PreAuthorize("hasPermission(#id, 'com.example.Document', 'delete')") public void deleteDocument(Long id) { documentRepository.deleteById(id); } }

Spring Security ACL uses integer bitmasks. BasePermission provides the standard set:

ConstantMask (int)Typical Use
BasePermission.READ1View / fetch an object.
BasePermission.WRITE2Edit / update an object.
BasePermission.CREATE4Create child objects.
BasePermission.DELETE8Delete an object.
BasePermission.ADMINISTRATION16Manage the ACL itself.

Extend BasePermission to add custom masks:

import org.springframework.security.acls.domain.BasePermission; import org.springframework.security.acls.model.Permission; public class DocumentPermission extends BasePermission { public static final Permission SHARE = new DocumentPermission(1 << 5, 'S'); // mask 32 public static final Permission PUBLISH = new DocumentPermission(1 << 6, 'P'); // mask 64 protected DocumentPermission(int mask, char code) { super(mask, code); } }
CriterionSpring ACLCustom PermissionEvaluator
Permission changes at runtime✅ Yes — stored in DB⚠️ Requires redeploy or extra logic
Simple ownership check⚠️ Overkill✅ Simpler, faster
Per-object, per-user permissions✅ Built-in🔧 Must implement manually
Permission inheritance (parent → child)✅ Built-in🔧 Must implement manually
Setup complexity❌ 4 tables + config✅ One class + @Bean
Performance✅ Cached (Caffeine/Redis)✅ Depends on your impl

Use Spring ACL when permissions are dynamic and user-controlled (document sharing, team membership). Use a custom PermissionEvaluator when ownership rules are fixed in code (e.g., "users can only see their own records").