这是什么意思?“用户不应该决定它是否是管理员。特权或安全系统应如此。”


55

问题中使用的示例将最少的数据传递给函数,以确定是否用户是管理员的最佳方法。一个常见的答案是:

user.isAdmin()

这引起了评论,该评论被重复了多次并被多次投票:

用户不应该确定它是否是管理员。特权或安全系统应该。与课程紧密联系并不意味着将其纳入课程是一个好主意。

我回答,

用户没有决定什么。用户对象/表存储有关每个用户的数据。实际用户不会改变自己的一切。

但这没有效果。显然,存在潜在的视角差异,这导致沟通困难。有人可以向我解释为什么user.isAdmin()不好,并绘制一个简短的草图来“完成”吗?

确实,我看不到将安全与受保护系统分开的优势。任何安全性文字都将要求从一开始就将安全性设计到系统中,并在开发,部署,维护乃至报废的每个阶段都要加以考虑。它不是可以固定在侧面的东西。但到目前为止,此评论有17票赞成票,表明我错过了一些重要的内容。


8
我不认为这很糟糕,特别是如果它只是用户数据库表中的一个标志。如果将来有变化,请将来进行更改。您也可以从用户对象调用安全系统。我曾经读过关于每个数据库/资源​​访问都应通过当前用户对象的设计模式的信息,以确保您绝不会尝试做不允许做的事情。可能很多,但是用户可以提供一个“特权”对象,该对象涵盖所有与安全性有关的内容。
Cephalopod

我认为有误会。对于您来说,“用户”是指使用该软件的人。对于他们来说,“用户”是指使用您的功能的代码。
菲利普

2
并不是您问的,但是在这种情况下,我最要注意的一点是,无论您放在哪里,都必须使用IsAdmin()方法。我通常建议不要对“ Admin”进行硬编码,而应仅具有一组可能的权限,而“ Admin”恰好具有整个权限。这样,当以后需要分割的管理角色,事情容易。而且它减少了特殊情况的逻辑。
psr

1
@Philipp我不这么认为...两个站点都在User用一种isAdmin()方法明确地谈论一个对象(无论它在幕后做什么)
Izkata

编辑太迟了;我的意思是“双方”,而不是“双方” ... = P
Izkata

Answers:


12

将用户视为键,将权限视为值。

不同的上下文可能对每个用户具有不同的权限。由于权限与上下文相关,因此您必须为每个上下文更改用户。因此最好将逻辑放在其他地方(例如上下文类),然后在需要的地方查找权限。

副作用是它可能使您的系统更安全,因为您不能忘记清除与管理无关的地方的admin标志。


40

该评论措辞不佳。

关键不在于用户对象是否“决定”它是管理员,试用用户,超级用户还是其他与众不同的对象。显然,它并没有决定任何事情,而是由您实施的整个软件系统都可以。关键是“用户” 类是应该代表其对象所代表的主体的角色,还是这是另一类的责任。


6
哎呀(这是我的评论),但您确实比我说的要好得多。
Marjan Venema 2013年

您是说要在单独的Role类而不是User类中对角色建模?您是否不建议使用像LDAP这样的单独系统?如果Role类做出的每个决定都需要查询适当的用户记录并且不需要其他信息怎么办?它是否仍应与用户分开,如果是,为什么?
GlenPeterson

2
@GlenPeterson:不一定,仅是类<>表,通常可以比表识别出更多的类。面向数据确实会导致在类上定义子列表,但是很多时候这些列表应在另一个“分类帐”(顺序)或某个此类传递给用户(或顺序)的类中进行定义以检索...与其说动词和名词,不如说是责任。
Marjan Venema 2013年

9
将用户视为键,将权限视为值。不同的上下文可能对每个用户具有不同的权限。由于权限与上下文相关,因此您要么必须为每个上下文更改用户。因此,最好将该逻辑放在其他地方(例如,上下文类)。
威伯特

