Подтвердить что ты не робот

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?

4b9b3361

Ответ 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 оба правильны и используются в соответствии с требованиями.