针对 PHP 应用程序进行基于数据库的身份验证,第 2 部分


作者:Michael McLaughlin

了解如何通过基于数据库的身份验证保护基于 PHP 的 Web 应用程序;本部分将探究细粒 度访问。

2007 年 5 月发布

在本文的第 1 部分中,您了解了基于数 据库的身份验证的工作方式以及如何在基于 PHP 的 Web 应用程序中实现该身份验证。

在第 2 部分中,您将详细了解之前构建的身份验证示例和用户项解决方案。虽然当前的解决方案 对所有用户一视同仁,但在实际的应用程序中,用户平等地访问所有内容的想法并不多见。相反,您 的用户可能具有角色和权限,根据他们在公司里的职责地位限制其对数据的访问。

Oracle8i 之后,Oracle Database 包括了一个称为虚拟专用数据库 (VPD) 的安全特性。通过 VPD 可以定 义、构建和应用安全策略。这些安全策略可以限制用户只访问特定的表或表的特定部分。利用这些策 略,您可以通过检查数据库在用户连接到 Oracle 实例时创建的会话级数据,来管理模式的访问权限 。VPD 的实现需要一个为不同模式授予权限的公共信息库模式。用户定义的数据库设计必须在公共信 息库表中提供一个条带化的列。然后,由 VPD 策略管理对这些表的访问。

VPD 技术依赖于数据库目录中存储的连接数据。连接数据识别和跟踪模式(用户)的活动。该连 接元数据只有一个域不由 Oracle 数据库定义和管理。CLIENT_INFO 列供开发人员在构建用户定义的 应用程序时使用。您可以将会话 CLIENT_INFO 列与数据库列结合使用,以在低于模式的粒度对数据 进行条带化并在单个模式中支持多个用户。Oracle E-Business 11i Suite 通过在视图中使用 CLIENT_INFO 列对多个机构和多种货币进行条带化。

组合使用这些技术,您可以通过安全策略实现细粒度访问权限和角色。或者,您也可以通过将 CLIENT_INFO 列绑定到身份验证模型中来管理细粒度访问。

Oracle 数据库连接架构

示例代码清单

程序名 语言 说明

create_identity_db2.sql

SQL

该脚本构建数据库并填充初始化 Web 应用程序所需的行

credentials2.inc

PHP

其包含的文件定义了您在所有 oci_connect() 函数调用中使用的三个全局常量

SignOnUserAdmin.php

PHP

该程序文件在调用 UserAdmin.php 程序前重设 PHP 会话 ID

UserAdmin.php

PHP

该程序文件 (a) 对新用户和返回的活动会话进行验证;(b) 在用户享有权限时生成一个表单,以 便将新用户输入 ACL;并 (c) 充当根据访问权限查看用户的门户。

UserView.php

PHP

该程序文件对新用户和返回的活动会话进行验证,并生成一个表单,以便根据访问权限查看用户

Oracle 数据库连接架构有两个约束面,作为 PHP 编程人员,您应该了解。第一个,实际的数据 库连接被称为数据库事件,并具有一个唯一的内部维护的数值。每个数据库连接在系统目录中创建一 行元数据。该行表示连接的长度,任何具有 DBA 角色权限的用户都可以通过 V$SESSION 视图对其进 行查询。数据库中 SESSIONID 的值是唯一的,与 PHP session_id() 函数的返回值无关。第二个, 当您使用 oci_pconnect() 函数打开一个持久性连接时,您只有一个数据库 SESSIONID 值;而当您 打开一个非持久性连接时,您可以有多个 SESSIONID 值。

oci_connect() 和 oci_new_connect() 函数每次运行时创建新的 SESSIONID 值。只有第一个 oci_pconnect() 函数调用创建一个 SESSIONID 值,但是后续的 oci_pconnect() 函数调用会重用打 开的会话。这减少了构建新连接所需的动态列集费用,保留了 Web 页面和数据库服务器之间的状态 信息。您还可以在单个 PHP 会话或响应-确认请求循环范围内拥有两个或多个非持久性连接。

虚拟专用数据库架构

VPD 架构的工作方式是将连接元数据和用户定义的安全策略组合到一个单独的安全模式中。安全 策略是用 PL/SQL 存储函数编写的过程编程指令。默认情况下,策略函数有两个形式参数 — 一个是代表 Oracle 模式(也称为用户帐户)的字符串,另一是表名。这些策略函数返回一个可变长 度的字符串,该字符串附加到针对表的任何 SQL 语句的 WHERE 子句中。通过 WHERE 子句,您可以 限制用户可查询和处理的行。您可以通过筛选限定列(如机构部门或安全组角色)上的数据来限制访 问。筛选过的表称为条带化的表或视图。

