添加链接
link之家
链接快照平台
  • 输入网页链接,自动生成快照
  • 标签化管理网页链接

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


作者:Michael McLaughlin

了解如何通过基于数据库的身份验证保护基于 PHP 的 Web 应用程序。

2007 年 5 月发布

管理对 Web 页面和应用程序的安全访问是一个常见问题。您希望允许那些受信任的用户访问数据,同时防止未经授权的用户获得数据的访问权。大多数情况下,基于数据库的身份验证是此类问题的解决方案。

身份验证系统包含一个访问控制列表 (ACL),该列表可列出用户凭证并将其匹配到指定的系统权限。凭证通常是一个用户名/口令对。凭证可以将用户链接到系统权限。系统权限允许帐户访问或修改数据,以及执行子系统或子例行程序。帐户可以是用户、组或系统。

在本文中,您将了解如何在基于 PHP 的 Web 应用程序中实现身份验证。还将了解如何设计和实现身份验证数据库模型,以及在基于浏览器的应用程序中计划和管理用户交互的所有方面。

在本文的 第 2 部分 中,您将探究数据库访问的不同方法,以及身份验证系统如何针对 Oracle 虚拟专用数据库 (VPD) 和内置的 DBMS_APPLICATION_INFO 程序包利用安全策略。

架构、身份验证和加密

架构。 当 Web 应用程序向 Apache HTTP 服务器请求信息时,会将信息从客户端发送到服务器。用户将该过程视为提交 URL 并收到 Web 页回复。URL 只是 URI 的公开部分;URI 包含 HTTP 头、Cookie 和一个 URL。信息作为 URI 中编码的名称/值对进行传送。

Cookie 是包含明文或密文的小型文本文件。Cookie 包含浏览器和服务器应用程序之间通信的当前事务状态。以前经常将 Cookie 的内容附加到 URI 以帮助维护事务状态。现在,单个会话 Cookie 将发送一个数值引用,以便将先前的名称/值 Cookie 对安全地存储在服务器上。该数值引用称为 Web 会话 ID。

使用 Web 应用程序会话时,会话 ID 和有关会话的过期信息作为特殊的会话 Cookie 发送。如果客户端浏览器禁止使用 Cookie,Web 应用程序可能会默认执行 URL 重写,从而将不公开的 URI 的会话 Cookie 重新定向到公开的 URL。通过将数据保留在服务器上并使用唯一会话 ID 进行存储,您可以获得两个好处 — 最小化 URL 的大小,以及确保用户交互的内部细节的安全。

示例代码清单

程序名 语言 说明
AddDbUser.php PHP 该程序文件对新用户以及返回的活动会话进行验证;并生成一个表单,以便将新用户输入 ACL。
create_identity_db1.sql SQL 该脚本构建数据库并填充必要行以初始化 Web 应用程序
Credentials1.inc PHP 其包含的文件定义了您在所有 oci_connect() 函数调用中使用的三个全局常量
SignOnDB.php PHP 该程序文件在调用 AddDbUser.php 程序之前设置 PHP 会话 ID
SignOnRealm.php PHP 该程序文件验证凭证然后将验证值呈现到静态 Web 页面

尽管将会话信息保留在服务器上更加安全,但您必须防止会话劫持。恶意用户可以通过以下方法劫持会话:(a) 窃取会话 Cookie,(b) 复制包括会话 ID 的 URL,或者 (c) 通过中间人攻击攫取会话。现代浏览器使得这些手段更加难以实现,并且 PHP 不会将物理会话 Cookie 写入文件系统。

Cookie 和会话架构的好处在于,如果您将数据存储在规范化数据模型中,则可以挖掘历史会话数据。通过这些数据,您可以了解客户如何使用您的 Web 应用程序,还可以了解客户的浏览模式和购买趋势。此类信息可让企业将营销目标对准个人而非群体。

身份验证。 您将使用两种身份验证模型之一在 Web 应用程序中验证用户身份。您可以在基本 HTTP/HTTPS 身份验证以及 Cookie 和会话身份验证模型之间进行选择。第三种身份验证模型(摘要 HTTP 身份验证)正在开发中,但可能几年内都无法提供完整的解决方案。