2
@Wilbert:上下文!这特别有启发性!是的,如果这些权限具有多个上下文,那么一切对我来说都是有意义的。通常,我会在单个上下文中看到这些权限,在这种情况下,将其粘贴在用户上是最简单的解决方案。显然,如果它们在第二上下文中的应用方式不同,则它们不属于此。如果您将其写为答案,我很可能会接受。谢谢。
GlenPeterson

30

我不会选择原始评论的措词方式,但它确实标识了潜在的合法问题。

具体而言,保证分离的关注点是身份验证授权

身份验证是指登录并获取身份的过程。系统就是这样来了解您的身份的,并用于诸如个性化,对象所有权等之类的事情。

授权是指您被允许做的事情,(通常)这不是您是谁来决定的。取而代之的是,它由某些安全策略(例如角色或权限)确定,这些策略不关心您的姓名或电子邮件地址。

这两个可以彼此正交变化。例如,您可以通过添加OpenID / OpenAuth提供程序来更改身份验证模型。您可以通过添加新角色或从RBAC更改为ABAC来更改安全策略。

具有讽刺意味的是,如果所有这些都归为一类或抽象,那么作为降低风险的最重要工具之一的安全代码将变成高风险。

我曾在身份验证和授权过于紧密的系统上工作。在一个系统中,有两个并行的用户数据库,每个数据库都用于一种“角色”。设计它的人员或团队显然从未考虑过单个物理用户可能同时担任两个角色,或者可能有多个角色共有的某些操作,或者User ID冲突可能存在问题。这是一个公认的极端示例,但是使用它非常痛苦。

Microsoft和Sun / Oracle(Java)将身份验证和授权信息的集合称为“ 安全主体”。它不是完美的,但是效果很好。例如,在.NET中,您具有IPrincipal,它封装IIdentity-前者是策略(授权)对象,而后者是身份(身份验证)。你可以合理质疑把一个决定其他的,但重要的是,大多数的代码编写将是抽象的只有一个,这意味着它很容易测试和重构。

一个User.IsAdmin字段没有什么问题... 除非也有一个User.Name字段。这表明“用户”的概念定义不正确,可悲的是,这是在安全性方面有些耳熟能详的开发人员中的一个常见错误。通常,身份策略应该共享的唯一内容是用户ID,而并非巧合的是,这正是Windows和* nix安全模型中如何实现的。

创建封装身份和策略的包装对象是完全可以接受的。例如,这将有助于创建仪表板屏幕,除了允许当前用户访问的各种小部件或链接之外,您还需要显示“ hello”消息。只要此包装器仅包装身份和策略信息,并且不声称拥有它即可。换句话说,只要它没有以合计root的形式出现

由于YAGNI等原因,当您初次设计新应用程序时,简单化的安全模型似乎总是个好主意,但它几乎总是最终会在以后给您带来麻烦,因为令人惊讶的是,增加了新功能!

因此,如果您知道什么最适合您,则将身份验证和授权信息分开。即使现在的“授权”就像“ IsAdmin”标志一样简单,但是如果它与身份验证信息不在同一类或表中,那么您仍然会更好,以便在需要时以及何时需要使用安全策略更改后,您无需在已经运行良好的身份验证系统上进行重建手术。


11
哦,我希望我有时间写这篇文章。再说一遍,如果我没有更多的思考,我可能无法像您那样做。很多时候,我的直觉告诉我要走另一条路,但是向自己或其他人解释为什么要花很多的心思和精力。感谢这篇文章。本来可以加倍的,但是SE不会让我...
Marjan Venema

这听起来与我正在从事的项目惊人地相似,您的回答给了我另一个视角。非常感谢!
布兰登

您写道:“ User.IsAdmin字段没有任何问题……除非也有User.Name字段”。但是什么isAdmin是方法?它可能只是委派给负责跟踪权限的对象。关系模型设计中的最佳实践(实体拥有的字段)不一定可以转换为OO模型中的最佳实践(对象可以响应的消息)。
KaptajnKold 2013年

