Как запретить доступ в Spring Security ACL: Полное руководство
Узнайте, как настроить Spring Security ACL для явного запрета доступа. Реализация кастомной стратегии, настройка базы данных и примеры кода для эффективного контроля доступа.
Как явно запретить доступ в Spring Security с помощью ACL (Access Control List)?
Всем привет!
У меня есть метод с аннотацией @PostAuthorize:
@PostAuthorize("hasPermission(returnObject, read)")
Profile getProfile() {
...
}
Как правильно настроить Spring Security, чтобы доступ к этому методу был открыт для всех пользователей, за исключением тех, у кого в базе данных явно указано granted=false?
Я хочу избежать необходимости добавлять разрешения для каждого пользователя в базу данных. Вместо этого я планирую добавлять в базу только тех пользователей, которым доступ должен быть запрещен.
Я уже изучал исходный код Spring Security, но не смог найти способа реализовать такую логику. Однако в официальной документации упоминается, что это возможно. Подскажите, как это можно сделать?
Чтобы явно запретить доступ в Spring Security с помощью ACL, вам нужно настроить базу данных ACL так, чтобы записи с granted=false имели приоритет над разрешениями по умолчанию. Это достигается через правильную настройку AclPermissionGrantingStrategy и базы данных ACL.
Содержание
- Основы Spring Security ACL
- Настройка базы данных ACL
- Реализация логики явного запрета доступа
- Конфигурация Spring Security
- Альтернативные подходы
- Пример реализации
Основы Spring Security ACL
Spring Security ACL предоставляет гибкую систему контроля доступа на уровне доменных объектов. Вместо того чтобы назначать разрешения каждому пользователю, ACL позволяет использовать отрицательные разрешения (denies), которые имеют приоритет над положительными.
Ключевые компоненты ACL в Spring Security:
AclEntry- запись о правах доступа для конкретного пользователя или группыSid(Security Identifier) - представляет пользователя или группуPermission- набор прав (read, write, delete, administer)AclPermissionGrantingStrategy- стратегия определения прав доступа
Интересно, что в Spring Security ACL реализован принцип “разрешено по умолчанию, запрещено явно”. Это значит, что если для объекта не установлены явные запреты, доступ разрешен всем. Но как только появляется запись с granted=false, она тут же блокирует доступ, даже если есть другие разрешения.
Настройка базы данных ACL
Для реализации вашей логики необходимо правильно настроить структуру базы данных ACL. Spring Security предоставляет стандартные схемы для различных баз данных.
-- Пример таблицы acl_entry для PostgreSQL
CREATE TABLE acl_entry (
id BIGINT NOT NULL,
acl_object_identity BIGINT NOT NULL,
ace_order INT NOT NULL,
sid BIGINT NOT NULL,
mask INTEGER NOT NULL,
granting BOOLEAN NOT NULL,
audit_success BOOLEAN NOT NULL,
audit_failure BOOLEAN NOT NULL
);
-- Добавление ограничений
ALTER TABLE acl_entry ADD CONSTRAINT fk_acl_entry_object FOREIGN KEY (acl_object_identity) REFERENCES acl_object_identity (id);
ALTER TABLE acl_entry ADD CONSTRAINT fk_acl_entry_sid FOREIGN KEY (sid) REFERENCES sid (id);
На практике, при работе с ACL, часто возникает вопрос: “Как обеспечить, чтобы запреты имели приоритет?” Ответ кроется в правильной настройке стратегии разрешения прав, но сначала нужно подготовить базу данных.
Реализация логики явного запрета доступа
Чтобы реализовать вашу логику (доступ для всех, кроме явно запрещенных), вам нужно:
- Создать кастомную стратегию разрешения прав:
public class CustomPermissionGrantingStrategy extends AclPermissionGrantingStrategy {
public CustomPermissionGrantingStrategy(AclAuthorizationStrategy aclAuthorizationStrategy) {
super(aclAuthorizationStrategy);
}
@Override
protected boolean hasPermission(Acl acl, Permission permission, Collection<Sid> sids) {
// Сначала проверяем наличие отрицательных разрешений
for (AclEntry entry : acl.getEntries()) {
if (!entry.isGranting() &&
entry.getPermission().getMask() == permission.getMask() &&
sids.contains(entry.getSid())) {
return false; // Явный запрет имеет приоритет
}
}
// Если нет явных запретов, проверяем положительные разрешения
return super.hasPermission(acl, permission, sids);
}
}
Вот здесь и кроется самое интересное. Вместо того чтобы полагаться на стандартную логику Spring Security, мы переопределяем метод hasPermission, чтобы он сначала проверял наличие запретов. Только если запретов нет - переходим к стандартной проверке разрешений.
- Настроить базу данных так, чтобы записи с
granted=falseимели приоритет:
-- Пример добавления записи с запретом доступа
INSERT INTO acl_entry (id, acl_object_identity, ace_order, sid, mask, granting, audit_success, audit_failure)
VALUES (nextval('acl_entry_id_seq'), 1, 0, 2, 1, false, false, false);
Конфигурация Spring Security
Полная конфигурация Spring Security для работы с ACL:
@Configuration
@EnableGlobalMethodSecurity(prePostEnabled = true)
public class SecurityConfig extends GlobalMethodSecurityConfiguration {
@Override
protected MethodSecurityExpressionHandler createExpressionHandler() {
DefaultMethodSecurityExpressionHandler handler =
new DefaultMethodSecurityExpressionHandler();
handler.setPermissionEvaluator(new AclPermissionEvaluator(aclService()));
return handler;
}
@Bean
public AclService aclService() {
return new JdbcMutableAclService(
dataSource(),
lookupStrategy(),
aclCache()
);
}
@Bean
public LookupStrategy lookupStrategy() {
return new BasicLookupStrategy(
dataSource(),
aclCache(),
aclAuthorizationStrategy(),
new ConsoleAuditLogger()
);
}
@Bean
public AclAuthorizationStrategy aclAuthorizationStrategy() {
return new AclAuthorizationStrategyImpl(
new SimpleGrantedAuthority("ROLE_ADMIN")
);
}
@Bean
public aclCache aclCache() {
return new EhCacheBasedAclCache(
ehCacheFactoryBean().getObject(),
permissionGrantingStrategy(),
aclAuthorizationStrategy()
);
}
@Bean
public EhCacheFactoryBean ehCacheFactoryBean() {
EhCacheFactoryBean ehCacheFactoryBean = new EhCacheFactoryBean();
ehCacheFactoryBean.setCacheManager(cacheManager());
return ehCacheFactoryBean;
}
@Bean
public PermissionGrantingStrategy permissionGrantingStrategy() {
return new CustomPermissionGrantingStrategy(aclAuthorizationStrategy());
}
}
Конфигурация может показаться сложной на первый взгляд, но если разобраться по шагам, то все становится на свои места. Главное - не забыть указать нашу кастомную стратегию разрешения прав в последнем бине.
Альтернативные подходы
1. Использование @PreAuthorize с кастомным выражением
@PreAuthorize("@aclSecurityService.canAccessProfile(authentication, #profileId)")
Profile getProfile(Long profileId) {
// ...
}
@Service
public class AclSecurityService {
public boolean canAccessProfile(Authentication authentication, Long profileId) {
// Проверяем, есть ли явный запрет для этого пользователя
if (aclService.hasExplicitDeny(profileId, authentication)) {
return false;
}
// Если нет запрета, доступ разрешен
return true;
}
}
Этот подход хорош тем, что выносим всю логику проверки в отдельный сервис, что делает код чище и проще для тестирования.
2. Использование Voter для кастомной логики
@Component
public class CustomAclVoter implements AccessDecisionVoter {
@Override
public int vote(Authentication authentication, Object object, Collection<ConfigAttribute> attributes) {
// Логика проверки ACL с приоритетом denies
if (hasExplicitDeny(authentication, object)) {
return ACCESS_DENIED;
}
return ACCESS_ABSTAIN; // Дальнейшая проверка другими voters
}
private boolean hasExplicitDeny(Authentication authentication, Object object) {
// Реализация проверки явного запрета
// ...
}
}
Voter-ы - это мощный механизм в Spring Security, который позволяет вмешиваться в процесс принятия решений о доступе на самом низком уровне.
Пример реализации
Полный пример реализации вашей задачи:
@Service
public class ProfileService {
@Autowired
private AclService aclService;
@Autowired
private MutableAclService mutableAclService;
@PostAuthorize("hasPermission(returnObject, 'read')")
public Profile getProfile(Long profileId) {
Profile profile = profileRepository.findById(profileId)
.orElseThrow(() -> new EntityNotFoundException("Profile not found"));
// Если профиль не имеет ACL записи, создаем ее с доступом для всех
if (!aclService.findAcls(new ObjectIdentityImpl(Profile.class, profileId)).iterator().hasNext()) {
createDefaultAclForProfile(profileId);
}
return profile;
}
private void createDefaultAclForProfile(Long profileId) {
ObjectIdentity oid = new ObjectIdentityImpl(Profile.class, profileId);
MutableAcl acl = mutableAclService.createAcl(oid);
// Добавляем разрешение для всех пользователей (SID = -1)
acl.insertAce(acl.getEntries().size(),
BasePermission.READ,
new PrincipalSid("everyone"),
true);
mutableAclService.updateAcl(acl);
}
public void denyAccessForUser(Long profileId, String username) {
ObjectIdentity oid = new ObjectIdentityImpl(Profile.class, profileId);
MutableAcl acl = (MutableAcl) mutableAclService.readAclById(oid);
// Добавляем запрет для конкретного пользователя
acl.insertAce(acl.getEntries().size(),
BasePermission.READ,
new PrincipalSid(username),
false);
mutableAclService.updateAcl(acl);
}
}
@Configuration
public class AclConfig {
@Bean
public LookupStrategy lookupStrategy(DataSource dataSource,
CacheManager cacheManager,
AclAuthorizationStrategy aclAuthorizationStrategy,
ConsoleAuditLogger auditLogger) {
return new BasicLookupStrategy(
dataSource,
new EhCacheBasedAclCache(
ehCacheFactoryBean(cacheManager).getObject(),
new AclPermissionGrantingStrategy(aclAuthorizationStrategy),
aclAuthorizationStrategy
),
aclAuthorizationStrategy,
auditLogger
);
}
@Bean
public EhCacheFactoryBean ehCacheFactoryBean(CacheManager cacheManager) {
EhCacheFactoryBean factory = new EhCacheFactoryBean();
factory.setCacheManager(cacheManager);
return factory;
}
@Bean
public AclAuthorizationStrategy aclAuthorizationStrategy() {
return new AclAuthorizationStrategyImpl(
new SimpleGrantedAuthority("ROLE_ADMIN")
);
}
}
Заключение
-
Ключевая идея: Используйте отрицательные разрешения (
granted=false) в ACL, которые имеют приоритет над положительными. -
Настройка базы данных: Создавайте записи с
granting=falseдля пользователей, которым доступ должен быть запрещен. -
Кастомная стратегия: Реализуйте собственную
PermissionGrantingStrategy, которая сначала проверяет наличие отрицательных разрешений. -
Автоматизация: Для профилей без ACL записей автоматически создавайте разрешение для всех пользователей.
-
Управление доступом: Для запрета доступа добавляйте только записи с
granted=falseв базу данных, не трогая разрешения по умолчанию.
Этот подход позволяет минимизировать количество записей в базе данных и обеспечивает необходимую гибкость контроля доступа. На практике он оказывается очень удобным, когда у вас есть открытый по умолчанию доступ, но нужно ограничить его для отдельных пользователей или групп.
Источники
- Spring Security ACL Documentation - Official Spring Security Guide
- Spring Security ACL Database Schema - Spring Framework
- AclPermissionGrantingStrategy - Spring Security API
- Access Control Lists in Spring Security - Baeldung Tutorial
- Custom Permission Evaluation in Spring Security - Spring Framework Blog