前两种身份验证模型可让您从 Web 页获得凭证,并根据 ACL 对其进行验证。无论浏览器是否接受 Cookie,它们都可让您正常工作。基本 HTTP/HTTPS 和摘要 HTTP 方法都针对领域进行验证(而不使用 Cookie),而会话管理至少需要写入一个会话 ID Cookie。

当您将会话 ID 作为 URL 的一部分发送时,URL 重写会带来一些安全风险,因为某些用户即时消息会将 URL 发送给其他人。这样,其他人就可以劫持授权,以访问或泄漏机密数据。URL 重写的替代方法是,将会话 ID 置于呈现的 Web 页面中作为隐藏域。尽管这会带来某些漏洞,但比将会话附加到 URL 的风险小。

PHP 验证脚本接收用户名和口令的名称/值对。然后,脚本将该名称/值对与 ACL 中存储的数据进行比较。在我们讨论的这两种身份验证模型中,这个过程都是相同的。尽管我们习惯以加密形式存储服务器端口令,但您应该知道,明文用户和口令值的任何泄漏都会危及系统的安全。

因此,您不应强迫用户创建复杂的口令,因为复杂的口令不容易被记住。用户会在记事本或黄色贴纸上记下这些难以记住的口令。书面记录的口令是 Web 应用程序的安全隐患,因为黑客可能会找到并滥用它们。

加密。 加密始终是 Web 应用程序开发、部署和管理的热门话题。

PHP Web 应用程序有两种加密方法。第一种涉及到加密协议。加密的协议(如 HTTPS)可以防止利用网络分析程序(通常称为数据包嗅探)进行的中间人攻击。第二种涉及到加密用户口令,以防止泄漏给服务器上的刺探程序。

如果加密的口令与源代码分开存储,则口令加密可以最好地保护您的系统免受攻击。如果 ACL 是文件系统上的一个文件,则一次服务器黑客攻击就会泄漏加密方法和加密的值。您可以将 ACL 存储在数据库中以降低加密口令的泄漏风险。

身份管理数据建模

构建身份管理数据模型可以很简单,也可以很复杂。最简单的模型是一个包含应用程序 ACL 的表。遗憾的是,该模型仅适用于基本 HTTP/HTTPS 身份验证。

实现 Cookie 和会话身份验证的最简单方法至少需要两个表。一个表包含 ACL,另一个表包含会话数据。下面描述了一个基本模型:



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

更有效的身份验证解决方案应该支持捕获和挖掘用户交互。这些不同于为单个事件(如鼠标单击)调用的 Web 页面列表。图 2 显示了一个可以捕获用户导航、成功登录以及失败登录的数据模型。在本文的示例中,该数据模型由 create_identity_db.sql 脚本创建(参见示例代码 zip 文件)。



图 2 详细的身份验证数据模型

较大的模型可让您根据对模块的运行时调用来跟踪已定义模块的版本。实际参数映射到目录项,如 Amazon.com 上的书籍。还添加了访问日志,以跟踪单个会话期间进行的多个连接。实现模型的具体细节视您的目标而异。图 2 还显示了一个支持目录订单应用程序中的版本控制的示例模型。目前,示例代码只能尝试捕获无效日志,ACCESS_LOG 表已重新命名,以反映与 INVALID_SESSION 表的差异。

身份验证过程模型

基本 HTTP/HTTPS 身份验证通过在领域中建立浏览器的凭证来实现。浏览器可以同时支持多个领域。领域在 XHTML 文档的头中设置。它们可以在表单中手动编写,也可以在 httpd.conf 文件的 AuthName 指令中设置。领域充当受保护服务器区域的卫士,可应用于一个或多个受保护的服务器区域。基本 HTTP/HTTPS 身份验证的一个优势在于,您不必编写登录或注销程序。浏览器提供登录表单,您通过关闭所有打开的浏览器窗口来注销。

基本 HTTP/HTTPS 身份验证方法的一个主要缺点是,您必须关闭所有的浏览器窗口才能停止活动的领域授权。这意味着,如果用户经过了验证,但在离开终端时浏览器仍然处于打开状态, 其他人就可以通过劫持身份来获取机密数据