@KaptajnKold:我一直在这个论坛的各种问答环节中阅读这种观点。如果存在方法,则直接执行操作还是委托它们都无关紧要,因为其他类可能依赖它所以它仍然被视为责任。您User.IsAdmin很可能会成为只调用call的单行代码PermissionManager.IsAdmin(this.Id),但是如果它被其他30个类引用,则不能更改或删除它。这就是SRP存在的原因。
亚伦诺特,2013年

而且,就关系模型而言,@ KaptajnKold我不确定您要做什么。它甚至更糟有一个IsAdmin 领域。在系统演变为RBAC或更复杂的东西之后,这种领域往往会持续存在,并逐渐与“真实”权限失去同步,人们忘记了他们不再应该使用它,而且...好吧,就说不。即使您认为只有两个角色“ admin”和“ non-admin”,也不要将其存储为布尔/位字段,不要进行规范化并具有权限表。
Aaronaught

28

好吧,至少它违反了单一责任原则。是否应该进行更改(多个管理员级别,甚至还有更多不同的角色?),这种设计相当混乱且难以维护。

考虑将安全系统与用户类别分离的优势,以便两者都可以具有单一的,定义明确的角色。您最终将获得更清洁,更易于维护的整体设计。


2
@GlenPeterson:不,没有安全性,用户可以存在。我的名字,显示名,我的地址,我的电子邮件地址等如何?您将它们放在哪里?标识(认证)和授权是完全不同的野兽。
Marjan Venema 2013年

2
class User标识,认证,授权安全三元组中的核心组件。这些在设计级别上紧密联系在一起,因此代码反映这种相互依赖性并非没有道理。
MSalters

1
问题是,User类是否应包含处理三元组的所有逻辑!
Zavior

3
@MSalters:如果它是此类逻辑的API,则即使它在其他地方委派了确切的逻辑,它也知道它。提出的观点是,授权(权限)是关于用户与其他内容的组合,因此不应成为用户或其他内容的一部分,而应是正确了解用户以及其他任何内容的权限系统的一部分正在获得许可。
Marjan Venema 2013年

2
Should there be changes-好吧,我们不知道会有变化。YAGNI和所有。必要时会进行这些更改吗?
2013年

10

我认为这个问题,也许措辞不佳,是因为它对User全班同学施加了太大的压力。不,User.isAdmin()正如那些评论所建议的那样,使用method不会带来安全问题。毕竟,如果有人对您的代码足够深入,可以为您的核心类之一注入恶意代码,那么您将遇到严重的问题。另一方面,User对于每个上下文确定它是否是管理员都没有道理。假设您添加了不同的角色:论坛的主持人,可以将帖子发布到首页的编辑者,可以编写内容供编辑者发布的作家,等等。通过将其放在User对象上的方法,您将最终获得太多的方法和太多的逻辑,这些逻辑和逻辑User与该类没有直接关系。

相反,您可以使用不同类型的权限的枚举和一个PermissionsManager类。也许您可以使用类似的方法来PermissionsManager.userHasPermission(User, Permission)返回布尔值。实际上,它看起来像(在Java中):

if (!PermissionsManager.userHasPermission(user, Permissions.EDITOR)) {
  Site.renderSecurityRejectionPage();
}

从本质上讲,它等效于Rails方法before_filter,该方法提供了在现实世界中这种类型的权限检查的示例。


6
它与相比有什么不同user.hasPermission(Permissions.EDITOR),除了它更冗长和过程化而不是OO?
Cephalopod

9
@Arian:因为user.hasPermissions在用户类中承担了太多的责任。它需要用户了解权限,而用户(类)应该能够在没有此类知识的情况下存在。您既不希望用户类自己知道如何打印或显示(构建表单),也可以将其留给打印机渲染器/构建器或显示渲染器/构建器类。
Marjan Venema 2013年

4
user.hasPermission(P)是正确的API,但这只是接口。当然,真正的决定必须在安全子系统中做出。为了解决syrion对测试的评论,您可以在测试期间替换该子系统。但是,直接的好处user.hasPermission(P)是您不会污染所有代码,而只是可以进行测试class User
MSalters 2013年

