【问题标题】:Spring Security hasPermission for Collection<Object>Spring Security hasPermission for Collection<Object>
【发布时间】:2017-10-30 06:49:25
【问题描述】:

我的工作应用程序受到方法级安全性的保护:

RestController:

@PreAuthorize("hasPermission(#product, 'WRITE')")
@RequestMapping(value = "/save", method = RequestMethod.POST)
public Product save(@RequestBody Product product) {
    return productService.save(product);
}

权限评估器:

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);
}

在此之后,我的权限服务处理集合对象,现在看起来像这样:

PmissionService:

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

【问题讨论】:

  • 感谢编辑 ;-)

标签: java spring spring-boot spring-security


【解决方案1】:

我有几个解决方法。

1.第一个是用我自己的MethodSecurityExpressionHandlerMethodSecurityExpressionRoot

创建一个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
        }
    }
}

【讨论】:

    【解决方案2】:

    是的,有一个聪明的方法。我可以告诉你我做了什么。

    @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 Authentication 中使用 tag 而不是 Authorization。

    休息,你用对了,@PreAuthorize 和@PreFilter 都是正确的,按要求使用。

    【讨论】:

    • 好吧,但我没有硬编码权限,它们存储为枚举值。
    • 是的,你当然做到了。我指的是 acl_entry 结构,其中代码存储在掩码中,并在 acl_entry 表中授予标志。此外,它还允许根据功能创建新的自定义权限,例如“删除、打印、下载”等。
    【解决方案3】:

    您可以使用@PreFilter annotation

    所以@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
                }
            } 
        }
    }
    

    注意:这不会阻止调用您的控制器方法。它只会得到一个空集合。

    【讨论】:

    • 这种方法的缺点是用户不会得到403 Access Denied,而是会被静默过滤,即使他想访问每个元素都被限制访问的元素集合。 GET 请求可能没问题,但 POST/PUT 则不行
    • 好的,但是@PreAuthorize 的问题在哪里。您的解决方案似乎很好。
    • 这是可行的解决方案,但我不喜欢所有这些instanceof 检查和强制转换,我正在寻找更聪明的方法,也许 Spring 有这个功能。
    • 我从未听说过另一个 Spring 解决方案,而不是使用 instanceof sorry...
    【解决方案4】:

    在某些情况下,SecurityExpressionRoot 的默认实现就足够了。 如果您的权限评估仅基于分析,例如产品所有者,您可以使用以下表达式:

    @GetMapping("")
    @PostAuthorize("hasPermission(returnObject.![#this.owner],'ProductOwner','READ')")
    public Collection<Product> getAllFiltering(<filters>) {...
    
    @PostMapping("/collection")
    @PreAuthorize("hasPermission(#products.![#this.owner],'ProductOwner','WRITE')")
    public Collection<Product> save(@RequestBody Collection<Product> products) {...
    
    @PutMapping("/collection")
    @PreAuthorize("hasPermission(@productRepository.findByIds(#products.![#this.id]).![#this.owner],'ProductOwner','WRITE')")
    public Collection<Product> update(@RequestBody Collection<Product> products) {...
    

    在这些情况下,您的 PermissionEvaluator 必须能够处理收集。您也可以继续将您的 PermissionEvaluator 用于单个产品:

    @GetMapping("/{id}")
    @PostAuthorize("hasPermission({ returnObject.owner },'ProductOwner','READ')")
    public Product getById(@PathVariable int id) {...
    

    或实现PermissionEvaluator 来分析传递的是数组还是单个值。

    #products.![#this.owner] - 参见“6.5.17 集合投影”; { returnObject.owner } - 参见“6.5.3 内联列表” 这里:https://docs.spring.io/spring/docs/3.0.x/reference/expressions.html

    【讨论】:

      猜你喜欢
      • 2012-01-02
      • 2018-02-03
      • 2015-09-11
      • 2013-06-25
      • 1970-01-01
      • 2014-06-03
      • 2013-10-23
      • 2020-08-03
      • 2018-01-17
      相关资源
      最近更新 更多