Spring Безопасность hasPermission для коллекции <Object>
У меня есть рабочее приложение, защищенное безопасностью на уровне метода:
RestController:
@PreAuthorize("hasPermission(#product, 'WRITE')")
@RequestMapping(value = "/save", method = RequestMethod.POST)
public Product save(@RequestBody Product product) {
return productService.save(product);
}
PermissionEvaluator:
public class SecurityPermissionEvaluator implements PermissionEvaluator {
private Logger log = LoggerFactory.getLogger(SecurityPermissionEvaluator.class);
private final PermissionService permissionService;
public SecurityPermissionEvaluator(PermissionService permissionService) {
this.permissionService = permissionService;
}
@Override
public boolean hasPermission(Authentication authentication, Object targetDomainObject, Object permission) {
CustomUserDetails userDetails = (CustomUserDetails) authentication.getPrincipal();
return permissionService.isAuthorized(userDetails.getUser(), targetDomainObject, permission.toString());
}
@Override
public boolean hasPermission(Authentication authentication, Serializable targetId, String targetType, Object permission) {
// almost the same implementation
}
}
И все работает нормально, пока я не реализовал API, который сохраняет коллекцию объектов. Логикой этой службы является обновление существующих объектов и/или создание новых объектов.
@PreAuthorize("hasPermission(#products, 'WRITE')")
@RequestMapping(value = "/saveCollection", method = RequestMethod.POST)
public Collection<Product> save(@RequestBody Collection<Product> products) {
return productService.save(products);
}
После этого моя служба разрешений обрабатывает объект коллекции и выглядит следующим образом:
PemissionService:
public class PermissionService {
public boolean isAuthorized(User user, Object targetDomainObject, String permission) {
if (targetDomainObject instanceof TopAppEntity) {
if (((TopAppEntity) targetDomainObject).getId() == null) {
// check authorities and give response
} else {
// check ACL and give response
}
} else if(targetDomainObject instanceof Collection) {
boolean isAuthorized = false;
Collection targetDomainObjects = (Collection) targetDomainObject;
for (Object targetObject : targetDomainObjects) {
isAuthorized = isAuthorized(user, targetObject, permission);
if (!isAuthorized) break;
}
return isAuthorized;
}
}
}
Мой вопрос:
Как я могу обрабатывать коллекции, используя @PreAuthorize("hasPermission(#object, '...')")
более элегантный способ? Существуют ли какие-либо реализации в Spring Security для обработки коллекций? По крайней мере, как я могу оптимизировать PemissionService
для обработки Collections
?
Ответы
Ответ 1
Вы можете использовать @PreFilter
аннотация.
Итак @PreFilter("hasPermission(filterTarget, '...')")
вызовет вашу PermissionService для каждого элемента коллекции.
public class PermissionService() {
public boolean isAuthorized(User user, Object targetDomainObject, String permission) {
if (targetDomainObject instanceof TopAppEntity) {
if (((TopAppEntity) targetDomainObject).getId() == null) {
// check authorities and give response
} else {
// check ACL and give response
}
}
}
}
Примечание. это не будет препятствовать вызову вашего метода контроллера. Он получает пустую коллекцию.
Ответ 2
У меня есть несколько способов обхода.
1. Первый - использовать мои собственные MethodSecurityExpressionHandler
и MethodSecurityExpressionRoot
.
Создание CustomMethodSecurityExpressionRoot
и определение метода, который будет нашим новым выражением для обработки Collection
. Он будет расширять SecurityExpressionRoot
, чтобы включить выражения по умолчанию:
public class CustomMethodSecurityExpressionRoot extends SecurityExpressionRoot implements MethodSecurityExpressionOperations {
private final PermissionEvaluator permissionEvaluator;
private final Authentication authentication;
private Object filterObject;
private Object returnObject;
private Object target;
public CustomMethodSecurityExpressionRoot(Authentication authentication, PermissionEvaluator permissionEvaluator) {
super(authentication);
this.authentication = authentication;
this.permissionEvaluator = permissionEvaluator;
super.setPermissionEvaluator(permissionEvaluator);
}
public boolean hasAccessToCollection(Collection<Object> collection, String permission) {
for (Object object : collection) {
if (!permissionEvaluator.hasPermission(authentication, object, permission))
return false;
}
return true;
}
@Override
public void setFilterObject(Object filterObject) {
this.filterObject = filterObject;
}
@Override
public Object getFilterObject() {
return filterObject;
}
@Override
public void setReturnObject(Object returnObject) {
this.returnObject = returnObject;
}
@Override
public Object getReturnObject() {
return returnObject;
}
@Override
public Object getThis() {
return target;
}
}
Создайте собственный обработчик выражений и введите CustomMethodSecurityExpressionRoot
:
public class CustomMethodSecurityExpressionHandler extends DefaultMethodSecurityExpressionHandler {
private final PermissionEvaluator permissionEvaluator;
public CustomMethodSecurityExpressionHandler(PermissionEvaluator permissionEvaluator) {
this.permissionEvaluator = permissionEvaluator;
super.setPermissionEvaluator(permissionEvaluator);
}
@Override
protected MethodSecurityExpressionOperations createSecurityExpressionRoot(
Authentication authentication, MethodInvocation invocation) {
CustomMethodSecurityExpressionRoot root =
new CustomMethodSecurityExpressionRoot(authentication, permissionEvaluator);
root.setTrustResolver(new AuthenticationTrustResolverImpl());
root.setRoleHierarchy(getRoleHierarchy());
return root;
}
}
Я также ввел SecurityPermissionEvaluator
, который используется в вопросе, поэтому он будет единственной точкой входа для пользовательских и стандартных выражений. В качестве альтернативного варианта мы можем вводить и использовать PermissionService
напрямую.
Настройка безопасности на уровне метода:
@EnableGlobalMethodSecurity(securedEnabled = true, prePostEnabled = true)
public class MethodSecurityConfig extends GlobalMethodSecurityConfiguration {
@Autowired
private PermissionService permissionService;
@Override
protected MethodSecurityExpressionHandler createExpressionHandler() {
PermissionEvaluator permissionEvaluator = new SecurityPermissionEvaluator(permissionService);
return new CustomMethodSecurityExpressionHandler(permissionEvaluator);
}
}
Теперь мы можем использовать новое выражение в RestController
:
@PreAuthorize("hasAccessToCollection(#products, 'WRITE')")
@RequestMapping(value = "/saveCollection", method = RequestMethod.POST)
public Collection<Product> save(@RequestBody Collection<Product> products) {
return productService.save(products);
}
В результате часть с обработкой коллекции в PermissionService
может быть опущена, поскольку мы вывели эту логику в пользовательское выражение.
2. Второе обходное решение - вызов метода напрямую с помощью SpEL.
Теперь я использую PermissionEvaluator
как Spring bean (любая служба может быть использована здесь, но я предпочитаю единственную точку входа снова)
@Component
public class SecurityPermissionEvaluator implements PermissionEvaluator {
@Autowired
private PermissionService permissionService;
@Override
public boolean hasPermission(Authentication authentication, Object targetDomainObject, Object permission) {
if (!(targetDomainObject instanceof TopAppEntity))
throw new IllegalArgumentException();
CustomUserDetails userDetails = (CustomUserDetails) authentication.getPrincipal();
return permissionService.isAuthorized(userDetails.getUser(), targetDomainObject, permission.toString());
}
@Override
public boolean hasPermission(Authentication authentication, Serializable targetId, String targetType, Object permission) {
CustomUserDetails userDetails = (CustomUserDetails) authentication.getPrincipal();
try {
return permissionService.isAuthorized(userDetails.getUser(), targetId,
Class.forName(targetType), String.valueOf(permission));
} catch (ClassNotFoundException e) {
throw new IllegalArgumentException("No class found " + targetType);
}
}
public boolean hasPermission(Authentication authentication, Collection<Object> targetDomainObjects, Object permission) {
for (Object targetDomainObject : targetDomainObjects) {
if (!hasPermission(authentication, targetDomainObject, permission))
return false;
}
return true;
}
}
Настройка безопасности метода:
@EnableGlobalMethodSecurity(securedEnabled = true, prePostEnabled = true)
public class MethodSecurityConfig extends GlobalMethodSecurityConfiguration {
@Autowired
private PermissionEvaluator permissionEvaluator;
@Autowired
private ApplicationContext applicationContext;
@Override
protected MethodSecurityExpressionHandler createExpressionHandler() {
DefaultMethodSecurityExpressionHandler expressionHandler =
new DefaultMethodSecurityExpressionHandler();
expressionHandler.setPermissionEvaluator(permissionEvaluator);
// Pay attention here, or Spring will not be able to resolve bean
expressionHandler.setApplicationContext(applicationContext);
return expressionHandler;
}
}
Использование службы в выражении:
@PreAuthorize("@securityPermissionEvaluator.hasPermission(authentication, #products, 'WRITE')")
@RequestMapping(value = "/saveCollection", method = RequestMethod.POST)
public Collection<Product> save(@RequestBody Collection<Product> products) {
return productService.save(products);
}
Spring beans, созданный по умолчанию с именем класса, если другое имя не указано.
Сводка: оба подхода основаны на использовании пользовательских сервисов, вызывающих их напрямую или регистрации их в виде выражений, и могут обрабатывать логику сбора до того, как они будут отправлены службе проверки полномочий, поэтому мы можем опустить ее часть:
@Service
public class PermissionService {
public boolean isAuthorized(User user, TopAppEntity domainEntity, String permission) {
// removed instanceof checks and can operate on domainEntity directly
if (domainEntity.getId() == null) {
// check authorities and give response
} else {
// check ACL and give response
}
}
}
Ответ 3
Да, есть умный способ. Я могу сказать, что я сделал.
@Component("MySecurityPermissionEvaluator ")
@Scope(value = "session")
public class PermissionService {
@Autowired
private PermissionEvaluator permissionEvaluator;
public boolean myPermission(Object obj, String permission) {
boolean isAuthorized = false;
Authentication a = SecurityContextHolder.getContext()
.getAuthentication();
if (null == obj) {
return isAuthorized;
}
if (a.getAuthorities().size() == 0) {
logger.error("For this authenticated object, no authorities could be found !");
return isAuthorized;
} else {
logger.error("Authorities found " + a.getAuthorities());
}
try {
isAuthorized = myPermissionEval
.hasPermission(a, obj, permission);
} catch (Exception e) {
logger.error("exception while analysisng permissions");
}
return isAuthorized;
}
Пожалуйста, не используйте жестко закодированные разрешения, используйте этот способ вместо этого,
import org.springframework.security.acls.domain.DefaultPermissionFactory;
public class MyPermissionFactory extends DefaultPermissionFactory {
public MyPermissionFactory() {
registerPublicPermissions(MyPermission.class);
}
}
Чтобы сделать пользовательские разрешения,
import org.springframework.security.acls.domain.BasePermission;
public class MyPermission extends BasePermission { //use this class for creating custom permissions
private static Map<String, Integer> customPerMap = new HashMap<String, Integer>();
static {
customPerMap.put("READ", 1);
customPerMap.put("WRITE", 2);
customPerMap.put("DELETE", 4);
customPerMap.put("PUT", 8);
}
/**
*Use the function while saving/ getting permission code
**/
public static Integer getCode(String permName) {
return customPerMap.get(permName.toUpperCase());
}
Если вам необходимо аутентифицировать URL-адреса на основе пользователей-администраторов или иерархии ролей, используйте тег в Spring Аутентификация, а не авторизация.
Отдых, вы используете правильно, @PreAuthorize и @PreFilter оба правильны и используются в соответствии с требованиями.