Cookie 和会话身份验证通过根据 ACL 检查用户凭证来实现。如果 ACL 存储在一个文件中,则意味着需要读取该文件,并将用户名和口令的名称/值对与已提交的明文用户名和加密口令进行比较。

如果 ACL 存储在数据库中,则意味着最初登录和后续连接时需要执行不同的身份验证。最初登录时,您将连接到数据库,并从数据库表中读取用户名和加密口令值;然后,将结果与已提交的明文用户名和加密口令进行比较。执行后续 Web 请求时,您将连接到数据库,读取会话 ID,然后将结果与已提交的会话 ID 进行比较。与基本 HTTP/HTTPS 身份验证不同,在 Web 浏览器打开的情况下,您也可以成功注销。

尽管会话 ID 是潜在的安全风险(由于会话劫持),但通过合理的预防措施可将该风险降至最低。通过将登录页面实现为 PHP 脚本(而非简单的文本 XHTML 表单),可以消除最大的安全隐患。示例 Cookie 和会话代码演示了这个方法。它可以确保共享计算机上的第二个用户不会利用先前的凭证登录。实现细节在下面的“Cookie 和会话身份验证模型”部分中说明。

基本 HTTP/HTTPS 身份验证模型。 基本 HTTP/HTTPS 身份验证模型针对一个领域授权一次,仅当所有浏览器窗口都关闭时才撤销领域验证。图 3 显示了在浏览器上下文中表示模型的活动图。Microsoft Internet Explorer 在身份验证失败之前提供了三次尝试机会,但您只需刷新页面,即可再获得三次连续尝试机会。Firefox 浏览器则不同,它会一直提示,直到用户取消身份验证过程。



图 3 基本 HTTP/HTTPS 身份验证活动图

您通过输入登录表单的 URL 启动一个 Web 应用程序会话,然后该会话将发布到服务器并将一条消息发送回浏览器,要求它收集并发送用户凭证以进行确认。当您将凭证提交给 SignOnDB.php 程序后,代码会尝试验证您的登录凭证。如果凭证有效,您就可以使用领域中的任何 Web 页。如果身份验证失败,系统会通过浏览器登录表单重新提示,如前所述。

实现基本 HTTP/HTTPS 身份验证后,PHP 将在 $_SERVER 数组中使用三个预先定义的变量名。基本 HTTP/HTTPS 身份验证将用户 ID 和口令分别放在 $_SERVER['PHP_AUTH_USER'] 和 $_SERVER['PHP_AUTH_PW'] 中。如果未设置第三个变量名 HTTP_AUTHORIZATION,您需要将这两个 $_SERVER 值提交到身份验证函数。服务器端程序验证用户身份之后,对 $_SERVER 数组的 HTTP_AUTHORIZATION 名的后续读取都将允许浏览器访问领域,直到浏览器关闭。

在基本 HTTP/HTTPS 身份验证模型中保护页面的一个简单方法是包含一个布尔控制变量,该变量的值为 false,直到通过身份验证。仅当布尔控制变量变为 true 时才显示内容。您的代码应该检查身份验证,并在您没有在领域中经过身份验证时提示您输入凭证。 该代码应该存在于安全 Web 站点的所有页面上。

通过检查这些预定义的 $_SERVER 变量的值,可在服务器端脚本中触发 HTTP 头验证。对 SignOnRealm.php 页面的请求会将这些值放在返回的头中,这将指示浏览器调出凭证输入对话框。单击凭证的 OK 按钮将向服务器端脚本发送一个新请求。随后,服务器端脚本将尝试验证提交的凭证,除非用户取消该过程。如果用户取消该过程,这些领域登录脚本通常会返回一条失败消息。如果连续三次或四次身份验证失败,某些站点还会关闭用户帐户,因为他人可能通过已知用户名不断尝试入侵系统。

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

1. 以 SYSTEM 特权用户身份登录数据库,并运行以下命令:

SQL> CREATE USER IDMGMT1 IDENTIFIED BY IDMGMT1;
SQL> GRANT CONNECT, RESOURCE TO IDMGMT1;

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

