在机密数据库中使用SQL Server 2005实现行级别和单元级别的安全
[quote][b]摘要[/b]本文描述了SQL Server 2005如何被用于支持行级别和单元级别 安全 (RLS/CLS)。这篇白皮书中提供了RLS和CLS 是如何被用于机密数据库安全需求的示例。[/quote]
[quote][b]内容列表[/b]
执行摘要
设想并设计原则
Fine-Grained 访问控制
安全标签的简要回顾
术语
为实现行级别和单元级别访问控制的安全标签
数据库解决方案概览
行级别安全
定义标签架构
角色
我能看到什么?
改变基本表
定义视图
插入、更新和删除
外部密钥
结合在一起(部分1)
单元级别安全
SQL Server中的加密
密钥访问控制
改变基本表
定义视图
插入/更新时加密单元数据
物理分割
结合在一起(部分2)
性能
行级别泄漏
评估顺序
缓解
Table-valued功能
加密
应用程序错误管理
缓解摘要
摘要
附录 A –INSTEAD OF 触发器实例
附录 B – 对称密钥加密选项
结论[/quote]
[quote][b]执行摘要[/b]
本文回顾了行级别和单元级别的基于标签的安全需求。本文介绍了基于Microsoft® SQL Server™2005提供的在安全标签和视图上行级别安全的设计。额外的允许单元级别安全的设计也会被介绍。单元级别 安全也同样受到安全标签的控制。
[b]设想并设计原则[/b]
本文的描述方法使得使用数据库应用程序的设想结构同每一位最终用户相关联。这既能被用于Windows集成身份验证也能用于SQL Server身份验证。这就排除了为一个单身份而产生的中间连接池的作用,这就是通常用于可测量和管理的好处。然而,系统是需要审核和有其它安全需求的,正好证明了行级别安全标签与审核需求是有关联的,在所有数据库连接时需要用户身份,因此,连接池通常不是一个选项。
这种设计同样能指导以下这些原则:
1. 依靠SQL Server和Microsoft Windows® 安全模型,避免循环验证体系。
2. 保持简单。在一些普通场景下,大量可能的安全标签被结合在一起变的非常大。应避免在仍然允许很多fine-grained 控制的数据时fine-grained 安全组的增长。
3. 这种设计用来衡量很大的上千万行数据库。
通常,目的是对用户帐户限制访问被管理员所管理的数据。这种设计不能利用行级别或 单元级别 安全来影响管理员,特别是 sysadmin 或 db_owner 的成员。拥有权力的用户总是能够访问存储在所有数据库中的数据。
[b]Fine-Grained 访问控制[/b]
基于用户权限的访问控制信息是很多计算机软件的基本原则。Microsoft Windows,例如,用访问控制列表 (ACLs)来控制用户访问NTFS文件系统上的文件和文件夹。Microsoft SQL Server 强制访问控制服务器登录、数据库及数据库中的对象(例如表)。在有些情况下,信息访问控制级别会扩展到特定的级别。Windows可以控制访问用户文件,但不能控制访问用户文件的各个部分。SQL Server 2000,像其它的RDBMSs,能够控制访问表,但不能对表提供行级别或 单元级别的安全。
在一些情况下,需要进行更细级别的访问控制。例如,一个病人和诊断的力表,可能被存储在一个单独的文件或表中。任何医生只能被允许视图自己相关病人的信息。在这种情况下,仅仅对文件设置一个ACL或执行对表一个GRANT/DENY SELECT 即可,不会遇到商业需求。
类似的情况在很多环境下都存在,包括金融、法律、政府和军事应用上。消费者秘密的需求是另一个驱动所控制的数据。
这种典型的数据库需求的方法是通过必要的逻辑程序代码来实现的。应用程序的商务逻辑层被过滤了,例如,在客户端-服务器类型的应用程序,客户端可能需要。对应用程序而言,这种方法是很有效的,但数据事实上是不安全的。一个用户通过Microsoft Access 或一个SQL查询工具连接到后台服务器,他拥有了SELECT的权限就能够无限制地访问表中的所有行。
另外一个通常的能够减轻上面问题的方法,是在存储程序中限制所有数据的访问。用户对潜在的表被拒绝了所有的权限,并且在执行了逻辑过滤的存储程序上授予执行权限。这种方法也有自身的缺点。例如,广告公司用户报告数据库是非常困难的。
什么是对用户帐户自动应用逻辑过滤需来呈现真实表(或视图)的需要呢?这种情况下,所有用户都能够访问病人表,但是对每个用户来说,SELECT * FROM Patient只会返回用户能看到的数据。
在介绍基于SQL Server 2005对这个问题的解决方案之前,我们先介绍安全标签,和一个普通的示例来定义对数据的fine-grained访问控制。[/quote]
[quote][b]安全标签的简要回顾[/b]
安全标签是描述敏感数据项(一个对象)的信息。是一个包括一个或多个种类记号的字符串。用户(主题)有权限来描述相同的记号。每个主题都有自己的标签。主题的标签被用来同另外访问对象的标签进行对比。
例如,以下表的片断有对安全标签的注解行。
表 1
[attach]71773[/attach]
一个系统包括有表2中所示用户帐户的数据。
表 2
[attach]71774[/attach]
每个用户的清除选项(作为安全标签)决定了他们能够访问哪一行。如果Alice对这张表输入SELECT * FROM <tablename>,她能够得到以下结果。
表 3
[attach]71775[/attach]
如果Bob输入SELECT * FROM <tablename>,他能够看到如表4中的不同结果。
表 4
[attach]71776[/attach]
访问控制能够得到比这更为复杂的结果。能够有更多的安全标签访问标准。例如,除了分级之外,一部分数据能够只对特定项目团队的成员可见。假设项目组叫做PROJECT Q,并且考虑以下例子。
表 5
[attach]71777[/attach]
让我们来更改我们用户的权限。
表 6
[attach]71778[/attach]
我们添加了Charlie,并赋予了清除值为TOP SECRET。我们将 Alice的标签加上PROJECT Q 标记。
现在,如果Alice输入 SELECT * FROM <tablename>,她能够看到表7所示结果。
表 7
[attach]71779[/attach]
如果 Charlie 输入 SELECT * FROM <tablename>,他能够看到如下结果。
表 8
[attach]71780[/attach]
尽管Charlie有TOP SECRET的清除值,但是他没有 PROJECT Q 的标记,所以他不能看到第一行。Alice既有SECRET 标记,也有PROJECT Q 标记,所以她能够看到第一行。第二行需要TOP SECRET的清除值,因此只对Charlie是可见的。
这种基本的方法能够被扩展到额外的标记。在一些世界范围的情况中,安全标签能够包括不同种类的一些标记,并且很多标签结合能够变得非常巨大。
术语
这部分介绍正确描述标签的术语以及标签的对比。一个标签是描述对象敏感性或其它主题权限的字符。标签是标记的集合。每个标记描述了一个特殊的权限。标签中的标记来自一个或多个种类。
表 9 显示了之前例子的详细分类。
表 9
[attach]71781[/attach]
如果主题标签支配对象标签,那么这个主题就能够被该对象所访问。给予两个标签, A 和B,如果标签B的每个种类都与标签A的标记相适合,那么标签 A 被认为支配标签B。决定标记是否适合要依靠每个种类的属性。为了我们的目的,每个种类能够被表现为以下属性。
表 10
[attach]71782[/attach]
这有一些例子进行说明。假设我们有一个两个种类的安全标签架构,如表11所示。
表 11
[attach]71783[/attach]
现在让我们看看标签的例子。在每个情况下,问题是:标签A能支配标签B么?
例子 1
[attach]71784[/attach]
比较这些标签,我们必须比较每个种类中的标记。
分级: 在标签A中SECRET标记适合标签B 中SECRET标记。
间隔: 标签A中的Q 间隔不适合标签B中的Q,G间隔,B上所有的间隔必须在A中出现。
因此,标签A不能支配标签B。[/quote]
[quote]例子 2
[attach]71785[/attach]
分级:标签A中的TOP SECRET标记适合标签B中的CONFIDENTIAL标记。
间隔:标签A中的 Q,G,BN间隔适合标签B中的 Q,G 间隔,所有标签B中的价格在A中均出现。
因此,标签A支配标签B。
例子3
[attach]71786[/attach]
分级:标签A中的SECRET标记适合标签B中的CONFIDENTIAL标记。
间隔:标签B没有间隔,这就意味这没有间隔需求。
因此,标签A支配标签B。
为实现行级别和单元级别访问控制的安全标签
安全标签可以被应用于很多环境中,不论公共还是私有部门。多级安全 (MLS)信息管理区域(一个管理多敏感级别信息的系统)从19世纪70年代开始发展起来。所以,以对数据和用户定义权限为范例的基于安全标签的设计出现了。
[b]数据库解决方案概览[/b]
下面会详细讨论对SQL Server 2005数据库添加基于标签和行级别和单元级别 安全的设计方案。这种设计定位于任何标签架构并且强制使用必须使用SQL Server 2005执行。
这种设计包括:
• 添加结构来定义标签种类和标记。
• 允许用户的标签通过基本标记的成员角色来直接进行定义(例如, Top Secret, Confidential; USA, UK, Taskforce Z)。
• 为 write-up, write-down,和其它控制模型提供写入数据。
• 在数据库中,能够进行可选择的加密来提供单元级别 安全,在 SQL Server 2005中使用了内部的,半管理的证书存储。
• 提供固定的指导方针,开发人员或管理人员能够开发自动执行基本输入选择的工具。
• 通过混合多种行级别安全解决方案,提供能够保护行级别安全弱点的策略。
[b]行级别安全[/b]
在SQL Server视图中,我们用于强制行级别的机制。视图允许用户预先设定的查询执行。同样,用户能够访问这个视图,但是被拒绝访问下面的表。 这就防止了用户通过视图间接访问到基本表。我们使用特殊方式构造应用到所有使用必要的逻辑、基于表的强制行级别安全的视图。
在一些情况下,视图被用于预先设置的复杂的查询,以使得能够简单地查询报告单一的数据库对象。我们不能做这些。我们的目的是通过同样的定义简化基本表的视图。用户(或应用程序)将查询或更新这些视图,就像是自身的表一样,并将其加入到其它表中。访问基本表将会被拒绝。
建立这些视图需要我们做以下四件事:
1. 建立一些定义了标签种类和标记的表,并对每一个安全标签赋值。这项操作只需要对每个数据库作一次。
2. 为记号值建立角色。在这个角色中的所有成员会被樱桃指派给用户的标签。这项操作只需要对每个数据库作一次。
3. 对基本表做一些更改。
4. 定义视图。
定义标签架构
我们通过建立少量的定义了元数据的表来开始。这些显示在图1的ER 图表中。tblCategory 表标签包含的种类。对每个种类,有一些可能的值作为种类的域。这些被定义在tblMarking 表中。如果是分等级的种类,有关父子关系的记录被定义在 tblMarkingHierarchy 表中。在这些表中的列基于早期安全标签级别的讨论而不被加以说明。
[attach]71787[/attach][/quote]
[quote]下面的SQL语句展示了(概括地)我们如何设置一个标签架构示例。
--Categories
INSERT tblCategory (ID, Name, CompareRule, DefaultRole)
VALUES (1, 'Classification', 'ANY', NULL)
INSERT tblCategory (ID, Name, CompareRule, DefaultRole)
VALUES (2, 'Compartment', 'ALL', 'public')
INSERT tblCategory (ID, Name, CompareRule, DefaultRole)
VALUES (3, 'Nationality', 'ANY', 'public')
INSERT tblCategory (ID, Name, CompareRule, DefaultRole)
VALUES (4, 'Need-to-Know', 'ANY', 'public')
GO
--Classification markings
INSERT tblMarking (CategoryID, MarkingRoleName, MarkingString,)
VALUES (1, 'T', 'T')
INSERT tblMarking (CategoryID, MarkingRoleName, MarkingString)
VALUES (1, 'S', 'S')
INSERT tblMarking (CategoryID, MarkingRoleName, MarkingString)
VALUES (1, 'C', 'C')
INSERT tblMarking (CategoryID, MarkingRoleName, MarkingString)
VALUES (1, 'U', 'U')
--Classification hierarchy
INSERT tblMarkingHierarchy (ParentCategoryID, ParentMarkingRoleName, ChildCategoryID, ChildMarkingRoleName) VALUES (1, 'T', 1, 'S')
INSERT tblMarkingHierarchy (ParentCategoryID, ParentMarkingRoleName, ChildCategoryID, ChildMarkingRoleName) VALUES (1, 'S', 1, 'C')
INSERT tblMarkingHierarchy (ParentCategoryID, ParentMarkingRoleName, ChildCategoryID, ChildMarkingRoleName) VALUES (1, 'C', 1, 'U')
--Compartment markings
INSERT tblMarking (CategoryID, RoleName, MarkingString)
VALUES (2, 'Q', 'Q')
INSERT tblMarking (CategoryID, RoleName, MarkingString)
VALUES (2, 'G', 'G')
INSERT tblMarking (CategoryID, RoleName, MarkingString)
SELECT 2, DefaultRole, 'none' FROM tblCategory WHERE ID = 2
Etc.…
接下来让我们关注tblUniqueLabel表。这张表被用于绘制对于唯一的ID的标记与特殊的安全标签实例的结合。由于效率的缘故,这张表根据需要被定制。一部分数据及其安全标签被添加到数据库中,被称作获取ID的存储进程会表现这些唯一的安全标签。如果在tblUniqueLabel 里没有相应的行,就会添加新的行并返回新的ID。我们想要在这张表中正好有我们需要的行,不多也不少。
最后, tblUniqueLabelMarking 表结合了单独的标记值和安全标签。
角色
定义标签架构,需要对SQL Server安全模型进行一些操作。为我们定义的每个种类中的标签,建立相应的数据库角色 。数据库角色必须坚持以下指导方针。
• 为可以使用任何或全部比较规则的不分等级的种类,为每个可能值建立一个角色。为了这些标记,角色的名字必须在tblMarking.MarkingRoleName 值中标出。
• 为分等级的种类,需要做相同的事。另外,角色必须被嵌套到分等级模型当中。对建立的每个角色,加入在表tblMarkingHierarchy 中定义的父角色作为一个成员。这种嵌套,例如,有Secret 清除选项的用户,能够访问该级别及以下级别的数据。
这些角色允许用户能够被赋予正好能够访问数据的安全标签的权限。这就是程序或数据库管理员(DBA)在用户系统中所应有的角色。
在下部分,我们将看到角色成员是如何被识别,以确定他们能够访问那些数据的。
我能看到什么?
在我们关心这些应用程序表之前,我们先建立一个集成了真正行级别安全逻辑的帮助视图。我们称之为vwVisibleLabels。定义起始点视图如下面代码所示。
SELECT ID, Label.ToString()
FROM tblUniqueLabel WITH (NOLOCK)
WHERE ….
WHERE语句定义了我们标签架构中的基于种类属性。最重要的属性是比较规则。为每个有任何对比规则的种类,添加以下语句到WHERE语句中。
ID IN (SELECT ID FROM tblUniqueLabelMarking WITH (NOLOCK)
WHERE CategoryID = <HardCodedCatID> AND IS_MEMBER(MarkingRoleName) = 1)
这条语句给了所有ID唯一的安全标签,包含了当前用户成员身份种类的标记。让我们更详细的看一下,提交查询扫描tblUniqueLabelMarking 表种类中的行。从这些行当中,选择了作为MarkingRoleName数据库角色成员中当前的一个用户。这些检查通过SQL Server内置更能IS_MEMBER 来完成。对这些行中的每一行,在tblUniqueLabel 中相应记录的ID都会被返回到外部查询。
那么比较规则为全部的种类如何呢?添加以下语句到WHERE语句中。添加以下语句到WHERE语句中。
1 = ALL(SELECT IS_MEMBER(MarkingRoleName) FROM tblUniqueLabelMarking (NOLOCK)
WHERE CategoryID = <HardCodedCatID> AND UniqueLabelID = tblUniqueLabel.ID)
这些语句的结果是需要用户拥有全部为了访问的应用标记。提交查询通过IS_MEMBER对每个在tblUniqueLabel 给出的相关标记返回一个值的列表。如果所有返回值都为1,这些语句适合。
比较规则为完全相反的种类,应该在WHERE 语句中使用如下语句:
ID IN
(SELECT KeyMappingID FROM tblUniqueLabelMarking(NOLOCK) Z
WHERE NOT EXISTS (SELECT MarkingRoleName
FROM tblMarking
WHERE IS_MEMBER(MarkingRoleName) = 1
AND CategoryID = 1 AND InternallyGenerated =0
EXCEPT
SELECT MarkingRoleName
FROM tblUniqueLabelMarking
WHERE CategoryID = 1
AND KeyMappingID = Z.KeyMappingID))
所有语句加入,使得到他们中有一个AND操作者。
结合所有我们之前的例子,你就会得到媒体视图的定义,如下所示:
CREATE VIEW vwVisibleLabels
AS
SELECT ID, Label.ToString()
FROM tblUniqueLabel WITH (NOLOCK)
WHERE
ID IN --Classification
(SELECT ID FROM tblUniqueLabelMarking WITH (NOLOCK)
WHERE CategoryID = 1 AND IS_MEMBER(MarkingRoleName) = 1)
AND --Compartments
1 = ALL(SELECT IS_MEMBER(MarkingRoleName) FROM tblUniqueLabelMarking
WHERE CategoryID = 2 AND UniqueLabelID = tblUniqueLabel.ID)
GO[/quote]
[quote]这个视图被称作vwVisibleLabels,但不能这么认为“这张列表显示了在数据库中所有我(当前用户)能访问的安全标签”。
表 12 总结了依靠每个种类中属性,设置标签架构的设计规则。
表 12
[attach]71790[/attach]
[attach]71791[/attach]
[attach]71792[/attach]
注意处理一些场景时,有一个额外的属性叫做NoMarkingBehavior。它控制着不带种类标记的访问评估。在多数场景中,没有标记意味着种类能够被忽视。在一些场景中,种类中缺省的标记意味着无人能够访问这些数据(除了通过RLS为帐户赋予了明确的权限)。
改变基本表
现在来看一下被用于到添加了行级别安全的基本表的改变。这些改变是次要的(在我们得到单元级别安全标题是会更多) 两列必须被添加到基本表中: RowLabel 和 RLSMappingID。 RowLabel对这行来说是没有赋值的安全标签 。 RLSMappingID 是从相应安全标签tblUniqueLabel 得到的整型ID。严格地说,只有RLSMappingID 是必需的。事实上,有RowLabel 列是违反标准格式的。在严格的安全场景下,在数据进入数据库时,通常需要安全标签同数据保持在一起。因此,在基本表中有RowLabel。 可能为了其它行的元数据要加入很多必要的列。无论特殊应用程序的策略是什么样的,都应该知道在基本表中RLSMappingID 是必需的。
当一行及其安全标签被插入到一张基本表时,响应这张表的RLSMappingID 必需被返回(或生成),并且被放置在新行中的RLSMappingID 列。同样地,如果一行被更新,导致安全标签被改变,那么 RLSMappingID 也会相应地改变。(安全标签的更新是根据每个场景的安全需要进行的;这是能够被禁止的)。
最终,在基本表RLSMappingID和RLSMappingID列上建立了一个外部密钥关联。
由于性能原因,可以在RLSMappingID列上建立一串索引。不要跳过这步,否则会影响性能!
定义视图
我们准备好了上一步操作。接下来将在基本表上建立一个视图,本质上说,在用户和应用程序看来是取代了这张表。这里就是视图的定义。
CREATE VIEW UserTable
AS
SELECT <base table column list which does not include RLSMappingID, or any columns from vwVisibleLabels>
FROM tblBaseTable (READCOMMTTED), vwVisibleLabels
WHERE tblBaseTable.RLSMappingID = vwVisibleLabels.ID
GO
GRANT SELECT ON UserTable TO <app_users>
DENY ALL ON tblBaseTable TO <app_users>
GO
以下有两部分视图的视图的定义:
• 通过在基本表上对可见标签视图加入安全标签识别符,这个标签需要依靠用户在安全组成员身份来对行控制访问基本表的优势就体现出来了。
• READCOMMITTED锁应用到基本表上以防止恶意读取基本表的数据。这就防止了未经处理的用户视图行中敏感数据的内容,但是那些安全标签标识符没有被改变。这种锁定视图不考虑读取转换隔离级别或在查询视图时所用的外在锁。REPEATABLE READ 和 SERIALIZABLE 在这里是同样可以被接受的。注意这种其它用户(更新者)有转换隔离级别除了READ UNCOMMITTED的假设。
我们现在有一个明显强制的基于标签的行级别安全视图,而没有任何程序逻辑,并且一直起效,即使应用程序层迂回访问。
当然,这种视图仅仅对从表中选择数据有好处。如果应用程序必需在表中插入、更新过删除行,这就只有很少的工作要做。
[b]插入、更新和删除[/b]
迄今为止来讨论任何从根本表中选择行。很多应用程序需要在表中写入信息,插入、更新或删除基于标签的行级别安全的表中的行,带来了一些问题。哪一行能够被用户更新?是否是任何限制安全标签阻止用户在行中插入新的内容?
要根据不同场景来回答这些问题。一些系统只允许 “read-down, write-down” 的行为,而其它系统只允许 “read-down, write-up”的行为。这些场景都能被支持,而且使用了很多相同的技术。
来支持插入和更新基本表内容,我们需要能够做以下这些事情:
• 允许插入或更新用户可能的视图。
• 需要正确的行标签。
• 需要生成新的标签来映射ID,或解决已存在ID的标签。
• 为一些场景的需要,强制write-down 或write-up逻辑。
• 完成插入/更新基本表。
这是通过定义为插入和更新定义INSTEAD OF 触发器而完成的。这些触发器检查合法的标签,生成或重新找回映射ID的标签,强制写权限检查,并且完成事实的对基本表的插入或更新。
INSTEAD OF 触发器的例子在附录A中显示,这有一些指出这些的代码。
首先,视图以下两行代码。
DECLARE @RowClassification SecurityLabel
SELECT @RowClassification = row_label FROM inserted
可以得到一些包括INSERT语句的行标签,被称之为用户定义的SecurityLabel 。SecurityLabel 集成了弥补特殊标签的值。SecurityLabel 能够同Dominates功能相比较, 一个Microsoft Visual C#® 用户定义的功能。这种方式比较了两种标签来看适合一个可以支配另一个。我们用在触发器中的这种方式来比较当前用户对他们感兴趣的数据安全标签的权限。
在比较之前,我们必须得到一个描述当前用户权限的SecurityLabel 实例。下面的代码做了这件事。
EXECUTE AS CALLER
SELECT @CallerName = CURRENT_USER
REVERT
DECLARE @UserClearance [SecurityLabel]
exec usp_GetUserLabel @CallerName, @UserClearance OUTPUT
usp_GetUserLabel 是一个检查当前用户角色成员的存储进程,并且生成一个标签来描述他们的权限级别。这个标签返回一个SecurityLabel 实例。我们想能够比较的任何用户权限标签。在我们例子的这种情况下,我们执行了write-down-only的需求,如下所示。
IF dbo.Dominates(@UserClearance, @RowClassification) = 0
RAISERROR('user rights not sufficient to write this data', 12, 1)
相似的代码能够被用于强制write-up 需求。(做为选择,如果需要数据标签插入用户的清除级别,我们能够跳过Dominates 检查并且简化从usp_GetUserLabel 返回的标签。
假设用户有适当的写入数据的权限,我们需要为行标签获取映射ID。usp_GetRLSMappingID 存储进程来执行这项工作。这个程序执行了必须重新找回ID或生成新的ID的工作。
用过映射ID,我们能够执行真正的插入基本表的操作。
更改或删除触发器的代码通常使用了相似的逻辑。[/quote]
[[i] 本帖最后由 xiaoxinlucky 于 2008-1-21 16:53 编辑 [/i]] [quote][b]外部密钥[/b]
在离开行级别安全主题之前,我们应该定位使用RLS保护表功能的问题。允许用户查看表中在另一张表中受限制的的行,会带来信息的泄露。这能够定位什么时间修改了基本表。当添加了 RLSMappingID时,这一列被添加到主密钥中。只要主密钥在另一张表中被提及,行级别控制就会产生,并且被用于查看整张表的定义。.
结合在一起(部分1)
为了得到由基于标签的行级别安全的存在的表,不同的设计方式现在被结合在一个分层的设计模式中。图 2描述了这个图形。底层是基本表自己。在基本表之上,我们为读取表建立了强制行级别安全的视图。这就需要支持旁边图表中的信息。
在RLS视图之上。建立了一个外部视图。这个外部视图选择了直接第一个视图。这个视图也包含INSTEAD OF 触发器来支持在表中插入、更新和删除。不是必需分开这两个视图,但是我们添加单元级别 安全在这个模型上是必要的。
[attach]71793[/attach]
只有最主要的视图和vwVisibleLabels 视图(在上图中阴影部分)被表现为用户登录。
这种分层设计有以下这些优点:
• 改变了主要设计点,使得更为简化和容易维持。
• 这种应用方法为表设计了固定的程式化的程序。
• 为了写入核心结构,集成了安全标签逻辑,而优于应用程序代码和存储程序。
在下面章节中,我们改善了这种方式,包括单元级别 标签和不同敏感程度访问特定的I/O设备的物理区域数据。
[b]单元级别安全[/b]
数据被控制在很好的细致级别上,而不是全部行上是完全有可能的。多数行可能需要对一个用户设置是可见的,当确定的更为敏感的单元可能需要附加权限才能视图。图3中显示了这种概念。不同图案描绘了不同应用到数据上的标签。当倒数第二行被行级别安全所控制时,有很多单元格分散穿过这张表,并且有它们自己特定的标签。
[attach]71794[/attach]
我们应该基于我们的权限来能够控制这些数据,更接近数据自身的可能。理想地,数据库能够从单元标签中简化显示数据,基于统一的连接用户身份。直到现在仍然不能很容易地支持这种模型。
SQL Server 2005 引入了在自身的数据库引擎中加密的功能。通过使用被数据库所管理的内部证书或密钥结构,这能够被用于加密和解密任意的数据。而不需要借助外部的证书或密钥来执行。
本文提出了完成理想的单元级别控制的设计,并且能够很容易地被执行和管理。
基本的设计目的是:
• 支持数据单元的任意标签。
• 动态评估用户的标签,只显示适合的单元。
• 大量卷有能够接受的性能。
[b]SQL Server中的加密[/b]
SQL Server 提供了内置的容易的加密和解密数据的功能,是通过证书、不对称密钥或对称密钥来实现的。管理了内部所有的证书存储。这些存储使用了分层次的加密,安全证书和密钥在一个级别层次之上。这些SQL Server 2005 的特性被称之为Secret Storage。
[attach]71795[/attach]
通过内部API实现的最快的加密模式是堆成密钥加密。这种模式适合处理大量的数据。对称加密密钥通过X509.v3证书被加密存储。
SQL Server 2005 支持一些对称加密的算法。这些算法在Windows Crypto API中被执行。 附录B 列出了支持的算法和每种算法的密钥长度。
在数据库连接区域中,SQL Server 2005 能够保持多种开放式对称密钥。通过“解开”,密钥 被从存储中重新得到,并被准备好了用于加密数据。当一部分数据被加密时,不需要指定使用对称密钥,取而代之的是,如果当前密钥被解密和解开了,这些加密字节引擎自动流向解开的对称密钥。这个密钥被用于解密和还原数据。如果当前密钥是不解开的,则返回值为空。
“解开”密钥的能力需要直接访问密钥上的访问控制列表ACL。
给予SQL Server 2005 这种加密机制的支持,有以下几种方法。
1. 为每个特定的在数据库中被用于标记数据的表建立一个对称密钥。
2. 通过相应的密钥在标签单元中加密数据。
3. 通过被解开用户标签的映射标签的密钥来控制访问密钥。提供了简单的在建立连接时解开所有密钥的方法。
4. 使用在基本表上的视图,包括在SELECT语句中调用加密API来定义这个视图。
以下有一个简单的例子来显示如何定义这个视图。
CREATE VIEW MyTable
AS
SELECT ID,
DecryptByKey(SensitiveData),
DecryptByKey(OtherSensitiveData),
NonSensitiveData,
FROM BaseTable
GO
给出了这种方法,让我们看看在用户选择了这个视图时发生了什么。被映射到标签的用户能够访问的所有密钥都被解开了。因此,带有标签的所有单元在SELECT 语句被执行的时候都将会被解密。相反地,被映射到标签的用户不能够访问的所有密钥将不会被解开。当SELECT语句被执行的时候,带有这些标签的单元返回空值,不会为用户提供任何在单元中的数据信息。 这种方法完成了细节,动态控制了我们查询的相关表中的数据。
[b]密钥访问控制[/b]
当然这种密钥的控制设计是非常适合的。SQL Server 2005 根据一个单独的SQL Server 规则定义了密钥上的权限。解开密钥的权限,例如,能够被赋予用户Bob或AppUsers组。在我们的场景当中,我们想基于任意制定规则的结合或基于定义了用户标签的角色成员的结合来控制密钥的权力。我们想避免为每个可能的标签结合来定义一个规则。这是决定用户能够访问相同的被控制的行的方法。
这种ACL的方式需要更为细致的方法。取代为用户授予密钥权限或角色的方法,我们授予一个系统定义的broker 用户帐户。我们称之为KeyBroker 帐户。 KeyBroker 能够解开密钥。用户和用户角色对密钥拒绝所有权限。我们获得一个能够被用户访问的标签列表,并请求KeyBroker 解开相应的密钥。
使用SQL Server 2005的新功能,我们能够定义一个被用户请求的存储程序 ,但会 “execute as” KeyBroker。这个程序在下面被显示出来。一个指针被定义在vwVisibleLabel 视图上。注意我们在视图中没有提及的两列: KeyName 和 CertName。这个指针从视图中选择了这两列。使用另外一个新的功能,我们临时回复请求用户和打开指针的身份。这就确保了从vwVisibleLabels 返回的基于请求用户标签(这就是角色成员的结合)的行。我们立即回复到KeyBroker 身份。在指针中,其它程序循环执行,使用加密时的专用证书来解开每个密钥。
CREATE PROCEDURE usp_EnableCellVisibility
WITH EXECUTE AS 'KeyBroker'
AS
DECLARE @KeyName nvarchar(256)
DECLARE @CertName nvarchar(256)
DECLARE Key_Cursor CURSOR LOCAL FORWARD_ONLY STATIC FOR
SELECT KeyName, CertName
FROM vwVisibleLabels
EXECUTE AS CALLER
--Since the cursor is STATIC, it is fully
--populated here based on the caller’s identity
OPEN Key_Cursor
REVERT
FETCH NEXT FROM Key_Cursor INTO @KeyName, @CertName
WHILE @@FETCH_STATUS = 0
BEGIN
open symmetric key @KeyName using certificate @CertName
FETCH NEXT FROM Key_Cursor INTO @KeyName, @CertName
END
CLOSE Key_Cursor
DEALLOCATE Key_Cursor
GO
存储程序自动回复给用户请求的内容完成。这种方法通过结合被用户标签控制的标签正确地解开了那些密钥。用户没有任何访问密钥的权限,因此不能解开其它的密钥。在这个欺骗程序中没有任何输入内容。评估权力仅仅是基于在SQL Server数据库角色成员身份的。
关于正确的对称密钥解开,如果受用户标签支配,在视图引起标签单元当中选择是可见的。所有其它的标签单元出现空。
这个令人期待的存储程序 (usp_EnableCellVisibility) 被称之为通过最终应用程序或最终用户建立数据库连接后的一次程序。被解开的密钥在连接过程中将会保持解开。一个相应的程序(usp_DisableCellVisibility) 被提供来解决关闭这些密钥的需求。这不是严格必须的,因为关闭连接就会清楚这些。
[b]改变基本表[/b]
改变基本表来支持单元级别的安全是次要的。更重要的是,列的数据类型必须被和加密数据值一样被保护起来。基本的功能EncryptByKey 返回 varbinary。这就能被存储在一个二进制的值(例如, varchar, nvarchar或 varbinary)。如果原始数据类型同二进制-像数字类型为整型的内容不一致,例如,列的数据类型必须被改变。在下一部分,我们将看到如何对用户来说数据类型不被改变。
确保正确的数据类型的改变是必须的,另一个改变可能被描述为一些场景,如保持应用到单元上的标签的列。主要的目的是遵守在单元数据中赋予标签元数据的策略需求。这能在tblUniqueLabel 表、用户自定义类型的SecurityLabel或原始数据的标签中做为ID进行存储。
[b]定义视图[/b]
最终,我们需要重新定义用户可访问的视图,包括在被保护单元中加密数据的逻辑。下面是什么是视图定义例子的代码。几乎同我们之前显示的讨论行级别安全的代码一样。仅有的不同是一些列被包装在SQL Server 2005 DecryptByKey 这个内置的功能中。为了简化这个例子,我们假设在基本表中已经有了一些列。
CREATE VIEW UserTable
AS
SELECT ID,
DecryptByKey(SensitiveData),
CONVERT(money, CONVERT(varchar(50), DecryptByKey(SensitiveMoneyData))),
NonSensitiveData,
FROM tblBaseTable (READCOMMITTED), vwVisibleLabel
WHERE tblBaseTable.RLSMappingID = vwVisibleLabel.ID
GO
GRANT SELECT ON UserTable TO <app_users>
DENY ALL ON tblBaseTable TO <app_users>
GO
像之前提到的一样,加密功能用特性或二进制数据做为输入和输出。如果受保护列的原始数据类型是数字的,例如,视图定义包含了一个varbinary 的变化,且加密输出到原始数据类型。在上边的视图的第三列中,显示了这个例子。
[b]插入/更新时加密单元数据[/b]
单元数据的加密能够被用于很多相同的形式,我们可以为了行级别的安全执行write-up/write-down 检查。在视图上定义INSTEAD OF 触发器执行write-up/write-down检查,并且基于它们的标签来加密单元。 附录A中的代码包括了这种逻辑的例子。
[b]物理分割[/b]
当设计一个从多级别等级中结合数据的系统时,在物理存储中混合数据的问题必须被定位。我们设计的部分必须允许通过不同的物理存储单位结合不同等级的数据分发。作为这里的其它方面,我们应整理这个解决方案以满足统一标准的需求,说明性的提及这种尽可能紧密联系到数据的设计。应用程序建立在数据管理解决方案之上,并不能解决解决这些问题。
这种需求能够通过使用分割表-SQL Server2005的另一项特性来定位。当然也能够通过使用分割视图-之前版本的SQL Server功能来定位,但是会复杂的多。
分割表允许分割功能的定义,它使用了在表中的一列把数据分割开。分割功能被映射到物理存储单元之上(就像文件)。这些文件被放置在不同的物理设备上。为了额外的控制方法,每个物理卷能够使用Windows 加密文件系统(EFS)来支持在物理媒体级别上进行更高级别的加密。(这些EFS的使用应当只在I/O硬件资源可用于弥补文件系统级别的加密所带来的性能影响上。)
[b]结合在一起(部分2)[/b]
结合单元级别的安全和通过先前行级别安全模型的物理分割,我们能有如图5这种设计方案。
像之前的讨论,这种技术展示了在代码中能够通过多种方法被执行。因此,这种分层的设计方案有以下这些优点:
• 改变了主要设计点,使得更为简化和容易维持。
• 对于表的应用设计的过程被固定和程式化。可以通过自动脚本方便地执行基于元数据的任务。
• 为了写入核心结构,集成了安全标签逻辑,而优于应用程序代码和存储程序。[/quote]
[quote][b]性能[/b]
在评估这种设计时密钥的问题会影响性能。可接受的解决方案必须还要保证安全,然而有一些可接受的对包含了数百万行的表性能影响。
一个参考执行设计测试了包含了一百万行的数据。测试服务器规格如下:
[attach]71796[/attach]
测试的详细描述不在这篇文档中。概括地,我们得到了以下结果。
输入性能影响: ~40% 降低
选择查询影响: < 10% 降低
统计查询影响: ~10-50% 降低
每个应用程序的性能是唯一的,因此这些结果不能被看作固定的方针。但是,这些测试能够指出这种设计影响性能的可接受的程度。注意这个有规则的在低性能(550MHz)并且是最低内存的服务器上的性能,假设由于应用程序影响带来了相同的性能,应该对当前低性能的系统进行硬件升级。
[b]行级别泄漏[/b]
这种基于视图的行级别安全设计有一些特殊的、有限的弱点。这种设计防止返回用户不能访问的行,无论数据是由应用程序、报告工具或通过SQL 查询工具的直接连接提交的查询。但是,有了正确的条件,通过对错误信息发出的推论,数据可能被暴露。这部分描述了这种条件和潜在的泄漏以及两种解决方法。
[b]评估顺序[/b]
如果特定的对用户可及的行级别安全的格式查询被执行,可能会返回一个错误,因为用户不能够访问表中的信息。
这种查询如下所示:
SELECT * FROM UserTable --actually a view
WHERE LastName = ‘Smith’ AND LEN(LastName)/0 > 10
假定当前用户能够访问LastName 为Smith 时,没有这一行,但是至少有一个用户不能访问。在内部,数据库服务器的查询将视图分解为很多的部分以优化性能。它会建立一个在视图中基于所有WHERE语句的条件和真实声明WHERE语句的查询。如果这些声明被成功执行,没有信息会泄漏。但是,如果WHERE语句包含了有问题的片断(就像通过零分割),就会有泄漏的风险。通过查询优化并依靠查询计划来生成,有问题的片断能够在它限制行可见性之前被评估。如果发生了,一个错误会被发出,意味着有不可见的行存在。
这里是上面说所的声明如何工作的。假设用户表是之前定义好的一个视图。当前的查询经过优化如下所示:
SELECT *
FROM BaseTable (READCOMMITTED)
WHERE (LastName = ‘Smith’ AND LEN(LastName)/0 > 10)
AND BaseTable.RLSMappingID IN
(SELECT ID FROM tblUniqueLabel
WHERE ID IN .. /* category predicates */)
假设优化器选择了首先使用LastName 索引来缩小在其它条件被评估之前的设置结果。当评估LEN(LastName)/0 > 10时,将会看到LastName =’Smith’ 这一行。这将会立即发出一个例外,预先的对行评估的安全片断。这种结果在LastName = ‘Smith’ 显示时,最少会出现一行,即使用户不能够看到任何这些行的内容。
这不是使用SQL Server进行基于视图的行级别安全的唯一问题。其它的行级别安全解决方案的问题,在查询计划产生之前注入了这些片断,会带来相同的风险。
对于这些弱点,遵守以下这些条件非常重要:
1. 一个用户必须能够直接提交SQL 查询,既可以通过SQL 查询工具,也可以通过SQL 注入少量写入前端应用程序。
2. 用户必须知道足够关于定义写出查询表的信息。
3. 查询优化器必须选择一个允许在访问行之前对有问题的片断进行评估的计划。这是一个可变化的功能,包括存在的索引,在表中分发数据以及原始的片断执行顺序。
4. 用户必须能够直接看到产生的错误。可以通过SQL 查询工具实现,但一些很好写入的应用程序不能返回这些错误的信息。
[b]缓解[/b]
在很多情况下,利用先前提到的弱点的机会时非常小的,以至于不需要采取任何行动。但是,完全有必要来减轻这些弱点,有三个建议的选项。除了额外的部分外,在本文中均使用相同的基本设计。[/quote]
[[i] 本帖最后由 xiaoxinlucky 于 2008-1-21 16:37 编辑 [/i]] [quote][b]Table-valued功能[/b]
消除这些弱点的一个方法是确认访问行的片断,在提供给任何用户之前被完全应用到了根本的数据之上。这能够使用table-valued 功能(TVF)来完成。TVF的任务能够从图5的修改版中看到。
[attach]71802[/attach]
除了能在TVF中视图执行行级别安全,图 6与图5相同。TVF通过用户访问视图被查询。TVF的定义如下代码所示。
CREATE FUNCTION [dbo].[fn_MyTable_tvf]()
RETURNS @ret TABLE
( <column specs from underlying RLS view> )
AS
BEGIN
INSERT @ret
SELECT <columns>
FROM vwRLSView
RETURN
END
GO
注意TVF必须是一个多种状态的table-valued功能。一个单一的table-valued功能将不会有作用。
在提供给任何用户的片断被评估之前,包装行级别访问的视图将强制它被像表一样处理。这能消除这个弱点。
这种方法的缺点是:
1. 增加了复杂性。
2. 性能。这需要完全的根本表,来应用行级别的安全,它通过用户可接受的视图将查询缓冲到变量表中。对于很小的表,所以这种影响是次要的。而对于很大的数据库(数百万行)来说,性能影响是巨大的。
[b]加密[/b]
第二种缓解的办法是使用相同的加密策略来描述单元级别的安全。假设设计的所有部分在图5中显示出来。因此,我们在数据中的每一行添加了额外的加密级别。除了单元级别的加密外,每个用户能访问的列也会被通过联合行标签使用对称密钥进行加密。这就确保了不管查询计划如何,如果用户的标签允许访问,他们总是能够读取数据值。
例如,假设一个有四列的简单表。这张表被通过图5所示的行级别和单元级别安全所保护起来。下表显示了一行的内容。
[attach]71803[/attach]
这行是行级别访问控制的标签。这个标签被映射在tblUniqueLabel 中ID为19的记录。第2和第4列被单元级别安全所保护。在这个显示行的例子中,第2列被通过标签1(K1)的密钥加密,第4列被通过标签(K2)的密钥加密。例子中并不关心标签是什么。
这种缓解方法应该应用到额外加密上,因为数据被存储在如下所示根本表上:
[attach]71804[/attach]
每个用户可访问的列被映射标签19(K19)的对称密钥加密。在INSTEAD OF 触发器做这项工作。用户可访问的视图在图5中,包括了解密声明,能够被修改以包括额外解密的代码,如下所示:
CREATE VIEW UserTable
AS
SELECT DecryptByKey(Column1),
DecryptByKey(DecryptByKey(Column2)),
DecryptByKey(Column3),
DecryptByKey(DecryptByKey(Column4))
FROM vwRLSView
WHERE ………… --omitted for clarity
为管理解开的单元级别安全的密钥,描述了相同的原则。只用用户标签(角色成员)符合,K19才能被解开。因此,一个潜在的查询计划是不能访问和揭露数据的。
这种方法也有以下缺点:
1. 增加了复杂性。
2. 性能。更多CPU时间花费在了加密/解密上。因为所有的查询列都被加密了,所有SQL索引不能够被用于加速查询。尽管有这些问题,但对大量数据而言,这种方法比TVF方法更好。
3. DBMS完整性的丢失。数据库不能强制完整限制,像索引或限制列的外部密钥那样包含加密的数据。
[b]应用程序错误管理[/b]
之前在数据库层有了两种缓解这个问题的方法。第三个方法就是在全部的应用程序的顺序部分上进行定位。在这种方法中,有一些公认的小风险,就是在制定隐蔽通道或相关通道是会产生一些错误。在介于层之间的解决方案(例如,反向数据访问层)仔细捕捉了错误并且记录下来。通过在数据库中捕捉特定的错误,并只传播常规错误信息给用户,这些细节的统计可用于来减少推论。记录这些错误提供了审核跟踪,能够监控可疑的信息,指出用户试图使用系统。
[b]缓解摘要[/b]
无论那种缓解方法,都应该基于对场景的判断来使用。为了给应用程序开发的机会,可能会产生一些复杂性的增加和性能的浪费。如果必要,这些方法都能够被用于消除弱点。[/quote]
[quote][b]摘要[/b]
本文所述的设计使得一个SQL Server 2005数据库能够支持基于任意安全标签架构的行级别和单元级别的安全。在行和单元上的访问限制通过像视图和SQL Server数据加密这样的内部结构被强制执行。对用户访问的评估是基于SQL Server内置的安全级别的。这些测试、安全基础的集合是使用ad hoc 的高级方法,可以通过定制代码来执行主要的认证和过滤步骤。
内部引擎就提供了基于安全标签的单元级别的访问控制的能力,这是空前的能力。在很多方面明显地改善了对敏感信息的管理,并且为运作需求提高了平衡数据性能和可用性的能力。
[b]附录 A –INSTEAD OF 触发器实例[/b]
这些代码只是举例说明了这种逻辑。非常清楚,这个例子只对单独行进行的操作起作用。
CREATE TRIGGER dbo.IO_Insert_titles ON titles
INSTEAD OF INSERT
AS
DECLARE @RLSMappingID int
DECLARE @KeyMappingID int
DECLARE @KeyName nvarchar(256)
DECLARE @CertName nvarchar(256)
DECLARE @KeyGUID uniqueidentifier
DECLARE @KeyAlreadyOpen bit
DECLARE @CallerName sysname
BEGIN TRY
--Bail if @@ROWCOUNT > 1 (temporary)
IF @@ROWCOUNT > 1
RAISERROR('Only one row at a time for sample!', 12, 1)
--Ensure row_label and advance_label are NOT NULL
DECLARE @RowClassification SecurityLabel
DECLARE @AdvanceClassification SecurityLabel
SELECT @RowClassification = row_label,
@AdvanceClassification = advance_label
FROM inserted
IF @RowClassification IS NULL
RAISERROR('row_label is required', 12, 1)
IF @AdvanceClassification IS NULL
RAISERROR('advance_label is required', 12, 1)
--Check write-down logic
EXECUTE AS CALLER
SELECT @CallerName = CURRENT_USER
REVERT
DECLARE @UserClearance SecurityLabel
exec usp_GetUserLabel @CallerName, @UserClearance OUTPUT
IF @UserClearance IS NULL
RAISERROR('user rights not sufficient to write this data', 12, 1)
IF dbo.Dominates(@UserClearance, @RowClassification) = 0
RAISERROR('user rights not sufficient to write this data', 12, 1)
IF dbo.Dominates(@UserClearance, @AdvanceClassification) = 0
RAISERROR('user rights not sufficient to write this data', 12, 1)
--Get RLSMappingID for row_label
exec usp_GetRLSMappingID @RowClassification, @RLSMappingID OUTPUT
--Get KeyName and CertName for advance_label
exec usp_GetLabelDetails @AdvanceClassification, @KeyMappingID OUTPUT, @KeyName OUTPUT, @CertName OUTPUT, @KeyGUID OUTPUT
exec usp_IsKeyOpen @KeyName, @KeyAlreadyOpen
EXEC('open symmetric key ' + @KeyName + ' using certificate ' + @CertName)
--Do insert, including rls_mapping_id and encryption of advance
INSERT INTO tblTitles
(title_id, title, type, pub_id, price, advance, advance_encrypted,
advance_label, royalty, ytd_sales, notes, pubdate, rls_mapping_id)
SELECT title_id,
title,
type,
pub_id,
price,
EncryptByKey(@KeyGUID, CONVERT(varchar(50), advance)),
1,
advance_label,
royalty,
ytd_sales,
notes,
pubdate,
@RLSMappingID
FROM inserted
END TRY
BEGIN CATCH
DECLARE @ErrorMessage NVARCHAR(400);
DECLARE @ErrorSeverity INT;
DECLARE @ErrorState INT;
SELECT @ErrorMessage = ERROR_MESSAGE();
SELECT @ErrorSeverity = ERROR_SEVERITY();
SELECT @ErrorState = ERROR_STATE();
IF @@TRANCOUNT > 0
ROLLBACK
RAISERROR (@ErrorMessage, @ErrorSeverity, @ErrorState);
END CATCH
--Whether successful or not, ensure any key opened by this routine is ---closed
BEGIN TRY
IF @KeyName IS NOT NULL AND @KeyAlreadyOpen = 0
EXEC('close symmetric key ' + @KeyName)
END TRY
BEGIN CATCH
--suppress error
END CATCH
GO
附录 B – 对称密钥加密选项
SQL Server 2005为对称密钥加密提供了一些算法。表中列出了支持的算法及相应的密钥长度。
表 13
[attach]71805[/attach]
为建立对称密钥,在SQL DDL中这些加密算法的选项很容易地被控制。这篇文档所描述设计的任何算法都能被使用。[/quote]
[quote]版权说明
这是一份初版文件,可能会于本软件产品正式发行之前依实况进行必要的修订。
本文件中所包含的信息代表 Microsoft Corporation 于发行日前针对该问题的观点。由于 Microsoft 必须反应市场条件的变更,因此不应解释为 Microsoft 的承诺。在发行日之后,Microsoft 不保证文件中任何信息的正确性。
此白皮书仅供参考使用。MICROSOFT 对于本文件中各项信息,不作任何明示、暗示或法定的保证。
使用者必须遵守所有适用的版权法律规定。在不影响版权各项权利的情况下,若无 Microsoft Corporation 的明确书面许可,本文件中的任何部份均不得因任何目的,以各种可能的形式或方式 (电子、机械、影印、录制或其它方式) 进行复制、储存或导入至检索系统或传送。
本文件中可能述及 Microsoft 的专利、专利应用程序、商标、版权或其它智能财产权。除非取得 Microsoft 明确书面授权声明,否则本文件并未授予这些专利、商标、版权或其它智能财产的授权。
除非另有说明,本文档中用作示例的公司、组织、产品、域名、电子邮件地址、徽标、人员、地点和事件均是虚构的,并不是有意或者不应推断为与任何真实的公司、组织、产品、域名、电子邮件地址、徽标、人员、地点或事件有关联。
2005 Microsoft Corporation。保留所有权利。
Microsoft ,Visual C# 和Windows是 Microsoft Corporation 在美国和/或其它国家/地区的注册商标或商标。
本文档中提到的真实公司和产品的名称均是各自所有者的商标。[/quote]
[[i] 本帖最后由 xiaoxinlucky 于 2008-1-21 16:50 编辑 [/i]]
页:
[1]