您可以通过创建一个模式/用户(如 SECURITY_ADMIN),然后授予该模式对 SYS.DBMS_RLS 存储 程序包的访问权限来实现 VPD。通过 DBMS_RLS 程序包,您可以添加、维护和删除安全策略。然后, 您可以在 SECURITY_ADMIN 模式中定义策略存储函数,并使用 DBMS_RLS 程序包将它们添加为策略。

策略维护围绕数据的屏障,该屏障通常位于另一个模式中。然后,拥有表的模式向一个或多个其 他模式授予访问和事务权限。该配置结构类似于 Oracle 数据库中存储过程的默认定义者权限模型。 默认情况下,存储过程以创建时所用的模式的权限进行操作。

在经典的定义者权限模型中,拥有模式通常不会将表和视图的访问和事务权限授予其他用户。该 模式仅授予对其存储过程的执行权限。存储过程保证用户按计划的方式访问和处理数据。遗憾的是, 定义者权限模型不支持根据访问源(用户或模式)将数据划分到各个角色中。

通过将 VPD 的策略与定义者权限模型相结合,您可以根据指定的角色和权限将数据划分为多个部 分。这是因为,现在可以通过有效地创建视图来像访问子集一样访问表。

图 1 说明了四个用户看到不同的片段或视图数据的想法。



图 1 定义者权限模型

将数据分到不同视图中是通过向表中添加条带化列实现的。您可以根据 VPD 安全策略函数中的模 式名称映射条带化列。除了数据库模式名称外,您还可以根据列值对表进行条带化。

图 2 说明如何通过数字伪键列(SYSTEM_USER 表中的 SYSTEM_USER_GROUP_ID)按公用值对用户 进行分组。



图 2 条带化的表

0 或 1 使您可以区分超级用户和最终用户并进行相应的分组。SYSTEM_USER_GROUP_ID 列值是伪 键或数字序列值,通常映射到在另一个表中的描述。当用户不是数据库模式时,该条带化模型正常工 作。

模式级安全是一种常用方法,它与定义方权限数据模型的传统方法一致。通过该用户级安全方法 ,您可以将非模式用户帐户映射为条带化列值。您可以使用其中任何一种方法实现 VPD。接下来的两 小节将介绍这两种安全方法的基本机制。

模式级安全模型

模式级安全模型实现起来非常简单。它要求所有用户都是模式。这通常是不受欢迎的业务模型, 而且会由于与授权和类似的维护活动有关的费用而造成极大的管理负担。

在该模型中,您的策略函数通过从存储会话元数据中查询上下文信息来保护模式的安全。只有 SYS_CONTEXT() 函数可以保护连接模式名称的安全。您在策略函数内部调用 SYS_CONTEXT() 函数, 如以下示例伪代码所示:

CREATE OR REPLACE FUNCTION policy_user_maintenance
( schema           VARCHAR2
, tab              VARCHAR2 )
RETURN VARCHAR2 IS

  -- Define local variables.
  schema_name      VARCHAR2(30);
  where_clause     VARCHAR2(100);

BEGIN
  -- Get schema name.
  schema_name := SELECT SYS_CONTEXT('userenv ','session_user ') FROM dual;

  CASE schema_name
    WHEN user_a THEN
       ... qualifying logic ...
    WHEN user_b THEN
       ... qualifying logic ...
    WHEN user_c THEN
       ... qualifying logic ...
    ELSE
       ... qualifying logic ...
  END CASE;

  -- Return where clause.
  RETURN where_clause;

END policy_user_maintenance;
/

真正的 POLICY_USER_MAINTENANCE() 函数(如示例 shell)仅在用户是模式时才能实现。您的 PHP 程序将需要进行身份验证,然后,每个连接语句将变为动态的。这意味着每个 PHP 函数调用都 需要具有访问各种模式凭证的权限。虽然这是一个差强人意的解决方案,但却是可行的(虽然不能象 生产解决方案那样让您获利)。

还有更好的解决方案。您可以通过补充会话元数据中存储的上下文信息来实现。正如下一小节中 所讨论的,可以通过将信息写入 V$SESSION 视图中的 CLIENT_INFO 列来实现。CLIENT_INFO 列是会 话元数据中的一个 64 个字符的用户定义的字符串。

用户级安全模型

用户级安全模型实现起来要比模式级模型复杂一些。实现用户级安全需要执行一个三个步骤的过 程。(a) 根据 ACL 验证凭证的身份,(b) 将验证的凭证写入到会话元数据中存储的上下文信息中, 并 (c) 编写配置文件存储函数以根据会话元数据中存储的用户定义的上下文信息进行验证。

验证凭证的身份。虽然如何实现身份验证由您决定,但还是应该使其简单。假 设您采用了一个类似的 SYSTEM_USER 表作为 ACL,您可以根据表中的用户名和口令进行身份验证。 然后,您可以获得主键列值的副本,使用该主键值作为会话元数据中的上下文标识符。这样就无需进 行大写、小写和大小写混合验证,但这看上去是一个神奇的数字解决方案。