SQL> CONNECT IDMGMT1/IDMGMT1@XE

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

SQL> @create_identity_db1.sql

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

 • SignOnRealm.php
 • SignOnDB.php
 • AddDbUser.php

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

测试基本 HTTP/HTTPS 身份验证。 可通过以下步骤测试领域身份管理:

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

http://localhost/SignOnRealm.php 

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

User Name Password
administrator welcome
guest guest



图 4 基本 HTTP/HTTPS 身份验证对话框

成功输入凭证后,您将看到以下页面,该页面返回了纯文本凭证和加密口令:



图 5 基本 HTTP/HTTPS 凭证验证确认

分析基本 HTTP/HTTPS 身份验证代码。 您输入用户名和口令之后,脚本将调用 verify_db_login() 函数。以下 PHP 代码演示了如何在领域中验证用户身份:

// Declare control variable.
$valid_user = false;
// Authenticate user.
if ((isset($_SERVER['PHP_AUTH_USER'])) && (isset($_SERVER['PHP_AUTH_PW'])))
  if (verify_db_login($_SERVER['PHP_AUTH_USER'],$_SERVER['PHP_AUTH_PW']))
    $valid_user = true;

该函数将打开一个 Oracle 数据库连接,并检查凭证是否有效。如果凭证有效,verify_db_login() 函数将返回 true;如果凭证无效,则返回 false。如果缺少 SYSTEM_USER 表,代码将要求您检查缺少的表。Web 页面的主代码显示在该函数中:

// Check for authorized account.
function verify_db_login($userid,$passwd)
  // Attempt connection.
  if ($c = @oci_connect(SCHEMA,PASSWD,TNS_ID))
    // Return a row.
    $s = oci_parse($c,"SELECT   NULL
                       FROM     system_user
                       WHERE    system_user_name = :userid
                       AND      system_user_password = :passwd
                       AND      SYSDATE BETWEEN start_date
                                        AND NVL(end_date,SYSDATE)");
    // Encrypt password.
    $newpassword = sha1($passwd);
    // Bind variables as strings.
    oci_bind_by_name($s,":userid",$userid);
    oci_bind_by_name($s,":passwd", $newpassword));
      // Execute the query.
      if (@oci_execute($s,OCI_DEFAULT))
        // Check for a validated user, also known as a fetched row.
        if (oci_fetch($s))
           return true;
          return false;
        // Print error when execution fails.
        $errorMessage = "Check for a missing SYSTEM_USER table.<br />";
        print $errorMessage;
    // Close connection.
    oci_close($c);
    $errorMessage = oci_error();
    print htmlentities($errorMessage['message'])."<br />";

对于 verify_db_function() 函数,需要注意两件事情。第一,查询返回一个空值,这将避免处理任何不必要的返回值。第二,明文口令使用 sha1() 函数加密,并指定给本地变量。

现在,您应该知道如何为基本 HTTP/HTTPS 身份验证构建所需的组件。与该身份验证模型相关的两个注意事项是:(1) 一旦您在浏览器中验证了凭证,即拥有完整的领域访问权;(2) 只能通过关闭 所有 浏览器窗口来注销。

Cookie 和会话模型。 Cookie 和会话模型很常用,因为您可以让用户登录和注销 Web 应用程序而无需关闭所有浏览器窗口。该身份验证模型也比基本 HTTP/HTTPS 模型更加复杂。可以通过多种方式实现 Cookie 和会话身份验证。

登录和注销操作在活动浏览器中进行。这种操作环境向 Cookie 和会话模型增加了较多的复杂性。例如,浏览器的后退、前进和刷新按钮需要在代码中进行特殊处理。刷新按钮不应尝试多次发送新用户身份验证。同样,用户注销应用程序之后,刷新按钮不应触发新的身份验证请求。遗憾的是,如果程序不阻止,它们就会这样做。

调用登录 PHP 脚本(而非 XHTML Web 页)是在 Web 应用程序中确保完整注销操作的最直接的方法。PHP 脚本可以重设会话值,而 XHTML Web 页则不能,因为在向目标页提交遗失的或新的凭证时,它会传递先前的会话值。

