分层架构中的授权过滤器

时间:2017-04-19 00:55:55

标签: authorization domain-driven-design acl ddd-repositories

拥有逻辑分层架构意味着我有一个层,只关注更新或获取更高层的数据。来自使用应用程序的一些查询必须仅返回控制应用程序的当前用户可以访问的实体。例如,在用户管理计算机的应用程序中,按名称搜索计算机应该只返回用户可以访问的匹配计算机。在此底层数据访问层中放置此过滤器,将查询限制为调用者可以访问的过滤器,还是必须从上层注入或传递此信息?

令我感到困惑的是,很明显只是为了浪费时间而把它扔掉,但是给这一层这个责任似乎不正确,因为我认为这是一个愚蠢的IO层(这个授权就像业务逻辑会阻止我改变它在一个地方的工作方式,并且对所有其他组件都是透明的。)

这种情况通常如何在理论上和实践中处理?

2 个答案:

答案 0 :(得分:1)

我在许多不同的地方看到过这种情况。与大多数编程问题一样,最佳解决方案取决于许多方面。

在DAL(数据访问层)

您需要确定当前经过身份验证的用户是谁,以便您知道要选择哪些列和/或要添加到数据库查询的子句。这可能会很快变得混乱。

正在序列化数据时

正在为视图序列化数据。根据序列化库的功能,您可以添加自己的自定义代码,该代码在序列化过程之前,期间(序列化每个字段时)或之后执行。这允许您决定哪些字段应该或不应该在数据的最终序列化版本中。

在查询上强制执行某些过滤器

如果您按照Command Query Responsibility Segregation(CQRS)进行操作,您将熟悉查询总线。您创建一个查询对象,将其发送到查询总线,它会查找执行查询并返回数据的查询的查询处理程序。

以您的示例为例,您只希望用户能够搜索与他相关的计算机。

class FindAllComputersQuery
{
    private $filters = [];

    public function __construct(array $filters = [])
    {
        $this->filters = $filters;
    }

    public function getFilters() : array
    {
        return $this->filters;
    }
}

查询处理程序

public function handle(FindAllComputersQuery $query) : array
{
    $filters = $query->getFilters();

    // Generate basic SQL to get all computers
    // If the 'userId' filter exists then add
    // some JOINs or WHEREs to the query to
    // filter the result set down to a specific user
}

查询总线通常具有中间件或类似的东西,这将允许您添加一些额外的功能,如在执行查询之前或之后执行某些代码。你可以:

创建一个QueryAuthorization中间件,在其处理程序执行查询之前执行。此QueryAuthorization中间件可以添加许多QueryAuthorizers

每次将查询发送到查询总线时,都会执行QueryAuthorization中间件(将查询传递给它)。

QueryAuthorization中间件然后遍历其所有QueryAuthorizers。如果找到支持当前查询的QueryAuthorizer,则会调用authorize($query, User $user)方法。

interface QueryAuthorizer
{
    public function supports($query) : bool;

    public function authorize($query, User $user);
}

class EnsureUserSeesOwnComputersOnly implements QueryAuthorizer
{

    public function supports($query) : bool
    {
        return ($query instanceof FindAllComputersQuery);
    }

    public function authorize($query, User $user)
    {
        // If the user is an ADMIN don't enforce anything.
        if ($user->hasRole('ADMIN')) {
            return;
        }

        $filters = $query->getFilters();

        // Ensure the 'userId' filter is set and that its value
        // is equal to the ID of the currently authenticated user.
        // You don't want the user to be able to put in another users ID.
        if (!isset($filters['userId'] || $filters['userId'] !== $user->getId())
        {
            // Throw some authorization exception because the required
            // userId filter was not supply or it was supplied but the ID
            // is not the ID of the currently authenticated user.
        }
    }

使用此方法,您可以拥有一个HTTP端点,该端点允许您检索所有计算机/computers并通过执行/computers?userId=some-user-id添加不同的过滤器。

在控制器中,您只需从请求中提取查询字符串参数(过滤器),创建查询对象并使用查询总线执行它。调用查询总线QueryAuthorization中间件,然后调用EnsureUserSeesOwnComputersOnly QueryAuthorizer

控制器

$filters = // Get query string parameters from HTTP request
$query = new FindAllComputersQuery($filters);
$computers = $this->queryBus->execute($query);

// Serialize the computers and return a JSON response

管理员可以向/computers发送请求,但用户必须至少向/computers?userId=USERS_OWN_ID_HERE发送请求。

使用这种方法,控制器和查询处理程序保持清晰,因为它们不必处理诸如授权之类的交叉问题。

使用此方法,您可以在需要时随时继续添加越来越多的QueryAuthorizers。查询可以包含任意数量的QueryAuthorizer个。您应该提供QueryAuthorizers个描述性名称,以便您立即了解每个人的所作所为。

答案 1 :(得分:1)

DDD中有许多架构。据说其中一些是分层的,有些则不是。在任何情况下,都只有layer(或slicecomponent或您想要调用它的任何内容),它只包含域代码;该域层不依赖于其他层,仅包含纯业务逻辑。该层的第一个客户端称为Application layer,其中包含Application services,其中包含授权责任;它检查是否允许用户执行某些命令(修改系统状态)或查询(查看某些数据)。虽然看起来这个authorization包含业务逻辑,但这个逻辑与core business logic不同。

因此,关于您的示例,如果用户有权访问某些计算机,则域代码不应自行检查。存储库应该公开Application services可用于过滤用户有权访问的计算机的过滤器。将此功能放在存储库中是可以的。实际上,在我看来,存储库不属于域层,甚至不属于repository interfaces

CQRS中,域层的命令端不包含任何存储库/基础结构代码/接口的跟踪,只包含纯域逻辑。 应用程序服务授权用户,加载聚合然后在其上调用方法(此处保留域代码)然后保留聚合。

在查询方面,可能存在读取模型中涉及的基础结构代码,以便甚至保持域层薄。 此处驻留可供应用程序服务使用的过滤器,以过滤用户有权访问的计算机,但这些过滤器未被域代码预先应用,它们只是暴露给{{1使用。

所以,结论是不要将授权代码与域/业务逻辑代码混合