从会话元数据中读取凭证以及向会话元数据写入凭证

向会话 CLIENT_INFO 列写入以及从其中读取的过程需要使用 DBMS_APPLICATION_INFO 程序包。 使用 DBMS_APPLICATION_INFO 程序包中的 SET_CLIENT_INFO 过程将数据写入到 V$SESSION 视图中 的 64 个字符的 CLIENT_INFO 列中。以下匿名 PL/SQL 块假设 SYSTEM_USER_ID 列值为 1:

BEGIN
   -- Write value to V$SESSION.CLIENT_INFO column.
   DBMS_APPLICATION_INFO.SET_CLIENT_INFO('1 ');
END;
/

现在,您可以通过调用 READ_CLIENT_INFO() 过程来读取该值。您应该使用 SQL*Plus 启用 SERVEROUTPUT 以查看运行以下程序时呈现的输出:

DECLARE
  client_info      VARCHAR2(64);
BEGIN
   -- Read value from V$SESSION.CLIENT_INTO column.
   DBMS_APPLICATION_INFO.READ_CLIENT_INFO(client_info);

   -- Print it to console.
   DBMS_OUTPUT.PUT_LINE('[ '||client_info||']');
END;
/

通过用户定义的会话列,您可以存储与 ACL 中的用户凭证有关的独特信息。您可以在用户身份验 证期间指定会话列值。然后,您可以通过会话 CLIENT_INFO 列来管理单个模式中的多个用户交互。 当经过验证的用户的会话 CLIENT_INFO 列值与表中的一个条带化列值匹配时,该用户可以访问表的 所有行或部分行。

编写安全配置文件存储过程。策略函数通过查询存储会话元数据中的上下文信 息来保护用户定义的 CLIENT_INFO 列值的安全。USERENV() 和 SYS_CONTEXT() 函数都可以保护用户 定义的列值的安全。

可以使用 USERENV() 或 SYS_CONTEXT() 函数查询数据库会话元数据。如果您不确定您的用户帐 户是否具有 DBA 角色权限,您也可以使用这些函数。SYS_CONTEXT() 函数更为灵活,通过它可以对 会话元数据进行更大范围的访问,您应该使用它来代替旧的 USERENV() 函数。下面演示如何使用 SYS_CONTEXT() 函数检查您是否具有 DBA 角色:

SELECT   sys_context('userenv ', 'isdba ')
FROM     dual;

如果您具有 DBA 角色,查询将返回一个布尔值 true,如果您没有该角色,则返回 false。 SYS_CONTEXT() 函数提供了另一种方法来查询 V$SESSION 视图中的连接信息,可用于受限的权限模 式。

您可以通过将 SCHEMA_NAME 变量替换为 CLIENT_INFO 变量来更改先前讨论的策略函数。然后, 使用 SYS_CONTEXT 函数从会话元数据中选择当前的 CLIENT_INFO 列值。安全策略过程中的其余逻辑 也要求从模式名称到用户定义的 CLIENT_INFO 列值的更改。下面显示了用户级安全管理过程的修改 后的伪代码安全策略函数:

CREATE OR REPLACE FUNCTION policy_user_maintenance
( client_info      VARCHAR2
, tab              VARCHAR2 )
RETURN VARCHAR2 IS

  -- Define local variables.
  client_info_name VARCHAR2(64);
  where_clause     VARCHAR2(100);

BEGIN
  -- Get schema name.
  client_info_name := SELECT SYS_CONTEXT('userenv ','client_info ') FROM dual;

  CASE client_info_name
    WHEN user_a THEN
       ... qualifying logic ...
    WHEN user_b THEN
       ... qualifying logic ...
    WHEN user_c THEN
       ... qualifying logic ...
    ELSE
       ... qualifying logic ...
  END CASE;

  -- Return where clause.
  RETURN where_clause;

END policy_user_maintenance;
/

身份验证过程模型

本部分提供的身份验证解决方案使用 CLIENT_INFO 会话列和条带化的表。该解决方案使用条带化 视图,从而避免了引入设置安全管理模式策略函数的复杂性。条带化视图包含可将用户会话数据值与 条带化列值连接在一起的 WHERE 子句。该方法模拟了 VPD 安全函数构建的动态 WHERE 子句。 Oracle E-Business Suite 件使用该方法访问包含特定于用户的机构和报告货币数据的动态视图。

该身份验证过程模型依赖在第 1 部分中介绍的两个表。这两个表如图 3 所示。



图 3 基本身份验证数据模型

虽然表的定义没有更改,但是该身份验证模型实现了利用 SYSTEM_USER 表中的 SYSTEM_USER_GROUP_ID 列提供的数据条带化的编程逻辑。