可以使用 PHP 中的几个预定义的变量来存储全局范围的变量。其中一个就是 $_SESSION 数组。它可让您添加名称-值对,其中名称是实际数据值的索引值。使用 $_SESSION 数组的好处是简单、灵活,但缺点是会导致与其他库的冲突。

session_start() 函数返回活动的 PHP 会话 ID 值。您必须先调用 session_start() 函数,然后再调用 session_regenerate_id() 函数,因为它实际上会调用 session_destroy() 函数。session_destroy() 依赖于 PHP 会话 ID 的存在,如果您调用之前不存在会话 ID,它将引发一个错误。调用具有实际 true 参数的 session_regenerate_id() 函数将重设 PHP 会话 ID。您使用运行时错误消除机制来确保,当浏览器上下文中不存在先前的会话时,调用 session_regenerate_id() 函数不会引发错误。

  // Start and regenerate session.
  session_start();
  @session_regenerate_id(true);
  $_SESSION['sessionid'] = session_id();

前面的脚本小程序在浏览器中更改了会话值,再次单击 Login 按钮将执行新的身份验证。为此,可以使用新的会话 ID 替代先前验证的 PHP 会话 ID。新的 PHP 会话 ID 将作为 URL 的一部分传送给下一个表单。

建立 PHP 会话之后,可以通过两种方法管理数据库连接:一种是在 PHP 模块和数据库之间使用持久连接,另一种是在这两者之间使用非持久连接。持久连接允许您启动一个可跨多个 Web 请求的事务范围,并且仅当您发出数据库提交命令时才结束。在持久连接的范围中,您可以运行锁定行未决更改或在未提交的更改之后的 SQL 和 PL/SQL 语句。非持久连接允许您运行 SQL 或 PL/SQL 语句,以便在打开和关闭连接期间查询或更改数据,该操作将在单个 Web 请求中发生。由单个连接限制的数据事务又称作自主事务,因为它们的持续时间仅限于连接的持续时间。

这些示例程序使用非持久性数据库连接对 Oracle 数据库进行查询和事务处理。因此,所有数据库事务都作为自主事务执行。

图 5 所示的 SignOnDB.php Web 页面在 XHTML 页面之前实现了这种类型的 PHP 脚本小程序。SignOnDB.php 表单中的 XHTML 呈现还作为 signOnForm() 函数存在于 AddDbUser.php 页面中。

您将在用户登入应用程序之后调用的所有 Web 页中放置 PHP 身份验证逻辑。目前,所有编程逻辑都位于 AddDbUser.php 页面中(如图 6 所示),但您可以将 12 个函数移到一个身份验证库中。

您的浏览器必须接受 Cookie,这些程序才能工作。如果禁用了 Cookie,您所能做的所有事情就是登录、注销并且无法输入新用户,因为这两个页面间的会话值已丢失。

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

1. 使用以下 URL 打开会话身份管理示例,假设您已经将代码放在 htdocs 目录中:

http://localhost/SignOnDB.php 

2. 可以使用基本 HTTP/HTTPS 身份验证中提供的帐户:



图 6 Cookie 和会话登录页面

3. 现在,您可以将新用户输入到 New User 表单中的凭证库,要求用户名是一个以字母开头、长度为 6-10 个字符的字符串:



图 7 Cookie 和会话的 Add New User 页面

4. 添加新用户之后,AddDBUser.php 脚本将显示 New User 表单的新副本。它将确认您是否成功添加了新用户,或者说明输入内容违反了哪条规则。您可以连接到 IDMGMT1 模式,并运行以下查询以检查新用户输入:

SELECT system_user_name, system_user_password FROM system_user; 

分析会话身份验证代码

PHP 程序的逻辑考虑了前进、后退和刷新浏览器按钮的典型细微差别。图 7 显示了所呈现的 Web 页面。当您使用浏览器导航按钮调用或刷新表单时,程序将确定 (a) 当前用户的会话是否已在数据库模型中注册,(b) 远程地址与验证时的客户端 IP 地址是否相同,以及 (c) 会话是否是当前的。如果会话的最近一次相关数据库活动在最近五分钟内发生,则会话就是当前的。

