分层架构中的验证和授权


13

我知道您在思考(或大喊大叫),“不是另一个问题,它要求验证在分层体系结构中属于什么?!?” 好吧,是的,但是希望这会在这个问题上有所不同。

我坚信验证的形式多种多样,是基于上下文的,并且在体系结构的每个级别都有所不同。这是后期的基础-有助于确定应在每个层中执行哪种类型的验证。另外,经常出现的一个问题是授权检查在哪里。

示例场景来自餐饮业务的应用程序。白天,司机可能会不定期地将卡车上载到现场的任何多余现金上交办公室。该应用程序允许用户通过收集驾驶员的ID和金额来记录“现金下降”。这是一些基本代码来说明涉及的层:

public class CashDropApi  // This is in the Service Facade Layer
{
    [WebInvoke(Method = "POST")]
    public void AddCashDrop(NewCashDropContract contract)
    {
        // 1
        Service.AddCashDrop(contract.Amount, contract.DriverId);
    }
}

public class CashDropService  // This is the Application Service in the Domain Layer
{
    public void AddCashDrop(Decimal amount, Int32 driverId)
    {
        // 2
        CommandBus.Send(new AddCashDropCommand(amount, driverId));
    }
}

internal class AddCashDropCommand  // This is a command object in Domain Layer
{
    public AddCashDropCommand(Decimal amount, Int32 driverId)
    {
        // 3
        Amount = amount;
        DriverId = driverId;
    }

    public Decimal Amount { get; private set; }
    public Int32 DriverId { get; private set; }
}

internal class AddCashDropCommandHandler : IHandle<AddCashDropCommand>
{
    internal ICashDropFactory Factory { get; set; }       // Set by IoC container
    internal ICashDropRepository CashDrops { get; set; }  // Set by IoC container
    internal IEmployeeRepository Employees { get; set; }  // Set by IoC container

    public void Handle(AddCashDropCommand command)
    {
        // 4
        var driver = Employees.GetById(command.DriverId);
        // 5
        var authorizedBy = CurrentUser as Employee;
        // 6
        var cashDrop = Factory.CreateCashDrop(command.Amount, driver, authorizedBy);
        // 7
        CashDrops.Add(cashDrop);
    }
}

public class CashDropFactory
{
    public CashDrop CreateCashDrop(Decimal amount, Employee driver, Employee authorizedBy)
    {
        // 8
        return new CashDrop(amount, driver, authorizedBy, DateTime.Now);
    }
}

public class CashDrop  // The domain object (entity)
{
    public CashDrop(Decimal amount, Employee driver, Employee authorizedBy, DateTime at)
    {
        // 9
        ...
    }
}

public class CashDropRepository // The implementation is in the Data Access Layer
{
    public void Add(CashDrop item)
    {
        // 10
        ...
    }
}

我指出了在代码中放置过验证检查的10个位置。我的问题是,在给定以下业务规则的情况下,您将对每种业务进行哪些检查(以及对长度,范围,格式,类型等的标准检查):

  1. 现金下降额必须大于零。
  2. 现金滴必须有一个有效的驱动程序。
  3. 必须授权当前用户添加现金滴(当前用户不是驱动程序)。

请分享您的想法,您对这种情况的看法或处理方式以及选择的理由。


SE并不是“促进理论和主观讨论”的正确平台。投票关闭。
tdammers

措辞不佳的声明。我真的在寻找最佳实践。
SonOfPirate

2
@tdammers-是的,正确的地方。至少它想要成为。在常见问题解答中:“允许主观问题。” 这就是为什么他们制作此网站而不是Stack Overflow。不要成为亲密的纳粹分子。如果问题很糟,它将变得晦涩难懂。
FastAl

@FastAI:与其说是“主观”部分,不如说是“讨论”,这困扰着我。
tdammers

我认为您可以在此处通过使用CashDropAmount值对象而不是使用来利用值对象Decimal。检查驱动程序是否存在将在命令处理程序中完成,授权规则也是如此。Approver approver = approverService.findById(employeeId)如果员工不在批准者角色中,您可以通过执行类似的操作来免费获得授权。Approver只会是一个价值对象,而不是一个实体。你也可以得到在AR摆脱你的工厂或工厂的使用方法,而不是:cashDrop = driver.dropCash(...)
plalx

Answers:


2

我同意您要验证的内容在应用程序的每一层中都会有所不同。我通常只验证执行当前方法中的代码所需的条件。我尝试将基础组件视为黑盒,并且不根据这些组件的实现方式进行验证。

因此,举例来说,在您的CashDropApi类中,我只会验证“合同”不为空。这可以防止NullReferenceExceptions,并且它是确保此方法正确执行所需要的全部。

我不知道我会验证服务或命令类中的任何内容,并且处理程序将仅出于与CashDropApi类相同的原因来验证“命令”不为null。我已经看到(并完成了)对工厂类和实体类的验证。一个或另一个是您要验证“金额”的值并且其他参数不为空(您的业务规则)的地方。

存储库仅应验证对象中包含的数据与数据库中定义的架构一致,并且daa操作将成功。例如,如果您的列不能为null或最大长度,等等。

至于安全检查,我认为这确实是一个意图问题。由于该规则旨在防止未经授权的访问,因此,我希望在过程中尽早进行此检查,以减少如果未授权用户的情况下我已采取的不必要步骤。我可能会将其放在CashDropApi中。


1

您的第一个业务规则

现金下降额必须大于零。

