在上一部分的学习中,我们讨论了计算机安全的基本概念,特别是CIA三元组。现在我们要深入探讨实现这些安全目标的核心机制:访问控制。访问控制是计算机安全的基础,几乎所有其他安全机制都建立在访问控制之上。如果你理解了访问控制,你就理解了安全系统如何工作的本质。

假设你在一家大型公司工作,公司的文件服务器上存储着各种敏感信息:财务数据、客户信息、研发文档、人事档案。不是每个人都能访问所有信息。财务部门需要访问财务数据,但不能访问研发文档。研发部门需要访问研发文档,但不能访问人事档案。这种“谁能访问什么”的规则,就是访问控制要解决的问题。
访问控制不仅仅是技术问题,它反映了现实世界中的权限和信任关系。在数字世界中,我们需要用技术手段来实现这些关系。但技术实现往往比现实世界的规则更复杂,因为我们需要处理各种边界情况、异常情况,以及攻击者可能利用的漏洞。
访问控制的核心问题是:如何决定一个主体(Subject)是否可以对一个客体(Object)执行某个操作(Operation)?这个看似简单的问题,在实际系统中却极其复杂。主体可能是用户、进程、服务账户等。客体可能是文件、数据库记录、网络端口、系统资源等。操作可能是读取、写入、执行、删除等。
访问控制是安全系统的"守门员"。它决定谁可以进入,谁可以做什么,谁被拒之门外。如果访问控制失效,整个安全系统就会崩溃。
访问控制模型提供了一种形式化的方法来描述和实现访问控制策略。不同的模型有不同的假设和适用场景。有些模型强调灵活性,有些模型强调安全性,有些模型强调性能。选择合适模型的关键是理解你的威胁模型和业务需求。
在实际系统中,访问控制往往不是单一模型,而是多个模型的组合。一个系统可能使用基于角色的访问控制(RBAC)来管理用户权限,使用访问控制列表(ACL)来管理文件权限,使用能力列表(Capability List)来管理进程权限。
在访问控制模型中,有三个基本元素:主体、客体和操作。
主体是请求访问的实体。在大多数系统中,主体是用户。但在更复杂的系统中,主体可能是进程、线程、服务账户、API密钥等。一个用户可能同时是多个主体——比如,同一个用户可能以不同的角色登录,每个角色对应不同的主体。
主体的身份需要通过认证(Authentication)来确认。但认证只是第一步,确认了"你是谁"之后,还需要授权(Authorization)来决定"你能做什么"。这是两个不同的概念,但在实际系统中经常被混淆。认证是验证身份,授权是授予权限。
主体和用户不是一一对应的关系。一个用户可能对应多个主体(不同的角色、不同的会话),一个主体也可能代表多个用户(服务账户、共享账户)。理解这种区别对于设计安全的访问控制系统至关重要。
在实际系统中,主体往往有层次结构。一个主体可能属于某个组,组可能属于某个部门,部门可能属于某个组织。这种层次结构使得权限管理更加灵活,但也增加了复杂性。权限继承、权限覆盖、权限冲突——这些都是需要处理的问题。