在程序逻辑查找新用户凭证并尝试验证用户之前,最初的登录表单和添加用户表单均会检查并验证已注册的会话。如果凭证经过身份验证,SignOnDB.php 脚本就会注册新会话并调用 AddDbUser.php 脚本。刷新 AddDbUser.php 脚本会调用 signOnForm() 函数(可模拟 SignOn.php 表单的行为)。如果 AddDbUser.php 脚本验证了凭证的身份,就会使用 addUserForm() 呈现一个页面;如果任一脚本拒绝了凭证,就会记录一个失败的登录尝试,并呈现一个新的登录页面。



图 8 Cookie 和会话身份验证活动图

Web 页面包含两个 XHTML 表单。一个表单可让您输入新用户,验证凭证是否符合规范(例如,以字母开头、长度为 6-10 个字符的字符串),然后将凭证插入数据库。另一个表单可让您通过调用 SignOnDB.php 脚本注销应用程序,这可重设会话,以防止在用户注销之后该会话在数据库中显示为已经过验证。

Web 页面将启动会话并将其指定给 $_SESSION 数组,将任何已提交的凭证指定给本地变量,然后查看当前验证的会话是否可用。这可通过以下代码实现:

  // Check for valid session and regenerate when session is invalid.
  if ((get_session($_SESSION['sessionid'],$userid,$passwd) == 0) ||
      (($_SESSION['userid'] != $userid) && ($userid)))
    // Regenerate session ID.
    session_regenerate_id(true);
    $_SESSION['sessionid'] = session_id();
    $authenticated = true;

get_session() 函数将打开一个数据库连接,并从 SYSTEM_USER 和 SYSTEM_SESSION 表的集合中查询结果集。查询结果可确定会话是否经过验证。该验证过程中有一部分代码可检查以确保请求源自同一个 IP 地址。经过身份验证后,将查询返回的值指定给 Web 页变量,以处理页面请求。同时,update_session() 函数会更新 LAST_UPDATE_DATE 列时间戳。如果身份验证失败,record_session() 函数会将该尝试记录到数据库中的 OBSOLETE_SESSION 表。