3
@MarjanVenema通过这种逻辑,用户将仍然是一个完全没有责任的数据对象。我并不是说如果需要这样一个复杂的系统,整个权限管理应该在用户类中实现。您说用户不需要了解权限,但是我看不到为什么其他所有类都需要知道如何为用户解析权限。这使得模拟和测试更加困难。
Cephalopod

6
@Arian:贫血类是一种气味,但有些类却没有任何行为……用户imho是这样的类,最好将其用作一大堆其他东西(他们想做的事情)的参数/根据是谁的要求来做出决定),而不是仅仅因为这样编码看起来很方便而被用作各种行为的容器。
Marjan Venema 2013年

9

Object.isX()用于表示对象和X之间的明确关系,可以将其表示为布尔结果,例如:

Water.isBoiling()

Circuit.isOpen()

在系统的每个上下文中, User.isAdmin()都具有“管理员”的单一含义,即在系统的每个部分中,用户都是管理员。

虽然听起来很简单,但实际的程序几乎永远都不适合该模型,但是肯定会需要用户管理[X]资源而不是[Y],或者需要更多不同类型的管理员([项目]管理员与[系统]管理员)。

这种情况通常需要对需求进行调查。很少,如果有的话,客户端会希望User.isAdmin()实际代表的关系,所以我会犹豫地实施任何此类解决方案,而无需澄清。


6

海报的构思只是一个不同且更复杂的设计。 我认为User.isAdmin()很好。即使您稍后介绍了一些复杂的权限模型,我也没有理由提出任何理由User.isAdmin()。稍后,您可能需要将User类拆分为一个代表登录用户的对象和一个代表该用户数据的静态对象。否则,您可能不会。为明天保存明天。


4
Save tomorrow for tomorrow。是的 当您发现您刚刚编写的紧密耦合的代码使您陷入困境,并且您必须重写它,因为您没有花费额外的20分钟使其解耦...
MirroredFate 2013年

5
@MirroredFate:完全有可能将User.isAdmin方法连接到任意权限机制,而无需将User类耦合到该特定机制。
凯文·克莱恩

3
为了使User.isAdmin耦合到任意权限机制,即使不耦合到特定机制,也需要...耦合。最小的是接口。而且该接口应该根本不存在。它给用户类(和界面)一些它根本不应该负责的东西。Aaronaught比我能更好地解释它(对此问题的所有其他答案的结合也是如此)。
Marjan Venema 2013年

8
@MirroredFate:我不在乎是否必须重写一个简单的实现来满足更复杂的要求。但是,我对于使用过度复杂的API却经历了非常糟糕的体验,这些API旨在满足人们从未想到的未来需求。
凯文·克莱恩

3
@kevincline虽然(定义上)过于复杂的API是一件坏事,但我并不是在建议不要编写过于复杂的API。我说的Save tomorrow for tomorrow是一种糟糕的设计方法,因为如果正确实施该系统,它将使问题变得微不足道。实际上,这种思想导致API过于复杂,因为一层又一层地添加了补丁,以弥补最初的不良设计。我想我的立场是measure twice, cut once
MirroredFate 2013年

5

真的不是问题...

我认为没问题User.isAdmin()。我当然喜欢它,而不是诸如之类的东西PermissionManager.userHasPermission(user, permissions.ADMIN),它以SRP的圣名使代码不太清晰,并且不会增加任何有价值的东西。

我认为某些人对SRP的解释有些不确切。我认为这很好,甚至最好让类具有丰富的接口。SRP只是意味着对象必须将不属于其单一职责的所有内容委派给协作者。如果用户的管理员角色比布尔字段更多,则将用户对象委派给PermissionsManager可能很有意义。但这并不意味着保留用户isAdmin方法也不是一个好主意。实际上,这意味着当您的应用程序从简单升级为复杂时,您需要更改使用User对象的代码。IOW,您的客户不需要知道实际要回答用户是否是管理员的问题。