客体是被访问的资源。在文件系统中,客体是文件和目录。在数据库中,客体是表、行、列。在网络中,客体是端口、服务、数据包。在操作系统中,客体是进程、内存区域、设备。
客体的粒度决定了访问控制的精细程度。粗粒度的访问控制(比如整个数据库)容易实现,但可能过于宽松。细粒度的访问控制(比如数据库中的单个字段)更安全,但实现和维护更复杂。在实际系统中,需要在安全性和复杂性之间找到平衡。
客体的属性也可能影响访问控制。一个文件可能有所有者、组、权限位等属性。一个数据库表可能有行级安全策略、列级安全策略等。
操作是主体对客体执行的动作。常见的操作包括读取(Read)、写入(Write)、执行(Execute)、删除(Delete)等。但在不同的系统中,操作可能有不同的含义。在文件系统中,读取操作意味着读取文件内容。在数据库中,读取操作可能意味着查询数据。在网络中,读取操作可能意味着接收数据包。
操作可能有参数。比如,读取操作可能有偏移量和长度参数,写入操作可能有数据和位置参数。这些参数可能影响访问控制的决策。一个主体可能被允许读取文件的前1000字节,但不能读取后面的内容。这种细粒度的控制需要更复杂的访问控制机制。
操作的安全性不仅取决于操作本身,还取决于操作的参数。一个“读取”操作,如果参数是“整个数据库”,可能比一个“写入”操作,如果参数是“单个字段”,更危险。访问控制系统必须考虑操作的完整上下文。
在实际系统中,操作往往有副作用。一个“读取”操作可能触发日志记录、审计、缓存更新等。一个“写入”操作可能触发数据验证、触发器执行、备份等。这些副作用可能影响安全性,也可能被攻击者利用。理解操作的完整语义,而不仅仅是操作的名称,是设计安全系统的关键。
访问控制矩阵(Access Control Matrix)是访问控制的形式化模型。它用一个矩阵来表示所有主体对所有客体的所有操作的权限。矩阵的行代表主体,列代表客体,矩阵中的每个单元格包含该主体对该客体可以执行的操作集合。
理论上,访问控制矩阵是完美的。它明确地定义了所有可能的访问关系,没有歧义,没有遗漏。但在实际系统中,访问控制矩阵几乎不可能直接实现。为什么?因为矩阵太大了。
假设一个系统有1000个用户(主体)和10000个文件(客体),访问控制矩阵就有1000万行。如果每个单元格需要存储操作集合,内存需求会非常巨大。更糟糕的是,矩阵是稀疏的——大多数主体对大多数客体没有权限,矩阵中大部分单元格是空的。存储一个稀疏矩阵是极其浪费的。
访问控制矩阵是理论模型,不是实现模型。它帮助我们理解访问控制的本质,但实际系统需要使用更高效的表示方法。
虽然访问控制矩阵不能直接实现,但它仍然是理解访问控制的重要工具。它帮助我们思考:访问控制决策需要什么信息?这些信息如何组织?如何高效地查询这些信息?实际系统中的访问控制机制,无论是ACL还是能力列表,都可以看作是访问控制矩阵的不同实现方式。
访问控制矩阵还有一个重要特性:它可以表示动态权限。矩阵中的单元格不是固定的,可以根据条件动态变化。比如,一个主体可能在工作时间可以访问某个客体,但在非工作时间不能访问。这种时间条件可以在矩阵中表示,但需要更复杂的实现机制。
能力列表(Capability List)是基于主体的访问控制实现方式。每个主体维护一个列表,列出该主体可以访问的所有客体及其权限。这个列表就是该主体的“能力”。
能力(Capability)是一个不可伪造的令牌,代表对某个客体的某种访问权限。能力本身包含客体的标识和允许的操作。主体要访问客体时,只需要出示相应的能力。系统验证能力是否有效,如果有效,就允许访问。
能力的不可伪造性是关键。如果攻击者可以伪造能力,整个访问控制系统就会失效。在实际系统中,能力通常由系统内核生成和管理,用户空间无法直接创建或修改能力。能力可能包含加密签名、时间戳、随机数等,以防止伪造。
能力的不可伪造性依赖于系统的完整性。如果攻击者可以修改内核代码,或者可以访问能力存储区域,能力的不可伪造性就会失效。这就是为什么系统安全如此重要。
能力列表的优势是查询效率高。当主体要访问客体时,只需要检查主体的能力列表,不需要检查客体的权限列表。这在主体数量少、客体数量多的场景中特别有用。比如,一个进程可能只需要访问少数几个文件,但系统中可能有成千上万个文件。
但能力列表也有劣势。当需要撤销权限时,系统需要找到所有拥有该权限的主体,并从它们的能力列表中删除相应的能力。这在主体数量多、权限变化频繁的场景中会很困难。比如,如果一个文件被删除,系统需要找到所有可以访问该文件的主体,并撤销它们的能力。
在实际系统中,能力列表往往与其他机制结合使用。比如,能力可能有过期时间,过期后自动失效。能力可能有撤销列表,系统定期检查能力是否在撤销列表中。这些机制可以缓解权限撤销的问题,但增加了系统复杂性。
访问控制列表(Access Control List,ACL)是基于客体的访问控制实现方式。每个客体维护一个列表,列出可以访问该客体的所有主体及其权限。这个列表就是该客体的“访问控制列表”。