// Get a valid 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.
      // The Oracle DATE data type is a timestamp where .003472222 is
      // equal to 5 minutes.
      $s = oci_parse($c,"SELECT   su.system_user_name
                         ,        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, error handling should be added.
      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');
          // 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');           }
             // Log attempted entry.
             record_session($c,$sessionid);
             return 0;
          // Record when not first login.
          if (!isset($userid) && !isset($passwd))
            record_session($c,$sessionid);
          // Return a zero.
          return 0;
        // Print error when oci_execute() fails.
        $errorMessage = "Check for a missing SYSTEM_USER or ";
        $errorMessage .= "SYSTEM_SESSION tables.<br />";
        print $errorMessage;
        return 0;
      // Close the connection.
      oci_close($c);
      $errorMessage = oci_error();
      print htmlentities($errorMessage['message'])."<br />";
      return 0;

您应该注意到,从 get_session() 函数调用的函数共享打开的数据库连接以确认现有会话。该目标可通过将连接作为实际参数传递给其他两个函数来完成。在单个数据库连接中管理数据库事务范围可让您将多个 SQL 和 PL/SQL 语句作为一个事务的一部分进行管理。这允许函数包含数据抽象层的单个对象,并允许您将嵌套的函数调用作为子例程来执行。



图 9 Cookie 和会话的 Add New User Bad Credential 页面

Web 页面中主代码的下一个部分将呈现不同版本的表单,因为它将用于多个目的。图 6 显示的是在最初登录到应用程序时呈现的表单。图 9 显示的是在尝试输入空用户名之后呈现的表单。如果可以在 SYSTEM_USER 表的 ACL 中添加新用户,代码就会调用 create_new_db_user() 函数,如下所示:

  // Check whether the program should:
  // -----------------------------------------------------------------
  //  Action #1: Verify new credentials and start a database session.
  //  Action #2: Continue a session on refresh button.
  //  Action #3: Provide a new form after adding a user.
  //  Action #4: Provide a new form after failing to add a user.
  // -----------------------------------------------------------------
  if (($authenticated) || (authenticate($userid,$passwd)))
    // Assign inputs to variables.
    $newuserid = @$_POST['newuserid'];
    $newpasswd = @$_POST['newpasswd'];
    // Set message and write new credentials.
    if ((isset($newuserid)) && (isset($newpasswd)) &&
        (($code = verify_credentials($newuserid,$newpasswd)) !== 0))
      // Render empty form with error message from prior attempt.
      addUserForm(array("code"=>$code
                       ,"form"=>"AddDbUser.php"
                       ,"userid"=>$newuserid));
      // Create new user only when authenticated.
      if (!(isset($userid)) && (isset($_SESSION['userid'])))
       create_new_db_user($_SESSION['db_userid'],$newuserid,$newpasswd);
      // Render fresh empty form.
      addUserForm(array("form"=>"AddDbUser.php"));
    // Destroy the session and force re-authentication.
    session_destroy();
    // Redirect to the login form.
    signOnForm();

确定已设置 $newuserid 和 $newpasswd 变量之后,嵌套的 if 语句过滤器将调用 create_new_db_user() 函数。这两个变量只能在 AddDbUser.php Web 页面中设置,这意味着,只有在用户经过身份验证之后,才能调用这两个变量。这也适用于对 AddDbUser.php Web 页的后续调用,因为单击 Add User 按钮会递归地调用同一个 Web 页面。

  // Add a new user to the authorized control list.
  function create_new_db_user($userid,$newuserid,$newpasswd)
    // 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,$newuserid))
        // Encrypt password.
        $newpassword = sha1($passwd);
        // Return database UID.
        $s = oci_parse($c,"INSERT INTO system_user
                           ( system_user_id
                           , system_user_name
                           , system_user_password
                           , system_user_group_id
                           , system_user_type
                           , created_by
                           , creation_date
                           , last_updated_by
                           , last_update_date )
                           VALUES
                           ( system_user_s1.nextval
                           , :newuserid
                           , :newpasswd
                           , :userid1
                           , SYSDATE
                           , :userid2
                           , SYSDATE)");
        // Bind the variables as strings.
        oci_bind_by_name($s,":newuserid",$newuserid);
        oci_bind_by_name($s,":newpasswd", $newpassword);
        oci_bind_by_name($s,":userid1",$userid);
        oci_bind_by_name($s,":userid2",$userid);
        // Execute the query, error handling should be added.
        if (!@oci_execute($s,OCI_COMMIT_ON_SUCCESS))
          // Print error when oci_execute() fails.
          $errorMessage = "Check for a missing SYSTEM_USER table.<br />";
          print $errorMessage;
      // Close the connection.
      oci_close($c);
      $errorMessage = oci_error();
      print htmlentities($errorMessage['message'])."<br />";

在 create_new_db_user() 函数中,将调用 is_inserted() 函数。这将进行检查,以确保在尝试将用户插入 SYSTEM_USER 表之前该用户不存在。与先前的嵌套函数示例一样,is_inserted() 函数将共享本地连接以便控制事务。此外,在将用户口令绑定到数据操作变量之前,sha1() 函数会将明文口令转换为加密字符串。

成功插入新用户之后, New User 表单会再次呈现,等待您插入其他用户或者单击 Log Out 按钮。 Log Out 按钮会将您返回到登录屏幕,在该屏幕中,您可以重置会话标识符。

总结

现在,您已经了解了身份管理的工作方式,以及如何实现基本身份管理解决方案,您应该已经熟悉术语、架构以及验证用户身份的方法。

您现在可以平等地管理用户验证和访问,但用户不是平等的。某些用户可能拥有不受限制的访问权,但大部分用户都拥有受限制的访问权。本文的第 2 部分将介绍如何将身份管理与支持细粒度访问控制的两项技术相结合。

尽管 VPD 功能是“高级功能”,但早期技术 DBMS_APPLICATION_INFO 也可用于 Oracle8 i 、Oracle9 i 和 Oracle Database 10 g 第 1 版。它还是支持 Oracle E-Business 11i Suite 身份验证的核心实用程序。这两项技术都可让您实现细粒度的访问权限和角色。

继续阅读 第 2 部分


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 的计算机信息技术系教授。