...但是您为什么真正想知道?

就是说,在我看来,您几乎不需要知道用户是否是管理员,而是能够回答其他问题,例如是否允许用户执行某些特定操作,例如更新小部件。如果是这样,我希望在Widget上有一个方法,例如isUpdatableBy(User user)

boolean isUpdatableBy(User user) {
    return user.isAdmin();
}

这样,窗口小部件负责知道必须满足哪些条件才能被用户更新,而用户则负责知道它是否是管理员。这种设计可以清楚地传达其意图,并且可以在需要的时候更轻松地过渡到更复杂的业务逻辑。

[编辑]

这个问题具有User.isAdmin()与使用PermissionManager.userHasPermission(...)

我想补充一下我的答案,如果我想知道用户是否是管理员(或具有管理员角色),为什么我更喜欢在User对象上调用方法而不是在PermissionManager对象上调用方法。

我认为可以假设您总是要依赖User类,无论您需要问这个用户是admin吗?这是您无法摆脱的依赖。但是,如果您需要将用户传递给另一个对象,以询问有关该对象的问题,则将在您已经对用户拥有的对象之上,对该对象创建新的依赖关系。如果这个问题被问了很多,那么在很多地方您会创建一个额外的依赖项,并且如果有任何一项依赖项要求您可能需要在很多地方进行更改。

将此与将依赖项移到User类中进行比较。现在,突然有了一个系统,在该系统中,客户端代码(需要问这个问题的代码是该用户是admin)没有与如何回答此问题的实现耦合。您可以自由地完全更改权限系统,并且只需要更新一个类中的一个方法即可。所有客户端代码保持不变。

isAdmin我看来,坚持在User 上没有方法是因为担心会在权限子系统的User类中创建依赖项,这类似于花一美元赚一毛钱。当然,可以避免在User类中使用一种依赖关系,但是要在需要询问问题的每个地方都创建一个依赖关系。不好讨价还价。


2
“ SRP只是意味着一个对象必须将不属于其单一职责的所有内容委派给协作者” -false。SRP的定义包含对象直接执行的所有操作以及它委派的所有操作。仅直接执行一项任务,但通过委派间接执行其他50项任务的类仍然(可能)违反SRP。
Aaronaught

KaptajnKold:我+1是因为我同意您的看法,对此我感到内。您的帖子增加了讨论,但没有回答问题。
GlenPeterson

@GlenPeterson好吧,我试图解决问题的一部分,即“看起来做对了”。”另外:您绝对不对同意我感到内::)
KaptajnKold 2013年

1
@JamesSnell也许吧。如果。但这是一个实现细节,我仍然将其限制在User的isAdmin方法中。这样,当用户的“管理性”从布尔型字段演变为更高级的字段时,不必更改客户端代码。您可能有很多地方需要知道用户是否是管理员,并且不想每次更改授权系统时都必须更改它们。
KaptajnKold 2013年

1
@JamesSnell也许我们误会了彼此?的问题,用户是否是管理员是不一样的问题,用户是否被允许执行特定动作。第一个问题的答案始终独立于上下文。第二个答案很大程度上取决于上下文。这是我在原始答案的后半部分试图解决的问题。
KaptajnKold 2013年

1

这次讨论使我想起了Eric Lippert的Blog Post,在有关类设计,安全性和正确性的类似讨论中引起了我的注意。

为什么要密封这么多框架类?

特别是,埃里克提出了以下观点:

4)安全。多态性的全部意义在于,您可以绕过看起来像动物但实际上是长颈鹿的对象。这里存在潜在的安全问题。

每次实现一个采用未密封类型实例的方法时,都必须将该方法编写为在面对该类型可能具有敌意的实例时具有鲁棒性。您不能依赖任何您知道对您的实现都是正确的不变量,因为某些敌对的网页可能会使您的实现成为子类,重写虚拟方法来做弄乱您的逻辑的事情,并将其传递给我。每次我密封一个类时,我可以写出使用该类的方法,并且确信我知道该类的作用。