当主体要访问客体时,系统检查客体的ACL,看该主体是否在列表中,以及是否有相应的权限。如果主体在列表中且有相应权限,就允许访问。否则,拒绝访问。
ACL的优势是权限管理直观。要查看谁可以访问某个文件,只需要查看该文件的ACL。要撤销某个文件的访问权限,只需要从该文件的ACL中删除相应的条目。这在客体数量少、权限管理频繁的场景中特别有用。
ACL是Unix/Linux文件系统权限模型的扩展。传统的Unix权限只有所有者、组、其他三个类别,每个类别有读、写、执行三个权限位。ACL允许为每个文件指定任意数量的用户和组的权限,提供了更细粒度的控制。
但ACL也有劣势。当需要查看某个主体可以访问哪些客体时,系统需要检查所有客体的ACL,这在客体数量多的场景中会很慢。当主体数量多时,每个客体的ACL可能会很长,存储和查询成本会增加。
在实际系统中,ACL往往使用组和通配符来减少存储空间。比如,一个文件的ACL可能包含“所有研发部门的用户都可以读取”,而不是列出每个研发部门用户的名称。这种抽象提高了效率,但也增加了复杂性。
ACL和能力列表是互补的。ACL适合回答“谁可以访问这个客体”的问题,能力列表适合回答“这个主体可以访问哪些客体”的问题。在实际系统中,可能需要同时支持两种机制,或者根据场景选择更合适的机制。
理论模型和实际实现之间往往有很大的差距。理解这些差距,以及如何在现实约束下实现安全的访问控制,是这一节要讨论的内容。
Unix/Linux文件系统使用经典的权限模型:每个文件有所有者(Owner)、组(Group)和其他(Others)三个类别,每个类别有读(Read)、写(Write)、执行(Execute)三个权限位。这个模型简单、高效,但粒度较粗。
|-rw-r--r-- 1 alice developers 4096 Jan 1 12:00 document.txt
这个权限字符串 -rw-r--r-- 表示:文件类型是普通文件(-),所有者(alice)有读写权限(rw-),组(developers)有读权限(r--),其他用户有读权限(r--)。
这个模型的局限性很明显。如果alice想要给bob读写权限,但不想给其他用户任何权限,她需要将bob添加到developers组,或者改变文件的所有者。但如果bob不应该属于developers组,或者alice不想改变文件所有者,这个模型就无法满足需求。
现代Unix/Linux系统通过ACL扩展了这个模型。使用setfacl和getfacl命令,可以为文件设置更细粒度的权限:
|setfacl -m u:bob:rw document.txt getfacl document.txt
这允许为特定用户设置权限,而不需要改变组结构或文件所有者。
Windows使用更复杂的访问控制模型。每个对象(文件、注册表项、服务等)都有一个安全描述符(Security Descriptor),包含:
DACL中的每个条目是一个访问控制条目(ACE),包含:
Windows还支持权限继承。子对象可以继承父对象的权限,这简化了权限管理,但也可能导致权限过于宽松的问题。
权限继承虽然方便,但也可能导致安全问题。如果一个目录的权限过于宽松,所有子文件和子目录都会继承这些权限。攻击者可能通过访问一个权限宽松的目录来访问敏感文件。

数据库系统的访问控制更加复杂。除了文件系统级别的访问控制,数据库还有自己的访问控制机制:
行级安全和列级安全是数据库访问控制的独特特性。它们允许基于数据内容动态决定访问权限。比如,一个用户可能只能看到自己创建的记录,或者只能看到特定状态的数据。这种细粒度的控制在文件系统中很难实现。
|-- 行级安全策略示例(PostgreSQL) CREATE POLICY user_isolation ON documents FOR ALL TO app_user USING (owner_id = current_user_id());
这个策略确保用户只能访问自己拥有的文档,即使他们被授予了访问documents表的权限。
Web应用的访问控制面临独特的挑战。Web应用通常有无状态的HTTP协议,需要在每个请求中验证用户身份和权限。这通常通过会话(Session)和令牌(Token)来实现。
Web应用的访问控制通常包括多个层次:
Web应用的访问控制经常出现"越权"漏洞。攻击者可能通过修改请求参数(如用户ID、资源ID)来访问其他用户的资源。防御这种攻击需要在每个层次都进行权限检查,不能只依赖URL或参数。
常见的Web访问控制错误包括:
正确的做法是在后端对每个请求都进行权限验证,使用不可预测的会话标识符,在业务逻辑之前进行权限检查,确保用户只能访问自己拥有的资源。
实现访问控制面临许多挑战。这些挑战不仅来自技术层面,也来自业务需求和用户体验。
最小权限原则要求只给用户必要的权限,但这可能影响用户体验。如果权限设置过于严格,用户可能无法完成工作,或者需要频繁请求权限提升。如果权限设置过于宽松,安全风险会增加。
在实际系统中,需要在安全性和可用性之间找到平衡。这可能意味着:
随着系统规模增长,权限管理变得越来越复杂。用户数量增加,资源数量增加,权限关系呈指数增长。手动管理权限变得不可行,需要自动化工具和流程。
权限管理工具应该支持:
但工具只是解决方案的一部分。还需要清晰的权限管理策略、定期的权限审查、以及用户培训。
传统的访问控制模型假设权限是静态的,但实际系统中,权限可能需要根据上下文动态变化。比如:
这些动态权限需要更复杂的访问控制机制,可能包括:
动态权限虽然灵活,但也增加了复杂性。系统需要实时收集和评估上下文信息,这可能有性能和安全影响。设计动态权限系统时,需要仔细考虑这些权衡。
撤销权限比授予权限更困难。当权限被撤销时,系统需要:
权限撤销的传播也是一个问题。如果一个用户被撤销了某个组的权限,该用户通过该组获得的所有权限都应该被撤销。但如果用户还通过其他方式获得了相同权限,撤销可能不会完全生效。
在实际系统中,权限撤销可能需要时间传播到所有系统。在这段时间内,用户可能仍然可以访问资源。这是访问控制系统的一个固有局限性。
访问控制是计算机安全的基础。理解访问控制的基本概念——主体、客体、操作、访问控制矩阵、能力列表、ACL——是理解所有安全机制的关键。 在实际系统中,访问控制不是单一模型,而是多个机制的组合。文件系统使用ACL,数据库使用行级和列级安全,Web应用使用会话和令牌。
在下一部分中,我们将讨论形式化安全模型。这些模型使用数学方法形式化地定义安全属性,为访问控制提供理论基础。