该身份验证模型仅针对超级用户和最终用户进行条带化,这些用户分别属于管理组和员工组。可 以通过查询数据库和检查 SYSTEM_USER_GROUP_ID 列中的一个值 1 来识别超级用户。最终用户的值 大于或等于 1。PHP 脚本读取 SYSTEM_USER_GROUP_ID 以将表单会话设置为超级用户或最终用户;它 们使用 DBMS_APPLICTION_INFO 程序包设置 CLIENT_INFO 列值以从条带化视图中读取数据。

本部分主要讨论在分析代码和身份管理解决方案之前,如何设置测试应用程序并进行测试。示 例模式和代码与第 1 部分的不同,它们使用了不同的数据库模式。这使您可以在系统上启用并 运行两个示例代码树,以便进行比较。

设置测试应用程序。演示应用程序使用了一个名为 IDMGMT2 的 Oracle 模式 ,口令与模式名相同。可通过以下步骤创建用户和环境:

1. 以 SYSTEM 特权用户身份登录数据库,并运行以下命令。由于 RESOURCE 角色的工作方式发生 了变化,因此使用 Oracle 10g 时必须显式授予 CREATE ALL VIEWS 权限:

SQL> CREATE USER IDMGMT2 IDENTIFIED BY IDMGMT2;
SQL> GRANT CONNECT, RESOURCE TO IDMGMT2;
SQL> GRANT CREATE ALL VIEWS TO IDMGMT2;

2. 连接到新的用户模式:

SQL> CONNECT IDMGMT2/IDMGMT2@XE

3. 在 IDMGMT2 模式下运行 create_identity_db2.sql 脚本,以创建所有必需的对象并填充 SYSTEM_USER 表:

SQL> @create_identity_db2.sql

4. 将以下文件放入 htdocs 目录或 htdocs 目录的子目录中:

 • SignOnUserAdmin.php
 • UserAdmin.php
 • UserView.php

这些步骤可以完成我们此时所需的设置。现在可以测试领域或会话身份管理示例。

测试会话身份验证。可通过以下步骤测试领域身份管理:

1. 使用以下 URL 打开领域身份管理示例:

http://localhost/SignOnUserAdmin.php

2. 可以使用下面提供的帐户作为使用基本 HTTP/HTTPS 身份验证的有效凭证。确保您没有启 用大写锁定键,因为这些凭证区分大小写。

User Name Password
administrator welcome
guest guest



图 4 Cookie 和会话登录页面

您将看到以下页面,您应该使用用户名“administrator”和口令 “welcome”以该测试系统中的特权用户(而不是受限的权限用户 “guest”)身份登录。

验证凭证的身份之后,程序会将您转至 New User (UserAdmin.php) 表单。您 会注意到该表单相比于以前的 New User (AddDBUser.php) 表单有一些改动。该 表单现在支持将新用户作为管理组或员工组的成员输入,而且还提供了一个视图用以查看输入的用户 。管理组成员有权添加新用户,而员工组的权限是受限的,不能添加新用户。

3. 现在,您可以将一个用户输入到管理组或员工组中。管理组是超级用户组,而员工组是针对最 终用户的。您应该输入以下两种用户:

  Administration Group Employee Group

User ID

jonesi

ravenwoodm

Password

jonesi

ravenwoodm

Indiana

Marion

Jones

Ravenwood

确保您没有启用大写锁定键,因为这些项区分大小写。如果没有单击 Administration Group 单选按钮,输入 Indiana Jones 时应该单击该按钮。单击 Employee Group 单选按钮,然后输入 Marion Ravenwood。在后续的导航步骤中,您将用到这两个帐 户。



图 5 Cookie 和会话登录页面

管理组是 New User (UserAdmin.php) 表单的默认设置。当您创建管理组用户 时,SYSTEM_USER_GROUP_ID 列的值为 0。在员工组中新建用户会在同一列中插入一个值 1。在管理 组中定义的用户有权创建其他用户,而在员工组中定义的用户不能创建新用户。

输入 Indiana Jones 和 Marion Ravenwood 之后,您将看到 New User 表单 ,如图 6 所示。该表单确认您已经输入了一个新用户。如果您的凭证不能满足设置的条件,同一行 将解释原因。



图 6 包含确认消息的主管理页面

4. 将 Indiana Jones 和 Marion Ravenwood 输入为“管理员”用户后,可以单击 View User 按钮查看用户 ACL 成员。



图 7 Administration Group 用户的 User Account View

4. 在您确认将 Indiana Jones 输入为系统管理员并将 Marion Ravenwood 输入为系统用户之后 ,应单击 Log Out 按钮。然后,应使用 guest 或 Marion Ravenwood 凭证以系 统用户的身份登录。图 8 显示了员工组用户看到的 New User 表单是什么样的。



图 8 受限用户帐户的主管理页面