这似乎是切线,但我认为这与其他张贴者提出的有关SOLID 单一责任原则的观点相吻合。将以下恶意类视为不实现User.IsAdmin方法或属性的原因。

public class MyUser : User
{

    public new boolean IsAdmin()
    {
        // You know it!
        return true;
    }

    // Anything else we can break today?

}

当然,这有点麻烦:尚无人建议创建IsAmdmin虚拟方法,但是如果您的用户/角色体系结构最终变得异常复杂,则可能会发生这种情况。(或者可能不是。考虑public class Admin : User { ... }:这样的类可能恰好在上面的代码中。)将恶意二进制文件放置到位不是常见的攻击媒介,并且比晦涩的用户库具有更大的混乱可能性-然后,再次可能是“ 特权升级”错误,它打开了真正的恶作剧之门。最终,如果恶意二进制或对象实例确实在运行时找到了方式,那么想象“特权或安全系统”以类似方式被替换就不费吹灰之力了。

但是请记住Eric的观点,如果将用户代码放在特定类型的易受攻击的系统中,也许您确实输了游戏。

噢,为了准确起见,我同意以下问题中的假设:“用户不应决定它是否是管理员。该特权或保障体系”一个User.IsAdmin方法是一个坏主意,如果你正在运行的系统的代码,你不要停留在代码的100%的控制-你应该做的,而不是Permissions.IsAdmin(User user)


??这如何相关?无论将安全策略放在何处,都始终可以用不安全的方式将其子类化。不管是用户,主体,策略,PermissionSet还是其他。这是一个完全无关的问题。
亚罗诺(Aaronaught)2013年

支持单一责任是一种安全论点。为了直接回答您的观点,安全和权限模块确实是一个密封的类,但是某些设计可能希望User类是虚拟的和继承的。这是博客文章的最后一点(因此可能是最不可能的/最不重要的?),也许我正在将这些问题混为一谈,但是根据消息来源,很明显,多态性可能是安全威胁,因此限制了责任。给定的类/对象更安全。
Patrick M

我不确定你为什么这么说。许多安全性和权限框架都是高度可扩展的。大多数与安全相关的(IPrincipal的,IIdentity的,ClaimSet等)的核心.NET类型的不仅不是密封的,但事实上是接口或抽象类,让你可以插入任何你想要的。Eric谈论的是您不希望System.String有可继承的东西,因为各种关键框架代码都对其工作方式进行了非常具体的假设。实际上,大多数“用户代码”(包括安全性)是可继承的,以允许进行两次测试。
亚伦诺特,2013年

1
无论如何,问题中的任何地方都没有提到多态性,如果您遵循作者对原始注释的链接,很显然,它一般不是在指代继承,甚至也不是类不变式,而是关于责任。
亚伦诺特,2013年

我知道没有提到它,但是类责任的原则与多态性直接相关。如果没有IsAdmin覆盖/合作的方法,就没有潜在的安全漏洞。回顾一下这些系统接口IPrincipal和的方法IIdentity,很明显,设计人员不同意我的观点,因此我承认了这一点。
Patrick M

0

这里的问题是:

if (user.isAdmin()) {
   doPriviledgedOp();
} else {
   // maybe we can anyway!
   doPriviledgedOp();
}

要么您已经正确设置了安全性,在这种情况下,特权方法应检查调用方是否具有足够的权限-在这种情况下,检查是多余的。或者,您没有并且正在信任一个非特权类来自己决定安全性。


4
什么是非特权阶级?
布兰登

1
实际上,您无法采取任何措施来阻止编写此类代码。无论您如何设计应用程序,在某处都会进行这样的显式检查,并且有人可能将其编写为不安全的。即使您的权限系统像用角色或权限属性装饰方法一样精简,也有人可能会错误地在其上抛出“公共”或“所有人”权限。因此,我真的看不到这如何使这种方法User.IsAdmin 特别成为问题。
亚伦诺特,2013年
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.