看起来像您的CashDrop实体和类的不变式AddCashDropCommand。我可以通过以下几种方式来强制不变式:

  1. 根据您的情况,采用按合同设计”路线,并结合使用前提条件,后置条件和[ContractInvariantMethod]来使用代码契约
  2. 如果传递的数量小于0,则在构造函数/ setter中编写显式代码,并抛出ArgumentException。

您的第二个规则本质上是更广泛的(根据问题的细节而定):有效是否表示驾驶员实体具有表明他们可以驾驶的标志(即未暂停驾驶执照),是否意味着驾驶员已经在当天实际工作,或者仅表示传递给CashDropApi的driverId在持久性存储中有效。

在上述任何一种情况下,都需要像在代码示例中一样导航域模型并从中获取Driver实例。因此,在这里您需要确保对存储库的调用不会返回null,在这种情况下,您的driverId无效,并且您无法继续进行任何处理。IEmployeeRepositorylocation 4

对于其他两项(我的假设)检查(驾驶员是否拥有有效的驾驶执照,今天是在工作的驾驶员),您正在执行业务规则。

我在这里倾向于使用对实体进行操作的验证器类的集合(就像Eric Evans的书《域驱动设计》中的规范模式一样)。我已经使用FluentValidation构建了这些规则和验证器。然后,我可以从更简单的规则中编写(并因此重用)更复杂/更完整的规则。而且我可以决定在体系结构中的哪些层上运行它们。但是我将它们全部编码在一个地方,而不是分散在整个系统中。

您的第三个规则涉及一个跨领域的问题:授权。由于您已经在使用IoC容器(假设您的IoC容器支持方法拦截),因此可以执行一些AOP。编写一个进行授权的apsect,您可以使用IoC容器在需要的地方注入这种授权行为。这样做的最大好处是您编写了一次逻辑,但是可以在系统中重用它。

要通过动态代理(Castle Windsor,Spring.NET,Ninject 3.0等)使用拦截,您的目标类需要实现接口或从基类继承。如果用户没有目标方法,则应在调用目标方法之前进行拦截,检查用户的权限,并阻止调用继续进行到实际方法(抛出异常,记录日志,返回指示失败的值或进行其他操作)正确执行操作的角色。

在您的情况下,您可以拦截到

CashDropService.AddCashDrop(...) 

AddCashDropCommandHandler.Handle(...)

这里的问题可能CashDropService由于没有接口/基类而无法被拦截。或AddCashDropCommandHandler不是由您的IoC创建的,因此您的IoC无法创建动态代理来拦截呼叫。Spring.NET具有有用的功能,您可以通过正则表达式将程序集中定位在类中的方法上,因此这可能起作用。

希望这能给您一些想法。


您能否解释一下我将如何“使用IoC容器在需要的地方注入这种授权行为”?这听起来很吸引人,但到目前为止,让AOP和IoC一起工作使我感到困惑。
SonOfPirate 2012年

至于其余的,我同意在构造器和/或设置器中放置验证,以防止对象进入无效状态(处理不变式)。但是除此之外,在转到IEmployeeRepository定位驱动程序后,还引用了null检查,因此您没有提供执行其余验证的任何详细信息。考虑到FluentValidation的使用和它提供的重用等,您将在给定模型中应用规则吗?
SonOfPirate 2012年

我已经编辑了答案-看看是否有帮助。至于“您将在给定模型中将规则应用于何处?”;大概在您的命令处理程序中的4、5、6、7。您可以访问存储库,这些存储库可以提供执行业务级别验证所需的信息。但是我认为这里还有其他人会不同意我的看法。
RobertMS,2012年

为了澄清,所有依赖项都被注入。我将其保留以保持参考代码简短。我的查询与在方面中具有依赖性有关,因为方面不是通过容器注入的。那么,例如,AuthorizationAspect如何获得对AuthorizationService的引用?
SonOfPirate

1

对于规则:

1-现金下降额必须大于零。

2-现金存款必须具有有效的驱动程序。

3-必须授权当前用户添加现金滴(当前用户不是驱动程序)。

我将在业务规则(1)的位置(1)中进行验证,并确保ID不为null或否(假设零有效)作为对规则(2)的预检查。原因是我的规则“不要越过具有错误数据的图层边界,您可以使用可用的信息进行检查”。如果服务将验证作为其对其他调用者的职责的一部分,则将是一个例外。在这种情况下,仅在此处进行验证就足够了。

对于规则(2)和(3),必须仅在数据库访问层(或数据库层本身)上执行此操作,因为它涉及数据库访问。无需故意在各层之间移动。

如果我们让GUI防止未经授权的用户按下启用该方案的按钮,则可以特别避免规则(3)。虽然这很难编写代码,但更好。

好问题!


+1授权-将其放在用户界面中是我在回答中未提及的另一种选择。
RobertMS,2012年

虽然在UI中进行授权检查确实可以为用户提供更多的交互体验,但是我正在开发基于服务的API,并且不能对调用者已执行或未执行哪些规则进行任何假设。因为可以轻松地将许多此类检查委托给UI,所以我选择将API项目用作发布的基础。我正在寻找最佳实践,而不是简单易用的教科书。
SonOfPirate

@ SonOfPirate,INMO,UI需要进行验证,因为它比服务更快并且具有比服务更多的数据(在某些情况下)。现在,如果您不希望服务不信任客户端,那么服务就不应在没有进行自身验证的情况下在其边界之外发送数据,因为这是服务职责的一部分。因此,我建议在再次将数据发送到数据库进行进一步处理之前,再次在服务中进行非db检查。
NoChance 2012年
By using our site, you acknowledge that you have read and understand our Cookie Policy and Privacy Policy.
Licensed under cc by-sa 3.0 with attribution required.