Contents
- Dependency
- ACL Database Schema
- ACL Configuration Beans
- Granting Permissions on Domain Objects
- Checking Permissions with hasPermission()
- Built-in Permission Masks
- When to Use ACL vs Custom PermissionEvaluator
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:
| Constant | Mask (int) | Typical Use |
| BasePermission.READ | 1 | View / fetch an object. |
| BasePermission.WRITE | 2 | Edit / update an object. |
| BasePermission.CREATE | 4 | Create child objects. |
| BasePermission.DELETE | 8 | Delete an object. |
| BasePermission.ADMINISTRATION | 16 | Manage 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);
}
}
| Criterion | Spring ACL | Custom 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").