实现此系统的最重要的步骤之一是设置 VPD 安全策略,它将保护 TRANSACTION 表中的数据。 这将在接下来的部分中讨论。
设置 VPD。
为我们的示例系统设置一个 VPD 解决方案包括以下步骤:
- 创建一个专用的用户帐户来管理安全策略,并授予其相应的权限来限制能够修改安全设置的用户的数量
- 创建环境空间 保留一个专用于存储 Oracle 的会话变量的安全内存位置
- 创建一个过程来初始化应用程序环境
- 创建一个安全策略函数
- 将此策略函数指定给需要保护的表以创建安全策略
使用 DBMS_RLS 程序包的功能创建安全策略。 为确保一般数据库用户不能擅自改动安全策略,您将创建一个专用用户帐户 (SEC_MANAGER) 并授予其权限来执行 DBMS_RLS 中的函数和过程:
GRANT EXECUTE ON DBMS_RLS TO SEC_MANAGER;
SEC_MANAGER 还需要有设置应用程序环境的能力:
GRANT CREATE ANY CONTEXT TO SEC_MANAGER;
自此,您将依赖使用 SEC_MANAGER 用户帐户来设置其余的结构。
由于前端 PHP 应用程序的所有用户都会重用 Oracle 用户帐户 (SALES) 访问数据库,因此您将不得不依赖 Oracle 的应用程序环境来跟踪用户的身份。 要做到这一点,您需要创建环境空间 (TEST_SECURITY):
CREATE CONTEXT TEST_SECURITY USING sec_manager.pkg_sec_public
并创建一个分配会话专用环境变量的过程。 最后,您需要为 SALES 用户帐户授予执行此过程的权限。 (查看清单 2 中的代码,这些代码用于在 PKG_SEC_PUBLIC 程序包内创建 SET_CONTEXT 过程,并向 SALES 用户授予在此包中执行该过程的权限 [GRANT EXECUTE ON PKG_SEC_PUBLIC TO SALES]。) 为获得更好的可管理性,也可以通过角色授予权限。 例如,如果某些时候应用程序要求一个以上的帐户,您可以创建一个 EMPLOYEE_ROLE,并将这个角色分配给任意新数据库用户帐户。
下一步是创建策略函数,用以执行在安全要求中列出的规则。 在我们的例子中,策略函数背后的逻辑非常简单:
- 如果环境无效或未设置,此函数应返回强制 VPD 隐藏所有销售交易的谓词。
- 如果环境有效且用户是销售主管,此函数应允许该用户查看所有销售交易。
- 如果用户是硬件部门或软件部门的销售经理,此函数应返回通过部门 ID (... WHERE DEP_ID = department id) 过滤销售交易的谓词。
- 最后,如果用户是销售代表,此函数应根据该用户的 ID (... WHERE EMP_ID = user id) 对行进行过滤。
策略函数位于 PKG_SEC_POLICY 程序包中,且对所有用户都可用 (GRANT EXECUTE ON PKG_SEC_POLICY TO SALES)。 (策略函数 (FN_SELECT) 的完整代码可以在示例代码下载中的清单 3 中找到。) 为简化代码,您还要创建一个辅助函数,它判断一个给定用户是否为经理,并返回此人的部门 ID。该辅助函数 (FN_MANAGER) 的代码位于清单 4 中,SALES 模式的内容如图 4 所示。
请注意,上面的情况只是实现给定安全要求的多种方法之一。 策略函数的结构和逻辑是完全由用户定义的,可以按需要做到尽可能地简单或复杂。
|
| 图 4. SALES 模式 |
最后,为了能够动态过滤销售交易记录,您必须将 FN_SELECT 函数附加到 TRANSACTION 表来创建安全策略。 这需要调用 DBMS_RLS 程序包的 ADD_POLICY 过程:
BEGIN
dbms_rls.add_policy( 'sec_manager', 'transaction', 'transaction_select',
'sec_manager', 'pkg_sec_policy.fn_select', 'select');)
END;
不可避免的是,查询安全策略保护的表将需要在数据库部分做一些额外的工作。 为 WHERE 子句中引用的列添加一个索引可以改善这种状况。
现在,通过附加由 FN_SELECT 函数返回的谓词,此策略已设置为动态修改所有进入的涉及到 TRANSACTION 表的 SELECT SQL 查询。 重要的是,安全策略并不限于 SELECT 语句,也可以用于监视 INSERT、UPDATE 和 DELETE 查询。 还可以将同一个策略用于一个以上的表、将多个策略应用于同一个表,或多个策略重用一个策略函数。 除了表以外,这些策略还可以附加在视图上。
实现 PHP 前端功能。
PHP 前端应用程序用于验证用户、呈现输出以及提供简单的会话管理。 可以使用以下一组脚本来实现:
- connect.inc.php 存储连接参数并按需要提供给数据库连接
- login_form.php 和 login.inc.php 用来收集用户凭证的窗体和执行身份验证并启动新用户会话的脚本
- authorize.inc.php 确认用户已登录(即存在一个有效会话)
- transaction_list.php 从数据库检索销售交易列表并将结果呈现为一个 HTML 表格
- demo.css 用于控制界面外观的简单的样式表
后缀 inc 表明应将此脚本包含在其他脚本内,并从其他脚本执行,而非从自己执行。 这并不是 PHP 要求,而只是一个不成文的惯例,用以帮助区分不同的脚本。 但是,请注意,在实际情况中,您可能希望将“includes”放置在一个 Web 用户不能直接访问的特殊目录(如 cgi-bin)中。
应用程序如何运行。
现在,我们来仔细看一下每个脚本以及它们是如何协同运行的。 图 5 显示了正常的应用程序流:
|
| 图 5. 前端应用程序流 |
要查看销售交易报表,用户必须调用 transaction_list.php — 它首先执行 authorize.inc.php 脚本以确保用户已登录且已通过系统身份验证:
require("authorize.inc.php");
authorize.inc.php 的工作是检查用户的会话是否包含一个名为“SESSION_TOKEN”的会话变量,并验证这一变量的内容是否有效。 为了简单起见,此脚本不执行除验证变量长度是否大于零之外的任何特殊检查。 强烈建议在将其用于任何验证例程之前过滤“SESSION_TOKEN”变量的内容。 如果此变量缺失或未初始化,脚本会将把用户重定向到登录窗体:
if (!strlen($_SESSION["SESSION_TOKEN"]) > 0) {
header("Location: login_form.php?return_url=".$strReturnURL."&error_msg=".urlencode($strErrorMessage));
exit
}
登录脚本由两部分组成: 用于收集用户凭证的窗体 (login_form.php) 以及验证用户输入并依照 LOGIN 表检查凭证的内含脚本 (login.inc.php)。 尽管不要求有一个独立的内含脚本,且很容易在窗体中包含相同的功能,但如果您想要将这个脚本与其他窗体重用,则该方法的灵活性稍大一些。
除了确保均已提供登录名和密码外,登录脚本不提供对用户输入的详尽验证。 不过,再一次强烈建议对用户的输入进行严格过滤,以使应用程序免受跨站脚本攻击 (cross-site scripting) 或 SQL 注入式攻击。
一旦输入通过验证,脚本就调用 connect.inc.php 的 GetConnection() 函数,打开一个数据库连接,并通过执行下列查询尝试获取用户的 ID:
SELECT emp_id,login FROM LOGIN
WHERE login='login_name'
AND password='password';
如果上述查询没有返回结果,则登录脚本向用户显示错误消息“Invalid user name or password”,并提示输入新的凭证。 否则,脚本启动一个新的 PHP 会话,并将从 LOGIN 表接收的用户 ID 指定为“SESSION_TOKEN”变量的内容。 用户 ID 是初始化附加在 TRANSACTION 表上的 VPD 策略的关键部分;因此,将这一信息作为一个明文会话变量是不适用于真实系统的。 有很多种方法可以保护“SESSION_TOKEN”变量的内容并使会话劫持 (session hijacking) 的风险最小。 这些方法可能包括加密、哈希运算或添加更多的参数,如截止时间戳记、加密 salt,甚至是将令牌有效载荷绑定到用户 IP 地址的某个部分。 不过,本文不讨论这些方法。
关于应用程序如何处理会话最后要注意的是,它要求用户在其浏览器中启用 cookie,不过,您可以通过在 GET 或 POST 请求上携带会话 ID 来避免这一点。
呈现数据。
最后,在登录成功且 authorize.inc.php 让用户通过后,就由 transaction_list.php 脚本来控制了。 transaction_list.php 首先所做的就是打开一个新的数据库连接:
$rsConnection = GetConnection($_SESSION["SESSION_TOKEN"]);
要确保这一次(与在登录阶段进行连接不同)连接函数获取保存在“SESSION_TOKEN”变量中的用户 ID(不过再次强调要保持警觉,在将其传递给函数前要过滤会话令牌的内容)。 然后,用户 ID 被提供给 SET_CONTEXT 过程,此过程在数据库服务器中为给定连接(即 Oracle 会话)初始化安全应用程序环境。
一旦创建了连接,脚本就执行此 SQL 查询:
SELECT T_ID,T_AMOUNT,EMP_NAME,EMP_POSITION,DEP_NAME
FROM TRANSACTION, EMPLOYEE, DEPARTMENT
WHERE TRANSACTION.EMP_ID = EMPLOYEE.EMP_ID
AND EMPLOYEE.DEP_ID = DEPARTMENT.DEP_ID;
如果调用成功,脚本就使用 PHP 的 Oracle 调用接口 (OCI) 函数 OCIFetchInto 遍历结果记录集并在一个 HTML 表格中呈现每个行。 为了简单起见,这个脚本使用一个显式 SQL 语句检索数据。不过,对真实系统来说,使用存储过程更为恰当。
VPD 与 PHP 组合的好处
分析这个示例系统的代码,很容易认识到用以 VPD 为中心的方法创建安全的 Web 应用程序的优势。 如您所见,transaction_list.php 脚本没有区分不同用户的代码,且实际上,对任何通过登录脚本的用户执行完全相同的 SQL 语句。 与之相似,connection.inc.php 脚本使用相同的 Oracle 帐户来打开数据库连接。 不过,虽然前端应用程序知道用户的身份(“SESSION_TOKEN”变量中的用户 ID),但它仅仅将其传递给数据库服务器,从而使数据库服务器(而不是 PHP 应用程序)来负责执行附加在 TRANSACTION 表上的安全策略。 如果您试图不提供用户身份进行连接会怎样呢? 应用程序仍将创建一个有效的 Oracle 连接,但是在这种情况下,此连接不附加有任何环境,这使安全策略将所有数据隐藏在 TRANSACTION 表中。 也就是说,对于任何这样的连接(通过 PHP 前端或直接到数据库),TRANSACTION 表会显示没有任何数据。
总的来说,将数据授权工作从前端应用程序延伸到数据库服务器有以下好处:
- 能更快开发前端应用程序和报表。
- 安全性提高 数据的安全性不再完全依赖于前端应用程序的安全性。
- 维护工作减少 安全策略更新在数据库中集中完成,无须单独更新每个前端应用程序。
试用
现在,一旦应用程序的后端和前端都已就绪,您就可以实施若干测试用例来确保系统功能符合安全要求。 TRANSACTION 表当前包含下列行:
- Transaction 1 by Peter, Software Department
- Transaction 2 by Alan, Software Department
- Transaction 3 by Andy, Hardware Department
下面表中列出了 5 个用例以及每个用例的预期行为和实际结果。
|
用例编号 |
用户的姓名和职位 |
登录名 |
预期输出 |
实际结果 |
| 1 | Bob 销售主管 | bob | 记录 1、2、3 | 请查看 图 6 |
| 2 | John 销售经理 软件部 | john | 记录 1、2 | 请查看 图 7 |
| 3 | Alan 销售代表 软件部 | alan | 记录 2 | 请查看 图 8 |
| 4 | Tony 销售经理 硬件部 | tony | 记录 3 | 请查看 图 9 |
| 5 | Dave 主管助理 | dave | 无记录 | 请查看 图 10 |
结论
本文中所讨论的示例应用程序旨在演示将 PHP 和 Oracle 相结合来实施一个访问受 VPD 保护数据的易于开发的前端的概念和好处。 请注意,示例系统为适应文章的内容做了简化。 尽管有功能很全,但它缺乏企业实用应用程序的某些特性,如对输入的过滤和验证、对 PHP 会话数据的充分保护机制、加密等等。 不过,这个应用程可以作为创建真实系统(可以提供访问机密且任务关键数据的轻型但安全的接口)的一个好起点。
Mikhail Seliverstov [mikhail.seliverstov@mcgill.ca] 是位于蒙特利尔的麦吉尔大学校友会的 Web 编程小组的领导人。 他专攻构建基于 Web 的大型系统的安全性方面的问题。 在过去 3 年中,Seliverstov 作为首席应用程序架构师和开发人员,帮助领导了一个大型的 PHP/Oracle 开发项目。
将您的意见发送给我们