Spring Security ACL provides instance-based security for domain objects through a comprehensive Access Control List implementation
—
This guide covers the complete setup of Spring Security ACL, including Spring configuration, database setup, and integration with Spring Security. Follow these steps to get ACL working in your application.
Setting up Spring Security ACL involves:
<properties>
<spring-security.version>6.5.1</spring-security.version>
<spring-boot.version>3.3.0</spring-boot.version>
</properties>
<dependencies>
<!-- Spring Security ACL -->
<dependency>
<groupId>org.springframework.security</groupId>
<artifactId>spring-security-acl</artifactId>
<version>${spring-security.version}</version>
</dependency>
<!-- Spring Security Config -->
<dependency>
<groupId>org.springframework.security</groupId>
<artifactId>spring-security-config</artifactId>
<version>${spring-security.version}</version>
</dependency>
<!-- Spring Boot Starter Security -->
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-security</artifactId>
</dependency>
<!-- Database dependencies (choose your database) -->
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-data-jpa</artifactId>
</dependency>
<!-- MySQL Driver -->
<dependency>
<groupId>mysql</groupId>
<artifactId>mysql-connector-java</artifactId>
<scope>runtime</scope>
</dependency>
<!-- Or PostgreSQL -->
<!--
<dependency>
<groupId>org.postgresql</groupId>
<artifactId>postgresql</artifactId>
<scope>runtime</scope>
</dependency>
-->
<!-- Connection Pool -->
<dependency>
<groupId>com.zaxxer</groupId>
<artifactId>HikariCP</artifactId>
</dependency>
<!-- Caching (optional but recommended) -->
<dependency>
<groupId>com.github.ben-manes.caffeine</groupId>
<artifactId>caffeine</artifactId>
</dependency>
</dependencies>dependencies {
implementation 'org.springframework.security:spring-security-acl:6.5.1'
implementation 'org.springframework.security:spring-security-config:6.5.1'
implementation 'org.springframework.boot:spring-boot-starter-security'
implementation 'org.springframework.boot:spring-boot-starter-data-jpa'
runtimeOnly 'mysql:mysql-connector-java'
// or: runtimeOnly 'org.postgresql:postgresql'
implementation 'com.zaxxer:HikariCP'
implementation 'com.github.ben-manes.caffeine:caffeine'
}The ACL module requires four tables. Here are the DDL statements for different databases:
-- ACL Class table - stores domain object types
CREATE TABLE acl_class (
id BIGINT UNSIGNED NOT NULL AUTO_INCREMENT PRIMARY KEY,
class VARCHAR(100) NOT NULL,
UNIQUE KEY unique_uk_2 (class)
) ENGINE=InnoDB;
-- ACL SID table - stores security identities (users and roles)
CREATE TABLE acl_sid (
id BIGINT UNSIGNED NOT NULL AUTO_INCREMENT PRIMARY KEY,
principal BOOLEAN NOT NULL,
sid VARCHAR(100) NOT NULL,
UNIQUE KEY unique_uk_3 (sid, principal)
) ENGINE=InnoDB;
-- ACL Object Identity table - stores domain object instances
CREATE TABLE acl_object_identity (
id BIGINT UNSIGNED NOT NULL AUTO_INCREMENT PRIMARY KEY,
object_id_class BIGINT UNSIGNED NOT NULL,
object_id_identity VARCHAR(36) NOT NULL,
parent_object BIGINT UNSIGNED,
owner_sid BIGINT UNSIGNED,
entries_inheriting BOOLEAN NOT NULL,
UNIQUE KEY unique_uk_4 (object_id_class, object_id_identity),
CONSTRAINT foreign_fk_1 FOREIGN KEY (parent_object) REFERENCES acl_object_identity (id),
CONSTRAINT foreign_fk_2 FOREIGN KEY (object_id_class) REFERENCES acl_class (id),
CONSTRAINT foreign_fk_3 FOREIGN KEY (owner_sid) REFERENCES acl_sid (id)
) ENGINE=InnoDB;
-- ACL Entry table - stores individual permission assignments
CREATE TABLE acl_entry (
id BIGINT UNSIGNED NOT NULL AUTO_INCREMENT PRIMARY KEY,
acl_object_identity BIGINT UNSIGNED NOT NULL,
ace_order INTEGER NOT NULL,
sid BIGINT UNSIGNED NOT NULL,
mask INTEGER UNSIGNED NOT NULL,
granting BOOLEAN NOT NULL,
audit_success BOOLEAN NOT NULL,
audit_failure BOOLEAN NOT NULL,
UNIQUE KEY unique_uk_5 (acl_object_identity, ace_order),
CONSTRAINT foreign_fk_4 FOREIGN KEY (acl_object_identity) REFERENCES acl_object_identity (id),
CONSTRAINT foreign_fk_5 FOREIGN KEY (sid) REFERENCES acl_sid (id)
) ENGINE=InnoDB;
-- Indexes for better performance
CREATE INDEX idx_acl_object_identity_parent ON acl_object_identity(parent_object);
CREATE INDEX idx_acl_entry_object_identity ON acl_entry(acl_object_identity);
CREATE INDEX idx_acl_entry_sid ON acl_entry(sid);-- ACL Class table
CREATE TABLE acl_class (
id BIGSERIAL NOT NULL PRIMARY KEY,
class VARCHAR(100) NOT NULL,
CONSTRAINT unique_uk_2 UNIQUE (class)
);
-- ACL SID table
CREATE TABLE acl_sid (
id BIGSERIAL NOT NULL PRIMARY KEY,
principal BOOLEAN NOT NULL,
sid VARCHAR(100) NOT NULL,
CONSTRAINT unique_uk_3 UNIQUE (sid, principal)
);
-- ACL Object Identity table
CREATE TABLE acl_object_identity (
id BIGSERIAL NOT NULL PRIMARY KEY,
object_id_class BIGINT NOT NULL,
object_id_identity VARCHAR(36) NOT NULL,
parent_object BIGINT,
owner_sid BIGINT,
entries_inheriting BOOLEAN NOT NULL,
CONSTRAINT unique_uk_4 UNIQUE (object_id_class, object_id_identity),
CONSTRAINT foreign_fk_1 FOREIGN KEY (parent_object) REFERENCES acl_object_identity (id),
CONSTRAINT foreign_fk_2 FOREIGN KEY (object_id_class) REFERENCES acl_class (id),
CONSTRAINT foreign_fk_3 FOREIGN KEY (owner_sid) REFERENCES acl_sid (id)
);
-- ACL Entry table
CREATE TABLE acl_entry (
id BIGSERIAL NOT NULL PRIMARY KEY,
acl_object_identity BIGINT NOT NULL,
ace_order INTEGER NOT NULL,
sid BIGINT NOT NULL,
mask INTEGER NOT NULL,
granting BOOLEAN NOT NULL,
audit_success BOOLEAN NOT NULL,
audit_failure BOOLEAN NOT NULL,
CONSTRAINT unique_uk_5 UNIQUE (acl_object_identity, ace_order),
CONSTRAINT foreign_fk_4 FOREIGN KEY (acl_object_identity) REFERENCES acl_object_identity (id),
CONSTRAINT foreign_fk_5 FOREIGN KEY (sid) REFERENCES acl_sid (id)
);
-- Indexes
CREATE INDEX idx_acl_object_identity_parent ON acl_object_identity(parent_object);
CREATE INDEX idx_acl_entry_object_identity ON acl_entry(acl_object_identity);
CREATE INDEX idx_acl_entry_sid ON acl_entry(sid);# Database connection
spring.datasource.url=jdbc:mysql://localhost:3306/acl_database?useSSL=false&serverTimezone=UTC
spring.datasource.username=acl_user
spring.datasource.password=your_password
spring.datasource.driver-class-name=com.mysql.cj.jdbc.Driver
# Connection pool settings
spring.datasource.hikari.maximum-pool-size=20
spring.datasource.hikari.minimum-idle=5
spring.datasource.hikari.connection-timeout=30000
spring.datasource.hikari.idle-timeout=600000
spring.datasource.hikari.max-lifetime=1800000
# JPA settings
spring.jpa.database-platform=org.hibernate.dialect.MySQL8Dialect
spring.jpa.show-sql=false
spring.jpa.hibernate.ddl-auto=validate
# ACL specific settings
logging.level.org.springframework.security.acls=DEBUG@Configuration
public class DataSourceConfig {
@Bean
@Primary
public DataSource dataSource() {
HikariConfig config = new HikariConfig();
config.setJdbcUrl("jdbc:mysql://localhost:3306/acl_database");
config.setUsername("acl_user");
config.setPassword("password");
config.setDriverClassName("com.mysql.cj.jdbc.Driver");
// Performance tuning
config.setMaximumPoolSize(20);
config.setMinimumIdle(5);
config.setConnectionTimeout(30000);
config.setIdleTimeout(600000);
config.setMaxLifetime(1800000);
// ACL-specific optimizations
config.addDataSourceProperty("cachePrepStmts", "true");
config.addDataSourceProperty("prepStmtCacheSize", "250");
config.addDataSourceProperty("prepStmtCacheSqlLimit", "2048");
config.addDataSourceProperty("useServerPrepStmts", "true");
return new HikariDataSource(config);
}
}@Configuration
@EnableGlobalMethodSecurity(prePostEnabled = true)
@EnableCaching
public class AclConfig {
@Autowired
private DataSource dataSource;
@Bean
public AclService aclService() {
JdbcMutableAclService service = new JdbcMutableAclService(
dataSource,
lookupStrategy(),
aclCache()
);
// Configure for your database
service.setClassIdentityQuery("SELECT @@IDENTITY"); // MySQL/SQL Server
service.setSidIdentityQuery("SELECT @@IDENTITY");
// For PostgreSQL use:
// service.setClassIdentityQuery("select currval(pg_get_serial_sequence('acl_class', 'id'))");
// service.setSidIdentityQuery("select currval(pg_get_serial_sequence('acl_sid', 'id'))");
return service;
}
@Bean
public MutableAclService mutableAclService() {
return (MutableAclService) aclService();
}
@Bean
public LookupStrategy lookupStrategy() {
return new BasicLookupStrategy(
dataSource,
aclCache(),
aclAuthorizationStrategy(),
permissionGrantingStrategy()
);
}
@Bean
public AclCache aclCache() {
return new SpringCacheBasedAclCache(
cacheManager().getCache("aclCache"),
permissionGrantingStrategy(),
aclAuthorizationStrategy()
);
}
@Bean
public PermissionGrantingStrategy permissionGrantingStrategy() {
return new DefaultPermissionGrantingStrategy(auditLogger());
}
@Bean
public AclAuthorizationStrategy aclAuthorizationStrategy() {
return new AclAuthorizationStrategyImpl(
new SimpleGrantedAuthority("ROLE_ADMIN"), // Change ownership
new SimpleGrantedAuthority("ROLE_ADMIN"), // Modify auditing
new SimpleGrantedAuthority("ROLE_ADMIN") // General changes
);
}
@Bean
public AuditLogger auditLogger() {
return new ConsoleAuditLogger();
}
@Bean
public PermissionFactory permissionFactory() {
DefaultPermissionFactory factory = new DefaultPermissionFactory();
// Register custom permissions if needed
factory.registerPublicPermissions(CustomPermission.class);
return factory;
}
@Bean
public CacheManager cacheManager() {
CaffeineCacheManager cacheManager = new CaffeineCacheManager("aclCache");
cacheManager.setCaffeine(Caffeine.newBuilder()
.maximumSize(10000)
.expireAfterWrite(Duration.ofMinutes(10))
.recordStats());
return cacheManager;
}
}@Configuration
@EnableGlobalMethodSecurity(prePostEnabled = true)
public class MethodSecurityConfig {
@Bean
public MethodSecurityExpressionHandler methodSecurityExpressionHandler(
AclService aclService,
PermissionFactory permissionFactory) {
DefaultMethodSecurityExpressionHandler handler = new DefaultMethodSecurityExpressionHandler();
// Configure permission evaluator
AclPermissionEvaluator permissionEvaluator = new AclPermissionEvaluator(aclService);
permissionEvaluator.setPermissionFactory(permissionFactory);
permissionEvaluator.setObjectIdentityRetrievalStrategy(objectIdentityRetrievalStrategy());
permissionEvaluator.setSidRetrievalStrategy(sidRetrievalStrategy());
handler.setPermissionEvaluator(permissionEvaluator);
// Configure permission cache optimizer
handler.setPermissionCacheOptimizer(new AclPermissionCacheOptimizer(aclService));
return handler;
}
@Bean
public ObjectIdentityRetrievalStrategy objectIdentityRetrievalStrategy() {
return new ObjectIdentityRetrievalStrategyImpl();
}
@Bean
public SidRetrievalStrategy sidRetrievalStrategy() {
return new SidRetrievalStrategyImpl();
}
}For Spring Boot applications, you can create auto-configuration:
@Configuration
@ConditionalOnClass(AclService.class)
@ConditionalOnProperty(name = "spring.security.acl.enabled", havingValue = "true", matchIfMissing = true)
@EnableConfigurationProperties(AclProperties.class)
public class AclAutoConfiguration {
@Bean
@ConditionalOnMissingBean
public AclService aclService(
DataSource dataSource,
LookupStrategy lookupStrategy,
AclCache aclCache) {
JdbcMutableAclService service = new JdbcMutableAclService(
dataSource, lookupStrategy, aclCache
);
// Auto-detect database type and configure identity queries
configureDatabaseSpecificQueries(service, dataSource);
return service;
}
private void configureDatabaseSpecificQueries(JdbcMutableAclService service, DataSource dataSource) {
try (Connection conn = dataSource.getConnection()) {
String databaseName = conn.getMetaData().getDatabaseProductName().toLowerCase();
if (databaseName.contains("mysql")) {
service.setClassIdentityQuery("SELECT LAST_INSERT_ID()");
service.setSidIdentityQuery("SELECT LAST_INSERT_ID()");
} else if (databaseName.contains("postgresql")) {
service.setClassIdentityQuery("select currval(pg_get_serial_sequence('acl_class', 'id'))");
service.setSidIdentityQuery("select currval(pg_get_serial_sequence('acl_sid', 'id'))");
} else if (databaseName.contains("h2")) {
service.setClassIdentityQuery("SELECT SCOPE_IDENTITY()");
service.setSidIdentityQuery("SELECT SCOPE_IDENTITY()");
}
} catch (SQLException e) {
throw new IllegalStateException("Could not determine database type", e);
}
}
}@ConfigurationProperties(prefix = "spring.security.acl")
public class AclProperties {
private boolean enabled = true;
private Cache cache = new Cache();
private Audit audit = new Audit();
// Getters and setters...
public static class Cache {
private String name = "aclCache";
private int maxSize = 10000;
private Duration expireAfterWrite = Duration.ofMinutes(10);
// Getters and setters...
}
public static class Audit {
private boolean enabled = true;
private String loggerType = "console"; // console, slf4j, custom
// Getters and setters...
}
}@Configuration
@EnableWebSecurity
public class WebSecurityConfig {
@Bean
public SecurityFilterChain filterChain(HttpSecurity http) throws Exception {
http
.authorizeHttpRequests(authz -> authz
.requestMatchers("/public/**").permitAll()
.requestMatchers("/admin/**").hasRole("ADMIN")
.anyRequest().authenticated()
)
.formLogin(form -> form
.loginPage("/login")
.defaultSuccessUrl("/dashboard")
.permitAll()
)
.logout(logout -> logout
.logoutUrl("/logout")
.logoutSuccessUrl("/login?logout")
.permitAll()
)
// Enable method security
.sessionManagement(session -> session
.sessionCreationPolicy(SessionCreationPolicy.IF_REQUIRED)
);
return http.build();
}
@Bean
public PasswordEncoder passwordEncoder() {
return new BCryptPasswordEncoder();
}
@Bean
public UserDetailsService userDetailsService() {
return new JdbcUserDetailsManager(dataSource());
}
}@Component
public class CustomAuthenticationProvider implements AuthenticationProvider {
@Autowired
private UserDetailsService userDetailsService;
@Autowired
private PasswordEncoder passwordEncoder;
@Override
public Authentication authenticate(Authentication authentication) throws AuthenticationException {
String username = authentication.getName();
String password = authentication.getCredentials().toString();
UserDetails userDetails = userDetailsService.loadUserByUsername(username);
if (passwordEncoder.matches(password, userDetails.getPassword())) {
return new UsernamePasswordAuthenticationToken(
userDetails, password, userDetails.getAuthorities()
);
}
throw new BadCredentialsException("Authentication failed");
}
@Override
public boolean supports(Class<?> authenticationType) {
return authenticationType.equals(UsernamePasswordAuthenticationToken.class);
}
}@TestConfiguration
public class TestAclConfig {
@Bean
@Primary
public DataSource testDataSource() {
return new EmbeddedDatabaseBuilder()
.setType(EmbeddedDatabaseType.H2)
.addScript("classpath:acl-schema.sql")
.addScript("classpath:acl-test-data.sql")
.build();
}
@Bean
@Primary
public AclService testAclService() {
JdbcMutableAclService service = new JdbcMutableAclService(
testDataSource(), testLookupStrategy(), testAclCache()
);
// H2 specific queries
service.setClassIdentityQuery("SELECT SCOPE_IDENTITY()");
service.setSidIdentityQuery("SELECT SCOPE_IDENTITY()");
return service;
}
}@SpringBootTest
@TestPropertySource(properties = {
"spring.datasource.url=jdbc:h2:mem:testdb",
"spring.jpa.hibernate.ddl-auto=create-drop"
})
@Sql(scripts = {
"classpath:acl-schema.sql",
"classpath:test-data.sql"
})
class AclIntegrationTest {
@Autowired
private MutableAclService aclService;
@Autowired
private DocumentService documentService;
@Test
@WithMockUser(username = "user1", roles = "USER")
void testDocumentAccessControl() {
// Create document with ACL
Document document = new Document("Test Document");
document = documentRepository.save(document);
ObjectIdentity identity = new ObjectIdentityImpl(Document.class, document.getId());
MutableAcl acl = aclService.createAcl(identity);
Sid userSid = new PrincipalSid("user1");
acl.insertAce(0, BasePermission.READ, userSid, true);
aclService.updateAcl(acl);
// Test access
Document result = documentService.getDocument(document.getId());
assertThat(result).isNotNull();
assertThat(result.getId()).isEqualTo(document.getId());
}
@Test
@WithMockUser(username = "user2", roles = "USER")
void testDocumentAccessDenied() {
// Document created for user1, accessed by user2
assertThatThrownBy(() -> documentService.getDocument(1L))
.isInstanceOf(AccessDeniedException.class);
}
}@Configuration
public class ProductionAclConfig {
@Bean
public LookupStrategy optimizedLookupStrategy() {
BasicLookupStrategy strategy = new BasicLookupStrategy(
dataSource(),
aclCache(),
aclAuthorizationStrategy(),
permissionGrantingStrategy()
);
// Optimize batch size for bulk operations
strategy.setBatchSize(50);
return strategy;
}
@Bean
public CacheManager productionCacheManager() {
CaffeineCacheManager cacheManager = new CaffeineCacheManager("aclCache");
cacheManager.setCaffeine(Caffeine.newBuilder()
.maximumSize(100000) // Larger cache for production
.expireAfterWrite(Duration.ofHours(1)) // Longer expiry
.expireAfterAccess(Duration.ofMinutes(30))
.recordStats()
);
return cacheManager;
}
}@Configuration
public class AclMonitoringConfig {
@Bean
public CacheMetricsBinderConfiguration cacheMetrics() {
return new CacheMetricsBinderConfiguration();
}
@EventListener
public void handleAclCacheEvent(CacheEvictEvent event) {
if ("aclCache".equals(event.getCacheName())) {
// Log cache evictions for monitoring
log.info("ACL cache evicted for key: {}", event.getKey());
}
}
}
@Component
public class AclPerformanceMonitor {
private final MeterRegistry meterRegistry;
public AclPerformanceMonitor(MeterRegistry meterRegistry) {
this.meterRegistry = meterRegistry;
}
@EventListener
public void onAclLookup(AclLookupEvent event) {
Timer.Sample sample = Timer.start(meterRegistry);
// Record ACL lookup times
sample.stop(Timer.builder("acl.lookup.duration")
.tag("type", event.getType())
.register(meterRegistry));
}
}@Configuration
public class SecureAclConfig {
@Bean
public AclAuthorizationStrategy restrictiveAuthorizationStrategy() {
// Only ADMIN can modify ACLs
return new AclAuthorizationStrategyImpl(
new SimpleGrantedAuthority("ROLE_ADMIN"),
new SimpleGrantedAuthority("ROLE_ADMIN"),
new SimpleGrantedAuthority("ROLE_ADMIN")
);
}
@Bean
public AuditLogger secureAuditLogger() {
// Log to secure audit system
return new Slf4jAuditLogger();
}
// Custom audit logger for compliance
public static class Slf4jAuditLogger implements AuditLogger {
private static final Logger auditLog = LoggerFactory.getLogger("ACL_AUDIT");
@Override
public void logIfNeeded(boolean granted, AccessControlEntry ace) {
if (auditLog.isInfoEnabled()) {
auditLog.info("ACL Decision: granted={}, sid={}, permission={}, object={}",
granted, ace.getSid(), ace.getPermission(), ace.getAcl().getObjectIdentity());
}
}
}
}// Problem: Connection pool exhaustion
// Solution: Tune connection pool settings
@Bean
public DataSource dataSource() {
HikariConfig config = new HikariConfig();
// ... other config
// Prevent connection leaks
config.setLeakDetectionThreshold(60000);
config.setConnectionTestQuery("SELECT 1");
return new HikariDataSource(config);
}// Problem: N+1 queries during permission checks
// Solution: Use batch loading and caching
@Service
public class OptimizedDocumentService {
// Use @PostFilter with cache optimizer
@PostFilter("hasPermission(filterObject, 'READ')")
public List<Document> getDocuments() {
return documentRepository.findAll(); // Single query + batch ACL load
}
}// Problem: Null pointer exceptions in permission expressions
// Solution: Add null checks
@PreAuthorize("@securityService.canAccess(#document)")
public void updateDocument(Document document) {
// Custom security service handles null checks
}
@Service
public class SecurityService {
public boolean canAccess(Object object) {
if (object == null) return false;
// Permission check logic
return permissionEvaluator.hasPermission(authentication, object, "WRITE");
}
}# Enable ACL debug logging
logging.level.org.springframework.security.acls=DEBUG
logging.level.org.springframework.security.access=DEBUG
# Enable SQL logging to see ACL queries
logging.level.org.hibernate.SQL=DEBUG
logging.level.org.hibernate.type.descriptor.sql.BasicBinder=TRACE
# Enable cache logging
logging.level.org.springframework.cache=DEBUGWith this comprehensive configuration, your Spring Security ACL setup should be production-ready with proper performance, security, and monitoring capabilities. The modular configuration approach allows you to customize individual components based on your specific requirements.
Install with Tessl CLI
npx tessl i tessl/maven-org-springframework-security--spring-security-acl