5. 您以 Marion Ravenwood 身份单击 View User 按钮时,只显示您的帐户。 这是因为表单返回了一个条带化的数据视图。



图 9 Employee Group 用户的 User Account View

现在,您已经测试了条带化的用户身份验证表单。下一部分分析身份管理是如何工作来支持这些 表单的。

分析 UserAdmin.php 会话身份验证代码。通过前面的文章中提到的对以前版 本的 AddDBUser.php 程序的若干更改可以进行此条带化行为。AddDBUser.php 程序已更改为 UserAdmin.php 脚本,包含以下更改:

  • 修改 get_session() 函数,以获取和设置合适的用户定义的会话元数据变量值。
  • 修改 create_new_db_user() 函数,以验证当前经过身份验证的用户是否是特权用户。
  • 修改 get_message() 函数和基表单逻辑,以提供用户输入的确认。
  • 如果用户不是特权用户,则禁用文本输入域、Add User 按钮和 Administration/Employee 组单选按钮。
  • verify_db_login() 函数的基代码现在捕获组值并将其赋值给 $_SESSION 变量。

您将通过在应用程序中实现类似的代码更改来构建条带化的 Web 应用程序。逐个介绍这五个业务 逻辑更改。

修改 get_session() 函数。get_session() 函数已在 AddDbUser.php 和 UserAdmin.php 版本间进行了更改,支持根据用户的组分配进行单独的请求处理。SELECT 语句现在 返回两个附加列 — 它们是 SYSTEM_USER_ID 和 SYSTEM_USER_GROUP_ID 值。get_session() 函数计算 SYSTEM_USER_GROUP_ID 值是否为 0,0 是该应用程序中的管理组。如果用户具有管理权限 ,该函数将组值赋给 $_SESSION['client_info'] 值。如果用户没有管理权限,该函数将单独的记录 标识符指定为 $_SESSION['client_info'] 值。

然后,您可以使用 $_SESSION['client_info'] 值在当前运行的脚本文件范围内控制以后的连接 。当您使用到 Oracle 数据库的非持久性连接时,在 PHP 程序范围内维护 $_SESSION ['client_info'] 值。如果您使用 oci_pconnect() 函数建立了所有持久性连接,只需在会话元数据 中设置 CLIENT_INFO 列值,即可避免该操作。

修改后的 get_session() 函数如下所示,更改用粗体文本突出显示:

function get_session($sessionid,$userid = null,$passwd = null)
{
  // Attempt connection and evaluate password.
  if ($c = @oci_connect(SCHEMA,PASSWD,TNS_ID))
  {
    // Assign metadata to local variable.
    $remote_address = $_SERVER['REMOTE_ADDR'];

    // Return database UID within 5 minutes of session registration.
    $s = oci_parse($c,"SELECT   su.system_user_id
                       ,        su.system_user_name
                       ,        su.system_user_group_id
                       ,        ss.system_remote_address
                       ,        ss.system_session_id
                       FROM     system_user su JOIN system_session ss
                       ON       su.system_user_id = ss.system_user_id
                       WHERE    ss.system_session_number = :sessionid
                       AND     (SYSDATE - ss.last_update_date) <=
                                  .003472222");

    // Bind the variables as strings.
    oci_bind_by_name($s,":sessionid",$sessionid);

    // Execute the query and raise missing table message on failure.
    if (@oci_execute($s,OCI_DEFAULT))
    {
      // Check for a validated user, also known as a fetched row.
      if (oci_fetch($s))
      {
        // Assign unqualified values.
        $_SESSION['userid'] = oci_result($s,'SYSTEM_USER_NAME');

        // Assign the privileged group or user primary key column value.
        if (oci_result($s,'SYSTEM_USER_GROUP_ID') == 0)
          $_SESSION['client_info'] = oci_result($s,'SYSTEM_USER_GROUP_ID');
        else
          $_SESSION['client_info'] = oci_result($s,'SYSTEM_USER_ID');

        // Check for same remote address.
        if ($remote_address == oci_result($s,'SYSTEM_REMOTE_ADDRESS'))
        {
          // Refresh last update timestamp of session.
          update_session($c,$sessionid,$remote_address);
          return (int) oci_result($s,'SYSTEM_SESSION_ID');
        }
        else
        {
          // Log attempted entry.
          record_session($c,$sessionid);
          return 0;
        }
      }
      else
      {
        // Record when not first login.
        if (!isset($userid) && !isset($passwd))
          record_session($c,$sessionid);
        return 0;
      }
    }
    else
    {
      // Set error message.
      set_error(__FUNCTION__,array('SYSTEM_USER','SYSTEM_SESSION'));
    }

    // Close the connection.
    oci_close($c);
  }
  else
  {
    $errorMessage = oci_error();
    print htmlentities($errorMessage['message'])."<br />";
  }
}

修改 create_new_db_user() 函数。create_new_db_user() 函数现在返回一 个布尔变量而不是空值。该更改使 UserAdmin.php 程序可以在控制结构内部调用该函数,如下所示 :

if (create_new_db_user($_SESSION['db_userid']
                      ,$newuserid
                      ,$newpasswd
                      ,$fname
                      ,$lname
                      ,$usergroup))
{
  // Set code to successful.
  $code = USER_VALID;

  // Render new form with successful acknowledgement.
  userAdminForm(array("code"=>$code
                     ,"form"=>"UserAdmin.php"
                     ,"userid"=>$newuserid));
}

该更改使您可以将成功消息附加到下一个呈现的 New User 表单的副本中。该 函数还可以防止未经授权的用户添加新用户。虽然添加新用户的功能通过基于经过验证的用户的权限 有条件地呈现表单进行限制,但还是应该在函数对该功能进行保护。预防措施是创建一个良好的设计 模型,该模型并不完全依赖于紧密耦合的行为。函数耦合会在维护编码期间丢失,您应确保所有函数 操作的独立性。这样,函数可供不同用户界面 (UI) 使用。

该应用程序 UI 利用此方法防止未经授权新建用户的尝试,另一种方法可能支持这种尝试但会向 用户报告他们未经授权。支持 UI 的函数(如该函数)应该足够灵活才能处理这两种方法。

create_new_db_user() 函数现在在输入时将 $written 控制变量设置为 false。然后,它检查活 动用户是否得到授权可以添加新用户。它通过读取 get_session() 函数设置的 $_SESSION ['client_info'] 值来进行此操作。将新用户成功添加到数据库中后,将控制变量重设为 true。完 整的函数如下所示:

// Add a new user to the authorized control list.
function create_new_db_user($userid,$nuserid,$npasswd,$fname,$lname
                           ,$ugroup)
{
  // Set control variable.
  $written = false;

  // Attempt connection and evaluate password.
  if ($c = @oci_connect(SCHEMA,PASSWD,TNS_ID))
  {
    // Check for prior insert, possible on web page refresh.
    if (!is_inserted($c,$nuserid) && ($_SESSION['client_info'] == 0))
    {
      // Return database UID.
      $s = oci_parse($c,"INSERT INTO system_user
                         ( system_user_id
                         , system_user_name
                         , system_user_password
                         , first_name
                         , last_name
                         , system_user_group_id
                         , system_user_type
                         , start_date
                         , created_by
                         , creation_date
                         , last_updated_by
                         , last_update_date )
                         VALUES
                         ( system_user_s1.nextval
                         , :newuserid
                         , :newpasswd
                         , :firstname
                         , :lastname
                         , :usergroup
                         , 1
                         , SYSDATE
                         , :userid1
                         , SYSDATE
                         , :userid2
                         , SYSDATE)");

      // Bind the variables as strings.
      oci_bind_by_name($s,":newuserid",$nuserid);
      oci_bind_by_name($s,":newpasswd",sha1($npasswd));
      oci_bind_by_name($s,":firstname",$fname);
      oci_bind_by_name($s,":lastname",$lname);
      oci_bind_by_name($s,":usergroup",$ugroup);
      oci_bind_by_name($s,":userid1",$userid);
      oci_bind_by_name($s,":userid2",$userid);

      // Execute the query print error handling for missing table.
      if (@oci_execute($s,OCI_COMMIT_ON_SUCCESS))
      {
        // Update control variable for insert.
        $written = true;
      }
      else
      {
        // Set error message.
        set_error(__FUNCTION__,array('SYSTEM_USER'));
      }
    }

    // Close the connection.
    oci_close($c);

    // Return control variable.
    return $written;
  }
  else
  {
    $errorMessage = oci_error();
    print htmlentities($errorMessage['message'])."<br />";
  }
}

修改 get_message() 函数。对 get_message() 函数的唯一更改是添加了两个 条件。一个条件确认成功输入了新用户,另一个条件支持未经授权的访问消息。您还应注意到,以前 的神奇数字已转换为常量,从而提高了代码的可读性。

有条件地禁用 Add User 和组单选按钮。userAdminForm() 函数现在有条件地 呈现文本输入域、Add UserAdministration/Employee 组 单选按钮。该更改实现了前面讨论的 UI 并可防止未经授权添加新用户的尝试。虽然在提交按钮被禁 用时,没有用到文本域和 Administration/Employee 组单选按钮,但是它们也同 样被禁用以免最终用户混淆。如果不符合以下条件语句,它们将被禁用:

if ((!@$_SESSION['client_info']) && (@$_SESSION['client_info'] == 0))

该语句检查会话级变量是否已经设置且等于 0,这是应用程序中唯一经过授权的帐户或特权帐户 。在实际的实现中,您可能会在一个数组中有一个授权帐户权限列表。假设在 $authorized_list 数 组变量中初始化一个特权 ACL,您最有可能使用类似下面的条件表达式:

if ((!@$_SESSION['client_info']) &&
    (array_search(@$_SESSION['client_info'],$authorized_list)))

修改 verify_db_login() 函数。该应用程序设计支持当用户在超时窗口内使 用相同的凭证注销和返回时更新会话。该设计要求您更新 verify_db_login() 函数的行为。您可以 通过支持该函数捕获和设置 $_SESSION['client_info'] 值来更改该函数,如下面的函数实现所示:

function verify_db_login($userid,$passwd)
{
  // Attempt connection and evaluate password.
  if ($c = @oci_connect(SCHEMA,PASSWD,TNS_ID))
  {
    // Return database UID.
    $s = oci_parse($c,"SELECT   system_user_id
                       ,        system_user_group_id
                       FROM     system_user
                       WHERE    system_user_name = :userid
                       AND      system_user_password = :passwd
                       AND      SYSDATE BETWEEN start_date
                                        AND NVL(end_date,SYSDATE)");

    // Assign encryted value to variable, avoiding E_STRICT error.
    $newpassword = sha1($passwd);

    // Bind the variables as strings.
    oci_bind_by_name($s,":userid",$userid);
    oci_bind_by_name($s,":passwd",$newpassword);

    // Execute the query and raise missing table message on failure.
    if (@oci_execute($s,OCI_DEFAULT))
    {
      // Check for a validated user, also known as a fetched row.
      if (oci_fetch($s))
      {
        // Confirm session and collect foreign key reference column.
        if ((!isset($_SESSION['session_id'])) ||
            (!isset_sessionid($c,$_SESSION['sessionid'])))
        {
          $_SESSION['db_userid'] = oci_result($s,'SYSTEM_USER_ID');
          $_SESSION['client_info'] = oci_result($s,'SYSTEM_USER_GROUP_ID');
          register_session($c,(int) $_SESSION['db_userid']
                          ,$_SESSION['sessionid']);
        }

        // User verified.
        return true;
      }
      else
      {
        // User not verified.
        return false;
      }
    }
    else
    {
      // Set error message.
      set_error(__FUNCTION__,array('SYSTEM_USER'));
    }

    // Close the connection.
    oci_close($c);
  }
  else
  {
    $errorMessage = oci_error();
    print htmlentities($errorMessage['message'])."<br />";
  }
}

上述五个分析回顾了对 UserAdmin.php 脚本的更改。下一部分将探讨新的 UserView.php 程序。

分析 UserView.php 会话身份验证代码。UserView.php 程序是新的,但是由 UserAdmin.php 程序中的业务逻辑发展而来的。新表单的开发需要三个新函数并删除了几个函数。它 还要求在数据库中开发一个条带化的 AUTHORIZED_USER 视图。

通过同步函数,您可以从各个脚本文件中删除它们并将它们放到一个共享库中以支持该应用程序 。这是构建您自己的身份管理解决方案时的下一个推荐的步骤。

添加的三个新函数为:

  • return_users() 函数,它从条带化视图获取结果。
  • set_client_info() 函数,它在数据库中设置用户定义的会话元数据值。
  • strip_special_characters() 函数,它使您可以在 PL/SQL 程序语句中使用其他不受支持的格 式的字符(如换行符)。

在探究 UserView.php 表单中的 PHP 代码如何实现架构和设计之前,您应首先了解条带化视图的 工作方式。以下是 create_identity_db2.sql 脚本中提供的视图定义:

CREATE OR REPLACE VIEW authorized_user AS
  SELECT   su.system_user_id AS user_id
  ,        su.system_user_name AS user_name
  ,        cl.common_lookup_meaning AS user_privilege
  ,        CASE
             WHEN su.first_name IS NOT NULL AND  su.last_name IS NOT NULL
             THEN su.first_name||' '|| su.last_name
             ELSE NULL
           END AS employee_name
  ,        su.system_user_group_id AS group_id
  FROM     system_user su JOIN common_lookup cl
  ON       su.system_user_group_id = cl.common_lookup_id
  WHERE    TO_NUMBER(NVL(SYS_CONTEXT('userenv','client_info'),-1)) = 0
  OR       su.system_user_id = 
             TO_NUMBER(NVL(SYS_CONTEXT('userenv','client_info'),-1))
  ORDER BY CASE
             WHEN first_name IS NULL
             THEN 0
             ELSE 1
           END
  ,        su.last_name
  ,        su.first_name
  ,        su.system_user_id;

WHERE 子句要求解析对 SYS_CONTEXT() 函数的调用。如果函数无法返回一个值,则空值替代值为 -1,这被设计为不返回任何行。因此,仅当 Web 应用程序将用户定义的数据库会话元数据 CLIENT_INFO 列值设置为一个匹配值时,该条带化视图才会发挥作用。

return_users() 函数。return_users() 函数从条带化视图获取结果并设置结 果的显示格式。该函数在查询数据前调用 set_client_info() 函数。set_client_info() 函数通过 调用 DBMS_APPLICATION_INFO() 过程来设置服务器会话 CLIENT_INFO 值。这将视图限制为只显示与 最终用户权限一致的记录。

当活动用户在 Administration 组中时,该视图返回与所有 ACL 用户有关的 信息;当活动用户在 Employee 组中时,仅返回与该活动用户有关的信息。当您 实现自己的身份管理系统时,这显然一个为了保持示例的简短而进行的限制,同时也是一个改进的机 会。下面是完整的 return_user() 函数,供您参考:

function return_users($userid)
{
  // Attempt connection and evaluate password.
  if ($c = @oci_connect(SCHEMA,PASSWD,TNS_ID))
  {
    // Set the connection striping.
    set_client_info($c,$userid);

    // Return database UID.
    $s = oci_parse($c,"SELECT   user_id
                       ,        user_name
                       ,        user_privilege
                       ,        employee_name
                       FROM     authorized_user");

    // Execute the query, error handling should be added.
    if (@oci_execute($s,OCI_DEFAULT))
    {
      // Set a first row control variable.
      $no_row_fetched = true;

      // Initialize the return variable in case no rows are found.
      $out = '';

      // Check for a validated user, also known as a fetched row.
      while (oci_fetch($s))
      {
        if ($no_row_fetched)
        {
          $out .= '<tr>';
          $out .= '<td align="center"><b>User 

ID</b></td>';
          $out .= '<td align="center"><b>User 

Name</b></td>';
          $out .= '<td align="center"><b>User 

Privilege</b></td>';
          $out .= '<td align="center"><b>Employee 

Name</b></td>';
          $out .= '</tr>';
          $no_row_fetched = false;
        }

        $out .= '<tr>';

        for ($i = 1;$i <= oci_num_fields($s);$i++)
          if (!is_null(oci_result($s,$i)))
          {
            if ($i == 1)
              $out .= '<td align="right">'.oci_result

($s,$i).'</td>';
            else
              $out .= '<td>'.oci_result($s,$i).'</td>';
          }
          else
            $out .= '<td> </td>';
        $out .= '</tr>';
      }
    }
    else
    {
      // Set error message.
      set_error(__FUNCTION__,array('AUTHORIZED_USER'));
    }

    // Close the connection.
    oci_close($c);

    // Return formatted string.
    return $out;
  }
  else
  {
    $errorMessage = oci_error();
    print htmlentities($errorMessage['message'])."<br />";
  }
}

set_client_info() 函数。set_client_info() 函数在匿名 PL/SQL 块中调用 PL/SQL 过程。

// Strip special characters, like carriage or line returns and tabs.
function set_client_info($c,$userid)
{
  // Declare a PL/SQL execution command.
  $stmt = "BEGIN
             dbms_application_info.set_client_info(:userid);
           END;";

  // Strip special characters to avoid ORA-06550 and PLS-00103 errors.
  $stmt = strip_special_characters($stmt);

  // Parse a query through the connection.
  $s = oci_parse($c,$stmt);

  // Map the local variable to a bind variable.
  oci_bind_by_name($s,':userid',$userid);

  // Run the procedure.
  if (oci_execute($s))
    return true;
  else
    return false;
}

strip_special_characters() 函数。strip_special_characters() 函数删除 导致 PL/SQL 分析器出现问题的制表符和换行符。这使您可以使用空格和换行符来增强代码的可读性 。该函数为:

function strip_special_characters($str)
{
  $out = "";
  for ($i = 0;$i < strlen($str);$i++)
    if ((ord($str[$i]) != 9) && (ord($str[$i]) != 10) &&
        (ord($str[$i]) != 13))
      $out .= $str[$i];
  // Return pre-parsed SQL statement.
  return $out;
}

总结

现在,您已经了解了身份管理在虚拟专用数据库环境中针对模式的工作方式,以及如何在单个模 式中为 ACL 中的各个用户配置应用程序。代码示例和分析介绍了如何根据经过身份验证的用户的条 带化值有选择地进行操作。用户可以存储在驻留在单个模式中的 ACL 表中。现在,您只需针对会话 CLIENT_INFO 列值构建策略函数,即可将提供的解决方案扩展为一个 VPD 模型。


Michael McLaughlin 是《Oracle Database 10g Express Edition PHP Web Programming》一书的作者,并与他人合著了《Oracle Database 10g PL/SQL Programming 》和《Expert Oracle PL/SQL》(由 Oracle 出版社出版),他同时还是 Brigham Young University — Idaho 的计算机信息技术系教授。