Oracle-和-Java-安全专家级教程-全-

Oracle 和 Java 安全专家级教程(全)

原文:Expert Oracle and Java Security

协议:CC BY-NC-SA 4.0

零、简介

每天,我都会阅读有关计算机安全攻击和漏洞以及随之而来的影响的电子邮件和文章。几十年来,我一直在从事计算机安全方面的工作,这方面的努力看不到尽头。信息技术领域的每一个专业人员都有义务保证计算机安全。我们大多数人都签署了计算行为准则,而且我们还与那些保护我们各自公司的计算资源的人结盟。

当然,计算机安全专业人员站在第一线,他们将系统和网络工程师以及管理员也卷入了这场争论。希望软件系统管理员——数据库管理员和 web 服务管理员——也能加入这些士兵。每天专门负责保护公司资源的人员的极限是多少?

应用员需要加入这场战斗。程序员不能盲目地依靠那些一线战斗人员的工作来保护他们的工作和职业生涯。在 IT 专业人员的队伍中,没有平民——我们都在战斗。

我说得好像这是一场战争。我认为我们是在应对所谓的网络战吗?不,不是真的。人们不会死去。但是这场战争已经变成了经济战争,威胁到我们每个人的财富和经济安全。这更像是盗窃,我们更像是警卫和警察。

你是公司计算机资源和数据的守卫,你需要履行这一职责。我的意图是给你提供工具和知识,你可以立即使用。但更重要的是,我想让你思考如何编写自己的防弹应用。你的需求将与本书中的有所不同,所以你要提出自己的防御措施。我希望,读完这本书并磨练你的技能后,你的武器库会大大加强。

在第一部分的第二章到第四章中,我们将为 Java 和 Oracle 的安全编程以及它们的共同点 Java 存储过程(JSP)打下基础。这不是对这些广阔领域的百科全书式的覆盖。相反,我关注的是涉及“安全”编程这一特殊学科的特定主题。

在第二部分的第五章到第七章中,我们只使用了几组协议,探索了几种不同的加密方法。同样,这不是百科全书。但是在阅读完这些章节后,您将准备好在您的应用中加密数据,以及在客户机-服务器应用中通过网络加密数据。您会发现,通过在 Oracle 数据库和客户端计算机上运行 Java,我们紧密耦合的客户端-服务器应用可以使用防弹的、特定于会话的加密来传输敏感数据。

在第三部分第八章到第十章中,我们将扩展我们的工具集,以包括单点登录、双因素身份验证和应用验证。这些做法将阻止入侵者访问我们的应用,并阻止未经授权的应用和用户访问我们的 Oracle 凭据和数据。单点登录还有一个额外的好处,就是减少了用户输入用户 ID 和密码的次数。另一方面,双因素身份验证要求用户拥有特定的移动电话或注册设备来接收授权她访问应用的代码。应用验证有助于确保应用获得授权访问,并允许我们将应用分配给用户。

在第六部分的第 11 和 12 章中,我们增强了我们一直在构建的安全性,然后构建了一个管理界面来使其运行并保持运行。我们将添加的安全增强功能包括特定于应用的磁盘加密,以及使用强化的 Oracle 数据库实例进行应用验证。在管理界面中,我们将讨论如何维护安全数据,并且我们还将了解一些在 Java 中开发 GUI 应用的良好实践。

在书中的几个地方,我们将准备一个模板,我们可以提供给其他应用程序员,以便他们可以实现相同的安全结构。在 GUI 应用中实现这种安全性的最简单和最完整的模板在第十二章——登录类中介绍。当你读到这一章的结尾时,你将成为专家。你可以为计算机安全尽一份力,也可以帮助你的同事做好他们的工作。谢谢大家!

一、引言

这本书介绍了 Oracle 和 Java 技术。我将在这些页面上编织 Oracle 数据库和 Java 安全的故事。我们要编织的特殊的线是代码。这是程序员的故事。

本案例将带您完成几项大型任务,帮助您开始保护 Oracle 应用。我们不会构建任何特定的应用,但会关注构建应用的安全性方面。为了使这种学习感觉像一种实际应用,我们将把我们的努力应用到 Oracle Database 11g 安装中可用的 HR 示例模式。

我希望保持对话的语气,因为我想教授安全编程的概念。我们将讨论安全编程。当你读完这本书的时候,你将为当今最困难的应用员任务做好准备:保护我们的应用和计算机。

要求

为了完成本书中的示例,您需要安装 Oracle Database 11g 企业版 11.2 版或更高版本。您还需要在工作站上安装 Java 开发工具包 1.5 或更高版本。就是这样。

我将在后面提到一些其他产品,但本书的目的是涵盖对 Oracle 和 Java 程序员有用的主题和方法,而不是致力于任何其他产品。但是,您应该注意到,我们在这里要做的很多事情都可以通过 Oracle corporation 和其他地方的商业产品获得。

值得注意的是,我们将在本书中构建的两个特性——网络上的数据加密和磁盘上的数据加密——在 Oracle 的一款名为 Oracle Advanced Security 的产品中均有提供。该产品相对容易配置和使用,尽管价格昂贵。但是,在一个安全性很差的数据库或应用上添加加密只会隐藏您作为一名程序员应该解决的问题。因此,即使您使用 Oracle Advanced Security,也应该学习如何用 Java 编写安全的 Oracle 数据库应用。

适用于 Windows 和 UNIX/Linux 用户

完成本书中的任务并不要求您是 Windows 用户。所有代码都是用 PL/SQL 和 Java 编写的,可以跨平台运行。然而,我不得不在这种材料的开发上有点专注。文件系统目录、命令提示符、环境设置、服务、脚本和进程的所有描述都是以 Microsoft Windows 为模型编写的。

为了避免出现强烈的 Windows 偏见,让我提醒您,我们在这里讨论的是 Java 和 Oracle。我使用 UNIX 的时间比 Windows 还长。直到 Windows 3.1,小号 winsock tcp/ip 栈问世,我才开始使用 Windows。我作为一名系统管理员开始使用 UNIX 和 SunOS 4.1 以及 Netware 服务器。然后,我做了几年商业 UNIX 系统管理员。我运行了几十台机器和半打以上的 UNIX 版本。我用很大的毅力集中管理了这些,并通过预定的 cron 更新彻底应用了 NFS 上的脚本和 Perl,并且了解了 BSD、System 5 和 AIX 风格的独特属性。我将它与标准化结合起来——例如 Korn shell(或 mksh)和 lprNG。

那么为什么不为 UNIX/Linux 用户写这本书呢?首先,你们可能已经知道如何在你们的 Linux 系统上用 Windows 语言来做我所说的事情。其次,我可以谈论 Windows,但很少提到 Windows 7 和 Windows XP 之间的区别。我不可能如此清晰地谈论 UNIX 和 Linux。UNIX 不是操作系统;它是许多操作系统的基础。OSX 也是如此。

此外,在 UNIX 中不仅有一个命令提示符,还有许多 shells。不是只有一种 UNIX 批处理/命令语言,而是有无数的脚本选项。

背景

那么,为什么要编写或研究可以购买到的现成代码呢?作为一名程序员,我相信了解我的计算机和服务器上的程序如何工作的重要性,即使这些程序不是我写的。我认为所有程序员天生都喜欢在代码中看到新功能;这是教育和授权。我写这本书的目的是让读者理解,也许还内置了职业发展的衡量标准。

如果您的目标是使用基本的 Oracle 数据库和 Java 服务实现应用安全性,而不需要花费数万美元,本书将为您提供实现这一目标的基础。您可以用 Java 编写安全的 Oracle 应用!

我会保持事情的清晰和简洁,所以我不会弄乱信息。这本书也是 Oracle 数据库和 Java 安全性的入门书(我喜欢这个词,不管你怎么读它)。我不打算假设太多的预知,但如果有些术语是陌生的,我建议快速互联网搜索澄清。此外,我非常感谢 Oracle 文档库和 Sun(Oracle)的 Java 文档,我建议您将这些资源的链接放在手边,以便经常参考。可以在以下位置找到它们:

??

如何使用这本书

你需要参考源代码。你可以从头到尾阅读这本书,理解一切,成为你应该成为的专家。但是你有其他的责任和分心。所以你需要离开,然后回来,不时地重新定位。这本书将假设你从头到尾阅读,并完全记住,但我从来没有遇到过这样做的人。

我更愿意假设你是凡人,像我一样。你会问自己这样的问题,“现在,我在哪里,我们在做什么?”此外,即使你愿意相信我们正在按照逻辑顺序进行,并且实际上正在取得进展,即使你可能有点迷失方向,你仍然会有问题,“这有什么关系?”

这些问题可以很容易地通过快速参考代码来回答。您将希望能够在代码中搜索您正在阅读的关键字,找到有问题的代码,并找到对它的引用。您将希望在上下文中看到代码,并且您将希望看到代码是如何开始和运行的。现在,我已经在本文中加入了大大小小的代码块,我不会在没有向你们展示的情况下谈论代码,但是你们会想在上下文中看到它。

我组织了文本和代码,以便轻松地携手同行。在大多数情况下,当您通读文本时,一次只需要打开一个源代码文件。在接下来的第二章中,您将作为几个不同的用户完成 Oracle 数据库安全任务——对于每个用户,都有一个相应的源代码文件。这一章是围绕这些文件组织的。事实上,如果您也打开一个 SQL 客户端并执行所讨论的代码的每一部分,那将是最好的。

本书的组织结构

这本书有四个主要部分。在第一部分(第二章–第四章)中,您将学习 Oracle 数据库安全编程、Java 安全编程和 Java 存储过程(运行在 Oracle 数据库引擎中的 Java)的技术细节。从 Oracle PL/SQL 编程新手和 Java 编程新手到使用 Java 的经验丰富的 Oracle 应用编程人员,这些章节将填补您为安全起见需要掌握的缺失概念。

在第二部分(第五章–第七章)中,我们构建了 Java 和 Oracle 数据库安全编程的基础。我们开发了强大的加密层,特别是传输中的加密。您将使用 Java Cryptography Extension (JCE)而不是购买的软件包或 SSL 服务为自己构建这个。因为加密在当今的安全编程中扮演着如此关键的角色,所以您需要牢固地掌握这些数据加密概念,并且您还需要更多的东西:您需要知道何时应用加密以及如何评估您的加密策略以确定您是否成功地保护了您的数据。我们会涵盖所有这些。

在第三部分(第八章–第十章)中,我们将探讨一些有趣的话题,这些话题将允许您提供贵公司正在寻求的解决方案:单点登录、双因素身份验证,以及您可能还没有意识到自己需要的东西:应用识别和授权。在应用授权方面,我们将为我们的应用密码提供一个安全的数据存储,它不仅可以避免嵌入密码,还可以提供增强的安全性以及易于管理和集中分发。

在第四部分的最后一节(第十一章–第十二章),我们将通过 Oracle 数据库存储中的额外数据加密、进一步强化的 Oracle 数据库以及一些额外的编程工作(如混淆)来强化安全性。我们还建立了一个简单的用户界面,用于管理我们为实现安全性而构建的表和数据,并且安全地做到这一点。

Java 对象和 Oracle 数据库结构

在本书中,我将使用某些在别处可以互换使用的不可互换的术语。Oracle 数据库中的模式、表、索引和其他项目通常被称为“??”对象“??”和“??”结构“??”。因为对象是 Java 中的一个技术术语,而不仅仅是事物的另一个词,所以我将这个词保留到我们讨论 Java 对象的时候。在 Oracle 数据库中,我通过它们的主要类型名来调用事物,并统称为结构,因此表将同时被称为结构。我还需要引用 Oracle 表、视图、索引等的集合,我将它们统称为结构。我们稍后将定义所有这些术语,所以如果它们对您来说是新的,请不要惊慌。

章节回顾

我真诚地希望你能喜欢这本书。我相信学习和使用这些材料的最好方法是花时间阅读每一章,当你读到它时执行代码。您将使用 Java 构建一个安全的 Oracle 应用环境。一定要测试我给你提供的所有地方的测试用例、场景和测试代码。如果您发现其他情况,也测试这些情况,当我需要解决问题时,请将您的意见和结果发送给我——我的目标是为您服务。

二、Oracle 数据库安全性

正如您可能想象的那样,如果我试图涵盖 Oracle 安全性的整个范围,本章的主题可以写满一整本书,甚至好几本书。相反,我将介绍一些要点,以及程序员可能会涉及到的 Oracle 安全性的一些特定方面。Oracle 基本安全性的一个例子是使用角色并向这些角色授予权限。程序员扩展 Oracle 安全性的一个例子是安全应用角色。

本章的基本目标如下:

  • 创建两个用户:一个安全管理员用户和一个应用安全用户。这些用户将被授予实现应用安全性的特权和委派责任。
  • 使用 Oracle 角色来控制访问和增强应用安全性,并了解由 Oracle 存储过程标识的安全应用角色。
  • 区分 EMPLOYEES 表中的敏感数据和非敏感数据。

准备钻研数据库管理和设计的许多方面。本章将帮助您了解 Oracle 安全性,并且我们将在本书的其余部分中以这里介绍的概念为基础。有些方面,比如代理连接,只有在上下文中呈现时才会真正清楚。我想确定你真的理解了书中的内容,而不仅仅是接触了一下。

寻找测试 Oracle 数据库

我们将立即投入工作。我希望您已经可以访问数据库中的 SYS 帐户。如果没有,希望你是服务器的数据库管理员(DBA)的好朋友,希望你是公认的数据库安全管理员。如果是这样的话,您可以让您的 DBA 朋友来完成必须由 SYS 或 DBA 完成的一小部分工作。

如果您想学习本章中的概念,但不想使用托管服务器,并且您的个人工作站有足够的计算机能力,那么您可以下载并安装 Oracle Database 11g Enterprise Edition server,并在本地执行我在本章中描述的任务。在任何情况下,我都强烈推荐这种方法,因为在将它们投入生产之前,您应该明确地演示和测试我在本章中描述的安全措施,以使您安心。

从现有的 Oracle 数据库工作

如果您使用现有的数据库安装,可能会有一些问题需要解决。如果您有一个默认的 Oracle 数据库 11.2 安装,那么您只需考虑安装后采取的步骤,看看您是否撤消了任何内置/默认的安全性。至少,您需要确保密码的复杂性和保密性。但是,如果您的数据库已经存在了一段时间,并且已经从以前的 Oracle 版本升级,那么您可能需要花费一些时间和精力来纠正安全问题。

我建议您采用 Oracle Technology Network 的 Arup Nanda 提供的资源,名为 Project Lockdown。项目锁定是一系列清单、任务和项目,将有效地启用和实施 Oracle 数据库安全性。您可以在 Oracle 技术网络网站上找到该资源,网址为www . Oracle . com/tech Network/articles/project-lockdown-133916 . pdf

项目锁定可能需要几个月的时间来完成,这取决于您当前的安全立场有多松懈。然而,最关键的前两个阶段可能会在一周内完成。

Oracle 用户和模式

一旦 Oracle Database 11g 启动并运行,您将需要考虑用户和用户安全性,甚至在考虑数据之前,因为数据是由用户创建的。即使不属于任何特定人员的应用数据库也与 Oracle 数据库中的用户相关联。本地数据库上的每个用户都有一个关联的模式,它基本上是 Oracle 结构(或对象)的一个有组织的存储分配,比如属于用户的表和索引。我们将讨论的用户列表见表 2-1 。

image

image

SQL*Plus、SQL Developer、JDeveloper 或 TOAD

我假设,如果您正在阅读本文,您已经作为一名 Oracle 开发人员做了一些工作。我将进一步假设您有一些用于在 Oracle 数据库上提交命令的工具,例如 SQLPlus 中的提示符或 TOAD、SQL Developer 或 JDeveloper 中的编辑器。SQLPlus 是 Oracle 提供的,所以如果您没有其他工具,您仍然可以使用 SQL*Plus。这就是完成本书概述的任务所需的全部内容。

images 注意这些工具中有些对多行命令更敏感。它们可能需要在多行命令后面的一行上有一个斜杠字符(/)。

本书源代码中包含的许多 SQL 脚本文件都需要编辑特定的值,以适合您的计算机、公司或家庭环境。这些值列在 SQL 文件的顶部。经过适当的编辑后,大多数 SQL 文件可以作为脚本执行;请确保您是以适当的 Oracle 用户身份连接的。

接下来几节的组织

我想让你尽可能容易地理解这些材料。为此,我将三个用户的任务和关注点分成几个部分:SYS(数据库管理员),我们将在这里创建的安全管理员,以及HR(人力资源)模式所有者。

在大多数情况下,数据库管理员可以完成所有这些任务,但是我的目的是通过授权来展示安全性。我们将让这些用户中的每一个完成特定于其委派职责的任务。每个用户要执行的命令被合并到每个用户的一个脚本文件中,这样我们就可以依次关注每个用户的任务和关注点,一次一个。

这些用户中的每一个都必须解决许多不同的主题,所有这些主题都与安全性相关。当我们朝着本文的目标努力的时候,这些关心的问题按照它们被需要的顺序被处理。我们将继续构建我在本书中介绍的所有主题。

在我们学习这些材料的过程中,你应该可以接触到代码。如果你正在远离你的计算机阅读这篇文章,我希望已经在文本中提供了足够的代码让你完全理解讨论。然而,这也是我的意图,你执行这个代码,并且这个文本将指导你理解在每一个结合点发生了什么。

当您处理代码时,您会发现执行各种脚本文件中的命令的最佳方式是将文件内容复制到 SQL 命令编辑器(如 TOAD 或 JDeveloper ),并一次执行一个命令。这优于将整个代码作为脚本执行,因为您将在执行时看到每个命令。记住,目标是理解安全编程,而不仅仅是实现安全软件。

作为 SYS 用户工作

有一个模式(同名),并且有许多属于他的帐户的结构。您可以以SYS的身份连接(登录)到数据库,但是该用户的一个独特要求是您必须指定您正在使用SYSDBA特权。(请注意,在本文中,我将对 Oracle 命令中的保留字以及默认的 Oracle 用户、模式和结构全部使用大写。)

CONNECT sys AS sysdba;

此时,您必须输入与SYS用户相关的密码。

images 注意你可以在名为 Chapter2/Sys.sql 的文件中找到以下命令的脚本。

系统权限

SYSDBA是独特的系统特权;姑且称之为“超级”系统特权。它提供了几乎无限的管理能力。它可以授予任何用户,但最好只授予SYS。作为SYS你可以查看谁有SYSDBA:

SELECT * FROM sys.v$pwfile_users;

在这个查询中,您请求从V$PWFILE_USERS视图中SELECT(参见)所有列的数据(*)。系统。视图名称前缀表示拥有该视图的模式(用户)。您将看到在这个查询中列出了几个其他的超级系统特权:SYSOPERSYSASM。这些也应该只授予SYS

除了超级系统特权之外,还可以授予几十种其他系统特权,以提供诸如创建其他用户和授予他们特权之类的管理能力。我们将在探索 Oracle 安全性的过程中授予特权。

除了系统特权之外,还有模式对象特权。例如,这些允许另一个用户读取你的数据。我们也将授予这些特权。它们对 Oracle 应用安全性的影响更具针对性。

角色

角色是授予权限的集合,比超级系统权限更精细、更多样、更有限。与授予SYSDBA不同,授予用户访问各种权限的首选方法是授予角色权限,然后授予用户角色。如果这样做,您总是可以替换作业中的不同用户,或者通过简单地将现有角色授予新用户,将作业所需的权限复制给另一个用户。(特权可以直接授予用户,但我们通常会避免这样做。)

例如,我可以创建一个名为appaccess的角色,并将读取应用表的能力授予该角色。然后,我可以将该角色授予用户。当我有另一个用户需要访问同一个应用时,我可以通过授予这个新用户appaccess角色来允许。当我删除第一个用户时,其他用户访问应用所需的权限不会在这个过程中消失。

如前所述,我需要授予对应用数据的访问权限,因为数据表将位于属于应用的模式中,并且每个用户都有自己的模式。用户不能读取其他模式中的数据,除非专门授权给他们或一个名为PUBLIC的特殊用户。授予PUBLIC的任何东西都授予所有用户。参见表 2-2 了解我们将讨论的 Oracle 角色列表。

image

DBA 角色

DBA角色以它所支持的工作命名:数据库管理员。它类似于SYSDBA超级系统特权。传统上,DBA角色被授予那些需要管理数据库的用户。DBA角色几乎和SYSDBA权限一样强大,但是可以对其进行修改,删除一些权限。

在 Oracle 的最新版本中,不鼓励数据库管理员使用 DBA 角色。相反,我们鼓励他们创建自己的角色,并授予所需的管理权限。因此,我们不会将DBA角色授予任何用户。

我们将创建一个安全管理员角色secadm_role。我们将在大多数管理操作中使用该角色。它将拥有各种特权,但仅限于本书范围所需的特权。这种方法遵循“最小特权”的概念,这意味着只提供手头任务所需的特权。

Oracle Database Vault 是一款允许您使用DBA和其他特权角色,同时限制其访问权限的产品。这是面向国防部和国家安全用户的,在这些用户中,数据库管理员不一定有权访问数据。

创建-会话角色

我们还将创建一个简单的角色create_session_role,它只有一个特权:CREATE SESSION。用户需要有CREATE SESSION权限才能连接到 Oracle 数据库。传统上,这是通过名为CONNECT的预定义角色(在安装 Oracle 时存在)来完成的。在 Oracle 数据库的当前版本中,CONNECT只有一个特权。但是在这种情况下,与DBA角色一样,Oracle 建议管理员创建自己的角色,而不要依赖预定义的角色(如CONNECT)。按照SYS的方式操作:

CREATE ROLE create_session_role NOT IDENTIFIED; GRANT CREATE SESSION TO create_session_role;

我们将把这个角色授予数据库中的所有用户。向每个用户授予CREATE SESSION特权所需的工作量与授予create_session_role所需的工作量完全相同。然而,角色作为特权的集中有一个好处。例如,如果我们想快速地将另一个特权授予所有用户,我们可以将它授予create_session_role;虽然,我不建议那样。更有可能的情况是,您需要保持数据库运行,但希望阻止所有用户连接到它。这可以通过一个命令来完成,从create_session_role中撤销CREATE_SESSION

未识别

我希望CREATE ROLE语法相当明显。唯一奇怪的特性是NOT IDENTIFIED关键字。这仅仅表明,当用户获得这个角色时,我们没有任何密码或编码过程来验证他对它的访问。这种类型的角色必须通过管理命令授予用户(或另一个角色)。通常,被指定为NOT IDENTIFIED的角色也是默认角色,当用户连接到 Oracle 数据库时会自动获得这些角色。这是最常见的角色配置。

使用角色

大多数角色被创建为默认角色。当用户连接到 Oracle 数据库时,他将获得所有默认角色,以及与这些角色相关的所有权限。任何常规NOT IDENTIFIED角色都可以设置为默认或非默认。

images 注意在 Oracle Database 11g 中,当用户首次连接时,不再获取由密码标识的默认角色,用户必须输入密码。稍后会详细介绍。

Oracle 用户可以随时通过执行 SET ROLE 命令获得已授予的角色。对于默认角色和非默认角色都是如此。

当我们将角色设置为一个新角色时,它将成为唯一正在使用的角色,即使其他角色之前也在使用。幸运的是,会话已经被创建了(当新角色被设置时,我们已经被连接了),所以我们可以忍受丢失create_session_role(这将会发生)。我可以想象一个场景,其中我们要求每个角色都被授予CREATE SESSION特权,这样当你SET ROLE时,你在新角色中仍然有CREATE SESSION

有一种方法可以添加到现有的角色中,并保留现有的角色。通过发出以下命令:

SET ROLE ALL;

该命令不能用于设置由密码或程序设定的IDENTIFIED角色。然而,在您获得一个受密码保护的角色或安全应用角色(我们将在一分钟内讨论这些)之后,您也可以SET ROLE ALL并恢复您的默认角色。

我应该提到,角色可以被授予角色,我们将这样做。设置您的角色将替换您以前拥有的角色,但不仅包括您设置的角色,还包括授予该角色的所有角色,以此类推,递归进行。这些级联授权是有限制的,因此不会形成无限循环。

受密码保护的角色

让我用一个关于 Oracle 11g 的说明来开始这个讨论:从这个版本的 Oracle 开始,为了从一个活动连接设置密码保护的角色,您必须提供密码,即使它是一个默认角色。这可能会让你措手不及。以前,如果某个角色受密码保护,但它是默认角色,则用户在连接时默认获得该角色。

我同意甲骨文的这一改变;出于同样的原因,我们在本书中将没有任何密码保护的角色。设置密码保护角色的唯一原因是,该角色被授予用户,但您不希望该用户使用该角色。您通过设置密码来保护角色,并且不要让用户知道。但是,我们不得不问,“为什么我们一开始就给他这个角色?”

更好的方法是只将角色授予需要它的用户。许多人可能作为特定的 Oracle 用户进行连接,其中一些人需要访问角色,而一些人不需要,这可能会使事情变得有些混乱。在这种情况下,我建议您要么让需要角色的人输入角色密码,要么更好地重新访问您的用户和角色并对它们进行分类。

一种可能性是有一个安全的应用角色,由一个过程来验证。可以根据用户 id 或某些组成员来授予该角色。则不需要角色密码。您只需要维护一个可接受的用户列表或组成员列表。

我观察到一个公司策略,用密码保护每个应用角色。这个想法是,坐在 SQLPlus 等通用 SQL 客户端上的人将无法访问该角色,因为他们不知道密码。但是在我们希望他们拥有角色的应用中,会调用一个过程,通过在数据库表中查找角色密码来设置角色。这种配置可能对临时 Oracle 用户获得应用角色造成了一点障碍;然而,这里要观察的简单事实是,被授予该角色的人可以通过应用调用的同一个过程或该过程中的同一个代码来访问该角色。在 SQLPlus 中,用户可以简单地调用过程,获取密码,并设置角色。

也许可以通过确保只使用某个特定的客户端应用来防止角色被盲目访问;同样,这是安全应用角色的工作。但是这里真正的问题是,一个角色被授予了一个 Oracle 用户,而管理员不希望该用户访问该角色。受密码保护的角色只能提供增强安全性的假象,除非您让特定用户记住并手动输入密码来设置角色。这就是 Oracle 11g 新策略背后的思想。

安全管理员用户

让我们开始定义我们的安全管理员。安全管理员将是一个独立的非个人用户;也就是说,一个帐户和密码可以委派给不同的人,他们轮流进入和退出工作职责。安全管理员将执行通常由DBA甚至SYS执行的任务,但是我们将把授予安全管理员的权限限制在与应用安全相关的方面。

首先,作为SYS,我们将一步创建用户secadm并授予他create_session_role。用一个真实的密码替换该命令中的“密码”:

GRANT create_session_role TO secadm IDENTIFIED BY password;

images 注意一定要给这个用户一个非常复杂的密码;它会强大到在坏人手里会很危险。该警告也适用于 SYS 和 SYSTEM 帐户。

安全管理员角色

接下来,我们将创建安全管理员角色secadm_role。该角色将被授予执行安全管理所需的所有权限。然后,我们可以将这个角色授予任何用户,但是我们将它限制为只有一个用户,secadm

首先,我想设置一些关于如何以及何时使用该角色的要求,所以我不会使用我们在create_session_role中看到的NOT IDENTIFIED关键字。相反,我将通过一个程序来鉴定(核实)它。你可以在这个命令中看到sys.p_check_secadm_access验证程序的名字:

CREATE ROLE secadm_role IDENTIFIED USING sys.p_check_secadm_access;

由过程标识的角色称为安全应用角色。

安全管理角色验证

正如我提到的,安全管理员需要的所有特权都将被授予secadm_role,所以我们正在尽力保护它。关键字IDENTIFIED USING sys.p_check_secadm_access表示当用户试图获取secadm_role时,他必须从名为p_check_secadm_access的过程中获取,该过程存在于SYS模式中。

存储过程(procedure)是在 Oracle 数据库上存储和运行(执行)的过程语言/结构化查询语言(PL/SQL)代码的命名块。一般来说,一个过程接受参数并执行工作。它还可以通过其参数返回信息。在 Oracle 中也有存储函数(functions),除了它们通常取值之外,与过程非常相似;做研究或计算;然后返回一个值作为结果。我们将同时使用过程和函数。

用于验证secadm_rolep_check_secadm_access的特定过程不带任何参数(传递给过程进行评估的参数或值),并且不返回任何结果。大多数过程都需要参数,但这不是必须的。

在清单 2-1 中,我们正在创建用于获取安全管理员角色的过程。注意,这个过程的简单目标是要求安全管理员与 Oracle 数据库服务器运行在同一台计算机上(IP 地址 127.0.0.1 也称为本地主机或回送地址)。此要求可能不适合您的系统;如果没有,您仍然可以创建这个过程,但是通过在前面放置两个破折号(减号)来注释掉以IFTHENEND IF开头的三行。您可以作为SYS执行该命令,程序将被创建。

清单 2-1。 p_check_secadm_access安全 App 角色的流程

CREATE OR REPLACE PROCEDURE sys.p_check_secadm_access AUTHID CURRENT_USER AS BEGIN     -- This is a comment     IF( SYS_CONTEXT( 'USERENV', 'IP_ADDRESS' ) = '127.0.0.1' )     THEN         EXECUTE IMMEDIATE 'SET ROLE secadm_role';     END IF; END; /

我们以后会想回来访问(替换)这个过程,添加我们认为适当的任何附加安全约束。目前,我们的安全管理员必须直接连接到 Oracle 数据库(在服务器上运行 SQL*Plus)。)

作为一名程序员,您将已经理解了IF / THEN / END IF语法,并意识到还有其他 PL/SQL 语法要求,如BEGIN / END和分号。

查看BEGIN标志和最后一个END标志之间的代码。在这之间,它用英语说,“如果用户的环境有一个本地主机的互联网协议 (IP)地址,那么立即将他的角色设置为secadm_role”我们使用SYS_CONTEXT 上下文用户环境中获取 IP 地址,并确定该地址是否等于 127.0.0.1 (localhost)。如果测试为真,那么我们立即将当前会话的角色设置为secadm_role

调用者的权利与定义者的权利

在我们上面定义的过程中,我们说AUTHID CURRENT_USER,以便这个过程作为当前用户执行,而不是作为SYS(定义这个过程的人。)所以在执行时,这个过程将使用所谓的“调用者的权利”(CURRENT_USER),而不是“定义者的权利”(默认或所有者,在本例中是SYS)。如果我们不这样做,当我们执行该过程时,环境和身份将看起来是SYS,但是我们希望能够确定特定的当前用户的可接受性(授权)。我们需要使用 invoker 权限执行的另一个原因是,否则我们不允许从过程中设置角色——我们只能为经过身份验证的(当前)用户设置角色。

安全管理员角色获取

我们还没有完成对安全管理员的定义。我们需要允许安全管理员执行我们创建的过程。我们通过发出以下命令来做到这一点:

GRANT EXECUTE ON sys.p_check_secadm_access TO secadm;

注意,我们不需要将secadm_role授予secadm;相反,如果用户满足过程的所有要求(p_check_secadm_access),那么IDENTIFIED USING过程将把他的角色设置为secadm_role。这里有一个类似的限制:要么授予角色,要么授予执行过程的能力,这将设置角色。在这两种情况下,您都通过要求特定授权来限制对角色的访问。但是,通过使用过程来设置角色,您可以对访问设置进一步的限制。

请注意,在这种情况下,我们直接向用户授予特权(执行过程),而不是像我们之前建议的那样,向角色授予权限,然后向用户授予角色。每个规则都有例外!这个特殊异常的原因是我们用一个过程来保护一个角色。我们不需要用角色来进一步保护过程。

安全管理员,secadm用户可以通过执行如下所示的过程来获取secadm_role。事实上,每次 secadm 连接到 Oracle 数据库时,他都需要调用那个过程来获得secadm_role角色。

EXEC sys.p_check_secadm_access;

授予安全管理员角色的系统权限

我们希望安全管理员拥有的大部分权限将由SYS授予:

GRANT     CREATE USER     ,ALTER USER     ,CREATE ROLE     ,GRANT ANY OBJECT PRIVILEGE     ,GRANT ANY PRIVILEGE     ,GRANT ANY ROLE     ,CREATE ANY PROCEDURE     ,CREATE ANY TRIGGER     ,CREATE ANY CONTEXT     ,CREATE PROFILE     ,AUDIT SYSTEM     ,AUDIT ANY TO secadm_role;

我们授予secadm_role的大部分系统特权允许我们做一些我们作为SYS已经在做的事情。我们将允许我们的安全管理员创建和修改用户,创建角色并向用户授予角色。secadm将授予角色使用其他模式中的 Oracle 对象(结构)的权限。他将向角色授予某些系统特权。他还将创建过程和触发器,它们类似于过程,但在特定事件发生时执行(我们稍后将对此进行更多讨论。)当我们到达第十章时,我们还将创建一个应用上下文。安全管理员将创建配置文件,我们将在第八章中看到。现在,我们依赖默认的配置文件。

我们还将作为安全管理员设置一些审计。我们将审计各种各样的系统事件,并且我们将审计对表和HR模式中其他结构的访问——因此有了AUDIT ANY特权。任何时候在对象特权授予中看到ANY,都可以理解为“在任何模式中”通常,用户在自己的模式中已经拥有这些特权。

这些并不是我们的安全管理员完成工作所需的所有系统和模式对象权限,但是它们将帮助我们开始工作。稍后我们将以SYS的身份回来,给安全管理员更多的特权。

审计线索

最后,作为SYS,我们将在审计线索本身上设置一些初始审计。这将阻止流氓数据库管理员做错事,然后通过删除他们的审计记录来清除他们的踪迹:

AUDIT SELECT, UPDATE, DELETE     ON sys.aud$     BY ACCESS;

当我们指定BY ACCESS进行审计时,我们是说我们想要详细的信息。另一个(可能是默认的)选项是BY SESSION。这提供了较少的细节,但仍然审计每个事件,而不是像早期版本的 Oracle 那样,只为每个会话提供一个审计记录。

数据字典

我们希望我们的安全管理员能够查看数据字典中的所有数据,数据字典是列出 Oracle 中的结构和系统数据的SYS模式中的视图集合。(视图是查看数据表的定义方式。)例如,我们可能希望列出所有数据库用户的详细信息:

SELECT * FROM sys.dba_users;

DBA_USERS视图中有许多列在PUBLIC数据字典视图中是不可用的:ALL_USERS(更少的细节)和USER_USERS(更多一点的细节,但只针对当前用户)。

默认情况下,大部分数据字典已经被授予PUBLIC,每个用户都可以选择。在大多数情况下,这是必要的。但是我们会在第十一章、加强我们的安全中更严格地处理这个问题。然而,选择数据字典的某些部分需要SELECT_CATALOG_ROLE。将该角色授予secadm_role:

GRANT select_catalog_role TO secadm_role;

请注意,这是授予角色的角色。从现在开始,当我们将角色设置为secadm_role时,我们也将拥有SELECT_CATALOG_ROLE

担任安全管理员

现在,我们的安全管理员已经被定义了完成工作所需的权限,我们将让他开始工作。继续并连接:

CONNECT secadm;

images 注意你可以在名为 Chapter2/SecAdm.sql 的文件中找到以下命令的脚本。

您应该还记得,在我们创建安全管理员角色secadm_role时,我们要求通过一个过程对其进行验证。我们只允许一个账户secadm执行该程序。立即执行获取secadm_role:

EXEC sys.p_check_secadm_access;

注意,当secadm连接到 Oracle 时,他不会自动获取secadm_role。因为是安全申请(验证)角色,没有直接授予secadm,所以不能作为默认角色。每次secadm用户连接时,他都必须执行该过程来获得他的安全管理员角色和权限。

这与直接授予用户的角色相反,角色最初是默认角色。可以取消设置默认角色的状态。

从 SQL*Plus 本地连接获取 secadm_role

总会遇到一些问题,如果您使用 SQLPlus 作为主要客户端,这里有一个问题可能会困扰您几次:当您坐在 Oracle 数据库的命令提示符下时,您可以本地连接到默认实例。通过执行不带任何参数的 SQLPlus 来实现,如下所示:

sqlplus

您可以通过输入用户名和密码以secadm用户的身份进行连接。如果您随后试图执行设置secadm_role的过程sys.p_check_secadm_access,它将不会成功。为什么不成功?我们的地址应该是 localhost 的地址,这应该没问题。嗯,当本地连接时,SQL*Plus 根本不使用网络—它只是直接与数据库对话。通过执行以下命令,您可以看到缺少 IP 地址信息:

SELECT SYS_CONTEXT( 'USERENV', 'IP_ADDRESS' ) FROM DUAL;

这对安全性有一些影响。像这样进行本地连接时,您输入的命令不会通过网络适配器,也不可能在网络上泄露给窥探设备。

那么,您可能会问,我们应该如何以secadm的身份连接,并在 Oracle 数据库上从 SQL*Plus 运行sys.p_check_secadm_access?有一种方法,只需要在命令行上添加用户名和实例名(在本例中是orcl)的参数,就像没有本地连接一样。实际上,这个上下文中的orcl是一个与实例同名的 TNS 别名。我们将在第十一章中讨论 TNS 别名。

sqlplus secadm@orcl

此时,您在会话上下文中有了一个 IP 地址,并且可以通过以下过程成功地设置角色:

SELECT SYS_CONTEXT( 'USERENV', 'IP_ADDRESS' ) FROM DUAL; EXEC sys.p_check_secadm_access;

在角色之间切换

当您在create_session_rolesecadm_role之间来回切换时,通过观察您当前的会话角色,您可以看到运行SET ROLE的效果。作为secadm用户,这样做并在执行每个SELECT查询时观察角色列表:

SELECT * FROM sys.session_roles; SET ROLE create_session_role; SELECT * FROM sys.session_roles; EXECUTE sys.p_check_secadm_access; SELECT * FROM sys.session_roles;

当您执行程序,然后从SESSION_ROLES中选择时,您将看到三个角色。该过程本身将您的角色设置为secadm_role。为了这个角色,我们授予了SELECT_CATALOG_ROLE。而 SELECT _CATALOG_ROLE已经被授予HS_ADMIN_SELECT_ROLE。所有这些都将被列为会话的当前角色。如果您的 Oracle 数据库上安装了其他程序包或不同版本,您可能会看到与这些程序包相关的其他角色。

创建应用安全用户

我们需要更多的用户来开发和展示我们的安全性。第一个用户是我们的应用安全用户,appsec。她将拥有使我们的应用安全工作的所有结构。

请注意我在 Oracle 安全(我们已委托给安全管理员)和应用安全之间所做的区分。安全管理员被授予了许多系统权限,因此他可以在 Oracle 中创建用户和执行其他任务。另一方面,应用安全用户将拥有许多用于应用安全流程的过程和结构。应用安全用户不会作为任何特定应用的一部分进行连接;相反,她将授权应用用户访问她的逻辑和数据。

GRANT create_session_role TO appsec IDENTIFIED BY password;

images 注意一定要给这个用户一个非常复杂的密码;她在数据库中没有重要的特权,但是她拥有一些我们不想暴露的逻辑和数据。

应用安全角色

我们的应用安全用户需要创建过程、函数、Java 存储过程、表和视图。创建这些项目时,appsec需要CREATE PROCEDURECREATE TABLECREATE VIEW系统权限。我们将这些特权授予名为appsec_role的角色,并将该角色授予appsec用户:

CREATE ROLE appsec_role NOT IDENTIFIED; GRANT CREATE PROCEDURE TO appsec_role; GRANT CREATE TABLE TO appsec_role; GRANT CREATE VIEW TO appsec_role; GRANT appsec_role TO appsec;

非默认角色

我们的应用安全用户只是偶尔需要这些特权(例如,CREATE PROCEDURE)。我们希望她根据需要将自己的角色设置为appsec_role,但目前这是一个默认角色。为了将创建为NOT IDENTIFIED的角色的行为从默认角色更改为非默认角色,我们需要发出ALTER USER命令。在下面的命令中,我们要求 Oracle 将授予appsec用户的所有角色都设为DEFAULT,除了我们想要排除的角色:

ALTER USER appsec DEFAULT ROLE ALL EXCEPT appsec_role;

此后,当appsec为了创建新结构而连接时,她将不得不发出SET ROLE命令之一。(不要以secadm用户的身份执行这些命令——这里,它们仅用于解释。)

SET ROLE appsec_role; SET ROLE ALL;

上面列出的第二个命令将为appsec设置当前会话,以启用ALL已经被授予的角色,包括默认和非默认角色。

创建应用用户

作为连接到 Oracle 运行应用的示例用户,我们将创建一个应用用户appusr。出于我们的目的,appusr并不局限于一个人,而是 Oracle 所称的“一个大型应用用户”模型用户。在这种模式下,许多人将使用一个应用,该应用将他们作为一个大的应用用户连接到 Oracle。他们不需要个人帐户和密码进行访问。

GRANT create_session_role TO appusr IDENTIFIED BY password;

images注意:请务必为该用户提供一个非常复杂的密码——他们可能会选择数据并更新应用数据。我们只希望该活动发生在应用内部。我希望你已经习惯了这个关于密码的警告,甚至可以预见到它。

个人账户

我们将有机会创建特定于个人的 Oracle 用户帐户;然而,维护这些用户(以及授予、验证和撤销所需的特权)的管理需求是一项巨大的责任。我认为有更好、更简单和更安全的方法来识别和授权数据库中的个人,我们将在第八章中讨论这个问题。

创建人力资源视图角色

我们将开始保护对人力资源(HR)示例模式中数据的访问。我们将创建一个名为hrview_role的角色。通过这个角色,我们将授权访问我们计划构建的各种应用所需的数据。一开始,我们只希望在我们的内部网络上的人们能够访问这些数据,而且只能在我们正常的办公时间(早上 7 点到晚上 7 点)访问。

为了完成这些约束,我们将创建角色,并要求通过一个过程对其进行验证。Oracle 称之为安全应用角色,因为这是它的功能–它是一个提供应用数据访问权限的角色,但它受到一些编码约束的保护。这和我们保护secadm_role的方法是一样的。

CREATE ROLE hrview_role IDENTIFIED USING appsec.p_check_hrview_access;

通过流程验证人力资源视图角色

正如我们之前对secadm_role所做的一样,我们将创建一个过程来保护对hrview_role的访问。在程序结束时,如果CURRENT_USER满足程序中编码的要求,我们将SET ROLEhrview_role

通过执行清单 2-2 中的代码作为secadm来创建程序。这里,我们在另一个模式中创建了一个过程。注意模式名 appsec。附加在过程名的前面。为此,需要secadm拥有CREATE ANY PROCEDURE系统特权。

清单 2-2。 p_check_hrview_access安全 App 角色的流程

CREATE OR REPLACE PROCEDURE appsec.p_check_hrview_access AUTHID CURRENT_USER AS BEGIN     IF( ( SYS_CONTEXT( 'USERENV', 'IP_ADDRESS' ) LIKE '192.168.%' OR         SYS_CONTEXT( 'USERENV', 'IP_ADDRESS' ) = '127.0.0.1' )     AND         TO_CHAR( SYSDATE, 'HH24' ) BETWEEN 7 AND 18     )     THEN         EXECUTE IMMEDIATE 'SET ROLE hrview_role';     END IF; END; /

注意,这里我们又有了语句AUTHID CURRENT_USER。我们将使用调用者的权限(IR)而不是默认定义者的权限(DR)来执行。在这个过程中,我们在IF语句中编码了两个测试。对于那些通过两个测试的会话,我们将角色设置为hrview_role

子网测试

p_check_hrview_access中的第一个测试,如清单 2-2 所示,从用户环境中获取客户端的 IP 地址,并测试它是否以字符串“192.168”开头LIKE语法表示地址应该由指定的字符后跟零个或多个字符组成(符号“%”是通配符)。该测试确保客户端存在于我们的内部公司子网上。(“192.168”是非路由子网,可以在防火墙后或测试子网中使用。它通常是用于通过 DSL 或电缆调制解调器连接到互联网的家庭网络的子网。)您的公司子网很可能与此子网不同,因此在创建之前,请修改该过程的代码。在命令提示符下,您可以通过发出以下命令来查找 Windows 工作站的 IP 地址和其他网络信息:

C:\>ipconfig /all

工作站 IP 地址的前两个八位字节(由点分隔的三位数或更少位数的集合,或二进制的八位数的集合)可能代表您的公司子网。请联系您的网络支持人员,以确定具体使用什么。

此过程还允许您从 Oracle 数据库服务器本身连接到 Oracle。如果 IP 地址是 127.0.0.1,即本地主机地址(即 Oracle 数据库服务器本身),则该过程也会成功。

测试正常营业时间

p_check_hrview_access中的第二个测试,如清单 2-2 所示,包含语句TO_CHAR (SYSDATE, 'HH24') BETWEEN 7 AND 18。让我们花点时间来分析一下。SYSDATE是 Oracle 数据库中当前时间和日期的名称。您可以从服务器SELECT(请求)SYSDATE,并且您可以在 Oracle 中更新数据时将日期值设置为等于SYSDATE。在这种情况下,我们请求SYSDATE并使用TO_CHAR函数将日期的小时(HH24)部分格式化为使用 24 小时格式的字符串。在这种格式下,当是 7AM 时,TO_CHAR函数会返回 7;晚上 7 点 19 分。所以我们检查时间是BETWEEN 7 和 18(在下午 6:59,这个测试仍然为真;但是晚上 7 点,假的。)我们已经确定我们的正常营业时间是早上 7 点到晚上 7 点,所以这些测试是有效的。

images 注意这里没有代码可以在晚上 7 点到达时断开用户与 Oracle 的连接。该代码仅阻止在晚上 7 点后建立进一步的连接。

允许应用用户获得人力资源视图角色

每个需要访问HR数据的用户都必须执行p_check_hrview_access程序来设置hrview_role。只有hrview_role可以访问数据,设置该角色的唯一方法是执行过程。我们将授予我们的应用用户appusr执行此过程的EXECUTE权限。如secadm所示,执行以下命令:

GRANT EXECUTE ON appsec.p_check_hrview_access TO appusr;

在这个过程中,我们必须将EXECUTE授予所有需要访问数据的用户。或者,我们可以向PUBLIC授予EXECUTE特权,但是只有当数据库的每个用户都需要访问HR数据时,我们才会这样做。不要不考虑后果就授予PUBLIC特权。

审核对安全管理员程序的更改

我们将通过建立一些额外的审计来结束本章的这一部分。因为我们作为SECADM工作,所以我们定义审计的默认结构在SECADM模式中。我们将审计模式中过程的任何变化,因为它们是安全相关的。我们需要确保任何变更都经过审核。

AUDIT ALTER ANY PROCEDURE BY ACCESS;

当 secconf.sql 脚本作为数据库创建的一部分运行时,这实际上是默认情况下审计的权限之一。

审核访问人力资源数据的失败尝试

我们对访问HR数据的第一次审计实际上是对试图执行p_check_hrview_access过程的审计,该过程设置了hrview_role。我们不想知道这个过程何时成功,但是我们想知道无效访问何时被尝试,所以我们使用关键字WHENEVER NOT SUCCESSFUL

AUDIT EXECUTE ON appsec.p_check_hrview_access     BY ACCESS     WHENEVER NOT SUCCESSFUL;

作为人力资源方案用户工作

对于本书提出的几个方面,我们将使用可以与 Oracle 11g 一起安装的HR示例模式。如果您还没有安装这个模式,那么在前面的步骤中配置对HR模式结构的审计就会遇到困难。请浏览 Oracle 网站,了解如何在事后安装示例模式。

现在,在安装 Oracle 时,您还有机会为HR用户配置一个密码。如果样本模式存在,但是没有配置HR用户(或者只是重新配置用户),您可以发出ALTER USER命令(作为SYSsecadm):

ALTER USER hr ACCOUNT UNLOCK IDENTIFIED BY password;

images 注意一定要给这个用户一个非常复杂的密码;人力资源部拥有我们试图保护的数据。

HR用户身份连接到 Oracle:

CONNECT hr;

HR在他自己的模式之外没有系统特权,但是被授予了连接数据库和在他自己的模式中创建许多结构所需的特权:表、视图、索引等等。

images 注意你可以在名为 Chapter2/HR.sql 的文件中找到以下命令的脚本。

HR 示例模式中的敏感数据

在可以与 Oracle 11g 一起安装的HR示例模式中,有一个名为EMPLOYEES的表。该表有两列,我们称之为“敏感的”:SALARYCOMMISSION_PCT。我们的目标是在不泄露敏感数据的情况下授予对该表的访问权限。

使用此命令查看EMPLOYEES表的内容,如HR:

SELECT * FROM hr.employees;

让我们假设我们公司的目标是发布一个在线人员目录(电话簿),并且这个表是主要的源数据。我们可以只导出电话簿中使用的数据的非敏感部分,但是最好是直接授予对该表中主要数据的安全访问权限,而不是复制数据(假设我们的数据库服务器支持额外的负载)。

员工的公共视野

授予对数据表的有限访问权的最基本方法是创建一个视图。视图就像我们放在数据表上的过滤器,可以只显示某些数据,重新组织数据,格式化数据,并提供来自多个表或其他视图的数据。我们将创建一个名为v_employees_public的视图,它只包含非敏感的数据列:

CREATE OR REPLACE VIEW hr.v_employees_public AS SELECT     employee_id,     first_name,     last_name,     email,     phone_number,     hire_date,     job_id,     manager_id,     department_id FROM hr.employees;

测试新视图,确保它只返回我们请求的数据。我们应该注意到SALARYCOMMISSION_PCT列丢失了:

SELECT * FROM hr.v_employees_public;

因为我们已经从这个视图中省略了敏感列(SALARYCOMMISSION_PCT),所以我们可以将对这个视图的访问权授予整个公司,而不会暴露敏感数据。我们可以把它授予PUBLIC;然而,我们仍然觉得有点保护我们的数据,我们想确保访问是受控的,所以我们将视图的访问权授予我们的安全应用角色,hrview_role而不是PUBLICHR用户有权授予对自己模式中结构的访问权限:

GRANT SELECT ON hr.v_employees_public TO hrview_role;

有了这个授权,我们就可以从视图中获取hrview_roleSELECT的数据。可以授予视图的其他常见特权有INSERTUPDATEDELETE。我们目前不会授予任何特权。hrview_role可以通过v_employees_public视图看到但不能修改非敏感数据。

员工的敏感观点

既然我们已经配置了非敏感视图,那么如何为EMPLOYEES表的所有列创建敏感视图应该是显而易见的。作为HR,我们创建一个视图:

CREATE OR REPLACE VIEW hr.v_employees_sensitive     AS SELECT *     FROM hr.employees;

星号(*)表示所有列,该视图从所有列中选择所有数据。测试该视图是否返回了来自EMPLOYEES表的所有数据:

SELECT * FROM hr.v_employees_sensitive;

这个视图在应用的某些时候可能很有用——例如,当我们想让某些财务分析师计算有多少人挣了一定的薪水,为每年的加薪做准备。我们可能还希望将这个视图用于一个财务应用,我们希望授予UPDATE对发放加薪的访问权。允许人力资源部门在雇佣和解雇员工时从数据库中选择INSERTDELETE员工,这也是非常有用的。

然而,考虑到数据的敏感性(你和我或者我们的经理挣多少钱),我们根本不打算授予对这个视图的访问权。稍后,我们将通过更加严密保护和加密的渠道提供对这些数据的访问。

我们关于人力资源数据的第一个审计语句在前面的“审计访问人力资源数据的失败尝试”一节中给出下面是我们对HR数据的第二份审计声明。有了它,我们打算审计对EMPLOYEES表的敏感视图的任何直接访问。稍后,我们将查看审计那些我们认为敏感的字段的选择,无论它们是从什么视图或表中选择的。

AUDIT SELECT ON hr.v_employees_sensitive BY ACCESS;

测试应用用户访问

为了测试我们的安全性,我们需要尝试我们认为不应该工作的事情(因为我们的安全措施)以及我们允许成功的事情。为此,您需要以appusr用户的身份连接到 Oracle:

CONNECT appusr;

images 注意你可以在名为 Chapter2/AppUsr.sql 的文件中找到以下命令的脚本。

我们认为,接下来的三个命令将会失败,因为appusr用户没有被直接授权访问HR模式中的任何内容。他只有默认角色,也没有访问权限。我们应该会看到一条错误消息,指出这些表或视图不存在。

SELECT * FROM hr.employees; SELECT * FROM hr.v_employees_sensitive; SELECT * FROM hr.v_employees_public;

现在我们将执行检查我们有效性的过程,然后将 role 设置为hrview_role。如果我们在公司的子网上(或者在 Oracle 数据库服务器上),并且时间在早上 7 点到晚上 7 点之间,那么这应该会成功:

EXEC appsec.p_check_hrview_access;

然后,我们将测试对HR模式中的EMPLOYEES结构的访问。我们不期望前两个命令起作用;我们应该再次看到一条错误消息,指出这些结构不存在。hrview_role不允许访问敏感数据;

SELECT * FROM hr.employees; SELECT * FROM hr.v_employees_sensitive;

在最后一个命令中,我们应该看到来自EMPLOYEES表的数据;然而,我们注意到数据中缺少了两列:SALARYCOMMISSION_PCT

SELECT * FROM hr.v_employees_public;

敏感视图的审计跟踪日志

让我们找到该访问的审计日志条目。HR用户无权读取审计日志(如果他尝试,他将被审计),但是如果你以secadm身份连接并执行以下命令,你将看到由HR查询v_employees_public生成的审计日志:

`EXEC sys.p_check_secadm_access;

SELECT OBJECT_NAME, STATEMENT_TYPE, RETURNCODE FROM DBA_COMMON_AUDIT_TRAIL
        WHERE DB_USER='HR'
        ORDER BY EXTENDED_TIMESTAMP DESC;`

当我们仍然以secadm的身份在这里时,我们应该尝试访问敏感视图,并查看这个失败尝试的审计日志条目(secadm无法读取敏感视图)。您还将看到由secadm执行的审计跟踪SELECT语句的日志。

SELECT OBJECT_NAME, STATEMENT_TYPE, RETURNCODE FROM DBA_COMMON_AUDIT_TRAIL         WHERE DB_USER='SECADM'         ORDER BY EXTENDED_TIMESTAMP DESC;

0 的RETURNCODE表示成功,而非零的RETURNCODE表示失败。

关于同义词

同义词就像 Oracle 中结构的额外名称,主要用于表和视图。同义词最有说服力的论据是使用公共同义词。如果我们为一个视图创建一个公共同义词,那么从该视图中选择的任何人都不需要在视图名前面加上模式名。HR用户可以执行以下操作来创建公共同义词。(不要这样做—此示例仅供讨论。)

CREATE PUBLIC SYNONYM v_employees_public FOR hr.v_employees_public;

这不会改变视图或数据的安全性或可访问性。但是,它允许角色hrview_role选择不带 hr. schema 名称前缀的数据,如下所示:

SELECT * FROM v_employees_public;

也可以为同义词指定一个不同于它所代表的结构名称的名称,如下所示:

CREATE PUBLIC SYNONYM employees FOR hr.v_employees_public;

也许这可以简化名称——如果我们希望用户在命令提示符下输入他们的所有查询,这可能会有所帮助。(不要这样做—此示例仅供讨论。)您可以想象,在设置数据库管理员之后出现的继任数据库管理员可能会有些困惑。员工可能会抱怨他们不能从EMPLOYEES中选择,继任管理员可能会回答说没有人可以,因为他不知道员工说的是同义词,而不是表或视图。同样,如果视图hr.v_employees_public被丢弃(删除),同义词将被破坏。

单个用户可能会创建一个专用同义词供自己使用(仅供特定用户/模式使用)。她可能会这样做,这样她就可以用她最喜欢的术语“同龄人”来称呼HR.EMPLOYEES然而,当她试图与她的同事共享任何数据库查询或代码时,他们会对她的私人同义词感到沮丧。使用她的私有同义词的查询对其他人不起作用。

我也观察过将同义词合并到安全策略中的尝试,隐藏具有相同名称的公共或私有同义词的原始表。相信这样的诡计是愚蠢的。

使用同义词的最好理由可能是让程序员编写代码稍微容易一些,因为他们不必记住用那些讨厌的模式名作为表和视图名的前缀。然而,我观察到当一个表或视图从开发环境(Oracle 实例)迁移到生产环境,但是公共同义词被留下时,应用会失败。我知道这不应该发生,但它发生了。那么数据就在生产环境中,但是应用失败了,因为它缺少同义词或者只是缺少代码中的模式名。这很难排除故障。

我建议您尽量不要使用同义词,只让开发人员用模式名前缀来表示他们所在的表和视图。我不指望每个人都听从我的建议,但请至少考虑一下。

同义词的另一个论点是引用另一个数据库实例上的结构。在这种情况下,同义词还可以包括数据库链接。我并不反对使用同义词,因为如果数据库链接名称发生变化,指定和更新起来肯定会更容易。我提醒您,无论数据出现在哪里,您都需要保护它,即使是跨数据库链接。还应该对非生产环境中生产数据的可见性和使用进行限制;更是如此,反之亦然。

章节回顾

现在,您有了一个安全管理员用户,他不是 DBA,但是可以处理所需的大多数 Oracle 安全任务。此外,您还有一个应用安全用户,他将处理 Oracle 应用安全所需的事务。

您已经锁定了我们的 Oracle 数据库,添加了一些基本的审计,并了解了数据字典视图。我们讨论了角色:预定义角色、默认角色、非默认角色和安全应用角色。在我们对应用安全性的第一次尝试中,我们构建了几个安全的应用角色和保护它们的过程。

在应用数据模式HR中,我们区分了敏感数据和非敏感数据,并只创建了非敏感数据的公共视图。我们还创建了敏感和非敏感数据的视图,但是我们还不允许任何人查看。然后我们查看审计跟踪日志中与我们的敏感视图EMPLOYEES相关的条目。

三、安全的 Java 开发概念

这一章超出了传统上在 Java 安全一章中所涵盖的内容,它并没有深入地涵盖严格意义上的 Java 安全主题。相反,它解决了基本的 Java 开发概念。我们应该确保我们在 Java 开发上有一个坚实的基础。你可以用 Java 开发非常安全的代码,但是如果你没有意识到你在做什么,你的代码可能会在不知不觉中变得不安全。

本章还将帮助 Java 集成开发环境(IDE)程序员更好地理解工具之外的开发过程(如 JDeveloper、NetBeans 或 Eclipse)。ide 做得很好,但是不要盲目地相信它能理解并执行安全的开发实践。如果你有很强的 Java 背景,这一章可能是不必要的——在这种情况下,只需浏览一下,看看是否有需要复习的地方。

Java 开发套件

Java Standard Edition(SE)Java Development Kit(JDK)既可以独立使用,也可以与 Sun(Oracle)的 IDE NetBeans 捆绑在一起使用。您可以从甲骨文的网站[java.sun.com](http://java.sun.com) ( [www.oracle.com/technetwork/java](http://www.oracle.com/technetwork/java))下载任何一个。您将需要 JDK 1.5 或更高版本,并且您会发现更早的版本,如 1.5,可能只在以前的版本网页上提供。

我们需要 JDK 1.5 或更高版本,因为这是包含在 Oracle 数据库中的 JDK 版本:Oracle Java 虚拟机。我们将很快讨论虚拟机。我们希望我们工作站上的 Java 版本至少与 Oracle 数据库上的相同。

您也可以下载 Java 运行时环境(JRE。)因为您正在下载 JDK,所以不需要同时下载 JRE。JRE 是运行 Java 应用所需的 JDK 的一部分,但是它没有编译 Java 所需的工具。

如果您安装了 Java IDE,那么您的 IDE 中也包含了 JDK。如果是 1.5 版或更高版本,您可以简单地使用 IDE 附带的 JDK。

Oracle Java 数据库连接

从甲骨文网站www.oracle.com/technetwork/indexes/downloads下载一份甲骨文 Java 数据库连接(JDBC)代码库。向下滚动到[驱动程序]。您下载的文件可以适用于最新版本的 Oracle 数据库(11g),并且必须适用于 JDK 1.5 或更高版本。但是,请注意,您不应该使用比您正在使用的 JDK 更新的 JDBC 驱动程序(例如,不要在 JDK 1.5 中使用 ojdbc6.jar )。所以你会下载 ojdbc5.jar ,或者 ojdbc6.jar 如果你用的是 JDK 1.6 或者更高版本。

JAR 文件目录分隔符

Java 归档(JAR)文件(类似于我们之前下载的ojdbc6.jar文件)是一个压缩的目录树,其中包含编译后的 Java 文件和其他文件,作为一个集合使用。您可以使用 JDK 中的 JAR 实用程序创建一个 JAR 文件,稍后我们也会这样做。您可以使用任何 ZIP 实用程序查看 JAR 文件的内容。

在 JAR 文件中,您会发现一个目录树。例如,在ojdbc6.jar文件中,您会看到这些目录(以及许多其他目录):

/oracle /oracle/sql

/oracle/sql目录中,您会看到类似于ARRAY.class的文件。ARRAY.class文件是一个编译后的 Java 文件。

不要因为我使用/(斜线)作为目录分隔符而受到干扰。这是 UNIX 的标准分隔符,也是 Java 的默认分隔符。Java 可以理解斜杠,但是在大多数情况下,标准的微软反斜杠必须被转义(解释)到 Java。反斜杠在 Java 中的另一个作用是作为转义字符本身,所以转义的反斜杠看起来像这样。以下两个文件名在 Windows 平台上的 Java 中是等效的,但我将始终使用第一种样式。

"C:/Program Files/Internet Explorer/SIGNUP/install.ins" "C:\\Program Files\\Internet Explorer\\SIGNUP\\install.ins”

Java 包

我们没有 Oracle JDBC 驱动程序的 Java 代码,但是我们可以推断出ARRAY类的代码(前面提到过)在一个名为ARRAY.java的文件中。在那个文件中有一个包声明:

package oracle.sql;

注意包名是如何与目录树相关联的,使用点(.)作为包的分隔符,而不是目录的斜杠。在甲骨文公司,公司开发人员将文件ARRAY.java保存在一个名为sql,的目录中,该目录位于一个名为oracle的目录中。他们在匹配的目录树中编译类。他们通过收集所有编译的内容来创建 JAR 文件,从oracle目录开始。

您需要记住这个基本概念:包等于目录路径。正如我们将看到的,包还提供了安全性,并影响我们引用 Java 代码以及编译和运行它的方式。

在命令提示符下开发

我指导过许多新的和有经验的开发人员,他们主要是用 IDE 完成了 Java 开发工作或培训。当我让他们从命令提示符进行故障排除或测试应用时,他们通常不知道从哪里开始。我相信您需要准备好从命令提示符执行(运行)Java,我们将在这里做一些工作。

环境

当您进入命令提示符并希望编译或运行您的代码时,您将需要几个操作系统环境设置:您的PATH查找 JDK 可执行文件,您的CLASSPATH查找您的代码和 Oracle JDBC 代码。无论最近的 JDK 可执行文件在哪里,它们都能够找到自己的代码库(Java 可以找到 Java 库,尽管并不总是这样),所以你不必在CLASSPATH中指定它们。

让我们假设您已经在这个目录中安装了 JDK:

C:\Program Files\java\jdk1.6

假设您也将 Oracle JDBC 文件放在这个位置:

C:\Program Files\java\ojdbc6.jar

您不需要设置环境变量,但是 Java 命令行语法的区别是很明显的。下面是从命令提示符运行 Java Oracle 应用MyApp的两个例子。在第一个命令中,我们设置并依赖于我们的环境设置。如果我们没有环境设置,则需要第二个选项。

java MyApp C:/Program Files/java/jdk1.6/bin/java –cp “.;C:\Program Files\java\ojdbc6.jar” MyApp

您可以很容易地看到,如果您的环境配置得当,从命令行运行会容易得多。如果您运行的是 Windows XP 或更高版本,请右键单击桌面上的我的电脑并选择属性,或者右键单击开始菜单中的计算机并选择属性。选择高级选项卡或按钮,然后单击环境变量。在顶部,用表 3-1 中列出的设置创建两个(新的)用户变量(或修改现有的)。

image

PATH 用户环境变量将自动附加到系统路径,并告诉 Windows 如何找到 Java 编译器(javac.exe)和 Java 运行时(java.exe)可执行文件。

注意,即使设置了路径,在路径的前面可能还有其他 java.exe 可执行文件。确保 javac.exe 和 java.exe 的版本都是 1.5 或更高版本。从“开始”菜单打开命令提示符,并检查这些命令的结果:

javac –version java –version

CLASSPATH用户环境变量中,我们说了几件事。首先点(。)告诉 Java 在当前目录中查找类。相信我,你不会想被抓到问 Java,“为什么你找不到文件——它就在这里?”我们指出的第二件事是 ojdbc6.jar 文件中的 Oracle 代码——我们希望 Java 自动找到该代码。我们要说的第三件事是,我们希望任何已经存在的系统环境CLASSPATH(% class path %)附加到我们的CLASSPATH

再详细说明一下,CLASSPATH是我们希望 Java 查找编译类的位置列表。这些地方实际上只是起点,可以包括目录(比如我们的“dot”当前目录)和归档文件(JAR 文件或 ZIP 文件)。当我们编译或运行自己的代码,需要使用已有的代码时,可以沿着CLASSPATH找到。Java 打开归档文件,查看包含的目录和文件,看看我们需要的东西是否在那里。如果我在代码中引用了oracle.sql.ARRAY,Java 最终会从CLASSPATH打开 ojdbc6.jar 文件,找到oracle目录。然后,它将继续查找sql目录,并从那里找到ARRAY.class文件。

开始 Java 语法

让我们在这里展示几个 Java 代码文件,这样我们就可以用具体的参考来讨论一些具体的编程问题。仅供讨论,我们将有两个。java 文件存在于名为mypkg的目录中,mypkg位于名为javadev的目录中。目录结构和文件名如下所示:

javadev/          mypkg/                 MyApp.java                 MyRef.java

images 注意你会在javadev/mypkg目录中找到文件。

考虑下面两个 Java 文件的代码清单,如清单 3-1 和清单 3-2 所示。注意,这些文件不做任何事情,但是它们是有效的 Java 代码。

清单 3-1。??MyApp.java

package mypkg; import oracle.sql.ARRAY; public class MyApp {     ARRAY myArray;     MyRef myRef; }

清单 3-2。??MyRef.java

package mypkg; public class MyRef { }

我们将在这里介绍 Java 代码语法的几个方面,但是我们将在稍后介绍大部分语法。注意,这两个 Java 代码文件都是以它们的包声明开始的。看一下MyRef.java,我们看到了带有修饰符public的类的简单声明。类的代码用一对花括号({})括起来。每个。java 文件必须只有一个顶级类声明,并且顶级类必须与文件同名(在本例中为 MyRef)。

images 注意通常这些顶级类被声明为public,但也不一定是。它们不能是私有的或受保护的,但是它们可以是缺省的,或者是不使用修饰符就可以访问的包。

此时的另一个观察结果在MyApp.java文件中。首先,我们有一个import语句,import oracle.sql.ARRAY。每次使用不同包中的类时,都需要这样的语句。如果我们使用了来自oracle.sql package的大量类,我们可以用简写语句import oracle.sql.*将它们全部导入。因为MyRefMyApp在同一个包中,所以我们不需要导入MyRef,即使我们在MyApp中引用了它。

还要注意,在MyApp的类定义(在花括号之间)中,我们声明了两个成员类:一个是ARRAY的实例(副本),我们称之为myArray,另一个是MyRef的实例,我们称之为myRef。注意 Java 是区分大小写的:比较myRefMyRef

我将经常使用术语成员实例。简单地说,用面向对象的说法,在计算机内存中创建的对象是任何种类的类的实例;我们称对象的创建为实例化。由另一个对象创建并在其中引用的对象称为创建它的对象的成员。

字节码编译和 Java 虚拟机

我已经用过编译这个词了,在这本书里你会看到很多。在 java 中,您将编写人类可读的代码,并将代码放在文件名带有. Java 扩展名的文件中。为了运行代码,首先需要编译它。编译步骤创建了一个扩展名为. class 的人类无法读取的文件。这个。计算机操作系统无法执行类文件;它是字节码格式。类文件由 Java 运行时环境(JRE)可执行文件 java.exe 运行。

Java 运行时在计算机内存中创建一个 Java 虚拟机(JVM ),它可以解释和运行字节码。让 JVM 读取并运行字节码的价值在于,在大多数情况下,字节码可以编写一次,在任何地方运行——这是 Java 语言的一个基本目标。您可以编写它,并在方便您(开发人员)的任何地方编译它,然后将它放在任何运行兼容 JVM 的计算机上:工作站、服务器、浏览器、手机或 web 服务器。JVM 处理与操作系统和硬件对话的所有细节。

我们将在我们的代码中看到这个概念的力量,我们将在我们的工作站上编写、编译和运行这些代码。然后,我们将它加载到 Oracle 数据库中(实际上,我们将它存储在数据库中),并让 Oracle JVM 也在 Oracle 中运行它。

使用 Java 编译器

JDK 在 bin 子目录中有许多命令行实用程序。其中之一是主要的 Java 编译器,javac.exe。另一个是运行应用的主要 Java 可执行文件,java.exe。

为了编译 Java 代码,您执行 javac.exe,将 Java 代码文件的名称作为参数传递,就像这样(假设您的命令提示符在目录javadev/mypkg中):

javac MyApp.java

您必须包括。代码文件名上的 java 扩展名。这个命令将在当前目录中找到文件MyApp.java,如果成功,它将把一个名为MyApp.class的编译后的 Java 文件放在当前目录中。不管MyApp是否在包装中,这都是真的。如果MyApp.java引用了其他不在当前目录下的编译类,那么必须沿着CLASSPATH找到它们(比如ojdbc6.jar列在CLASSPATH里编译器就能找到oracle.sql.ARRAY)。

如果沿着CLASSPATH可以找到其他由MyApp引用的 Java 代码文件,并且这些代码自上次编译以来没有被编译或更新过,那么javac.exe也会编译这些类。引用的代码将被编译,编译后的类文件将与引用的 Java 代码文件放在同一个目录中。比如MyRef.class会和MyRef.java放在同一个目录下。

此外,通过指定通配符,可以编译多个 Java 类或特定目录中的所有 Java 类,如下所示:

javac *.java javac mypkg/*.java

查找引用的代码/类

编译 Java 有一个“陷阱”,您需要理解和预测,或者至少快速识别。当您的代码引用不在您的CLASSPATH上的其他 Java 代码时,Java 编译器将找不到该代码,即使被引用的代码在当前目录中。假设您的代码在名为mypkg的包(目录)中,并且您的MyApp.java引用同一个包中的MyRef.java。如果您的CLASSPATH没有引用mypkg子目录的父目录,那么javac.exe将找不到MyRef.java文件。有圆点(。)不会解决这个问题,因为mypkg目录不在当前目录中,而是在mypkg中。记住,CLASSPATH(偶数点)仅仅是包的起始位置列表。在当前目录中,我们找不到名为mypkg的包(子目录),也找不到名为mypkg/MyRef.class的代码或类文件。

纠正这个问题的一个方法是将目录更改为mypkg的父目录。一旦到了那里,你就可以编译你的代码了。)会在mypkg目录中找到引用的代码。然而,在这种情况下,您需要告诉javac.exe您的MyApp.java代码在子目录中(在这个上下文中,正斜杠和反斜杠都可以作为目录分隔符),就像这样:

javac mypkg/MyApp.java

这是一种有效的方法,但是解决这个问题的最好方法可能是拥有一个开发目录(像javadev)并将所有的包目录放在这个目录下。然后将您的开发目录添加到您的CLASSPATH中。

set CLASSPATH = C:\javadev;%CLASSPATH%

通常,尤其是如果您使用 IDE 管理您的开发,您将不能将您的包放在一个单独的开发目录中。每个项目都将文件存储在单独的目录中。

也许现在您在想,“伙计,我就让 IDE 来为我处理这一切吧!”我不想贬低 IDEs,但是满足于采取那种方法就像自愿去坐牢,因为工作很辛苦。不要放弃努力!通常,您不需要 IDE 来编写、编译和运行您的代码,所以利用这些机会在命令提示符下工作。一旦你设置了你的环境变量,这只是一个了解包和CLASSPATH以及识别问题的问题。

运行编译好的代码

一旦您的代码被编译,运行(执行)它就很容易了:

java mypkg.MyApp

请注意,要运行一个 Java 类,您需要指定它所在的包。在这方面没有捷径可走。

沿着CLASSPATH寻找应用所需的所有类的要求同样适用于执行,就像它们适用于编译一样。为了运行您的 Java 代码,正如刚才所示,java.exe需要能够在CLASSPATH中列出的一个起始点中找到mypkg包。在我们的例子中,如果您在javadev目录中,并且您的CLASSPATH中有“点”,那么java exe可以找到mypkg,可以在mypkg/MyApp.class中运行你的代码。对于你的代码所引用的所有类,比如oracle.sql.ARRAY,都必须如此。对于该引用,java.exe需要找到CLASSPATH中列出的ojdbc6.jar

除了在您的操作系统环境中定义CLASSPATH,还有一种替代方法。您可以使用-cp参数将CLASSPATH传递给java.exe,或者传递给javac.exe

java -cp “.;C:\Program Files\java\ojdbc6.jar” mypkg.MyApp

在这个例子中,我们提醒 java.exe 提供了CLASSPATH参数(-cp)。引号是必需的,因为目录名 Program Files 中有一个空格。注意,在这种情况下,正斜杠也是有效的。

Java 代码和语法概念

我将给出一个简单的 Java 代码的例子。它有 20 行长,不太复杂,但是它将向您介绍许多 Java 语法概念。你不需要记住这些概念的细节——它们会出现在你用 Java 编写的每个程序中。

在接下来的讨论中,我们将考虑清单 3-3 中代码的一些方面。请在这一页放上书签,以便阅读下面几节时参考。

清单 3-3。??MyApp2.java

package pkg2; import oracle.sql.ARRAY; import mypkg.MyRef; public class MyApp2 {     private ARRAY myArray = null;     static MyRef myRef;     public static void main( String[] args ) {         MyApp2 m = new MyApp2();         MyRef useRef = new MyRef();         m.setRef( useRef );         ARRAY mA = m.getArray();         myRef = new MyRef();     }     public ARRAY getArray() {         return myArray;     }     void setRef( MyRef useRef ) {         myRef = useRef;     } }

images 注意在名为 javadev/pkg2/MyApp2.java 的文件中找到这段代码

继续我们之前关于 Java 语法的讨论,我们看到这个新代码,MyApp2.java,在一个新的包(目录)中, pkg2 。出于这个原因,我们必须在原始包中导入(引用)MyRef 代码。回想一下,为了编译和运行这段代码,需要从您的CLASSPATH中的某个起点找到这两个包。

方法

MyApp2 ( MyApp2.java)的代码中,你会看到三个代码块,分别名为main()getArray()setRef()。通过在定义类体MyApp2的花括号中寻找开花括号和闭花括号来找到它们。这三个代码块被称为方法(程序员可能把它们想象成子例程或函数)。

每个标准方法都需要两件事:一组圆括号,参数(输入值或对象)可以在其中传递给方法,以及返回类型的声明(传递回调用该方法的任何代码)。当我在本文中提到方法时,我会添加左括号和右括号。稍后,我将更详细地讨论publicprivate修饰符。现在,我们知道它们不是返回类型。

先看中间的方法。getArray()有一组空括号,表示它没有任何输入参数。在方法名之前,它有单词ARRAY,这表明它返回一个类型为ARRAY的对象。

注意,main()setRef()方法都有一个返回类型void,,这是一种表示这些方法不返回任何东西的方式。

main()方法接受一个名为args的参数,该参数的类型是一个Strings数组。字符串是一系列字符的术语,比如单词“巨大的”,或者句子“这很好!”任何类型的数组都由左方括号和右方括号([ ])表示。数组中的任何单个元素都可以通过在方括号中放置一个整数来表示它的位置和索引来引用。例如,args[0]是传递给main()方法的Strings数组的第一个元素。定义数组时,可以将方括号放在类型或名称上;以下是相同的:

String[] sAr; String sAr[];

还有一种不同于标准方法的方法,它被称为构造函数。当在计算机内存中创建一个类的实例时,调用构造函数。构造函数没有返回类型,构造函数方法的名称与类的名称相同。例如,MyApp2的构造函数可能如下所示:

public MyApp2() { }

请注意,这里没有指明返回类型。如果您没有在代码中定义构造函数,您的类将使用默认的构造函数,它看起来很像这个示例构造函数。还要将这个默认构造函数与MyApp2类定义进行比较;他们是非常不同的东西,但有一些相似之处:public MyApp2() {public class MyApp2 {相比。

在我们的MyApp2示例代码中,如清单 3-3 所示,我们有一些语句将成员变量设置为一个值。例如,我们说了下面的话:

ARRAY mA = m.getArray();

在描述这个语句时,我很可能会说,“设置mA等于m.getArray()”就像我会说,“将m.getArray()转换为mA.”但是我应该说的是,您需要理解的是,成员变量mA是一个指向类型为ARRAY的对象的指针。我在这条语句中所做的是将指针的值设置为由m.getArray()返回的对象在内存中的地址。即使这样也不精确。更准确地说,m.getArray()正在返回一个指向ARRAY,位置的指针,而我正在将指针mA设置到同一个位置。

这很重要,因为你需要记住,当你传递一个对象并将成员变量设置为相等时,你并不是在复制它,你只不过是设置了更多指向同一位置的指针。唯一一次创建一个对象是当你通过调用new或者一些生成新对象并返回它的方法来实例化它的时候。我们还会看一下clone方法,它可以用来返回一个对象的新副本的地址,而不是返回原对象的地址。

下面的例子可能会有所帮助。在第一个例子中,我们只是将另一个成员指向现有的对象;在第二个例子中,创建了一个新的对象,我们将新的成员变量指向它。当我们在第一个例子中设置名称成员时,我们在原来存在的对象中设置值;指向原始对象的任何其他代码都将看到更改后的名称。在第二个例子中,到目前为止,这种变化只是局部可见的。

`ExampleObject newMember = existingObject;
newMember.name = “New Name”;

ExampleObject newMember = new ExampleObject();
newMember.name = “Another Name”;`

原始值会发生一些不同的事情。它们不是 Java 对象,在所有引用中传递的是它们的值,而不是它们的地址。

成员

对象既有方法又有成员。面向对象编程中的成员是变量。我说的变量是指一个指针。这个变量可能指向一个原语—比如一个int(整数)—或者一个 Java 对象(比如一个Date类)。在 Java 中,Strings是对象,而不是原语。普通的旧数组也是对象(没有方法)。

通常,方法和变量都被称为类的“成员”,但我更喜欢称之为“方法和成员变量”。在这个讨论中,方法不被称为成员。此外,成员变量被正确地称为字段。我忍不住偶尔把它扔进去。如果我使用术语字段 s 和方法,我就不需要说成员;但是我喜欢成员这个词,而且我有说这个词的习惯,所以我会说成员方法

在我们的示例代码中,MyApp2有几个成员。我们看到的前两个被命名为myArraymyRef。这两个都是类成员变量,因为它们存在于类中,在任何方法之外,这可以从代码中看到(在下面重复)。方法知道类成员变量,正如你在getArray()方法中看到的,它返回类成员myArray。您还可以看到,setRef()方法将myRef类成员的值设置为等于传递给方法useRef的参数。

    private **ARRAY myArray** = null;     static **MyRef myRef**;     public ARRAY getArray() {         return **myArray**;     }     void setRef( MyRef useRef ) {         **myRef** = useRef;     }

如果您查看一下main()方法(在下面重复),您将看到有三个成员被声明:museRefmA。这些是方法成员,因为它们只存在于方法中。请注意,它们可以在其他地方使用。例如,main()方法将useRef交给m.setRef()方法,然后后者设置myRef类成员(记住,我们是处理和设置内存指针或引用)。main()方法是独一无二的,稍后我将在介绍static修饰符时详细讨论它。

    public static void main( String[] args ) {         **MyApp2 m** = new MyApp2();         **MyRef useRef** = new MyRef();         m.setRef( useRef );         **ARRAY mA** = m.getArray();         myRef = new MyRef();     }

物体

词语对象实例实际上是可以互换的。在创建任何实例之前,您(理论上)已经有了类。只有在内存中实例化新实例时,实例才存在。你有可以移动和存储的对象,即使你不知道它们是什么类型。对象可以放在磁盘上或通过网络发送,即使它们不作为实例存在于内存中。

因此,您在内存中创建一个类类型的实例,它是一个可以移出内存(存储在磁盘上)的对象,此时它不是一个实例,但仍然可以作为对象存在。

类和空

我已经在我们的讨论中使用了术语实例。在清单 3-3 中,有三个类的实例被创建。要挑选出实例,请查找单词 new 。每当你看到语句new,一个对象正在被实例化。

有时你会调用其他类来获得某个类型的实例;然而,即使您在代码中没有使用单词new,这个单词也会在下面的某个地方被用来获取实例。在MyApp2的示例代码中,我们看到main()方法将成员mA设置为等于从方法getArray()返回的ARRAY。你可能会注意到,MyApp2中的getArray()方法没有使用new语句,当我们在类级别(top)定义myArray时,我们不说new。相反,我们在定义中使用了单词null。当方法getArray()返回myArray时,它返回的是null而不是ARRAY的实例。当main()方法将mA设置为等于从getArray()返回的值时,它将mA设置为等于null。这不是很有成效,但它是有效的 Java 代码。

为了解释null,我们将考虑以下语句的含义:

MyRef useRef = new MyRef(); new MyRef(); ARRAY myArray; ARRAY myArray = null;

第一行实例化了一个新的MyRef类实例,并为它分配了一个成员引用(名称)。我们将使用的名称是useRef

我们可以实例化一个新的实例,而不像第二行那样给它指定名称。这通常是在图形用户界面(GUI)应用开始时完成的。首字母Frame被实例化并显示在屏幕上——不需要名字。

我们还可以为某个类型的类创建一个新名称,并且不将它分配给任何实例,如第三行所示。第四行实际上与第三行相同,除了在第四行中我们明确地告诉 Java,我们正在将成员名myArray分配给一个不存在的实例,因此是null。实际上,myArray指向了没有的内存位置。

我们说成员变量名指向实例。在任何时候,我们都可以将一个成员变量名指向另一个实例。在硬件中,这实际上是将成员指向内存中的一个地址。当我们将成员指向null时,我们说它不指向内存中的任何地址。

垃圾收集

计算机内存中的 Java 应用可以想象成一个装满 Java 对象的篮子,所有对象都相互引用,调用彼此的方法并返回数据,获取和设置彼此的成员。在 Java 中,我们不必手动跟踪哪些对象正在使用,也不必释放未使用对象的内存,Java 运行时通过一个称为垃圾收集的过程来完成这项工作。当一个对象不再被购物篮中的任何其他对象引用时,它会从购物篮中掉出。它不会消失,直到周期性的垃圾收集过程遍历内存并将掉落的对象清除掉。

原语

除了对象(如前所述),我们还有 Java 原语。一些原语类型有int(整数)、bytechar(字符)。成员变量可以是 Java 类(对象)和原语(值)的类型。

原语没有任何方法,也没有任何成员。您可以拥有原语数组,就像 Java 对象数组一样。

也有封装原语的 Java 类,允许我们在对象实例中保存原语,并从实例中检索原语。比如Integer,就是一个可以封装一个int的类。从Integer中,我们可以通过调用Integer.parseInt( String s )得到一个Stringint值。

有些 Java 构造只能处理对象,不能处理原语;例如,Vector(一个动态数组对象),所以如果我们打算将原始值存储在Vector中,我们将需要封装它们。

字符串

A String是不可变的 Java 对象,意思是你不能改变它;但是,您可以用一个new String来替换它(将您的成员变量指向它)。String有看起来像是你在改变对象的方法,但事实并非如此。例如,您可以调用String.toUpperCase()方法:

myString.toUpperCase();

这个对toUpperCase()的调用根本不会改变myString的值,但是它有一个返回类型String,返回的是大写的myString的值。将myString设置为大写的唯一方法是将其设置为等于返回值。实际上,您将myString设置为指向由toUpperCase()方法创建的新大写字母String:

myString = myString.toUpperCase();

处理Strings时一个非常常见的错误是试图像处理原始类型一样比较它们,例如使用等号:

if( stringOne == stringTwo )

这是非常有效的代码,但是不能完成您想要的测试。这就像在这样的测试中不小心使用了一个等号一样糟糕:

if( int1 = int2 )

我想这是为了比较int1int2,但它最终将int1的值设置为等于int2的值。糟糕,非常糟糕——太糟糕了。

因为String是一个对象,所以需要像对待对象一样对待它,调用它的方法进行比较。我们称之为String.equals()法来比较Strings

if( stringOne.equals( stringTwo ) )

使用对象方法进行比较的要求适用于所有对象类型。然而,在处理Strings时,在这方面遇到编码错误是最常见的:在程序员看来,它们可能更像原语。

静态修饰符和 main()方法

你已经看到了修饰语static。在我们的代码示例中,在main()方法的定义中,以及在myRef成员的定义中。成员和方法都可以是静态的。为了描述静态意味着什么,让我退后一秒钟。

假设您执行了java.exe,并且正在运行一些 Java 代码。想象一下持有一堆对象(实例)的 JVM。如果我们的代码创建了 100 个MyApp类的实例,每个实例都有我们MyApp.java代码的某些方面;然而,有些方面在 JVM 中只存在一次。那些每个 JVM 只存在一次的东西被标为static。属于static的项目存在于MyApp的任何实例之外,由所有人共享。存在于类实例之外的一个有趣的现象是,类的那些方面static甚至在你没有创建实例时也是可用的。

例如,我的代码中的第一行可能是这样的:

if( MyApp2.myRef == null ) {

在那一行中,我没有通过名字引用的MyApp2的实例;相反,我指的是类本身,并测试静态类成员变量myRef的值。

现在,这正是我们开始运行一些 Java 代码所需要的。默认情况下,如果我执行java.exe并给它一个运行的类名,它就会运行main()方法。如果我在执行java.exe时在命令行上提供任何参数,这些参数将作为参数传递给main()方法:一个Strings数组。例如,如果我发出这个命令:

java MyApp2 this is a test

然后有四个参数传递给MyApp2main()方法:this、is、a 和 test。

下面是我们代码中对main()的定义:

public static void main( String[] args ) {

在下一节我描述了修饰语public之后,您将会知道整个定义的含义。对于您想要从命令提示符运行的任何类来说,main()方法的特定语法是相同的(除了您可以调用Strings的数组、参数、任何您想要的东西,而不仅仅是args)。

我们需要一个静态方法来开始运行 Java 代码,因为此时我们还没有实例化任何类。再看一下清单 3-3 中的main()方法(以下重复)。我们在那里做一些非常典型的事情。在main()方法中,我们实例化了类本身的一个实例——我们创建了一个new MyApp2实例,名为m。为了使用MyApp2的非静态方法,我们需要在我们的MyApp2实例中调用它们,所以我们调用m.setRef()m.getArray()。前缀 m .表示我们正在调用名为mMyApp2实例的方法。

public static void main( String[] args ) { **        MyApp2 m = new MyApp2();**         MyRef useRef = new MyRef();         **m.setRef**( useRef );         ARRAY mA = m.getArray();         **myRef** = new MyRef();     }

注意main()方法定义中的最后一件事。它将静态成员myRef的值设置为MyRef的一个实例。在静态方法中,只能引用该类的静态成员。如果我们试图在main()中设置myArray的值,我们会在编译时得到一个错误,因为直到我们创建了MyApp2的一个实例,才有myArray成员——它不是static

公共和私有修饰符

publicprivate修饰符可以同时存在于方法和成员变量上。这些有助于将一个类的用户可以直接访问的项目与该类的用户不能(直接)看到的项目区分开来。这些是范围修饰符。这就是publicprivate的重要之处:之所以重要,是因为它对安全性有一些影响,我们将在后面讨论。

在清单 3-3 中,myArray成员是私有的,所以使用这个类的人不能直接获取或修改myArray。但是,我们已经提供了方法getArray(),它是公共的,可以将myArray返回给任何人。

    **private** ARRAY myArray = null;     static MyRef myRef;     **public** ARRAY getArray() {         return myArray;     }     void setRef( MyRef useRef ) {         myRef = useRef;     }

默认情况下,没有声明为privatepublic的方法和成员,如myRef成员和setRef()方法,通常只能被来自同一个包的其他类访问。一个附加的范围修饰符是protected。Protected 作用域类似于默认作用域,除了它允许您允许在您的包之外定义的类的子类查看您的成员和方法。

一种设计模式(方法)声明所有的类成员变量都是private,然后开始建立 getters 和 setters(公共方法,为其他对象提供获取或设置私有成员的方法)来读取和写入成员。这是 JavaBeans 的一个特性,通常也是 Enterprise Java Beans 的一个特性,有助于它们作为 IDE 中的组件。由于它们的 getter 和 setter,它们可以被分发和合并到 IDE 或应用服务器中,而无需事先了解或定义。通过反射(或 XML 定义),IDE 可以列出 bean 的公共 getter 和 setter 方法,并提供对私有成员的访问。

我们在示例代码中为成员getArray()提供了一个 getter 方法。我们还为myRef成员:setRef()提供了一个 setter 方法;虽然,我们的方法名不符合 JavaBeans 标准,因为我们不是在编写 JavaBeans,我们不需要实现所有的样板代码。

异常情况

好的、安全的代码必须有一个处理错误的计划。Java 包含了一个框架,我们可以在这个框架上构建错误(异常)处理。在处理异常时,您有几种选择,Java 可以帮助您完成您选择的以下任何一种:

  • 对异常不做任何事情,但是抛出给调用异常代码的代码。抛出异常会生成一个堆栈跟踪,它列出了异常发生的位置(行号和代码)以及调用这个代码块(方法)的位置和调用那个方法的位置,等等。您可以打印堆栈跟踪,并查看您所在的位置和发生的情况。通常,异常本身会告诉您异常发生的原因;不需要进一步的故障排除。
  • 捕捉异常并在本地处理,然后要么继续运行,要么中断代码,甚至在捕捉到异常后抛出异常。
  • 捕捉异常并引发不同的异常。

异常处理语法

让我们探索一些异常处理的语法和功能。清单 3-4 ,ExceptionDemoA.java,显示了main()方法d: trycatch中的两个块,每个块都被花括号包围。它们在一起,所以我将catch块连接到try块的右花括号中。在第二个模块catch中,我们抓住了将军Exception

images 注意你可以在名为 Chapter3 的文件夹中找到这个 Java 文件和下一个文件。

清单 3-4。??ExceptionDemoA.java

import java.io.FileInputStream; public class ExceptionDemoA {     public static void main( String[] args ) {         try {             ExceptionDemoA m = new ExceptionDemoA();             String mS = m.doWork();             System.out.println( mS );         **} catch**( Exception x ) {             System.out.println( x.toString() );         }         System.exit(0);     }     String doWork() throws Exception {         FileInputStream tempFileIS = new FileInputStream( "C:/Windows/win.ini" );         tempFileIS.open();         //…         if( tempFileIS != null ) tempFileIS.close();         return "success";     } }

注意,doWork()方法是用throws Exception声明的。Exception类是所有异常的母体,代表了其中的任何一个。她的一些子异常是处理文件或网络时的IOException,处理数据库时的SQLException,以及 Java 代码试图调用不存在的对象的方法时的NullPointerException。我们可以为预期的每种异常设置一个单独的catch块。一个方法可以抛出可能发生的多种类型的异常。

每当我们调用一个可能抛出任意数量异常的外部代码块(在本例中,我们对m.doWork()方法的调用)时,捕捉通用的Exception是合适的。如果不管我们捕捉到什么异常,我们都以相同的方式处理它,也是合适的,就像这个例子一样。

将异常消息打印到系统输出流

无论我们捕获的异常是什么,我们都将使用以下命令在 catch 块中打印标识:

System.out.println( x.toString() );

Java 中的每个对象都有一个toString()方法,包括异常对象和您将要创建的对象。toString()方法是从Object对象继承而来的——稍后会详细介绍。toString()方法返回一个String类型,可以用来提供对象的身份或一些细节——在异常的情况下,它提供异常名。

System.out.println()将文本发送到命令提示符窗口,以行结束字符结束。各种计算机的行尾字符各不相同,例如,从 Windows 到 UNIX。行以回车符和/或换行符结束。

这些术语来自打字机时代,那时纸在滑架上绕着压盘滚动,当你打字时滑架从右向左移动。在每一行的末尾,打字员会摇动一个控制杆,滚动滚筒来输入一行,然后向左推动控制杆,这将使滑架回到起点。

将行尾字符附加到字符串意味着下一个字符将出现在当前行第一个字符的下面,即下一行。

堆叠呼叫

注意,我们调用了catch块中的x.toString(),并立即将返回的String作为参数传递给System.out.println()。在打印之前,我们没有创建一个String成员变量来保存值。由于没有更好的名字,我将称之为堆栈调用。看看能否在下一节的代码中找到两个非常相似的调用示例,清单 3-5 ,ExceptionDemoB.java。我们将在第六章中开始频繁使用堆栈调用。

作为异常处理的一部分进行清理

清单 3-4 中ExceptionDemoA.java的代码有问题。(以下重复代码)。有些在doWork()方法中完成的事情也需要在那里撤消。首先,我们打开一个FileInputStream(可能是为了读取一个文件的内容)。从那以后,任何事情都可能发生——特别是,一个异常可能发生。

    String doWork() **throws Exception** {         FileInputStream tempFileIS = new FileInputStream( "C:/Windows/win.ini" );         **tempFileIS.open();**         //…         if( tempFileIS != null ) **tempFileIS.close();**         return "success";     }

如果在我们关闭FileInputStream之前抛出了一个异常,那么doWork()方法会把这个异常抛出给调用它的人(参见定义doWork()那一行的throws修饰符)。关闭FileInputStream ( close()方法)的那一行永远不会到达,在某些情况下,文件会保持打开状态。这可能会给计算机上运行的应用带来可怕的后果。在许多情况下,当前在一个应用中打开的文件无法被另一个应用读取,因此在异常发生后未关闭的文件可能无法备份,并且可能会锁定其他计算机操作。

在 finally 块中清理

有了异常处理,还有第三个叫做finally的块可以使用,如清单 3-5 中的所示。对于每个try块,还必须有一个catch块、一个finally块或两者都有。我们很少有没有catch块的try(参见本章后面的侧栏“try / finally附加调试和同步”)。其思想是,无论您成功完成了try块,还是在完成try之前生成并捕获了一个异常,您都可以在finally中进行清理。即使您的catch块抛出一个异常,finally块也会运行。看看清单 3-5 中方法的语法。

清单 3-5。??ExceptionDemoB.java

import java.io.FileInputStream; public class ExceptionDemoB {     public static void main( String[] args ) {         ExceptionDemoB m = new ExceptionDemoB();         System.out.println( m.doWork() );         System.exit(0);     }     String doWork() {         String returnString = "attempt";         FileInputStream tempFileIS = null; **        try {**             tempFileIS = new FileInputStream( "C:/Windows/win.ini" );             **tempFileIS.open();**             //…             returnString = "success"; **        } catch( Exception x ) {**             System.out.println( x.toString() ); **        } finally {**             try {                 if( tempFileIS != null ) tempFileIS.close();             } catch( Exception y ){} **        }**         return returnString;     } }

doWork()中,我们在FileInputStreamopen()周围有一个try块。如果发生任何事情导致抛出异常,我们将在下一个块中捕获它。然后不管我们是否捕捉到异常,我们都将进入finally块并执行FileInputStream.close()方法。

你可能有几个问题。第一个可能是,“为什么我们不能同时关闭 t rycatch块中的FiIeInputStream答案是你做到。然而,重复代码是一种不好的做法,您可能会遇到catch块中的close()的问题,这将导致另一个异常被处理,从而增加了故障排除的工作量。

你可能有的第二个问题是。“为什么我们不把FileInputStream.close()放在trycatch的外面,跳过finally块呢?”这不是一个好主意,因为有时你会捕捉到一个异常并抛出它或另一个异常,就像这样:

} catch( IOException x ) {     throw new AppException( x.toString() ); } FileInputStream.close();

如果这是您的代码,并且您捕获了一个IOException,您将永远不会到达FileInputStream.close()行。您的catch中的throw语句,因为它没有在这里被捕获,将退出当前方法,向调用该方法的任何代码抛出一个AppException

回头看看我们的示例,您会在finally块中看到这组块:

        } finally { **            try {**                 if( tempFileIS != null ) tempFileIS.close();             **} catch**( Exception y ){}         }

关闭FileInputStream可以抛出一个IOException,我们需要处理它(抓住它或者抛出它)。在这一点上,我选择抓住它,什么也不做。我将这作为我的标准语法用于一个finally块。一般来说,我们只是在做清理工作,我可能做的任何工作都应该已经完成了,否则就会抛出一个异常,我已经处理过了。

如果我不能清理这个(例如,关闭FileInputStream),那么我可能需要修复一些东西,但是如果我已经做到了这一步,那么我已经使用了资源,在这种情况下我应该能够关闭它。如果我没有使用它,那么我已经处理了一个相关的异常,这里的另一个异常是多余的。所以我通常在我的finally块中的catch块中什么也不做。

试/最后附加调试和同步

有一种情况,我发现需要一组tryfinally模块,而没有catch模块。如果你已经编写了一个方法,并且你需要临时添加调试到其中,而又不至于过度弄乱你现有的代码,你可以使用一个tryfinally块对。看看下面这个名为methodName()的方法的框架:

returnType methodName() throws Exception {     try {         while() {             if() {             }         }         for() {         }     catch( Exception x ) {     } finally {     } }

要对此方法进行临时现场调试,我们可以在try块之前声明一个文件输出,然后在整个方法中向其写入调试消息。为了完整性、安全性和良好的实践,我们会将文件flush()close()到现有的finally块中。唯一的问题是,我们把代码弄得乱七八糟,做了这些修改之后再清理(去掉调试)会给我们带来更多的错误。简而言之,这种方法会产生看起来更像永久调试代码的代码,如下所示:

`returnType methodName() throws Exception {
    PrintStream debugOut = null;
    try {
        debugOut = new PrintStream ( new FileOutputStream( “debug.txt” ) );
        while() {
            debugOut.println( “message 1” );
            if() {
            }
        }
        for() {
        }
    catch( Exception x ) {
    } finally {
        if( debugOut != null ) {
            debugOut.flush();

debugOut.close();
        }
    }
}`

我们可以使用更简单的try / finally模块对来降低引入误差的可能性。如果我们在我们的示例方法中发现了已知发生在while块中的问题,那么我们就可以用我们的故障诊断来解决这个块。(请注意,您不能通过在现有块的开始或结束处放置新的代码块来破坏现有块。)我们的解决方案可能如下所示(添加了粗体代码):

`returnType methodName() throws Exception {
    try {

// temp code
        PrintStream debugOut = null;
        try {
        debugOut = new PrintStream ( new FileOutputStream( “debug.txt” ) );
        // to here

while() {
            debugOut.println( “message 1” );
            if() {
                debugOut.println( “message 2” );
            }
            debugOut.println( “message 3” );
        }

// temp code
        debugOut.println( “message 4” );
        } finally {
            if( debugOut != null ) {
                debugOut.flush();

debugOut.close();
            }
        }
        // to here

for() {
        }
    catch( Exception x ) {
    } finally {
    }
}`

我们已经成功地在需要的地方添加了现场调试,并且通过在finally块中关闭我们的文件输出,我们使用了良好的编码实践。注意,如果我们的方法还没有捕捉或抛出一个IOException,我们将需要在新的finally块之前有一个新的catch块,并且我们将需要在我们添加的finally块中有一组try / catch块,在flush()close()周围。清理(移除调试)只需要移除代码的开始和结束部分(我们新添加的tryfinally声明以及周围的代码)以及移除每个debugOut.println()语句。

当像这样跟踪代码时,最好记住上下文和同步需求。如果这是一个由多线程服务器应用调用的方法,那么我们需要确保一次只有一个用户打开、写入和关闭文件。实现这一点的一种方法是将方法声明为同步(在返回类型声明之前,将synchronized关键字添加到方法签名中)。另一种不涉及改变现有方法签名的方法是同步对象上的。它可以是任何类型的对象,但它应该是一个静态类成员,这样每个人都可以同步同一件事情。

通常,您会尝试同步代码的最小部分,在这种情况下,您可能会同步每个debugOut.println()语句。但是在这种情况下,您可能还需要将您的debugOut PrintStream声明为静态类成员。然而,为了给我们的现场调试添加同步,我们将同步一个更大的部分,并允许methodName()方法的每个请求从打开到关闭独占访问 debug.txt 文件。它看起来会像这样:

`static String synchOnThis = “”;
returnType methodName() throws Exception {

try {

PrintStream debugOut = null;

synchronized( synchOnThis ) {

try {
        debugOut = new PrintStream ( new FileOutputStream( “debug.txt” ) );

while() {
            debugOut.println( “message 1” );
            if() {
                debugOut.println( “message 2” );
            }
            debugOut.println( “message 3” );
        }

debugOut.println( “message 4” );
        } finally {
            if( debugOut != null ) {
                debugOut.flush();

debugOut.close();
            }
        } }

for() {
        }
    catch( Exception x ) {
    } finally {
    }
}`

注意,我们在添加的try块之前打开了一个synchronized块,并且在添加的finally块之后关闭了synchronized块(用一个额外的大括号括起来)。那个synchronized块中的所有东西都将被一次对methodName()方法的一次调用(由一个用户)独占执行。

我们正在对名为synchOnThis的空白String进行同步。那是一个static阶层的成员。让我指出我们可以如何利用这种同步安排。假设我们在同一个类中有一个额外的方法需要调试。我们可以用第二种方法添加相同的try / finally块并进行调试,再次在synchOnThis成员上同步。不仅对这些方法的每个调用都将获得对我们文件的独占访问权,而且对任一方法的调用都将获得独占访问权——例如,第一个方法中的某个人不能访问该文件,而第二个方法中的某个人拥有该文件的synchronized。如果您想将一个不同类中的方法调试到这个相同的文件中,您可以在这个类中的同一个静态成员synchOnThis上进行同步。

您必须理解同步中的一个假设:同步只在单个 JVM 中有效。例如,如果在同一台计算机上有两台 web 服务器,每台服务器都可能运行自己的 JVM。例如,这两个 JVM 不能协调对文件的同步访问。因此,如果其中一个 JVM 正在写入文件,另一个 JVM 可能会同时尝试写入文件。这很可能会失败。

异常处理方法

关于异常处理有各种各样的理念和实践。我还没有看到任何我完全同意的提议。然而,我确实有我喜欢的实践,而且我也努力做到始终如一。请允许我提出一些有助于定义我的方法的想法。

当一个异常可以编码时,不要编码多个异常

我发现这是一种非常罕见的情况,它会促使我对一个try块中抛出的特定异常做一件事,而对同一块中的另一种异常做不同的事。所以我通常每个try区块只有一个catch区块。这个规则的一个“例外”是当我抛出一般的应用异常时。

此外,我会经常将trycatch块放在其他tryfinally块中。你已经看到我在前面列出的finally区块中这样做了。

在异常发生时捕捉并处理它

立即处理异常。如果您不确定在当前方法中要做什么,那么从更远程的方法中做一些事情将会更加困难。这种情况的一个“例外”是当您的设计打算抛出异常以创建完整的堆栈跟踪时。

我很感激 Sun 和 Oracle 公司提供的类被设计成将它们的异常抛出到我的代码中。他们当然不能像我这样作为应用程序员来处理它们。但对你来说是真的,就像我的研究生教授曾经说过的那样:“你现在是医生了。”处理异常是你的工作。

几乎不用说,很多时候处理一个异常的最好方法就是忽略它。我经常忽略异常的一个方法是返回一个空白的String。比方说,以为例,您将从 Oracle 数据库中选择的一个电话号码传递给一个 Java 方法,该方法将电话号码格式化为圆括号和破折号(例如,( 800)555-1212)。但是,当前记录有一个null电话号码。你不会想为这种琐碎的事情抛出一个异常。您可以做以下两件事之一(或两件都做):

  1. 测试来电号码是否为null并立即返回空白的String.
  2. 格式化任何东西,如果它抛出异常,返回一个空的String——但是不要从你的电话号码格式化方法抛出异常。

你为什么不采用第一种方法呢?除了null值之外,格式化电话号码可能还有其他问题。电话号码太短或者包含字母字符怎么办?因此,为了防止这个简单的格式问题引发异常,我们更倾向于第(2)种方法。请注意,这个示例是针对来自数据库的数据,如果您正在处理用户输入,您可能希望通知用户格式问题,以便他们可以修复它们。

从你的 Catch 模块中给出反馈

有些异常需要记录和交流,特别是在系统管理员需要立即采取措施纠正错误的情况下,或者在应用程序员需要解决处理问题的情况下。在这些情况下,需要传输足够的情况细节(如堆栈跟踪和正在处理的数据)。有三种常见的数据传输方式:

  • 将其记录在错误日志文件或数据库中。
  • 向电话支持地址发送电子邮件或其他消息。
  • 将消息打印到控制台或应用窗口。

在客户机/服务器应用中,应用用户很少需要获得错误的详细信息,所以将数据发送到控制台或应用窗口无助于解决问题,但是您确实需要用一条合理的消息通知用户有问题,比如“发生了一个错误。我们已经通知了管理员。请稍后再试。谢谢你。”

如果错误不太紧急,您可能会依赖于记录详细信息,并希望管理员在某个时候查看日志;否则,您可能需要通过电子邮件或其他消息系统直接向管理员发送消息。

控制任何重大停机中异常报告的数量

当捕捉异常并记录或发送电子邮件或其他消息时,要注意过度报告问题可能会使问题变得更糟。当组织中的每个人同时遇到相同的问题时,您不希望管理员的电子邮件立即被 1,000 或 10,000 条完全相同的错误消息填满。你需要控制流量。

我的方法(这里不详细说明)是发送前 10 条相同的消息,然后前 100 条中的第 10 条,最后第 100 条。这样,即使生成了 1,000 条消息,管理员得到的消息也不到 30 条。

考虑抛出一个通用异常

对于 web 应用,我使用了抛出一般异常的方法。不管你在应用的方法中有多深入,当你捕捉到一个异常时,你处理它,然后抛出一个通用的应用异常。您抛出的异常没什么特别的——它是一个普通的异常,可能扩展了Exception,但是没有其他成员。

为什么要创建这样一个外观作为一般的例外?我们的应用需要一些简单而独特的东西,可以像旗帜一样使用。通用异常是一个标志,它让我们知道我们已经处理了异常,只是通知了命令链。在所有的应用方法中,无论有多深,您也会捕捉到这个通用的应用异常。姑且称此为立面例外GenAppException。您的标准try / catch语法应该是这样的:

try {         … } catch( GenAppException g ) {         throw g; } catch( Exception x ) {         // Deal with x         throw new GenAppException(); } finally{         … }

这里要注意的是,无论我们在这里捕捉到什么异常,如果我们不能忽略它,我们最终都会抛出一个GenAppException。我们要么抛出从更深层次捕捉到的GenAppException,要么抛出一个新的。

当您捕获这些通用应用异常时,您知道两件事:

  • 我们需要注意一个例外。
  • 它是在代码的前面处理的,所以我们需要做的就是传递它(抛出它)。

关键是:当您到达顶层,准备将 web 应用中的某些内容呈现给用户的浏览器时,您会捕捉到任何异常,并向用户呈现一个合理的错误页面。他们不想读取您的堆栈跟踪或异常详细信息,显示这些数据可能是一个安全问题。当您最初处理最初的异常时,所有这些信息都应该被记录下来或通过电子邮件发送给系统管理员或应用程序员。

关闭 finally 块中的本地资源

我们已经在示例代码中讨论过这一点。让我总结一下,每次你打开一个数据库ConnectionStatement,ResultSet,你需要将open()和所有对该资源的使用嵌入到一个try块中,并关闭相关联的finally块中的资源。这个规则不仅适用于甲骨文Connections,也适用于FilesBuffersStreams等等。

Java 虚拟机沙箱

我们已经研究了 JVM,看到它是编译后的 Java 字节码和运行它的特定机器硬件和操作系统之间的接口。这使得 Java 字节码能够“在任何地方运行”——也就是说,在任何有 JVM 的地方。

除了提供可移植性,JVM 还可以通过建立运行时安全沙箱来增强安全性。沙箱是一组适用于 JVM 中运行的所有 Java 代码的规则。在大多数安全策略有效的情况下,这些规则会阻止 Java 代码读写硬盘,并阻止打开网络连接。当 JVM 在浏览器(如 Internet Explorer 或 FireFox)中运行时,Java 代码(通常是 Java applet)不允许与网络上的任何其他机器通信,除了加载它的机器(提供包含 applet 标记的网页的 web 服务器)。

在命令提示符和许多 Java web 服务器中,JVM 中没有沙箱。在这种情况下,您的 Java 代码可以打开和写入文件,并从网络上的其他机器读取。

在 Oracle JVM 中,沙箱相当死板,阻止了 Java 代码对硬盘的读写;并且完全不能在网络上通信;但是,它提供了对 Oracle 数据库的开放访问,但受到授予执行 Java 代码的 Oracle 用户及其角色的 Oracle 特权的限制。

对于每个沙箱,可以通过多种方式授予额外的权限。对于小程序,代码需要用证书签名才能被授予额外的特权。当它在浏览器中运行时,浏览器向计算机用户提供证书,用户必须接受证书和特权,特权才能生效。一旦证书被接受,applet 就被允许执行特权操作。

在 Oracle 数据库上,权限由管理命令授予,并存储在数据库中。特权授予可以是宽泛的,也可以是非常细化的。我们将对运行在 Oracle 数据库中的 Java 代码使用扩展权限,以便通过互联网与其他机器进行通信。当我们读到第九章时,你会看到一些非常细致的特权授予。

章节回顾

尽管这只是对 Java 语言的一个简要介绍,但它已经为您提供了足够的基础知识。你不能把安全建立在沙滩上;相反,你需要一个基础。Java 编程中有许多选项和风格,我们希望有意识地选择我们的编码实践来保证、增强和维护安全性。

我们学习了包以及它们与目录路径的关系。我们还学习了CLASSPATH环境变量。

本章介绍了一大堆术语和概念,包括类、实例、对象、方法、成员和构造函数。此外,修改符staticpublicprivate也被覆盖。我们讨论了main()方法。

最后,我们讨论了异常处理和try / catch / finally语法。

我们将在本书的其余部分使用所有这些 Java 语法元素。如果他们现在对你来说是新的,不要担心。很快它们就会成为你的第二天性。

四、Java 存储过程

当我第一次发现 Java 存储过程时,我向我工作的公司的 IT 部门正式介绍了我的发现,给出了几个例子,包括在 Oracle 数据库中使用 Java 来发送电子邮件、读取网页和计算用于应用用户认证的 Unix crypt 值。那是在 2001 年,我们在阅读该案文时将会看到,我仍然在提出同样的想法。然而,直到现在这些想法才得以实现,因为 Oracle 数据库 11g 升级到了 Oracle JVM。在 Oracle database 11g 中,Oracle 升级了 Oracle JVM,使其可以在 Java 1.5 版上运行,其中包括对 JVM 中包含的 Java 加密扩展(JCE)包的重大升级。

那么,Java 加密和 Oracle 有什么关系呢?标准的 Oracle 数据库不提供现成的传输到客户端应用的数据加密。您可以购买诸如 Oracle Advanced Security 之类的附加组件来提供它,但是随着 Oracle JVM 在数据库中运行,我们可以使用 Java 在 Oracle 数据库上进行数据加密。在 Oracle 数据库中获得 Java 功能的唯一方法是通过 Java 存储过程。在 Oracle 数据库中运行用 Java 编写的程序将会给我们带来加密和更多好处。

在以前版本的 Oracle JVM 中运行增强加密没有任何障碍,自从 2001 年我第一次发表演讲以来,我一直在这样做。唯一的问题是努力和标准化,以及算法的审查(接受)。在 Java 1.5 中增强 JCE 之前,我们必须获取算法或自己编写算法,并手动安装到 Oracle 数据库中。然而,对于加密,我们现在能够调用 JCE 中包含的标准函数和算法。

在本章中,我们将学习如何从 Oracle 数据库上的 SQL 查询连接到 Oracle JVM,以便让 Java 执行我们的处理并将数据返回给我们。我们还将讲述从 Java 连接到 Oracle 数据库和运行数据库查询的基础知识。其实我们会从 Oracle 通过 Java 连接,用 Java 读 Oracle。

Java 存储过程示例

对程序员来说,没有什么比代码更清楚了。让我将要展示的例子作为开始理解 Java 存储过程如何工作的指南。你会看到一个 Oracle 头,以粗体显示在清单 4-1 中的 Java 代码。在 Java 代码之后,您还将观察 Oracle 函数的定义,Java 代码封装了 Java 代码。您不能从 Oracle 查询中直接调用 Java 代码,但是您可以调用 Oracle 函数或过程,然后它将调用并运行 Java。

在这里放一个书签,这样你可以在接下来的讨论中参考清单 4-1 。我们很快会将这段代码加载到 Oracle 数据库中。

清单 4-1。 MyApp4f_get_oracle_time

`SET ROLE appsec_role;

CREATE OR REPLACE AND RESOLVE JAVA SOURCE NAMED myapp4 AS

package pkg4;
import java.sql.*;
import oracle.jdbc.driver.OracleDriver;
public class MyApp4 {
    public static String getOracleTime() {
        String timeString = null;
        Statement stmt = null;
        try {
            //Class.forName( "oracle.jdbc.driver.OracleDriver" );
            //new oracle.jdbc.OracleDriver();
            //Connection conn = new OracleDriver().defaultConnection();
            Connection conn = DriverManager.getConnection("jdbc:default:connection");
            stmt = conn.createStatement();
            ResultSet rs = stmt.executeQuery( "select sysdate from dual" );
            if( rs.next() ) {
                timeString = rs.getString(1);
            }
        } catch( Exception x ) {
            timeString = x.toString();
        } finally {
            try {
                if( stmt != null ) stmt.close();
            } catch( Exception y ) {}
        }
        return timeString;
    }
}
/

CREATE OR REPLACE FUNCTION f_get_oracle_time
    RETURN VARCHAR2
    AS LANGUAGE JAVA
    NAME 'pkg4.MyApp4.getOracleTime() return java.lang.String';
/`

images 清单 4-1 中的代码可以在名为 Chapter4/AppSec.sql. 的文件中找到

在我们进一步讨论之前,我想指出这个示例代码的一点。我知道我说过包,像我们的示例类MyApp4中的pkg4,是和目录协调的,但是没有 pkg4 目录。我还说过,在每个 *中总是定义了一个公共类。java 文件,其类具有与该文件相同的名称。然而在我们的例子中,没有MyApp4.java文件,我们拥有的文件甚至没有命名为“myapp 4”dot 任何东西。

这种差异的原因是我们只在 Oracle 数据库上定义了这个类。现在,我们不能在客户端编译或运行它。当我们将它加载到 Oracle 数据库中时,我所说的关于包和类的事情突然又变成了现实。Oracle 数据库创建一个虚拟包目录,它可以像搜索类路径一样搜索该目录以找到类。此外,它像普通的 *一样编译代码。java 文件并创建类文件,即虚拟文件,存储在数据库中。

获取加载 Java 存储过程的权限

应用安全用户appsec需要CREATE PROCEDURE特权来将 Java 加载到数据库中。她通过appsec_role获得这种特权。回想一下,我们将appsec_role设置为appsec用户的非默认角色。由于这个原因,当appsec连接到 Oracle 数据库时,她在开始时没有这个角色。相反,她必须通过为当前会话设置appsec_role来启用它。显示的方法(SET ROLE appsec_role)就足够了,正如您回忆的那样,新角色成为appsec在当前会话中的唯一角色。另一种方法是使用以下命令请求在当前会话中启用授予appsec的所有角色(默认和非默认):

SET ROLE ALL;

在 Oracle 数据库中加载 Java

有几种方法可以将 Java 代码放入 Oracle 数据库。一种方法是使用 loadjava.exe ?? 实用程序。该实用程序在 Oracle 数据库和某些版本的 Oracle 客户端软件中都可用。它与sqlplus.exe和其他 Oracle 应用在同一个 bin 目录中。使用 loadjava ,我们可以向 Oracle 数据库提交一个 java 文件(或 sqlj 文件,java 代码中嵌入了 SQL)、一个类文件,甚至一个 jar 文件。服务器以适当的方式处理我们提交的任何内容。这里有一个示例loadjava命令,我们可能已经使用它来加载一个名为MyApp4.java的 Java 文件(参见清单 4-1 )。

loadjava -force -resolve -user appsec/password@orcl pkg4/MyApp4.java

该命令将作为appsec用户连接到 Oracle 数据库,然后从 pkg4 目录中读取MyApp4.java文件,并将其提交给 Oracle 数据库。新代码将覆盖以前存在的同名代码,Oracle 数据库将编译代码并将虚拟的 MyApp4.class 文件放在虚拟的 pkg4 目录中。

我们可以使用的另一种将 Java 代码加载到 Oracle 数据库的方法是我们在示例中使用的方法,清单 4-1 。我们在这个 Oracle 语句后列出了 MyApp4 类的 Java 代码:

CREATE OR REPLACE AND RESOLVE JAVA SOURCE NAMED myapp4 AS

我们可以在连接到 Oracle 数据库时运行这个CREATE语句,它将完成我们通过loadjava命令完成的相同事情。

在 Java 存储过程中处理异常

正如你在清单 4-1 中的MyApp4类的getOracleTime()方法中看到的,我们从一个try / catch / finally块开始。无论何时处理数据库,都必须准备好捕捉或抛出一个SQLException。有很多情况下会抛出SQLException;例如,如果您拼错了表名或列名,或者您没有权限读取数据,或者 Oracle 数据库有响应问题。IOExceptions也可以在做输入/输出(IO)时生成。与 Oracle 数据库通信是 I/O,读写文件也是 I/O,从网络资源(如 web 服务器)读取数据也是 I/O。

根据第三章中关于异常处理的讨论,我们选择捕捉所有异常并在本地处理它们,将我们将要返回的字符串设置为等于从Exception.toString()返回的值。当我们到达try / catch / finally块的末尾时,最后一行返回timeString,如果成功可能是 Oracle 数据库数据和时间,否则将是Exception.toString()消息。(请注意Exception中的toString()方法不是静态的。你需要一个Exception的实例,比如x,来调用x.toString()。我说Exception.toString()只是为了讨论清楚。)

下面是一个示例Exception.toString()消息:

java.sql.SQLException: Invalid Oracle URL specified.

finally块中,我们关闭了Statement对象stmt。同样,我们正在与 Oracle 数据库打交道,因此我们必须做好准备,抓住任何SQLExceptions。然而,在这种情况下,我们不需要做任何有例外的事情。注意,在我们试图关闭它之前,我们测试了一下stmt是否为null。如果我们从未实例化过Statement对象,该测试将阻止我们生成NullPointerException。(NullPointerException是当我们试图使用一个还没有实例化的类的方法或成员时产生的。)也许你已经猜对了,我们不需要测试stmt是不是null。因为我们在一个try / catch块中,并且没有做任何异常,我们可以尝试关闭stmt,它要么成功,要么我们将捕获NullPointerException。这是真的,但是我不喜欢不必要地生成异常,所以我们测试stmt是否是null

在我们探索 Java 的过程中,另一个值得注意的地方是对timeStringstmt的声明。首先,注意我们在try块之前声明(提到)了它们。我们需要这样做,这样我们就可以将timeString返回到try块之外,这样我们就可以关闭finally块中的stmt,它在try块之外。成员变量是上下文相关的,只存在于它们的声明范围内。也就是说,成员变量只存在于声明它的块(花括号)中。例如,ResultSet成员不存在于try块之外。

关于声明timeStringstmt要注意的第二件事是,它们最初都被声明为null。正如我前面提到的,即使我们没有将它们声明为null,它们也不会指向任何对象(null)。这适用于所有 Java 对象,不仅仅是字符串和语句。但是编译器前瞻,看到我们要返回timeString,它不喜欢返回未知数。如果您试图用这两行代码编译一个方法:

String myString; Return myString;

编译器会报错,拒绝编译你的代码,说myString可能还没有被初始化(设置为指向一个对象或者一个值)。但是如果你把myString的声明改成:

String myString = null;

然后编译器将(至少)接受null值负责,它将编译你的代码。

你可能会问,为什么当我们读完ResultSet时,编译器看不到timeString稍后被初始化(我们称之为timeString = rs.getString(1))? Javac 是一个非常聪明的编译器。它看到了那里的初始化,但注意到初始化是有条件地发生的——也就是说,它发生在if语句之后。

*那么,stmt为什么要声明为null?原因非常相似。如果你像这样声明stmt,甚至没有初始化到null;然后,当编译器向前看,看到您要在if块中测试stmt时,它会报告一个错误,说明stmt可能还没有初始化。

Statement stmt; … if( stmt == null ) …

在这种情况下,抱怨是我们试图在我们的逻辑中使用一些可能还没有初始化的东西。Java 希望做出明智的决定,并认为这是潜在的混乱,即使在编译时也是如此。

从 Java 调用 Oracle 数据库

在名为getOracleTime()的 Java 方法中,在清单 4-1 中,调用了四个类,用于与 Oracle 数据库接口:OracleDriverConnectionStatementResultSet。下面的部分描述了这四个类以及它们的作用。

OracleDriver

第一个调用是加载 Java 数据库连接(JDBC)驱动注册表(list)中的OracleDriver。(可以使用多个驱动;例如 Oracle 数据库、SQL Server 和 MySQL。每个都需要注册。)我展示了注册OracleDriver的几个选项。它们都被注释了,原因我稍后会解释。

    //Class.forName( "oracle.jdbc.driver.OracleDriver" );     //new oracle.jdbc.OracleDriver();     //Connection conn = new OracleDriver().defaultConnection();

第一个选项使用调用实例化一个OracleDriver类,看不见,通过使用Class.forName()调用。Class.forName()方法是一种通过简单地声明对象的完全限定名来实例化对象的方法。通过以这种方式实例化OracleDriver,它被自动加载到驱动注册表中。这是装载OracleDriver的标准做法,你会经常看到。

第二个选项是实例化一个new OracleDriver(),它会自动加载到驱动注册表中。最后,我们可以选择创建一个新的OracleDriver实例(再次自动加载到驱动注册表中),在同一行代码中,使用OracleDriver来获得一个Connection对象。

现在,让我告诉你为什么所有这些调用都被注释了。原因有二。首先,我们将这段代码作为 Oracle 数据库上的 Java 存储过程运行。作为 Oracle 会话的一部分,Oracle JVM 自动加载并注册OracleDriver

第二个原因是,从 Java 版本开始,OracleDriver会根据需要自动加载(在CLASSPATH中有 ojdbc6.jar 和来自连接字符串的提示,例如" jdbc: oracle :thin ")。这不太可靠,因为作为开发人员,您可以选择用任何版本的 Java 编译和运行这些代码。您只需要拥有 JDK 1.5 或更高版本就可以阅读本书,因此我们将继续在您的客户端工作站上运行的任何代码中加载OracleDriver

联系与陈述

接下来在清单 4-1 中,我们建立一个到 Oracle 数据库的连接(在下面重复)。我们根据缺省值定义连接,“ jdbc:default:connection。“我们将把这段 Java 代码放入 Oracle 数据库,因此我们不需要指定 Oracle 服务器或 Oracle 监听器端口号,也不需要识别 Oracle 用户或密码。默认情况下,存储在数据库中的 Java 作为与加载 Java 的模式相关联的用户运行。因为我们使用appsec用户创建这个 Java 结构,所以它将使用默认的角色和特权appsec运行。

Connection conn = DriverManager.getConnection("jdbc:default:connection"); stmt = conn.createStatement();

我们使用我们的Connection实例通过调用conn.createStatement()来创建一个Statement。注意,这些是 Java 中使用的标准 Java 类,用于与任何供应商的 SQL 数据库对话,包括 SQL Server 和 DB2。然而,因为我们请求(或默认接收)了OracleDriver来给我们Connection,所以我们有一个特定于 Oracle 数据库的Connection

每个数据库供应商都必须实现这些标准接口(连接和语句),以使我们的 Java 代码能够使用特定于供应商的驱动程序与他们的数据库进行对话。特定于 Oracle 的驱动程序和类是通过在我们的CLASSPATH中包含 ojdbc6.jar 得到的。我们将使用另外几种类型的StatementsConnections——这些变化是特定于 Oracle 数据库的。

结果集

一旦你有了你的Statement对象,你就可以执行 Oracle 命令,就像我们在例子中执行的一样,在下面的例子中重复。当我们通过调用stmt.executeQuery()执行查询时,我们期望得到数据。来自查询的数据在一个ResultSet对象中返回(在我们的例子中是rs)。通过调用ResultSetnext()方法,我们将下一行(第一行)的值带入ResultSet对象,这样我们就可以用一个“getter”方法请求每个值(查询中指定的行中的每一列),就像我们的示例代码中对rs.getString()的调用一样。

ResultSet rs = **stmt.executeQuery**( "select sysdate from dual" ); if( **rs.next**() ) { timeString = **rs.getString**(1); }

关于ResultSet“getter”方法的一个有趣的事情是,对于一些返回的类型,翻译可以自动完成。例如,在我们的代码中,我们请求一个Date类型(值),因为在我们的查询中我们SELECT了(请求)Oracle 数据库的时间和日期SYSDATE的值。然而,在我们对ResultSet的调用中,我们要求调用getString(),因此ResultSet执行从Date类型到String类型的转换。

把在ResultSet中返回的数据想象成一个带有多行数据条目的电子表格,每一行都有多列相关的值。当您调用ResultSet.next()时,您将获得当前ResultSet值空间中的下一行的等价物。在那里,您可以使用列号从每一列中获取值。在我们的示例中,我们使用语句从第一列中获取值:

rs.getString(1);

唯一的是,ResultSet的索引是从 1 开始的;也就是说,第一列值位于索引 1 处。这和 Java 里的其他东西(我能想到的)都不一样。Java 数组和Collections通常是基于 0 的,第一个元素在索引 0 处。另外,String中的字符位置从 0 开始。

在我们尝试使用它之前,我们几乎总是想看看在ResultSet中是否有任何数据被返回。出于这个原因,我们在if条件语句中放置了一个ResultSet.getString()调用,用于测试是否有下一行。或者,我们可以说:

rs.next(); rs.getString(1);

然而,如果我们这样做了并且没有“下一个”,那么ResultSet值空间将是null,当我们调用rs.getString(1)时,我们将生成一个Exception

此外,我们几乎总是希望在ResultSet中返回一行或多行,而调用if( rs.next() )只会测试是否返回一行。当我们希望得到几行时,我们将使用一个while块来处理ResultSet,例如:

while( rs.next() ) { … }

Java 存储过程中的方法语法

我在第三章的中描述了main()方法中修饰符static的作用。在 Java 存储过程中,我们会看到 Oracle 数据库只从 Java 存储过程中调用static方法。这与我们看到的main()方法的原因类似:Oracle 调用这些方法时没有预先实例化该类的对象。在调用getOracleTime()方法之前,我们不创建MyApp4 ( 清单 4-1 )的实例。然而,正如main()方法可以实例化自身或任何其他对象一样,这些作为 Java 存储过程调用的静态方法(如getOracleTime())可以根据需要实例化自身或其他对象(并非静态方法中使用的所有内容都是静态的)。

从 Oracle 数据库调用 Java

清单 4-1 中的 Java 存储过程(函数)示例代码的第二部分是创建函数f_get_oracle_time的 Oracle 语句。语法(在下面重复)应该是熟悉的——大多数 Oracle 关键字与我们在第二章中创建过程时看到的相同。对于 Java 存储过程(和函数),我们修改过程定义,指出执行处理的代码使用的是不同的语言——在我们的例子中是 Java。我们通过提供完整的 Java 方法规范来指明将调用哪些 Java 代码:

CREATE OR REPLACE FUNCTION f_get_oracle_time **    RETURN VARCHAR2**     AS **LANGUAGE JAVA**     NAME '**pkg4.MyApp4.getOracleTime() return java.lang.String';**

/

注意,我们的 Java 方法规范包括包名(pkg4)、类名(MyApp4)和方法名getOracleTime()。在这个例子中,我们还看到从 Java 发回的返回类型被指定为java.lang.String。如果你看一下 Oracle 函数返回类型的定义,它说:

RETURN VARCHAR2

Oracle VARCHAR2类型对应于 Java String类型。如果您还没有猜到, java.lang 是 Sun (Oracle)保存String类的包。在 Java 存储过程定义中,需要将完全限定的包指定为参数和返回值类型的一部分。

我们确实需要协调 Java 返回类型与 Oracle 数据库中可用的返回类型的一致性。oracle 在 oracle.sql 包中包含了封装其返回类型的 Java 类,对于许多 Oracle 类型,在 java.sql 包中有相应的泛型 Java 类。随着我们的继续,我们将会看到其中的一些。有关 Oracle 数据库类型和相关 Java 类型的更多信息,请参考 Oracle 文档以及名为 Oracle 数据库 JDBC 开发人员指南的文档的附录 A。

尽管这种技术被称为 Java 存储过程,但是您将会看到,在大多数情况下,我们将从 Oracle 函数中调用 Java 方法。Java 方法只能返回一个值。虽然这个值可以是一个复杂的对象数组,也可以是一个有许多成员的对象,但它仍然是一个单一的返回值——一个类似于 Oracle 函数的模型。Oracle 将存储过程定义为执行进程的代码,将存储函数定义为返回值的代码。当然,一个 Oracle 过程可以通过OUT参数返回多个值,如果我们可以将它映射到一个返回多个值的 Java 存储过程,那就方便多了,但是我们不容易做到这一点。当然,Oracle 函数可以在返回值之前执行一个过程。Oracle 函数也可以通过OUT参数返回多个值,但是不鼓励这样做。

VARCHAR 或 VARCHAR2

在创建数据库表和编写 PL/SQL 时,习惯上将字符串列和变量定义为 VARCHAR2 类型。由于历史原因,没有 VARCHAR1。Oracle 数据库目前认为 VARCHAR 是 VARCHAR2 的同义词(PL/SQL 中的一个子类型),但是 Oracle 在其官方文档中警告不要使用 VARCHAR。您可以通过以下链接阅读该警告:

http://download.oracle.com/docs/cd/E11882_01/server.112/e17118/sql_elements001.htm#sthref117

没有理由违背 Oracle 的建议,在创建表和编写 PL/SQL 时,我推荐 VARCHAR2。使用 VARCHAR2 作为字符串类型有一个例外,这个例外发生在编写 Java 代码与数据库交互时。稍后,当我们在一条InsertUpdate语句中定义想要从 Java 传递到 Oracle 的参数时,我们将称它们为OracleTypes.VARCHAR。没有定义OracleTypes.VARCHAR2OracleTypes是 Oracle 提供的一个类,所以VARCHAR2作为一个类型在它不存在的时候很显眼。

这里需要记住的重要一点是,在创建数据库列或定义 PL/SQL 变量时使用 VARCHAR2,并且使用 OracleTypes。VARCHAR 在定义 Java 变量或参数时,要与 Oracle 数据库中的 VARCHAR2 值兼容。

安装和测试示例代码

运行清单 4-1 中的代码。您可以使用任何一个 SQLPlus* 、 SQL DeveloperJDeveloperTOAD 或任何其他能够执行代码的数据库代码编辑器来运行它。第一部分是将您的角色设置为appsec_role的一行。清单 4-1 的第二部分创建了名为MyApp4的 Java 源代码。第三部分创建了f_get_oracle_time函数。

运行代码后,您可以通过调用 Oracle 函数f_get_oracle_time来测试 Java 存储过程。该函数将从 Oracle 数据库返回当前日期和时间。

SELECT f_get_oracle_time FROM DUAL;

这里有一个关于FROM DUAL子句的注释。为了选择数据,SELECT查询必须有一个源。源通常是 Oracle 表或视图。然而,在这种情况下,我们从函数中选择返回值。通过说FROM DUAL,我们满足了在SELECT查询语法中对源的要求,但本质上是说“来自数据库”DUAL实际上是一个虚拟表,其中一列名为DUMMY,一行名为DUMMY=‘X’。您可以从以下查询中看到这一点:

SELECT * FROM DUAL;

如果您现在认为我们可以从任何表或视图中选择SYSDATE,那么您是正确的!然而,我们将得到我们指定的视图或表中的每一行的结果。尝试这个查询,您将看到如下所示的结果:

`SELECT USERNAME, SYSDATE FROM SYS.ALL_USERS ORDER BY USERNAME;

ANONYMOUS           09-JUN-11
APEX_030200         09-JUN-11
APEX_PUBLIC_USER    09-JUN-11
APPQOSSYS           09-JUN-11
APPSEC              09-JUN-11
APPUSR              09-JUN-11
APPVER              09-JUN-11`

为每个用户返回一行,但是我们只为每行返回 username 和SYSDATE值。这是我们可以利用的SELECT语句的一个特性。例如,如果我们从人员列表中选择所有经理,我们可以在名字和姓氏之间插入单词“尖头头发”,如下所示:

SELECT FIRST_NAME || ' Pointy-Haired ' || LAST_NAME     FROM HR.EMPLOYEES     WHERE EMPLOYEE_ID IN (         SELECT DISTINCT MANAGER_ID FROM EMPLOYEES );

双管“||”是在 Oracle 数据库中连接字符串的一种方式。我们在括号中有一个子查询,它选择了在EMPLOYEESMANAGER_ID列中列出的所有 distinct (unique) EMPLOYEE_ID。对于所有把这些MANAGER_ID中的一个作为EMPLOYEE_ID的员工,我们把他们的FIRST_NAME、单词“尖头发”和他们的LAST_NAME连在一起,只是为了好玩。

我们可以为查询返回的每一行选择我们想要的任何内容。我们可以SELECT SYSDATE为每一排,或者我们可以SELECT为每一排“第一个尖头发的老板”。

查看参与者名册

下面是我们的示例代码测试参与者的简要介绍:

  1. 您的客户端(Oracle 命令行)正在呼叫。
  2. Oracle 数据库上封装的函数。
  3. 名为MyApp4 ( 清单 4-1 )的 Java 源代码进行查询。
  4. Oracle 数据库选择SYSDATE.

当我们创建MyApp4 Java 源代码时,Oracle 数据库编译了代码并创建了包 pkg4 和类MyApp4。我们的 Oracle 函数调用了MyApp4类中的静态方法getOracleTime()。在getOracleTime()方法中,我们打开一个到 Oracle 数据库的连接并请求系统时间SYSDATE,然后我们将它作为String返回。我们的函数接收时间字符串并将其作为VARCHAR2返回给我们的客户端,客户端显示日期和时间字符串。

你可以在图 4-1 中看到这些参与者之间的关系。

images

图 4-1。Java 存储过程的流程

在图 4-1 中,我想强调两点。首先,Java 存储过程、函数f_get_oracle_time和相关联的 Java 方法getOracleTime()是如此紧密相关,以至于我将它们放在一个框中。你可能会说他们“连在一起”函数f_get_oracle_time所做的就是将其参数传递给 Java 方法,并返回该方法返回的值。

我想强调的第二件事是,当返回值沿着它的返回路径传递时,它被连续地视为一个DATE类型,然后是一个String类型,然后是一个VARCHAR2类型。

打扫卫生

我们刚刚看到的仅仅是 Java 存储过程如何操作的一个例子。通常情况下,您不会仅仅为了从SELECT语句中获取时间而调用 Java 存储过程。调用内置的SYSDATE函数会简单得多。例如:

SELECT SYSDATE FROM DUAL;

Java 存储过程的真正用处是做一些在 PL/SQL 中很难或不可能做的事情。

因此,让我们使用以下命令从数据库中删除这个示例:

DROP FUNCTION f_get_oracle_time; DROP JAVA SOURCE myapp4;

Oracle Java 虚拟机

当 Oracle 公司第一次在其数据库中包含一个 JVM 时,它称之为 Aurora。您仍然可以在 Oracle Database 11g 包含的目录和文件中找到一些对 Aurora 的引用。

Oracle Database 11g 中的 Oracle JVM 是高度定制的,不能升级或替换为从 Sun (Oracle corporation)下载的标准 JVM。Oracle JVM 有一个全面的安全结构和沙箱,它与 Oracle 数据库紧密相关。这个沙箱是 Oracle JVM 无法被标准 JVM 取代的部分原因。沙盒安全设置可以从数据库中查询并使用 Oracle 命令进行设置(我们将在第九章的中讨论双因素认证时看到)。

基于 Java SE 1.5 的 Oracle JVM

可能是因为 Oracle 公司构建 Oracle JVM 需要做大量的裁剪工作,所以它是当前标准 JVM 之后的一个版本。Oracle JVM 基于 Java 标准版(SE)1.5 版,目前支持 1.6 版。1.7 于 2011 年 7 月发布。谈论版本真的会让这样的文字看起来过时,这一段可能会很快落后;但是几年后,当您的工作站上安装了 Java 1.7 或 1.8,并且您正在使用 Oracle Database 12 时,您可能仍在使用基于 Java 1.5 的 Oracle JVM,Oracle JVM 远远落后于它。

无论如何,Java 1.5 是我们在 Oracle 数据库中需要的,以支持我们对 JCE 的使用。所以我们不会批判,也不会哀叹。

每个 Oracle 会话一个单独的 JVM

当您连接到 Oracle 数据库并使用 Java 时,您会启动一个单独的 JVM,仅供您的会话使用。您不能与其他正在运行的 JVM(Oracle 会话)共享任何 Java 对象或数据,除非您将它们存储在数据库中供其他人查询或通过网络端口服务(电子邮件、ftp、web 等)传递这些对象。)

当您断开与 Oracle 数据库的连接时,您的 Oracle 会话关闭,JVM 终止。如果您立即与 Oracle 建立新的连接,您将获得一个全新的 JVM,并且以前的 JVM 中的任何内容都不会被保留或转移到新的 JVM 中。

这对我们有影响。首先,因为我们将生成加密密钥(您可能已经猜到了),所以我们希望在关闭与 Oracle 数据库的连接并丢弃密钥之前,使用它们来回发送加密数据。我们可以将密钥保存在数据库中,或者 Oracle 所谓的钱包中,这基本上是服务器和/或客户端硬盘上的安全文件。但是如果它们被存放在任何地方,我们就不知道谁会接触到它们。不,我们要做的是动态生成密钥并使用一段时间,在密钥交换和多次查询期间保持连接打开,只有在完成时才关闭连接。

Oracle JVM 安全执行程序

我们在上一章的最后简要讨论了 Oracle JVM 沙箱。概括地说,这个特殊沙箱阻止 Oracle 数据库中的 Java 代码读写硬盘;并且完全不能在网络上通信;但是,它提供了对 Oracle 数据库的开放访问,但受到授予执行 Java 代码的 Oracle 用户及其角色的 Oracle 特权的限制。例如,如果用户HR拥有并正在执行 Java 存储过程,它将可以访问所有的HR模式数据。

Oracle 数据库中的 Java 代码可以被授予扩展的安全特权,以执行通常被沙箱拒绝的功能。这些权限由管理命令授予,并存储在数据库中。特权授予可以是宽泛的,也可以是非常细化的。我们将对运行在 Oracle 数据库中的 Java 代码使用扩展权限,以便通过互联网与其他机器进行通信。当我们读到第九章时,你会看到一些非常细致的特权授予。

Oracle JVM 中禁用了自动提交

在 SQL 数据库中,COMMIT表示将更改合并到现有数据库中。在执行COMMIT之前,数据更新保存在缓存中。进行更新的会话可以看到这些更改,但其他人看到的是更新前的数据。当会话执行COMMIT时,每个人都可以看到所做的更改。对于有多个步骤的事务来说,这是一个强大的概念。在单个会话中,可以执行每个步骤,并且每个后续步骤的操作都可以基于会话中已经进行的更新。当所有步骤都完成时,会话可以发出一个COMMIT,所有的更新都将对现有数据库进行。但是,如果在任何后续步骤中出现问题,所有已完成的步骤都可以被回滚(反转),而不是写入现有的数据库。你可以看到,如果银行把钱从你这里转到我这里,这是多么的方便——他们会从你的账户里把钱转到我的账户里。但是如果他们不愿意把你的钱给我,你希望他们也把他们从你那里拿走的那一步逆转过来。

通常,在 Oracle 数据库中,需要时必须发出一个COMMIT,这是在每次数据更新之后的某个时间(INSERTUPDATEDELETE)。更新数据的 Oracle 过程不应该发出COMMIT——它们假设它们只是事务的一部分,当所有部分都成功时,调用它们的任何代码都会发出COMMIT。《Oracle 数据库架构专家:Oracle 数据库 9i、10g 和 11g 编程技术和解决方案》一书的作者 Tom Kyte(2010 年出版)在他的博客(asktom.oracle.com)中解释说,过程是原子工作单元,所有事务必须由调用者(调用过程的人)控制(和执行)。

自动提交是一种操作模式,在这种模式下,每次更新都会立即提交到数据库。您可以通过执行以下命令来启用此功能:

SET AUTOCOMIT ON;

您可能认为这将使多步事务性更新的能力失效,您是对的。但是,这是 JDBC 的默认模式。因此,在 Java 客户端,当您更新数据库时,更新会立即提交,并且您无法回滚。一个好处是,您不必使用 JDBC 来执行另一个 Oracle 语句来进行更新。

实际上你可以通过调用你的ConnectionsetAutoCommit()方法来关闭 JDBC 的自动提交模式,就像这样:

conn.setAutoCommit(false);

然后,在适当的时候,您可以使用以下命令之一手动提交或回滚事务:

conn.commit(); conn.rollback();

自动提交是纯 Java 不能“随处运行”的一个领域对于运行在 Oracle 数据库中的 Java 代码(Java 存储过程),默认模式是关闭自动提交。这坚持了过程是原子性的,事务由调用者提交的主张,但这与你对 JDBC 的期望相反。

这是我们开发 Java 存储过程时要记住的事情。如果我们更新数据,我们需要发出一个COMMIT

章节回顾

这是一个相当简短的章节,在这一章中,我们已经能够关注在 Oracle 数据库中运行 Java,或者 Java 存储过程。我们看到了几种将 Java 加载到 Oracle 中的技术。

这可能偏离了主题,但是我们也详细讨论了使用ConnectionStatement类从 Java 调用 Oracle 数据库的反向过程。我们还讨论了FROM DUALSELECT查询。

正如我们必须做的那样,我们后退一步来讨论一般的编程问题。在这一章中,我们讨论了异常处理的一些附加方面,并且讨论了成员的声明和初始化。

最重要的是,我们讨论了 Oracle JVM 及其瞬态特性以及与特定 Oracle 会话的关联。此外,我们还讨论了 Oracle JVM 沙箱。*

五、公钥加密

我相信你听说过公钥加密(PKE)。您可能每天都在使用它——如果不是在您的代码或服务器上,那么就是在互联网上。当您在浏览器中使用安全套接字层(SSL)时,或者当您在地址栏中看到锁或钥匙符号并看到https:\\(带 s)作为协议时,您正在使用 PKE 和其他加密。SSL 用于向在线商家发送加密形式的私人数据,如您的信用卡号。

PKE 的基本概念是两个实体共享一组两个密钥(一个公钥,一个私钥),并使用它们相互加密和解密数据。这些密钥相互捆绑,使得用密钥对中的一个(例如,私钥)加密的任何东西只能用另一个(例如,公钥)解密。这种加密/解密是双向的。

PKE 最重要的方面是其中一个密钥是私有的。私钥从不透露给任何人;然而,公钥可以给任何和所有请求者。这对隐私有什么帮助?

请求公钥的一方可以确信,如果该密钥成功地解密了数据,那么它就来自具有私钥的实体。只有私钥可以加密可以用公钥解密的数据。

反之亦然:只有公钥可以加密私钥可以解密的数据,因此使用公钥的人可以确保只有拥有私钥的实体才能解密他们加密的数据。

因为公钥是公开的,所以任何人都可以解密发件人发送的数据。此外,任何人都可以加密数据,并将其发送给创作者解密。这不是一个缺陷,但这是 PKE 的一个方面,我们需要意识到和解释。

在客户端生成密钥

我们将让客户端计算机生成一组密钥。该计算机将公钥的工件(组件)发送到 Oracle 数据库,以便 Oracle 可以构建公钥的副本。然后,Oracle 数据库可以使用只有原始客户端可以解密的公钥来加密数据。

这种方法听起来像是一个完整的解决方案,但是有几个问题我们要等到下一章讨论秘密口令加密时才能解决。首先,任何人都可以在穿越网络时读取公钥工件(也就是说,任何人都可以像嗅探器一样,通过软件读取通过网络的所有数据包。)这意味着我们必须假设每个人都有公钥,并且每个人都能看到和解密客户端用私钥加密并发送给服务器的数据(如果有的话)。

第二个问题是 PKE,至少是我们正在使用的版本,不适合加密大量数据。它是一种块大小有限的分组密码。例如,如果我们的 PKE 密钥的块大小被限制为 128 字节,我们将不得不把数据分成该大小的多个部分,并分别加密每个部分。在此事务的另一方,接收方必须解密每个部分,并重组原始数据。

为了处理更大量的数据,有两种方法:密码块链接(CBC)和流加密。使用 CBC,大型数据被分解成适当大小的块进行加密,然后被解密并自动为用户重新组装。(咻,这下我们肩上的担子轻松多了。)使用流加密,每个位、字节或字节块在通过流时都会被加密/解密。流只是数据传输的通道。你将字节的数据放入一个流中,并以同样的顺序取出字节的数据:先进先出(fifo)。当向存储读取/写入数据时,或者通过网络,或者只是从内存中的一个位置(结构)到另一个位置时,可以存在流。

RSA 公钥加密

我们将在 PKE 加密算法中使用 RSA 公钥加密。RSA 代表算法创造者的姓氏:Rivest、Shamir 和 Adleman。

因为 RSA 使用不同的密钥进行加密(例如,私有密钥)和解密(例如,公共密钥),所以它被称为非对称算法。所有 PKE 都是非对称加密。由于密钥长度很长,RSA 是一种非常值得信赖的加密算法。

生成和使用 RSA 密钥的 Java 代码

我们实现 Oracle 数据库和 Java 安全性的所有代码都将驻留在一个 Java 类中(有一些小的例外;我们将有一些单独的 Java 类来测试我们的过程)。当我们浏览本书的剩余章节时,我们将分阶段开发安全代码,随着我们的进展增加层次和概念。我们的单个 Java 类将随着时间的推移而增长。

我们的类将被称为OracleJavaSecure,我们将在一个名为orajavsec的包中定义它。因为我们没有这个文件的单一版本,所以我们将有多个目录(每章一个)来存放这个 Java 代码的不同版本。这将使编译和运行有点困难,但是我将根据需要提供引用这些文件的说明。

images 注意你可以在文件chapter 5/orajavsec/Oracle javasecure . Java中找到以下代码。我建议您打开该文件,并在我们阅读本章时参考它。

创建一组密钥

清单 5-1 显示了用于创建一组 PKE 密钥的代码。这段代码和本章中的其他 Java 代码都来自于OracleJavaSecure类。

清单 5-1。创建 PKE 键,makeLocRSAKeys()

private static SecureRandom random;     private static int keyLengthRSA = 1024;     private static Key locRSAPrivKey;     private static RSAPublicKey locRSAPubKey;     private static void makeLocRSAKeys() throws Exception {         random = new SecureRandom();         **KeyPairGenerator generator** = KeyPairGenerator.getInstance( "RSA" );         generator.initialize( keyLengthRSA, random );         **KeyPair pair** = generator.generateKeyPair();         locRSAPrivKey = pair.getPrivate();         locRSAPubKey = ( RSAPublicKey )pair.getPublic();     }

我们使用名为KeyPairGenerator的 JCE 类来生成我们的私有和公共密钥。首先,我们实例化一个名为generatorKeyPairGenerator。我们指定generator将创建符合 RSA 算法的密钥。(注意,我们调用静态方法KeyPairGenerator.getInstance(),并要求它获取自身的一个实例。)我们用密钥长度(1024 位)和名为randomSecureRandom类实例来初始化generatorSecureRandom是一个值得信赖的随机数生成器。在下一章中,我们将使用random达到更大的目的。

images 总有一天,一个 1024 位的 RSA 密钥可能不够用,但此时,它仍被认为是相对不可破解的。

KeyPairGenerator生成一个单独的对象:一个我们称之为pairKeyPair。通过调用KeyPair中的方法,我们得到了自己独立的公钥和私钥(locRSAPubKeylocRSAPrivKey)。还要注意,在这个语句中,我们将从KeyPair获得的公钥转换为RSAPublicKey类型:

locRSAPubKey = ( **RSAPublicKey** )pair.getPublic();

只要被强制转换到的对象实现了手头的对象(是其超集),强制转换就可以向该对象添加功能。在这种情况下,RSAPublicKeyPublicKey类的超集。当RSAPublicKey被定义时,这个定义是这样的:

public class RSAPublicKey implements PublicKey …

我们将PublicKey转换为RSAPublicKey,这样我们就可以使用一些只存在于RSAPublicKey类的方法。我们将在下一节看到这些。

当我们运行清单 5-1 中的时,我们以这些键结束。locRSAPrivKey是我们的私钥。我们不会与任何人分享我们的私钥。我们将使用私钥进行加密和解密,因此拥有我们公钥的实体可以解密来自我们的数据,并加密发送给我们的数据。locRSAPubKey是我们密钥对的公钥。我们打算把那把钥匙给所有请求者。

通过网络传递公钥

我们打算把locRSAPubKey交给所有请求者(实际上我们只是直接交给 Oracle 数据库);然而,我们不知道(出于争论的原因)它们将在哪个平台上运行,所以我们不能给它们我们的 Java 加密扩展(JCE)版本的密钥。相反,我们会给他们两个工件,它们可以一起用于在任何系统上构建我们的公钥。

我们只能用我们的两个工件构建另一个相同类型的密钥。我们的神器是locRSAPubModlocRSAPubExp。这些是 RSA 公钥模数和指数,我们可以用这两个数字来计算公钥。清单 5-2 展示了我们如何生成那些工件。

清单 5-2。生成公钥工件

private static BigInteger locRSAPubMod = null;     private static BigInteger locRSAPubExp;     …     locRSAPubMod = locRSAPubKey.getModulus();     locRSAPubExp = locRSAPubKey.getPublicExponent();

方法getModulus()getPublicExponent()RSAPublicKey类中可用,但在PublicKey类中不可用。为了使用这些方法,我们将PublicKey转换为RSAPublicKey(见上一节)。本质上,我们要求 RSA 公钥给我们它的模和指数的值。

这些数值太大,如果不将它们包装在 Java 对象中就无法处理。我们将它们作为BigInteger对象。它们可能是非常大的数字(尤其是模数),不太适合标准的原始类型,如integerslongs,因此,我们经常将这些数字当作Strings来处理和传输。

你可能已经注意到我们声明locRSAPubModnull。你会从第四章中的“声明和初始化成员”一节中回忆起我们这样做的原因。在这种情况下,我们计划测试locRSAPubMod是否是null,看看我们是否需要生成密钥,或者它们是否已经存在。

序列化对象

我们不仅限于在 Oracle 数据库之间来回传输简单类型的值。在 Oracle 中,我们可以定义保存数组或数据结构的新类型,我们可以用RAW格式(有序的字节序列)传输对象。如果要传输非常大的对象,可以在 Oracle 中以二进制大对象(BLOB s)的形式传输和存储。

对于通过网络传输 Java 对象和存储 Java 对象,有一个要求;也就是说,Java 对象必须实现Serializable接口。通常,接口是一组成员和方法,它们必须由实现接口的任何类来实现(在代码中设置)。(我们将在第十章中创建自己的接口。)但是,有了Serializable接口,就没有要实现的成员或方法;相反,实现接口只是表明您打算打包您的类的一个实例以便传输或存储。你可以通过一个ObjectStream发送一个Serializable对象,或者用一个ObjectWriter类将它写入一个File

有些对象无法序列化(无法实现Serializable)。如果某个对象的状态是通过关联而不是作为值存在的,那么该对象就不能被序列化。例如,如果我们的对象有一个 Oracle Connection成员,那么连接就不能被序列化(您不能保存一个连接供以后使用),我们的对象也不能被序列化。

从工件构建公钥

在加密/解密过程的另一方面,我们想要获得公钥的必要构件(组件),并且我们想要重建密钥。我们将在一分钟内查看如何将组件从客户机传输到服务器,但是对于这个讨论和测试,我们可以简单地在本地获取它们并构建密钥(我们将在服务器端使用相同的代码)。清单 5-3 显示了从密钥的组成部分构建公钥的代码。

清单 5-3。从构件中生成公钥,makeExtRSAPubKey()

    private static RSAPublicKey extRSAPubKey = null;     private static void makeExtRSAPubKey( BigInteger extRSAPubModulus,         BigInteger extRSAPubExponent ) throws Exception     {         RSAPublicKeySpec keySpec =             **new RSAPublicKeySpec( extRSAPubModulus, extRSAPubExponent );**         KeyFactory kFactory = KeyFactory.getInstance( "RSA" );         extRSAPubKey = ( RSAPublicKey )kFactory.generatePublic( keySpec );     }

我们将 RSA 模数和 RSA 指数交给这个方法,makeExtRSAPubKey()。然后我们实例化一个名为keySpecRSAPublicKeySpec的新实例,它是一个键的规范对象。基于 RSA 算法,我们还得到一个名为kFactoryKeyFactory实例。我们将keySpec传递给kFactory来生成公共 RSA 密钥。这个密钥相当于最初生成的公钥,因此我们可以用它来解码用原始私钥编码的数据。

这一切意味着什么?我们将如何使用这个所谓的等价键?我们在客户端生成密钥对。然后,我们将公钥模数和指数传递给 Oracle 数据库。我们在 Oracle 数据库上组装一个相同的公钥。此时,我们的计划是用 Oracle 上的公钥加密数据,并将加密的数据传输到客户端。只有客户端能够使用私钥解密数据。

生成 RSA 密码

密钥实际上只不过是一种数学构造;它本身并不具备我们需要使用的功能。相反,我们需要有一个可以使用密钥来完成我们工作的Cipher类。我们用清单 5-4 中的代码生成CiphercipherRSA

清单 5-4。生成 RSA 密码,makeCryptUtilities()

    private static Cipher cipherRSA;     private static void makeCryptUtilities() throws Exception {         cipherRSA = Cipher.getInstance( "RSA" );     }

当我们调用makeLocRSAKeys()时,将从客户端调用我们的makeCryptUtilities()方法,当我们调用makeExtRSAPubKey()时,将从 Oracle 数据库调用方法。

Cipher类可以生成各种类型的密码。我们特别创建了一个使用 RSA 算法的Cipher的新实例。

注意,我们不说new Cipher(),而是调用静态Cipher.getInstance()方法来获取自身的一个特定实例。如果你回顾一下我们到目前为止在本章中看到的代码,你会发现我们经常没有用new语句在代码中实例化一个对象;相反,我们从某个提供者方法、某个工厂或某个其他提供者那里获得新的实例。重要的是要明白,在这个过程中的某个地方,某种方法得到了一个new实例来交付给我们。

使用 RSA 密码

我们将使用cipherRSA来做加密和解密。要使用它,我们必须向它提供密钥,并告诉它使用什么模式(加密或解密),然后我们将要求它对一些数据执行该任务。清单 5-5 中的cipherRSA方法调用将用于该过程。

清单 5-5。初始化密码进行加密

cipherRSA.init( **Cipher.ENCRYPT_MODE**, extRSAPubKey, random ); byte[] encodedBytes = cipherRSA.**doFinal( clearBytes );**

这里调用的第一个方法用于将Cipher初始化为特定的模式:加密或解密。在这种情况下,我们要求它准备好加密数据(Cipher.ENCRYPT_MODE)。为了解密,我们将把Cipher初始化为Cipher.DECRYPT_MODE

我们调用的第二个cipherRSA方法是doFinal()。这个方法对我们在参数中提供的数据进行加密或解密(例如,加密clearBytes)。注意,CipherdoFinal()方法将一个字节数组作为其参数,并返回一个字节数组。出于这个原因,在我们执行加密时,我们将把Strings和其他对象与字节数组相互转换。还要注意,一个字节只是八个比特,一个比特只是一个被关闭或打开的存储器位置(值为 0 或 1)。一个字节数组只是一系列的字节集合,并作为一个列表来处理。如果对其中的一些概念不熟悉,可以在[www.wikipedia.org](http://www.wikipedia.org)探索计算机架构。

JAVA 常量

注意我们如何在清单 5-5 中的初始化调用中指定ENCRYPT_MODE。这是一个成员变量(注意,它不是一个方法调用——没有括号或参数。)还要注意,它必须是静态的;我们不引用一个特定的Cipher实例来获取ENCRYPT_MODE,而是引用静态类定义。最后,请注意成员名全部是大写的。根据 Java 命名约定,这个成员变量的大写字母和单词之间的下划线字符表示这是一个常量。在Cipher,它会这样宣布:

public static final int ENCRYPT_MODE = <some int>;

修饰符final有助于将这个成员标识为一个常量,但是final可能不是您最初所想的意思。这并不意味着该值不能改变,而是指内存中指向的地址不能改变为新地址。对于原始的和简单的类型,像int ENCRYPT_MODE,这是一回事。这包括Strings,因为它们是不可变的(参见第三章)。

然而,如果你有一个常量成员Date,定义如下

public static final Date BERLIN_WALL_FELL = new Date( 89, 10, 9 );

你不能阻止某人以后来设置:

BERLIN_WALL_FELL.setYear( 80 );

即使这个Date被声明为static final。改善历史不是很好吗!

这种用三个整数代表年、月、日来定义Date的方法已被弃用,这意味着它仍然可用,但不再被批准在代码中使用。在 JDK 的未来版本中,可能会移除不推荐使用的功能。我在这里使用它只是为了说明的目的。也许这个Date构造函数被弃用的原因是指定整数的不一致性。这个建构函式会将 1900 加入年份规格,并将 1 加入月份。月的规格是从 0 开始的(0-11),但是日是从 1 开始的(1-31)。

注意,BERLIN_WALL_FELL Date成员仍然指向Date的同一个实例(我们没有实例化一个新的Date),但是我们改变了值。事实上,那个成员变量满足了一个 Java 常量的所有要求,但结果却是可变的(而不是常量)!所以,我的建议是选择一些其他的、更简单的、原始的或不可变的类型作为常量,就像这个String:

public static final **String** BERLIN_WALL_FELL = “November 9, 1989”;

如果您必须使用一个可变的类实例作为常量,那么我建议您将其设为私有,并拥有一个公共的 getter 方法。不要提供 setter 方法。此外,回想一下,当您通过 getter 方法提供对象时,您传递的是对原始对象的引用,而不是将原始对象的副本发送给调用者。如果你只是返回你的私有成员,并且它是可变的,那么调用者可以改变它。为了避免这个问题,通过克隆对象来创建常量类类型的新实例,并返回克隆(实际上是返回对克隆的引用)。

这一切看起来可能是这样的:

private static final Date BERLIN_WALL_FELL = new Date( 89, 10, 9 ); public static final Date getBERLIN_WALL_FELL() {      return (Date)BERLIN_WALL_FELL.clone(); }

因为我们的常量是私有的,没有人能修改它,除非我们公开它。我们允许人们通过 getter 方法获得它的值,getBERLIN_WALL_FELL()。然而,我们不返回对我们的常数的引用,而是返回对我们的常数的克隆的引用。我们可以这样做,因为Date实现了Cloneable接口。注意,clone()方法返回一个Object,我们必须将其转换为Date才能返回。

调用这个 getter 的人总是可以改变他个人对这个Date的引用或Date的值;但即便如此,每个调用原始getBERLIN_WALL_FELL()方法的人都会得到一个带有正确值的Date

当我们在讨论final修饰符时,我们应该注意到在最后一节中getBERLIN_WALL_FELL()方法被声明为final。这有几个影响。我们需要准备坚持的第一个想法是,我们称之为final的任何东西都不应该改变。主要原因是,如果我们修改了代码,任何引用我们称为final 代码都需要重新编译。我们应该认为这些final方法是常量方法。(也许我们应该同意通过给我们的方法所有大写名称在单词之间加下划线来表明这一事实。目前还没有这方面的惯例。)这种重新编译所有引用代码的想法的例外情况很难识别,因此这种想法就像是您应该遵循的规则。

final方法和final成员一样,可以自动内联编译,这意味着编译时代码或值包含在 referrer 字节码中。这有一些性能上的好处,但是如果final代码发生变化,重新编译的需求通常会超过这一点。

将我们的方法声明为final的第二个效果是它们不能被实现我们的类的其他类覆盖。这是我们将OracleJavaSecure.java中的方法声明为final的主要原因。我们正在保护我们的逻辑不被修改或欺骗。我们必须遵守这样的规则:我们的final方法永远不会改变,或者说final方法都是private,这样我们就是唯一引用它们的人。在下一节中继续这个想法。

获取 RSA 公钥工件

您可能已经注意到,在清单 5-1 到 5-4 中,我们所有的键和组件不仅声明了static,还声明了private。我们不提供任何直接的访问权限。如果你非常精明,你可能还会注意到我们到目前为止的方法都被声明为private:即makeLocRSAKeys()makeCryptUtilities()makeExtRSAPubKey()都是private。我们从不期望我们的客户端应用,也不期望 Oracle 中的 Java 存储过程直接调用这些方法。我们将定义一些public方法,这些方法将根据需要调用这些private方法。

我们现在将遇到的前两个public方法是为了获得公共 RSA 密钥工件。我们将要求客户端应用调用清单 5-6 中显示的这些方法。

清单 5-6。获取公钥的方法构件,getLocRSAPubMod()getLocRSAPubExp()

    private static BigInteger **locRSAPubMod = null;**     private static BigInteger locRSAPubExp;     **public** static final String getLocRSAPubMod() {         String rtrnString = "getLocRSAPubMod() failed";         try {             **if ( null == locRSAPubMod )** makeLocRSAKeys();             rtrnString = locRSAPubMod**.toString();**         } catch ( Exception x ) {             x.printStackTrace();         }         return rtrnString;     }     **public** static final String getLocRSAPubExp() {         String rtrnString = "getLocRSAPubExp() failed";         try {             **if ( null** == locRSAPubMod ) makeLocRSAKeys();             rtrnString = locRSAPubExp**.toString();**         } catch ( Exception x ) {}         return rtrnString;     }

我想指出这些public方法中的几点。首先,注意我们测试了locRSAPubMod是否是null。回想一下,当我们第一次声明locRSAPubMod(在这段代码的顶部)时,我们将其设置为null。(如果我们用这个语句private static BigInteger locRSAPubMod声明它,它也应该是null,但是因为我们把它放到了一个if测试中,当我们试图编译时,我们会得到一个“可能没有被初始化”的错误。)如果在if测试中,locRSAPubMod不再是null,那么我们一定已经创建了 RSA 密钥对。但是,如果仍然是null,那么我们就把private方法叫做makeLocRSAKeys()

另外,请注意测试语法if( null == locRSAPubMod ),它与if( locRSAPubMod == null )相同。使用第一种语法有一个好处。使用一个等号而不是两个等号的常见错误,在第二种语法中不会被编译器捕捉到。但是在第一种语法中,这将被标记为错误if( null = locRSAPubMod )

这些成员中的每一个,locRSAPubModlocRSAPubExp都是BigInteger类——一种在 Oracle 中不存在的类型。我前面提到过,我们经常把这些大数作为Strings来处理和传输,这里就是这样。我们称之为BigInteger.toString()法。当我们到达 Oracle JVM 时,我们将基于这些Strings重新创建BigIntegers

我们正在返回一个StringrtrnString,如果有问题(一个Exception或其他获取locRSAPubModlocRSAPubExp的问题),那么我们可以在我们的常规返回值中返回一个错误消息。您可以看到,我们将每个方法顶部的错误消息定义为rtrnString,在一种情况下为“getLocRSAPubMod() failed”。然后,如果我们成功了,我们用请求的密钥工件覆盖rtrnString。但是在有问题的情况下,我们发送回原始消息。在客户机/服务器应用中,当出现问题时,我们需要发送有意义的错误消息。

因为此时我们正在客户端上运行,所以我们可以很容易地包含一个语句来从异常中转储堆栈跟踪,就像我们在上面第一个方法的 catch 块中所做的那样。这通常是我们类中调用的第一个方法,所以这是一个捕捉Exception的好地方,如果有任何方法的话。请注意,在我提供的代码中,故障排除和调试已经完成,大部分错误报告已经删除。

使用静态方法和私有构造函数

回想一下,对于每个需要 Java 的 Oracle 会话,我们都有一个新的 Oracle JVM。只要我们维护那个会话,我们的 JVM 就会被保留。还记得 Java 存储过程只能调用static方法,因为没有实例可以从存储过程中调用。我们总是可以实例化OracleJavaSecure类,然后使用实例的非静态方法;然而,如果我们能够做到,并且我们能够做到,将我们做的和接触的每一件事static都放在我们的班级里,事情就会简单得多。到目前为止,您可以在我们看到的代码示例中看到这种方法。

注意,在方法内部声明的任何成员都不能声明为staticstatic修饰符只能在类级别(即类成员)应用于我们方法之外的项目。另一方面,static方法中引用的任何类成员都必须static。这是因为在一个static方法中,你只能引用这个类中的成员,无论有没有这个类的实例(例如static成员)。

确保我们的类永远存在的一种方法是保证它永远不会被实例化。我们可以通过将该类的默认且唯一的构造函数设为private来保证这一点。我们已经用清单 5-7 中的代码完成了这个任务。

清单 5-7。私人建造师

    **private** OracleJavaSecure() {     }

从静态初始化器实例化连接成员

在我们的代码中,我们需要一个 Oracle Connection成员。当我们在客户端运行时,我们会将一个Connection传递给一个 setter 方法来存储在我们的类中使用的Connection,但是当在 Oracle 数据库中运行时,我们没有一个好的方法来实例化Connection。我们可以在每个可能用到的public static方法中实例化Connection,但是更好的方法是有一个静态初始化器。这种方法看起来就像我们在清单 5-8 中看到的。

清单 5-8。连接的静态初始化器

    private static Connection conn;     **static** {         try {             **conn** = new OracleDriver().defaultConnection();         } catch ( Exception x ) {}     }

独立代码块前面的static修饰符是一个静态初始化器——这不是一个方法。注意,我们必须准备捕捉任何可能由实例化Connection产生的异常。我们不必对可能的异常做任何事情(如果有一个Exception,那么我们的问题比我们的代码更系统化),但是我们必须建立try / catch块来使代码有效编译。

作为一个static块,它执行一次,存在于类中,在类的外部,没有类的实例。

客户端和服务器使用一个代码

对我来说,用 Java 和 Java 存储过程编码最令人愉快的一个方面是能够编写在客户机/服务器通信双方都可以操作的代码。这在测试我们的代码时尤其有用——在许多情况下,我们可以在将代码部署到 Oracle 数据库之前,在我们的工作站上测试所有代码。

我们代码的某些方面只能在客户端执行;例如生成 RSA 密钥对的方法。同样,我们代码的某些部分通常只在 Oracle 数据库上执行,例如从工件中重建我们的 RSA 公钥。但是对于测试,我们可以在我们的工作站上执行 Oracle 指定的方法。

images 请参考附录 A 中OracleJavaSecure所有方法的列表及其主要用途(客户端或 Oracle 数据库或两者)的说明。

在其他情况下,我们将不得不提供特定的逻辑来允许我们的代码在两种环境中运行。一个典型的例子是Connection成员。在上一节中,我提到了静态初始化器,它将实例化一个用于 Oracle 数据库的Connection。该代码也在客户端上运行,但是没有任何效果——没有供代码连接的本地默认 Oracle 数据库。所以我们提供了一个叫做setConnection()的额外方法,它将从一些客户端应用代码中获取一个Connection,并将static Connection成员设置为那个Connection,如清单 5-9 所示。

清单 5-9。客户端连接的 Setter 方法,setConnection()

    public static final void setConnection( Connection c ) {         conn = c;     }

我们还会有一些在客户机或服务器上运行的特定逻辑的例子,我会在后面指出。

在我的成员命名方案中,我可以选择任何具有相同效果的名称,但是我选择用 loc 前缀命名我在本地生成的键和工件,比如locRSAPrivkey。传递给我的工件和从那些工件生成的键,我都加上一个 ext 前缀(表示“外部”),比如extRSAPubKey。这将有助于我们把事情搞清楚,特别是当我们在工作站上执行客户端和服务器端代码的时候。

在客户端进行测试

我们在 OracleJavaSecure 中的代码旨在作为客户机-服务器应用运行,在一端(例如服务器)加密数据,在另一端(例如客户机)解密数据。然而,能够测试我们的代码而不引入一些客户端-服务器通信的复杂性是很好的。因此,我们将只在客户端测试我们所有的代码。在客户端,我们将生成密钥,用私钥加密数据,从公钥工件构建等价的公钥,并用等价的公钥解密数据。

编写 main()方法

根据我的经验,放置测试代码的最佳位置是在你测试的函数所在的类中。显然,你必须运行你的代码来测试它,如果你能从命令提示符下运行它,那么你就不需要构建或者依赖另一个类来进行测试。每当我们谈论从命令提示符执行时,我们都在谈论使用main()方法。OracleJavaSecure.main()方法的客户端专用测试部分如清单 5-10 所示。

清单 5-10。仅在客户端测试的方法,OracleJavaSecure.main(),A 部分

    public static void main( String[] args ) {         try {             // As a client application             String clientPubModulus = getLocRSAPubMod();             String clientPubExponent = getLocRSAPubExp();             // Send modulus and exponent to Oracle Database, then             // As if I were the Oracle Database             makeExtRSAPubKey( clientPubModulus, clientPubExponent );             cipherRSA.init( Cipher.ENCRYPT_MODE, extRSAPubKey, random );             Date **clientDate = new Date();**             String **sampleData = clientDate.toString();**             byte[] clearBytes = sampleData.getBytes();             byte[] **cryptBytes = cipherRSA.doFinal( clearBytes );**             // Send the cryptBytes back to the client application, then             // As a client application             cipherRSA.init( Cipher.DECRYPT_MODE, locRSAPrivKey );             byte[] newClearBytes = cipherRSA.doFinal( cryptBytes );             String newSampleData = new String( newClearBytes );             System.out.println( "Client date: " + newSampleData );         } catch ( Exception x ) {             **x.printStackTrace();**         }         **System.exit( 0 );**     }

main()方法中,我们有两个方法String成员,clientPubModulusclientPubExponent。我们通过调用两个公共方法getLocRSAPubMod()getLocRSAPubExp()来设置这些成员。我们充当客户端应用,调用这些方法。我们继续充当客户端应用,但只是想象将这些值发送到 Oracle 数据库(此时,我们只是在本地使用这些值。)

现在,假设我们是 Oracle 数据库,我们使用刚刚“收到”的那些值,并调用makeExtRSAPubKey()方法来生成公钥的副本。(虽然,在服务器上那个方法是private,不会被直接调用。)然后服务器使用(我们使用)那个公钥来加密一些明文样本数据。我们使用的样本数据sampleData是我们通过实例化一个新的Date类获得的日期和时间。

我们使用我们构建的公钥副本extRSAPubKey初始化cipherRSA,并将其设置为ENCRYPT_MODE。然后我们调用doFinal()方法,将clearBytes中的加密成中的cryptBytes。再次注意,cipherRSA.doFinal()方法将一个字节数组作为其参数,并返回一个字节数组,因此我们使用String.getBytes()sampleData String转换为一个字节数组,以便将clearBytes提交给cipherRSA.doFinal()。我们以cryptBytes结束,这是一个加密数据的字节数组。

然后回到客户端,我们“接收”加密的字节数组。(调用 Oracle 数据库返回加密数据的过程比第一次测试要复杂一些,我们将在本章的后半部分看到详细信息。)作为客户端,然后,我们使用locRSAPrivKey私钥初始化我们自己的cipherRSA,到DECRYPT_MODE.,然后我们调用doFinal()方法。这将返回原始的明文字节数组,从中我们创建一个名为newSampleData的新的String实例,并打印出来。如果一切顺利,我们将看到的是输出的客户端日期和时间newSampleData

注意在清单 5-10 的末尾有一个对System.exit(0)的调用。这个调用实际上终止了正在运行的 JVM。将该调用包含在大多数main()方法的末尾是很重要的,因为如果您的应用正在运行其他代码线程,当您到达main()的末尾时,JVM 可能不会终止,即使您已经完成了它。请注意,对于 GUI 应用,您通常不会调用main()中的System.exit(0),因为您的main()方法会显示 GUI 界面,然后结束。main()结束,但是 GUI 一直存在,直到 GUI 中的某个控件(如退出按钮)发出类似的System.exit(0)调用。顺便说一下,0(零)表示退出,状态代码正常。任何其他数字都被视为错误代码,操作系统可以根据需要检测到。

还要注意catch块和我们对Exception.printStackTrace()的调用。通过该调用,main()方法中捕获的任何异常都将打印出每个连续方法调用的列表,以及每个连续调用的行号。发生异常的main()中的行号将在列表的底部,它调用的方法和发生异常的行号将在上面列出,依此类推。列表顶部是发生的特定异常以及发生异常的方法和行号。这个列表被称为堆栈跟踪。

运行代码

如果你已经按照第三章中的描述设置了PATHCLASSPATH,那么你的工作就容易多了。先运行一个命令提示符(开始菜单 images 程序 images 附件 images 命令提示符)。然后将目录更改为 第五章 。使用以下命令编译代码:

javac orajavsec/OracleJavaSecure.java

然后使用以下命令运行同一目录中的代码:

java orajavsec.OracleJavaSecure

如果你没有设置你的PATHCLASSPATH,那么你需要找到你的编译器(【javac.exe】??)。您需要转到前面提到的同一个目录。例如,如果【javac.exe】的在的 C:\java\jdk1.6\bin 中,而的 ojdb6.jar* 在 C:\java 中,您将执行以下命令(从 第五章 目录中的命令提示符)😗

C:\java\jdk1.6\bin\javac.exe –cp .;C:\java\ojdbc6.jar orajavsec/OracleJavaSecure.java C:\java\jdk1.6\bin\java.exe –cp .;C:\java\ojdbc6.jar orajavsec.OracleJavaSecure

因此,您将看到显示的客户端日期:

Client date: Sat Dec 04 11:29:39 EST 2010

这个日期string是在客户机上生成的,用公钥加密,用私钥解密。

密钥交换

到目前为止,我们已经创建了我们的 RSA 私钥和公钥,并演示了如何构建一个包含两个部分的公钥副本:指数和模数。我们还演示了用我们的公钥副本加密一个Date字符串,并用私钥解密它。这些是我们将要利用的 RSA 密钥对的所有方面;但是,我们将在 Oracle 数据库上构建私钥的副本,在那里进行加密,并在客户机上用私钥解密数据。为此,我们需要一些 Oracle 结构。

我们将在我们的应用安全模式中定义一个函数和一个过程。为此,以用户appsec的身份连接到 Oracle,并将您的角色设置为特权用户appsec_role:

CONNECT appsec; SET ROLE appsec_role;

images 你可以在名为chapter 5/appsec . SQL的文件中找到以下命令的脚本。

创建一个用公钥加密数据的函数

清单 5-11 中的 Oracle 脚本定义了一个调用OracleJavaSecure.getRSACryptData()方法的函数。这将使用我们将在 Oracle 数据库上构建的 RSA 公钥的副本来加密数据。以应用安全用户appsec的身份连接到 Oracle 数据库时,执行清单 5-11 中的命令。

清单 5-11。 Oracle 函数对 RSA 数据加密

CREATE OR REPLACE FUNCTION f_get_rsa_crypt(     ext_rsa_mod VARCHAR2, ext_rsa_exp VARCHAR2, cleartext VARCHAR2 )     RETURN RAW     AS LANGUAGE JAVA     **NAME** 'orajavsec.OracleJavaSecure.getRSACryptData( java.lang.String, java.lang.String, java.lang.String ) return oracle.sql.RAW'; /

我们将向函数传递三个VARCHAR2 ( String)参数:公钥模数、公钥指数和我们想要加密的明文字符串。

注意,NAME关键字必须指向一行文本。这里我们有自动换行,但是 Java 方法的定义必须都出现在一行中。此外,输入和输出参数都必须是完全限定的,它们必须在类名前面加上完整的路径。这包括像java.lang.String这样自然属于 Java 语言一部分的类。

回想一下,Oracle 函数可以接受参数、做功并返回单个值。在这种情况下,我们将加密一个字符串,并将其作为RAW数据类型返回。RAW是一个不可变的字节数组,可以在 Oracle 数据库和 Java 之间传输,不需要任何解释或翻译(强制转换)。RAW数据保留了 Oracle 和 Java、服务器和客户端之间的保真度。一个RAW数据元素的限制是 PL/SQL 代码中的 32767 字节(我们的例子)或者存储在数据库中的 2000 字节。

创建一个以加密形式获取 SYSDATE 的过程

我们会添加一个不调用 Java 的过程,所以它不是 Java 存储过程,但是它调用了我们的 Java 存储函数,f_get_rsa_crypt。作为appsec用户执行清单 5-12 中的命令。

清单 5-12。 Oracle 程序对日期和时间字符串进行加密,p_get_rsa_crypt_sysdate

CREATE OR REPLACE PROCEDURE p_get_rsa_crypt_sysdate(     ext_rsa_mod    IN VARCHAR2,     ext_rsa_exp    IN VARCHAR2,     crypt_sysdate **OUT** RAW,     m_err_no      **OUT** NUMBER,     m_err_txt     **OUT** VARCHAR2 ) IS BEGIN     m_err_no := 0;     crypt_sysdate := f_get_rsa_crypt( ext_rsa_mod, ext_rsa_exp,         TO_CHAR( CURRENT_TIMESTAMP, 'DY MON DD HH24:MI:SS TZD YYYY' ) ); EXCEPTION     WHEN OTHERS THEN         m_err_no := **SQLCODE;**         m_err_txt := **SQLERRM;** END p_get_rsa_crypt_sysdate; /

这是相当简单的程序。它从 Oracle 数据库获取CURRENT_TIMESTAMP,使用TO_CHAR SQL 函数将其转换为字符串,使用f_get_rsa_crypt(我们之前定义的)对其进行加密,然后将其返回给调用者。

那么,为什么我用看似错误处理的东西把它复杂化了呢?现在是为这种类型的程序引入模板的最佳时机。我们不想让 Oracle 数据库承担处理我们的过程可能产生的大多数错误的任务,我们还希望能够在我们的应用中处理这些错误。我们需要 Oracle 数据库告诉我们错误是什么,并且我们需要能够以逻辑的方式处理它。

出于这个原因,我们已经将我们的几个参数声明为OUT参数。第一个OUT参数是一个RAW,它包含我们加密的时间戳字符串。第二个OUT参数是 Oracle 数据库生成的错误号——每种 Oracle 错误类型都有一个不同的错误号,在 Oracle 文档中有索引。第三个OUT参数是错误消息。

在我们的模板中,当一个异常发生时,我们不会把它扔回到调用应用;相反,我们允许过程以有序的方式完成,并在OUT参数中将错误号(SQLCODE)和消息(SQLERRM)返回给调用者。回到调用代码,在我们处理任何其他返回的数据之前,我们将检查错误号是否是除 0 之外的任何值,如果是,我们将它报告给用户或在应用中处理它。如果有错误,继续处理数据通常是无益的。在我们的代码中,后面会有这个模板的例子。

关于时区的一句话

您会注意到清单 5-12 中的通过这个调用获得了 Oracle 数据库的当前时间:

TO_CHAR( CURRENT_TIMESTAMP, 'DY MON DD HH24:MI:SS TZD YYYY' )

我们的目标是将本地客户机上的 Java 所给出的时间格式进行匹配,只是为了便于演示。对我来说,Java 现在的时间是:

Sat Dec 04 11:29:39 EST 2010

请注意,格式字符串包括表示时区的元素“TZD”。我的时区是EST(东部标准时间)。为了得到时区,我们使用CURRENT_TIMESTAMP而不是SYSDATE。如果需要,必须将 Oracle 时间戳数据定向为保留时区。通常,Oracle 数据库为会话期间生成的数据假定客户端的时区;但是,会话提供的时区通常与格林威治标准时间(GMT)相差几个小时。生活在美国东南部的夏令时意味着我们的时区通常比格林威治时间晚 5 小时。

注意,为了得到完全相同的结果,您必须将 Java 返回的日期String大写。当我们将结果与 Oracle 数据库返回的结果进行比较时,我们会看到 Oracle 给出了全大写的星期名称和月份名称。展望未来,我们将看到这些日期和时间表示:

Client date: **Sat Dec** 04 14:59:49 EST 2010 Server date: **SAT DEC** 04 14:59:50 EST 2010

你可能有兴趣知道(或者只是想一想)许多城市、州、国家和大洲都有这个时差(GMT 减 5)。因此,知道偏移量并不能保证 Oracle 数据库能够以 TZD 格式报告时区;虽然,偏移是可以报告的。

对于某些 Oracle 客户端,如 TOAD,客户端不会通知服务器要使用的时区名称。服务器知道偏移量,但不知道名称。观察这些命令的输出来观察现象。

SELECT TO_CHAR( CURRENT_TIMESTAMP, 'DY MON DD HH24:MI:SS TZD YYYY' ) FROM DUAL; SELECT * FROM sys.gv_$timezone_names     WHERE tzname LIKE 'America%' --AND tzabbrev = 'EST' ; ALTER SESSION SET TIME_ZONE = 'America/New_York'; SELECT TO_CHAR( CURRENT_TIMESTAMP, 'DY MON DD HH24:MI:SS TZD YYYY' ) FROM DUAL;

在修改我们的会话以指定我们的时区名称后,Oracle 数据库可以正确地报告EST或您的当前时区。以下是我所看到的结果:

SAT DEC 04 11:57:49  2010 SAT DEC 04 11:58:31 EST 2010

另一方面,Java 客户端会自动通知 Oracle 数据库它的时区名称,因此我们不需要手动设置它。时区有更多的复杂性,我不在这里介绍,特别是它与夏令时有关。

将 OracleJavaSecure Java 加载到 Oracle 数据库中

现在我们已经定义了 Oracle 函数和过程,我们准备将 Java 逻辑(代码)放入 Oracle 数据库。有几种方法可以做到这一点。第一种是使用 Oracle 客户机和服务器软件附带的 loadjava 实用程序。我们给自己增加了一点难度,因为完成该命令所需的角色作为应用安全人员,appsec用户不是默认角色。不使用 loadjava ,让我们以appsec的身份连接到 Oracle 数据库(或者保持连接)。

下一行在我们的 Java 代码的顶部,在OracleJavaSecure.java文件中。取消对它的注释,并将整个代码复制到您的 Oracle 客户端。

CREATE OR REPLACE AND RESOLVE JAVA SOURCE NAMED appsec."orajavsec/OracleJavaSecure" AS …

为了安全起见,向下滚动到类主体,确保在连接字符串中没有有效的密码。如果是这样,在 Oracle 数据库中执行该命令之前,请从连接字符串中删除口令。您不希望在 Oracle 上的代码中存储任何密码——那里不需要密码,拥有密码会使黑客可以获得密码。

    private static String appsecConnString =         "jdbc:oracle:thin:AppSec/password@localhost:1521:Orcl";

在您的 Oracle 客户端(如 SQL*Plus)中执行脚本,将 Java 代码加载到 Oracle 数据库中,Oracle 数据库会对其进行编译。

用公钥加密数据

之前,我们在 Oracle 函数f_get_rsa_crypt的定义中看到,我们打算调用一个名为getRSACryptData()的方法。在上一节中,我们刚刚将该 Java 方法加载到 Oracle 数据库中。getRSACryptData()的代码如清单 5-13 所示。我们将让 Oracle 数据库用公钥加密数据,稍后我们将在客户端用私钥解密数据。

清单 5-13。用公钥加密数据,getRSACryptData()

public static final RAW getRSACryptData( String extRSAPubMod,         String extRSAPubExp, String clearText )     {         RAW rtrnRaw =             new RAW( "getRSACryptData() failed".**getBytes**() );         try {             if ( ( null == extRSAPubKey ) ||                 ( !saveExtRSAPubMod.equals( extRSAPubMod ) ) )                 makeExtRSAPubKey( extRSAPubMod, extRSAPubExp );             cipherRSA.init( **Cipher.ENCRYPT_MODE**, extRSAPubKey, random );             rtrnRaw = new RAW( cipherRSA.**doFinal**( clearText.getBytes() ) );         } catch ( Exception x ) **{}**         return rtrnRaw;     }

在开发过程中,我有机会在getRSACryptData()和类似的方法中捕捉异常,但是在被调试的代码中,我依靠另一种机制来报告错误。你可以看到catch块在左括号{之后有一个右括号}。这是一种忽略异常的方式。无论是否抛出异常,该方法都将成功返回一个有效的RAW值。如果完成加密时出现问题(在try块的最后一行),那么我们最初的“错误”消息将在RAW中返回。该原始消息通知客户端该方法失败。我们通过从getBytes()获取String的字节并将它们发送给RAW构造函数,将消息翻译成RAW。如果执行代码时没有问题,那么我们的成员rtrnRaw被设置为加密值,那个被返回;否则以RAW形式返回错误信息。

我们在getRSACryptData()中做的第一个测试是看我们是否已经建立了 RSA 公钥的副本。每次调用这个方法时,我们都将它传递给公共构件,但是我们只想构建一次密钥。因此,我们测试extRSAPubKey是否是null。我们还测试了模数工件的值是否发生了变化(不应该发生这种情况)。如果其中任何一个为真,我们通过调用makeExtRSAPubKey()构建我们的 RSA 公钥副本。

我们初始化cipherRSA来做加密,Cipher.ENCRYPT_MODE。然后我们通过调用doFinal()方法来加密明文。

在本章之后,我们将不会直接从我们的客户代码中调用getRSACryptData(),所以我们将把它从public改为private方法。最终,我们将使用 RSA 公钥加密的唯一内容是一个秘密密码短语。我们将在下一章探讨这个问题。

使用堆叠呼叫

让我指出清单 5-13 中一行代码的语法:

rtrnRaw = new RAW( cipherRSA.doFinal( clearText.getBytes() ) );

我们调用clearText StringgetBytes()方法,然后不是指向一个字节数组成员,而是将字节直接传递给cipherRSA.doFinal()方法进行加密(它接受一个字节数组并返回一个字节数组)。然后,我们将数组传递给一个新的RAW实例构造函数,而不是将一个本地字节数组成员指向加密的字节。

我称这种语法为堆栈调用,我喜欢它。比起宽敞的代码,我更喜欢密集的代码。这只是我的看法,它更容易阅读和打印。读堆栈调用的关键是先看最里面的一对括号,从里到外读。这比从右向左阅读更难。

用私钥解密数据

本章演示中我们需要的下一个方法是用我们的 RSA 私钥解密客户端数据的方法。清单 5-14 定义了这个方法。

清单 5-14。用私钥解密数据,getRSADecryptData()

public static final String getRSADecryptData( RAW cryptData ) {         String rtrnString = "getRSADecryptData failed";         try {             cipherRSA.init( Cipher.DECRYPT_MODE, locRSAPrivKey );             rtrnString = new String( cipherRSA.doFinal( cryptData.getBytes() ) );         } catch ( Exception x ) {             x.printStackTrace();         }         return rtrnString;     }

这个方法接受一个名为cryptDataRAW参数。代码从cryptData获取字节数组,并将其传递给cipherRSA.doFinal()方法进行解密。我们从解密的字节数组中实例化一个新的String,并将那个String返回给调用者。

您将认识到这种方法和上面描述的getRSACryptData()方法在错误处理方面的相似之处。我们在这里采取的一个额外措施是打印出任何抛出的Exception的堆栈跟踪。

在以后的章节中不需要这种方法,因为我们不会用 RSA 公钥加密任何旧数据;相反,使用 RSA 密钥,我们将只加密/解密我们在下一章中创建的密码密钥的特定工件。因此,我们将专门为此目的重新打包这个解密调用。

客户端和服务器端测试

在本章的前面,你在你的工作站上编译了OracleJavaSecure.java。现在让我们对代码做一些修改。首先,如果您保存了我们安装在 Oracle 数据库上的版本,那么您可能已经取消了第一行的注释。如果是,请再次注释:

//CREATE OR REPLACE AND RESOLVE JAVA SOURCE NAMED appsec."orajavsec/OracleJavaSecure" AS

从文件顶部往下一点,在类代码的主体中,将名为testingOnServerboolean设置为true。这个改变将导致第二部分main()方法的执行。

    private static boolean testingOnServer = **true;**     private static String appsecConnString =         "jdbc:oracle:thin:AppSec/**password**@localhost:1521:Orcl";

此外,我们希望从这个客户机代码连接到 Oracle 数据库,所以编辑下一行,输入作为appsec用户连接所需的正确主机、端口、实例和密码。请务必保存您的更改。

main()方法中,在我们之前讨论的代码之后,我们有另一个测试客户机/服务器公钥加密的部分。我们检查我们之前设置的testingOnServer boolean,如果true我们继续我们的测试。我们要做的第一件事是基于我们上面编辑的appsecConnString建立我们的甲骨文Connectionconn。参见清单 5-15 。

清单 5-15。在客户端和服务器上测试 PKE

    if( testingOnServer ) {         // Since not on the Server, must load Oracle-specific Driver         Class.forName( "oracle.jdbc.driver.OracleDriver" );         // This will set the static member "conn" to a new Connection         conn = DriverManager.getConnection( appsecConnString );

在 OracleCallableStatement 语句中使用 IN 和 OUT 参数

同样在main()方法中,您将看到我们准备一个OracleCallableStatement的代码。我们使用的是那种类型的Statement类,这样我们就可以读取OUT参数。注意,当我们准备调用时,在清单 5-16 的中,我们列出了过程名,并且有一个关于我们将要传递的参数的问号列表。我们还将参数 3、4 和 5 注册为OUT参数,稍后我们将能够读取这些参数。如果我们没有以这种方式注册参数,我们可能会在以后读取它们时感到惊讶——它们将不可用。

清单 5-16。用于客户端/服务器加密的 OracleCallableStatement 语句

    OracleCallableStatement stmt =         ( OracleCallableStatement )conn.prepareCall(         "CALL p_get_rsa_crypt_sysdate(?,?,?,?,?)" );     **stmt.registerOutParameter**( 3, OracleTypes.RAW );     **stmt.registerOutParameter**( 4, OracleTypes.NUMBER );     **stmt.registerOutParameter**( 5, OracleTypes.VARCHAR );     **stmt.setString**( 1, clientPubModulus );     **stmt.setString**( 2, clientPubExponent );     stmt.setNull(   3, OracleTypes.RAW );     stmt.setInt(    4, 0 );     stmt.setNull(   5, OracleTypes.VARCHAR );     stmt.executeUpdate();

准备好Statement后,我们设置输入参数。我们必须传递给该过程的唯一参数是我们的 RSA 公钥模数和指数。我们为每个参数调用stmt.setString()

但是,请注意,我们使用stmt.setNull()方法将其余参数设置为null,或者在使用int的情况下设置为 0。关于将这些参数设置为 null,有两件事需要提及:首先,这可能是不必要的,因为我们不在 Oracle 过程中读取或测试这些参数的值,但是我们这样做是为了满足我们的愿望,即解决所有潜在的问题。把它看作是安全编程方法的又一部分——涵盖所有基础。第二,这些是我们期望从中读取的相同参数— OUT参数。我们希望确保,除非 Oracle 过程在这些参数中放入一个值,否则不会有任何内容可供读取。

Oracle 过程可以将其参数定义为三种类型之一:IN(默认)、OUTINOUT。有时,您会希望使用单个INOUT参数向 Oracle 过程提交数据并读取返回的结果。我发现使用INOUT参数会更简洁,但在过程代码中也会有点混乱——决定当前正在处理什么值。因此,为了更加安全,我们将避免使用INOUT参数。

当我们注册OUT参数和将这些参数设置为null时,我们都必须声明参数的数据类型。我们在OracleTypes类中引用类型,如OracleTypes.RAWOracleTypes.NUMBER。我们在 oracle.jdbc 包中找到OracleTypes(来自 ojdbc6.jar )并从那里导入它。前面我们提到了在使用VARCHAR而不是VARCHAR2.时的不一致性,这是在OracleTypes类中,正如你在清单 5-16 中看到的。

这段代码的最后一行执行语句。也就是说,它调用 Oracle 存储过程,传递 IN 参数,并检索 OUT 参数。

处理 Oracle 数据库报告的错误

我们将构建 Oracle 过程,在两个OUT参数中发回错误号和错误消息。这将是我们在处理过程中处理错误的标准方法——它将处理错误的责任交给了应用开发人员。来自main()方法、清单 5-17 的这段代码跟随Statement的执行。首先我们读取errNo参数。如果它不等于零,说明出现了一些问题,我们打印出错误消息;否则,我们将继续。

清单 5-17。处理存储过程中的错误

    int errNo = stmt.getInt( 4 );     if( errNo != 0 ) {         String errMsg = stmt.getString( 5 );         System.out.println( "Oracle error " + errNo + ", " + errMsg );         System.out.**println( (stmt.getRAW( 3 )).toString() );**     }

注意我们是如何打印出在第 3 个参数RAW中返回给我们的数据的。通常它会保存来自p_get_rsa_crypt_sysdate过程的加密数据(SYSDATE);但是当出现错误时,它将保存 Java 方法的名称,在 Oracle JVM 中运行,错误发生在那里。除非您去做这些工作,或者在 Oracle JVM 中记录错误并通读日志,否则很难收集这些信息。我们将双管齐下。

在客户端解密

通常,当没有错误时,我们读取与错误无关的OUT参数,数字 3,即加密数据的RAW元素。在这里,我们读取它并将值赋给cryptData RAW。然后我们调用getRSADecryptData()方法来解密。最后,我们打印出数据,即服务器时间戳。这些动作如清单 5-18 所示。

清单 5-18。从存储过程中解密数据

    else {         RAW cryptData = stmt.getRAW( 3 );         newSampleData = getRSADecryptData( cryptData );         System.out.println( "Server date: " + newSampleData );     }

再次运行我们的代码

如果您在完成这个过程中有任何问题,请参考本章的前一节“运行我们的代码”您必须按照“将 OracleJavaSecure Java 加载到 Oracle 数据库”一节中的描述,将 Java 代码加载到 Oracle 数据库中。

您将把目录更改为 第五章 ,并使用以下命令编译代码。提醒一下,您需要在您的CLASSPATH中有 ojdbc.jar

javac orajavsec/OracleJavaSecure.java

然后使用以下命令运行同一目录中的代码:

java orajavsec.OracleJavaSecure

观察结果

随着你在本章后半部分对OracleJavaSecure.java所做的编辑,main()方法将继续运行客户机/服务器测试。下面显示的第一行是仅在客户端上运行的加密/解密方法打印出来的;第二行来自客户机/服务器加密/解密过程:

Client date: Sat Dec 04 14:59:49 EST 2010 Server date: SAT DEC 04 14:59:50 EST 2010

这两组测试都使用我们重建的 RSA 公钥来加密日期String,并使用 RSA 私钥来解密日期String。在第二组测试中,公钥在 Oracle 数据库上重建,日期String在那里加密。然后,加密的RAW数据被返回到客户端,在那里被解密和打印。哒哒!您已经完成了客户端-服务器加密数据传输!这个过程描述在图 5-1 中。

images

图 5-1。客户端/服务器公钥加密

在图 5-1 中有一段被勾勒出来并标注为【A】。该部分描述了 RSA 密钥对的生成。在该轮廓内是一个密钥的图像,它表示存在于客户端上的 RSA 密钥对。

客户端应用对 Oracle 数据库进行一次调用,调用名为p_get_rsa_crypt_sysdate的过程。在该调用中,RSA 公钥模数和指数被发送到数据库。在数据库上,调用一个 java 存储过程f_get_rsa_crypt,这个过程又调用 Oracle JVM 中的 Java,调用一个名为getRSACryptData()的方法。这是构建等效 RSA 公钥的起点,如图 5-1 右侧的密钥图像所示。

Oracle JVM 使用等效的公钥加密来自 Oracle 数据库的CURRENT_TIMESTAMP值,然后将加密的值返回给客户端应用。从那里,客户端将使用 RSA 私钥解密该值。私钥仅存在于该客户机中,因此只有该客户机可以解密 Oracle 数据库返回的数据。

移除示范甲骨文结构

我们将不需要我们在本章中构建的 Oracle 函数或过程。它们只是用于演示我们的客户机/服务器 RSA 公钥加密。我们将在下一章构建一些更持久的函数和过程。但是因为我们已经完成了这里的工作,所以让我们以用户的身份使用下面的命令清理并删除这些结构:

DROP PROCEDURE p_get_rsa_crypt_sysdate; DROP FUNCTION f_get_rsa_crypt;

章节回顾

也许这在短短的几页中涵盖了很多内容,但是并不需要很多代码。我想你会同意,如果这是做公钥加密所需要的,我们都应该这样做!

以下是我们采取的与 PKE 相关的措施:

  • 创建了公钥和私钥集。
  • 得到了公钥的模数和指数。
  • 根据模数和指数重新创建了公钥。
  • 生成一个合适的Cipher.
  • 初始化Cipher以使用公钥或私钥进行加密或解密。
  • 调用Cipher.doFinal()方法进行加密和解密。

除了 PKE 方面,我们还学习了Serializable接口,以及如何序列化对象来传递和存储它们。

我们还建立了一个从OracleCallableStatement调用 Oracle 过程的标准表单,这将允许我们交换数据,并以一种由应用程序员处理的受控方式向客户端提供良好的错误报告。

我们讨论了final方法、Java 常量、static方法、private构造函数和static初始化器,并附带讨论了时区。

所有这些都发生在我们对本书中将要构建和扩展的OracleJavaSecure类的介绍中。

六、秘密密码加密

我所说的秘密密码加密(password encryption)也被称为共享秘密密码加密和基于密码的加密(PBE)。基本上,这个想法是两方都知道一个密码短语(或密码——我会交替使用这两个词),并且他们都使用这个密码来加密消息或数据。收件人使用相同的密码来解密邮件和数据。没有其他人可以解密数据,因为密码是一个秘密,只有双方共享。

密码加密对我们有好处,原因有几个,主要是它可以用于加密更大的数据块。我们将使用美国数据加密标准(DES)和密码块链接(CBC ),自动将任何大小的数据分成适当的块进行加密;解密后,将结果组合回原始数据。

密码加密的另一个好处是没有公钥,也就是说,没有人知道我们用来加密数据的密钥,假设我们已经充分保护了密码。另一种方法是在客户机和服务器上都使用公钥加密,每一方都用另一方的公钥加密数据,只有接收者才能读取。为了增加保证,让每个人首先用自己的私钥加密他们的消息,然后用另一个人的公钥。仔细想想,你会发现不仅收件人可以解密信息,而且预期的发件人也可以发送信息。(添加一个像 VeriSign 这样的可信证书颁发机构(CA ),您就有了身份保证——每个人都是他们所说的那个人。)然而,我们将仅通过我们的秘密密码加密获得这些好处中的大部分。

我们将在 Oracle 服务器上创建一个密码,并秘密地将其传递回客户端。我们将通过使用客户端的 RSA 公钥对密码进行加密来使其保密。为此,客户机已经将公钥工件(模数和指数)传递给了 Oracle 服务器。只有客户端可以使用私钥解密密码短语。

除了 RSA 公钥加密之外,使用密码加密的最后一个好处是,任何攻击者都必须攻击这两种协议才能拦截我们的数据。

接近

当你通读本章时,你会想要打开参考文件,以跟随完整的代码清单。首先,我们将讨论我们为秘密密码加密实现的 Java 代码,因为我们将在 Java 中构建加密密钥并进行加密。然而,在本章结束之前,我们不会编译和运行 Java 代码。然后我们将分两个阶段运行它:第一个阶段将在客户端计算机上用 Java 进行加密和解密;第二阶段将完成客户端计算机和 Oracle 数据库之间的密码加密密钥交换,并演示客户端/服务器加密/解密。

在本章末尾进入测试阶段之前,我们还将讨论这个过程所需的 Oracle SQL 代码。正如我们讨论的那样,您可以随意执行 SQL 代码来创建我们在 Oracle 数据库上需要的结构,或者您可以在我们进行第二阶段测试之前执行所有的 SQL 代码。

Java 密码加密代码

我们将返回来添加代码并编辑我们在上一章中介绍的OracleJavaSecure类。这个类将构成我们在客户端和 Oracle 数据库上的所有安全流程的核心。随着我们在这一部分的进展,打开 OracleJavaSecure.java并参考完整的代码清单,你会受益匪浅。我们将替换 Oracle 数据库中的OracleJavaSecure类,并在我们的客户端计算机上编译和运行更新后的代码,直到本章结束并进行一些测试。

共享秘密密码钥匙的神器

有几个基于 DES 密码的加密工件必须在客户机和服务器之间共享,以便在每一端都有相同的加密密钥和Cipher。首先,有一个密码短语,只有参与加密对话的双方知道。

还有另外两个工件必须共享,但是在特定的上下文中通常被视为常量。这些是 salt 和迭代计数。将这两个参数固定为常数是一个主要缺点。供应商通常会混淆(隐藏)他们的代码,以隐藏这些值。任何能够窃取 salt 和迭代次数的黑客在解密你的数据上都有优势。

我们的计划是为每个会话生成三个不同的工件。我们将使用SecureRandom实例来生成随机迭代计数和随机 salt。我们还将从随机可接受的字符中生成最大长度的密码短语。

生成密码和工件

既然我们在这个主题上,让我们继续下去,看看我们如何生成这些工件。我们在makeSessionSecretDESPassPhrase()方法中完成,其代码如清单 6-1 所示。

清单 6-1。生成 DES 密码神器makeSessionSecretDESPassPhrase()

`    private static SecureRandom random = new SecureRandom();

private static final int SALT_LENGTH = 8;
    private static int maxPassPhraseBytes;
    private static char[] sessionSecretDESPassPhraseChars = null;
    private static byte[] salt;
    private static int iterationCount;
    private static void makeSessionSecretDESPassPhrase() {
        // Pass Phrase, Buffer size is limited by RSACipher class (on Oracle JVM)
        // Max size of data to encrypt is equal to the key bytes minus padding
        // (key.bitlength/8)-PAD_PKCS1_LENGTH (11 Bytes)
        maxPassPhraseBytes = ( keyLengthRSA/8 ) - 11;
        sessionSecretDESPassPhraseChars = new char[maxPassPhraseBytes];
        for( int i = 0; i < maxPassPhraseBytes; i++ ) {
            // I want printable ASCII characters for PassPhrase
            sessionSecretDESPassPhraseChars[i] =
                    ( char )( random.nextInt( 126 - 32 ) + 32 );         }
        // Appreciate the power of random
        iterationCount = random.nextInt( 10 ) + 15;
        salt = new byte[SALT_LENGTH];
        for( int i = 0; i < SALT_LENGTH; i++ ) {
            salt[i] = ( byte )random.nextInt( 256 );
        }
    }`

images 注意你可以在文件 第六章/orajavsec/Oracle javasecure . Java .中找到这段代码

计算密码的大小

在清单 6-1 中你会注意到的第一件事是密码短语的最大长度的定义。它是根据 RSA 密钥的大小计算的。

        maxPassPhraseBytes = ( keyLengthRSA/8 ) - 11;

我们将密码长度基于 RSA 密钥大小的原因是,我们将使用公钥加密我们的密码,而 RSA 只能加密小于其自身密钥的字节数组,减去填充。

我们可以让maxPassPhraseBytes成为一个常数,但是如果我们不知道它的来源,我们可能会尝试更大的东西(好吧,我试过了)。此外,我们可能会在某个时候增加 RSA 密钥的大小,这将自动转换为更长的密码长度。

尊重随机的力量

在我们的方法中,如清单 6-1 所示,我们实例化了一个大小为maxPassPhraseBytes的字节数组,并在for循环中用随机字符填充它。请注意随机字符的参数。我们希望我们的密码短语由 ASCII 范围 32 到 126 的可打印字符组成。

我们将迭代次数设置为 15 到 25 之间的一个随机数。

我们将用 0 到 256 之间的随机字节填充 salt 字节数组。我们的 salt 字节数组大小固定为八个字节。我们将SALT_LENGTH声明为常量(static final)。

我们基于密码的加密的所有这些构件将为每个 Oracle 会话生成,并传递回客户端。在传输之前,将使用 RSA 公钥对它们进行加密。客户端将使用私钥解密它们。然后,有了这些工件,客户机将创建一个相同的密码密钥,用于发送和接收加密数据。

初始化静态类成员

我们将把两个静态类成员的初始化从它们之前在方法中的位置移到类体中。参见清单 6-2 。我们统一了创建这些组件的过程,而不是两次单独调用makeCryptUtilities()方法(一次在客户端,另一次在服务器端)。我们将在定义时实例化SecureRandom,并且我们将在静态初始化块中实例化cipherRSA(根据需要,捕捉异常)。

清单 6-2。静态类成员

`    private static SecureRandom random = new SecureRandom();
    private static Cipher cipherRSA;
    static {
        try {
            cipherRSA = Cipher.getInstance( "RSA" );
        } catch( Exception x ) {}
    }

private static Cipher cipherDES;
    private static SecretKey sessionSecretDESKey = null;
    private static AlgorithmParameterSpec paramSpec;

private static String sessionSecretDESAlgorithm = "PBEWithSHA1AndDESede";`

除了上一节描述的基于 DES 密码的加密密钥的构件之外,我们将声明密钥本身和我们将使用的 DES Cipher。我们还将为 DES 密钥构建和使用一个AlgorithmParameterSpec成员。

评估 Java 1.5 基于密码的加密漏洞

我们可以选择使用什么算法来加密我们的秘密密码。美国(DES)不错,但三重 DES (3DES 或 DESede)更强,AES 更是如此;SHA1 是比 MD5 更好的加密散列。所以我们想使用完全由PBEWithSHA1AndDESede表示的基于密码的加密算法,我们指定它如清单 6-2 所示。

但是等等,有个问题。在 Java Runtime Edition 版中,有一个 bug。这个链接有报道:[bugs.sun.com/bugdatabase/view_bug.do?bug_id=6332761](http://bugs.sun.com/bugdatabase/view_bug.do?bug_id=6332761)

Oracle JVM 是基于 JRE 1.5 的,所以它显示了这个错误。当我们生成指定我们的首选算法的密钥时,密钥生成器将返回一个类型为PBEWithMD5AndDES的较弱的密钥。

编码自动升级:协商算法

程序员的工作是调试问题,比如我们遇到的上一节中描述的 bug,并架设跨越障碍的桥梁。我们将有一个方法向我们展示实际使用中的算法;这将显示错误。此外,我们不会假设我们指定的就是我们得到的——我们会将实际的算法返回给客户端,并使用实际的算法构建密码密钥的副本。

这种方法的好处是,我们将协商一个通用算法,并继续指定一个更强的加密算法。继续指定更强的算法将使我们的代码倾向于在 Oracle JVM 中可用时使用更强的算法。

所以我们将指定PBEWithSHA1AndDESede,但此时我们将使用PBEWithMD5AndDES。当 Oracle 下一次升级 Oracle JVM 时,我们准备使用更强的算法。这两种算法都使用 CBC 作为它们的模式,所以它们对我们正在做的事情同样适用。

生成密码密钥

现在我们已经拥有了秘密密码密钥的所有构件,让我们来构建密钥。在清单 6-3 中,我们有一个创建密钥的方法makeSessionSecretDESKey()。该方法的第一步是检查是否已经生成了密钥(passphrase char array 是否为空),如果还没有生成,我们调用前面描述的方法makeSessionSecretDESPassPhrase()来构建工件。

清单 6-3。生成密码密钥,makeSessionSecretDESKey()

    private static void makeSessionSecretDESKey() throws Exception {         // DES Pass Phrase is generated on server and passed to client         if( null == sessionSecretDESPassPhraseChars )             makeSessionSecretDESPassPhrase();         paramSpec = new PBEParameterSpec( salt, iterationCount );         KeySpec keySpec = new PBEKeySpec( sessionSecretDESPassPhraseChars, salt,                 iterationCount );         // Try with recommended algorithm         sessionSecretDESKey = SecretKeyFactory.getInstance(             sessionSecretDESAlgorithm ).generateSecret( keySpec );         // See what algorithm used         sessionSecretDESAlgorithm = sessionSecretDESKey.getAlgorithm();         cipherDES = Cipher.getInstance( sessionSecretDESKey.getAlgorithm() );     }

使用我们的秘密密码密匙工件,我们首先实例化paramSpec,它是一个static类成员。我们在多个方法中使用了那个paramSpec成员,所以我们将它创建为一个静态类成员。它将在将来 Java 存储过程从同一个 Oracle 会话调用静态方法时可用。

我们还实例化了一个KeySpec类,它是方法的本地类,只在这里使用。keySpec成员被SecretKeyFactory用来生成sessionSecretDESAlgorithm成员中描述的算法类型的密钥。这种算法类型会受到 bug 的影响,所以我们实际上会得到一个 Oracle JVM 版本支持的算法类型的键。之后,我们通过调用sessionSecretDESKey.getAlgorithm()得到实际的算法。我们还获得了基于该算法的 DES Cipher的一个实例。

请记住,我们已经将 RSA 公钥从客户端传递给了 Oracle。现在,在 Oracle 数据库上,我们正在构建我们的秘密密码密钥,我们将使用客户机公钥对其工件进行加密,并将其传递回客户机。我们还会把实际的算法回传到客户端,所以我们在客户端建立一个通用的算法。

用公开的 RSA 密钥加密

在上一章中,我们已经看到了这段代码。我们使用我们的服务器构建的 RSA 公钥副本来加密(Oracle 上的)明文数据。然而,以前这是一个公共方法,我们直接调用它来演示 RSA 公钥加密。现在我们使用它作为一个实用方法来加密我们的秘密密码密钥的工件,以发送到客户端,所以我们已经使它成为私有的。看看清单 6-4 中的代码。

清单 6-4。用 RSA 公钥加密,getRSACryptData()

    private static final RAW getRSACryptData( String extRSAPubMod,         String extRSAPubExp, **String clearText** ) throws Exception     {         byte[] clearBytes = clearText.getBytes();         return getRSACryptData( extRSAPubMod, extRSAPubExp, clearBytes );     }
    private static final RAW getRSACryptData( String extRSAPubMod,         String extRSAPubExp, **byte[] clearBytes** ) throws Exception     {         if( ( null == extRSAPubKey ) ||             ( !saveExtRSAPubMod.equals( extRSAPubMod ) ) )             makeExtRSAPubKey( extRSAPubMod, extRSAPubExp );         cipherRSA.init( Cipher.ENCRYPT_MODE, extRSAPubKey, random );         return new RAW( cipherRSA.doFinal( clearBytes ) );     }

还要注意,我们现在有两个同名的方法:getRSACryptData()。然而,通过采用不同的参数,它们是不同的:一个采用String形式的明文;另一个是字节数组形式。据说这些方法有不同的签名。当我们有多个同名但不同签名的方法时,我们称这个方法为重载。

第一种方法获取明文String并创建一个字节数组。然后为了避免重复任何代码,第一个方法简单地调用第二个方法并返回第二个方法返回的内容:加密的文本。我们将多次调用这些方法来加密我们的秘密密码密钥的所有工件。

向客户端返回秘密密码密钥工件

你会看到以下四种方法之间有如此多的相似之处,以至于我们将只描述每个方面一次。这四种方法返回密码短语、算法、salt 和迭代次数,当它们组合在一起时,可以用来生成 DES 加密密码密钥。

这些方法会从 Java 存储过程中调用,所以分别是publicstatic。注意,这些方法将 RSA 公钥工件作为参数,它们将使用这些参数来加密秘密密码密钥工件。这些方法将关键工件作为RAW数据类型返回,因此我们保持了 Oracle 数据库和客户机之间的数据保真度。让我们看看清单 6-5 中的第一个。它返回密码短语。

清单 6-5。加密口令,getCryptSessionSecretDESPassPhrase()

    public static final RAW getCryptSessionSecretDESPassPhrase(         String extRSAPubMod, String extRSAPubExp )     {         RAW rtrnRAW =             new RAW( "getCryptSessionSecretDESPassPhrase() failed".getBytes() );         try {             if( null == sessionSecretDESKey ) makeSessionSecretDESKey();             byte[] sessionSecretDESPassPhraseBytes =                 charArrayToByteArray( sessionSecretDESPassPhraseChars );             rtrnRAW = getRSACryptData( extRSAPubMod, extRSAPubExp,                 sessionSecretDESPassPhraseBytes );         } catch( Exception x ) {             **java.io.CharArrayWriter errorText =**                 **new java.io.CharArrayWriter( 32767 );**             **x.printStackTrace( new java.io.PrintWriter( errorText ) );**             **rtrnRAW = new RAW( errorText.toString().getBytes() );**         }         return rtrnRAW;
    }

我们已经讨论了很多Exception处理。这个方法通常被称为当前四人组中的第一个,包括更多的错误报告。因为我们正在返回一个RAW,所以我们最多可以返回 32,767 字节的数据。在catch块中,我们在内存数组中实例化了一个大小为 32,767 个字符的CharArrayWriter。然后通过堆栈调用,我们实例化一个PrintWriter,它指向CharArrayWriter。然后我们将Exception 钉痕打印到那个PrintWriter上。结果,我们在一个 char 数组编写器errorText中得到堆栈跟踪。我们调用errorText.toString().getBytes()来获取堆栈跟踪的字节数组,然后从中实例化一个RAW,我们可以将它传递回客户端。

在客户端,如果我们在解密从返回的RAW中寻找的密码短语时遇到困难,我们可以将RAW读取为String,并查看Exception堆栈跟踪。在客户机/服务器环境中,当您希望看到服务器看到的错误,而不仅仅是客户机上的错误时,这是一种方便的故障排除实践。

清单 6-6 展示了这一堆方法中的第二个。这个函数返回 Oracle 数据库中使用的实际算法的名称。在try块中,您将看到所有这些方法共有的一个特性。我们测试一下sessionSecretDESKey是否是null,如果是,我们调用makeSessionSecretDESKey()(前面描述过)来创建秘密的密码密钥。

清单 6-6。加密算法名称,getCryptSessionSecretDESAlgorithm ()

    public static final RAW getCryptSessionSecretDESAlgorithm(         String extRSAPubMod, String extRSAPubExp )     {         RAW rtrnRAW =             new RAW( "getCryptSessionSecretDESAlgorithm() failed"**.getBytes()** );         try {             **if( null == sessionSecretDESKey ) makeSessionSecretDESKey();**             rtrnRAW = **getRSACryptData**( extRSAPubMod, extRSAPubExp,                 sessionSecretDESAlgorithm );         } catch( Exception x ) {}         return rtrnRAW;     }

try块中的最后一个公共调用是对前面描述的getRSACryptData()方法的调用)来加密秘密密码密钥工件;在这种情况下,是算法名。这会生成一个包含加密工件的RAW数据类型,它将被返回给客户端。

请注意调用异常时返回的内容。我们仍然返回rtrnRAW,但是它的值是字符串"getCryptSessionSecretDESAlgorithm() failed"的字节。将此报告给客户有助于故障排除。另外,请注意我们是如何获得那个String的字节的——我们将引号之间的值视为已经是一个 String对象,调用getBytes()方法。这是允许的吗?没错。

这组中的最后两个方法,如清单 6-7 和清单 6-8 所示,将 salt 和迭代计数作为加密的RAW数据类型返回。

清单 6-7。加密盐,getCryptSessionSecretDESSalt ()

    public static final RAW getCryptSessionSecretDESSalt( String extRSAPubMod,         String extRSAPubExp )     {         RAW rtrnRAW = new RAW( "getCryptSessionSecretDESSalt() failed".getBytes() );         try {             if( null == sessionSecretDESKey ) makeSessionSecretDESKey();
            rtrnRAW = getRSACryptData( extRSAPubMod, extRSAPubExp, salt );         } catch( Exception x ) {}         return rtrnRAW;     }

清单 6-8。加密计数,getCryptSessionSecretDESIterationCount ()

    public static final RAW getCryptSessionSecretDESIterationCount(         String extRSAPubMod, String extRSAPubExp )     {         RAW rtrnRAW =             new RAW( "getCryptSessionSecretDESIterationCount() failed".getBytes() );         try {             if( null == sessionSecretDESKey ) makeSessionSecretDESKey();             **byte[] sessionSecretDESIterationCountBytes =**                 **{ ( byte )iterationCount };**             rtrnRAW = getRSACryptData( extRSAPubMod, extRSAPubExp,                 sessionSecretDESIterationCountBytes );         } catch( Exception x ) {}         return rtrnRAW;     }

返回加密迭代计数的最后一个方法创建了一个字节数组byte。数组中唯一的byte是迭代计数int,转换为byte。这样,我们可以调用相同的方法将迭代计数加密为一个字节数组。当我们在客户端解密时,我们将不得不逆转这个过程。

使用我们的秘密密码加密数据

我们将在客户机和 Oracle 数据库上调用相同的方法,使用密码密钥加密数据。清单 6-9 中的语法现在应该很熟悉了。该方法获取一个String明文数据,并返回一个RAW加密数据。注意,使用sessionSecretDESKey初始化Cipher与我们使用 RSA 密钥的方式非常相似,除了我们还提供了paramSpec

清单 6-9。用密码加密数据,getCryptData()

    public static final RAW getCryptData( String clearData ) {         if( null == clearData ) return null;         RAW rtrnRAW = new RAW( "getCryptData() failed".getBytes() );         try {             if( null == sessionSecretDESKey ) makeSessionSecretDESKey();             **cipherDES.init( Cipher.ENCRYPT_MODE, sessionSecretDESKey, paramSpec );**             rtrnRAW = new RAW( cipherDES.doFinal( clearData.getBytes() ) );         } catch( Exception x ) {}         return rtrnRAW;     }

像我们之前描述的加密和返回我们的秘密密码密钥工件的方法一样,这个方法测试sessionSecretDESKey是否已经被实例化,如果没有,尝试创建它。这在 Oracle 数据库上是一种很好的做法,但是在客户机上就太放肆了(在客户机上,我们不希望生成秘密的口令密钥。)开发人员必须理解,客户端必须首先从 Oracle 数据库获得密码密钥,然后才可以使用它来加密数据,以便插入或更新到 Oracle。如果开发人员没有遵循这个指导方针,不会有任何伤害,但是他们的代码将不会工作。

Oracle 秘密密码加密结构

在上一章中,我们创建了一个 Oracle 函数和一个过程来演示我们的 RSA 公钥加密在客户机/服务器环境中的使用。现在,我们将创建一个包含多个函数和过程(包括 Java 存储过程)的 Oracle 包,来处理我们的秘密密码加密。

该包将被放置在应用安全模式中。作为appsec用户,首先使用以下命令将您的角色设置为非默认角色appsec_role:

SET ROLE appsec_role;

images 注意你可以在名为 Chapter6/AppSec.sql 的文件中找到以下命令的脚本。

当您阅读本节时,您可以跟随参考的代码文件,并在上下文中查看本文中讨论的代码。此外,在阅读 Oracle 结构时,您可以执行代码来创建 Oracle 结构。我们将在本章末尾运行测试时使用这些结构。

打包获取秘密密码工件和加密数据

Oracle 数据库中的包是一组可以配置为一组的函数和过程。访问包中的函数和过程的权限是通过授予包的可执行权限来授予的。到第七章时,我们将了解 Oracle 包的另一个好处:我们可以定义新的数据类型,并在 Oracle 包中使用它们。

Oracle 包有两部分,规范和主体。规范为每个过程或函数提供了签名,但是实际的代码只包含在主体中。包的这种两部分身份允许 PL/SQL 程序员共享功能(规范)而不共享代码(主体)。你这样做可能是为了职责分离、通过混淆实现安全性,或者仅仅是为了保护你的知识产权。c 程序员会认为这种方法类似于每个代码文件都有一个头文件。

应用安全包规范

Oracle 包规范仅定义函数和过程,列出函数的预期参数和返回类型。清单 6-10 显示了app_sec_pkg包规范。作为appsec用户执行此操作。

清单 6-10。密码加密包规范

`CREATE OR REPLACE PACKAGE appsec.app_sec_pkg IS

-- For Chapter 6 testing only - move to app in later versions of this package     PROCEDURE p_get_shared_passphrase(
        ext_modulus               VARCHAR2,
        ext_exponent              VARCHAR2,
        secret_pass_salt      OUT RAW,
        secret_pass_count     OUT RAW,
        secret_pass_algorithm OUT RAW,
        secret_pass           OUT RAW,
        m_err_no              OUT NUMBER,
        m_err_txt             OUT VARCHAR2 );

-- For Chapter 6 testing only - remove in later versions of this package
    PROCEDURE p_get_des_crypt_test_data(
        ext_modulus               VARCHAR2,
        ext_exponent              VARCHAR2,
        secret_pass_salt      OUT RAW,
        secret_pass_count     OUT RAW,
        secret_pass_algorithm OUT RAW,
        secret_pass           OUT RAW,
        m_err_no              OUT NUMBER,
        m_err_txt             OUT VARCHAR2,
        test_data                 VARCHAR2,
        crypt_data            OUT RAW );

FUNCTION f_get_crypt_secret_pass( ext_modulus VARCHAR2,
        ext_exponent VARCHAR2 ) RETURN RAW;

FUNCTION f_get_crypt_secret_algorithm( ext_modulus VARCHAR2,
        ext_exponent VARCHAR2 ) RETURN RAW;

FUNCTION f_get_crypt_secret_salt( ext_modulus VARCHAR2,
        ext_exponent VARCHAR2 ) RETURN RAW;

FUNCTION f_get_crypt_secret_count( ext_modulus VARCHAR2,
        ext_exponent VARCHAR2 ) RETURN RAW;

FUNCTION f_get_crypt_data( clear_text VARCHAR2 ) RETURN RAW;

FUNCTION f_get_decrypt_data( crypt_data RAW ) RETURN VARCHAR2;

-- For Chapter 6 testing only - remove in later versions of this package
    FUNCTION f_show_algorithm RETURN VARCHAR2;

END app_sec_pkg;
/`

参见封装规范后半部分的功能列表。我们有一个函数返回每个秘密密码密钥工件的加密数据:f_get_crypt_secret_passf_get_crypt_secret_ algorithmf_get_crypt_secret_ saltf_get_crypt_secret_ count。我们也有函数来加密明文并返回一个加密的RAWf_get_crypt_data,以及解密一个RAW并返回明文、f_get_decrypt_data(我们还没有看到该过程的 Java 部分)。

在我们的函数上面,我们指定了两个过程:p_get_shared_passphrasep_get_des_crypt_test_data。这些过程中的每一个都将客户机 RSA 公钥模数和指数作为输入参数,并将秘密密码密钥工件作为OUT参数返回。我们正在处理错误,就像我们在前一章中描述的那样,使用OUT参数作为错误号和错误文本。此外,p_get_des_crypt_test_data过程接受一个明文输入参数,并返回一个加密的RAW作为附加的OUT参数。

这两个过程仅用于本章中的测试,并将在以后的章节中从包中移除。最后一个功能f_show_algorithm,也仅用于本章中的测试,稍后将被删除。

应用安全包主体:函数

我们的程序包规范中的函数和过程定义必须在我们的程序包主体中精确复制。主体不仅包含定义,还包含过程和函数的代码。此时您可以执行此程序包体;该包将在 Oracle 数据库中创建。

这里有一个来自清单主体的示例函数供我们考虑,在清单 6-11 中。我们将 RSA 公钥模数和指数传递给函数f_get_crypt_secret_pass。它将它们传递给名为getCryptSessionSecretDESPassPhrase()的 Java 方法(如上所述)。这个 Java 方法返回一个用 RSA 公钥加密的RAW,即秘密密码密匙 passphrase。该函数返回 Java 方法返回给它的原始值。

清单 6-11。返回密码短语的功能

    FUNCTION f_get_crypt_secret_pass( ext_modulus VARCHAR2,                    ext_exponent VARCHAR2 )     **RETURN RAW**     AS LANGUAGE JAVA     NAME 'orajavsec.OracleJavaSecure.getCryptSessionSecretDESPassPhrase( java.lang.String, java.lang.String ) **return oracle.sql.RAW';**

采用相同的方法来获得秘密密码密钥的每个工件,并获得加密和解密的数据。我们还有一个函数,它没有输入参数,返回算法字符串作为一个OUT参数。那个函数f_show_algorithm,只是为了本章的测试。

应用安全包主体:过程

包主体中的过程是我们主要工作的焦点。这些程序如清单 6-12 和清单 6-13 所示。请注意,这两个程序仅用于本章中的测试,在以后的章节中将被删除和替换。我们要看的第一个过程是p_get_shared_passphrase,它将一个秘密的密码密钥返回给客户机。如果这个键还不存在,当我们调用f_get_crypt_secret_salt时,它将被创建。请记住,我们的加密密钥是特定于 Oracle 会话的,因此我们需要保持会话打开,以便使用秘密密码密钥进行加密。

清单 6-12。获取共享密码密钥的程序,p_get_shared_passphrase

    PROCEDURE p_get_shared_passphrase(         ext_modulus               VARCHAR2,         ext_exponent              VARCHAR2,         secret_pass_salt      OUT RAW,
        secret_pass_count     OUT RAW,         secret_pass_algorithm OUT RAW,         secret_pass           OUT RAW,         m_err_no              OUT NUMBER,         m_err_txt             OUT VARCHAR2 )     IS BEGIN         m_err_no := 0;         secret_pass_salt := **f_get_crypt_secret_salt**( ext_modulus, ext_exponent );         secret_pass_count := f_get_crypt_secret_count( ext_modulus, ext_exponent );         secret_pass := f_get_crypt_secret_pass( ext_modulus, ext_exponent );         secret_pass_algorithm :=             f_get_crypt_secret_algorithm( ext_modulus, ext_exponent );     EXCEPTION         WHEN OTHERS THEN             m_err_no := SQLCODE;             m_err_txt := SQLERRM;     END p_get_shared_passphrase;

观察清单 6-12 中的p_get_shared_passphraseOracle 程序中的INOUT参数列表。我们提供 RSA 公钥的模数和指数,然后得到秘密密码密钥的伪像、错误号和文本。每个工件都是通过调用一个 Oracle 函数获得的,如下所示:

secret_pass := f_get_crypt_secret_pass( ext_modulus, ext_exponent );

f_get_crypt_secret_pass一样,从p_get_shared_passphrase调用的每个函数都是 Java 存储过程。我们在清单 6-11 中看到了其中一个函数的代码。Java 存储过程的所有实质性工作都是在 Oracle 数据库上用我们的 Java 代码OracleJavaSecure完成的

我们在这里要看的第二个程序是p_get_des_crypt_test_data。除了p_get_des_crypt_test_data有两个额外的参数,一个IN和一个OUT之外,它与p_get_shared_passphrase非常相似,如清单 6-13 所示。这些参数将用于向 Oracle 数据库提交明文,并以加密形式返回该文本—用秘密口令密钥加密。

清单 6-13。获取加密数据的程序,p_get_des_crypt_test_data

PROCEDURE p_get_des_crypt_test_data(         ext_modulus               VARCHAR2,         ext_exponent              VARCHAR2,         secret_pass_salt      OUT RAW,         secret_pass_count     OUT RAW,         secret_pass_algorithm OUT RAW,         secret_pass           OUT RAW,         m_err_no              OUT NUMBER,         m_err_txt             OUT VARCHAR2,         **test_data                 VARCHAR2,**         **crypt_data            OUT RAW )**     IS BEGIN         m_err_no := 0;         secret_pass_salt := f_get_crypt_secret_salt( ext_modulus, ext_exponent );         secret_pass_count := f_get_crypt_secret_count( ext_modulus, ext_exponent );         secret_pass := f_get_crypt_secret_pass( ext_modulus, ext_exponent );         secret_pass_algorithm :=             f_get_crypt_secret_algorithm( ext_modulus, ext_exponent );         **crypt_data := f_get_crypt_data( test_data );**     EXCEPTION         WHEN OTHERS THEN             m_err_no := SQLCODE;             m_err_txt := SQLERRM;     END p_get_des_crypt_test_data;

我们将明文test_data从客户端发送到 Oracle,这个过程通过调用f_get_crypt_data函数在加密后返回crypt_data。该函数也是一个 Java 存储过程。

用于秘密密码解密的 Java 方法

一旦我们调用了appsec过程来将 DES 秘密密码密钥工件和加密数据返回给客户机,我们需要

1)用 RSA 私钥解密工件

2)生成 DES 加密口令密钥

3)用秘密口令密钥解密数据

作为一条规则,我试图限制我要求开发人员完成工作的步骤数量。当开发人员可以调用一个方法来完成其他调用时,为什么要让他们调用三个方法呢?应用开发人员的目标是解密数据,因此我们为他们提供了一种方法来完成这项工作。

images 你可以在文件chapter 6/orajavsec/Oracle javasecure . Java中找到这段代码。

使用密码密钥解密数据

在客户端应用调用了p_get_des_crypt_test_data过程之后,我们让他们调用清单 6-14 中所示的方法getDecryptData()

清单 6-14。建立秘密密码并解密数据,getDecryptData()

public static final String getDecryptData( RAW cryptData,         RAW cryptSecretDESPassPhrase, RAW cryptSecretDESAlgorithm,         RAW cryptSecretDESSalt, RAW cryptSecretDESIterationCount )     {         String rtrnString = "getDecryptData() A failed";         try {             if( ( null == sessionSecretDESKey ) || **testAsClientAndServer** ) {                 decryptSessionSecretDESPassPhrase( cryptSecretDESPassPhrase,                     cryptSecretDESAlgorithm, cryptSecretDESSalt,                     cryptSecretDESIterationCount );                 makeSessionSecretDESKey();             }             rtrnString = **getDecryptData**( cryptData );         } catch( Exception x ) {             x.printStackTrace();         }         return rtrnString;     }

我们在try块中做的第一件事是测试sessionSecretDESKey是否已经被实例化。如果没有,那么我们调用两个方法:decryptSessionSecretDESPassPhrase()(在下一节讨论)和makeSessionSecretDESKey()。我们在本章前面讨论了makeSessionSecretDESKey()——它与我们最初在 Oracle 数据库上调用的构建密码密钥的方法相同。我们在客户端再次调用它来构建一个相同的密钥。

当我们测试我们是否已经有了sessionSecretDESKey,我们也测试了boolean testAsClientAndServer。除非我们从main()方法测试OracleJavaSecure类,否则testAsClientAndServer boolean总是false。在main()中,当我们将此boolean设置为true时,我们可以在测试的不同阶段用来自 Oracle 数据库的密钥替换本地生成的 DES 加密密码密钥。我们将在本章稍后检查main()方法的代码。

getDecryptData()方法重载了一个版本,该版本假设已经构建了秘密密码密钥并进行解密。它接受一个RAW并将明文作为一个String返回。第一个getDecryptData()方法(如上所示)调用第二个getDecryptData()方法,参见清单 6-15 。

清单 6-15。用已有的密码解密数据,getDecryptData()

    public static final String getDecryptData( RAW cryptData ) {         if( null == cryptData ) return null;         String rtrnString = "getDecryptData() B failed";         try {             cipherDES.init( Cipher.DECRYPT_MODE, sessionSecretDESKey, paramSpec );             rtrnString = new String( cipherDES.doFinal( cryptData.getBytes() ) );         } catch( Exception x ) {             //x.printStackTrace();             //rtrnString = x.toString();         }         return rtrnString;     }

这个相同的第二个getDecryptData()方法也被调用来解密 Oracle 数据库上的数据,用于来自客户端的加密数据插入和更新。在 Oracle 数据库中,我们大概知道我们已经有了 DES 秘密密码密钥。

使用 RSA 私钥解密 DES 密码

decryptSessionSecretDESPassPhrase()方法使用客户机的 RSA 私钥来解密服务器 DES 秘密密码密钥的所有工件。代码显示在清单 6-16 中。

清单 6-16。解密秘密密码钥匙神器,decryptSessionSecretDESPassPhrase()

    private static void decryptSessionSecretDESPassPhrase(         RAW cryptSecretDESPassPhrase, RAW cryptSecretDESAlgorithm,         RAW cryptSecretDESSalt, RAW cryptSecretDESIterationCount )         throws Exception
    {         cipherRSA.init( Cipher.DECRYPT_MODE, locRSAPrivKey );         **byte[] cryptBytes;**         cryptBytes = cryptSecretDESPassPhrase**.getBytes();**         sessionSecretDESPassPhraseChars =             **byteArrayToCharArray**( cipherRSA.doFinal( cryptBytes ) );         cryptBytes = cryptSecretDESAlgorithm.getBytes();         sessionSecretDESAlgorithm = **new String**( cipherRSA.doFinal( cryptBytes ) );         cryptBytes = cryptSecretDESSalt.getBytes();         salt = cipherRSA.doFinal( cryptBytes );         cryptBytes = cryptSecretDESIterationCount.getBytes();         iterationCount = cipherRSA.doFinal( cryptBytes )**[0];**         //System.out.println( "\n" + new String( sessionSecretDESPassPhraseChars ) );         //System.out.println( sessionSecretDESAlgorithm );         //System.out.println( new String( salt ) );         //System.out.println( iterationCount );     }

对于每个工件,我们将加密的RAW转换成一个字节数组,并将其传递给cipherRSA成员进行解密。我们使用从Cipher返回的字节数组,用适当的数据类型填充静态类成员。通过调用byteArrayToCharArray()方法将值保存为salt的字节数组,通过实例化new String()将值保存为sessionSecretDESPassPhraseChars的字符数组,将值保存为sessionSecretDESAlgorithmString,并将单个byte自动转换为iterationCountint

如果您有兴趣在这些特定于会话的随机工件和协商的算法到达客户端时观察它们,您可以在方法末尾取消对System.out.println()调用的注释。然而,您应该只是暂时这样做:在下一章的代码中已经删除了System.out.println()调用。

数组转换的辅助方法

在前面代码的两个地方,我们调用了一些我们在OracleJavaSecure代码中定义的辅助数组转换方法,如清单 6-17 所示。一个是字节数组,将其转换为字符数组。另一个则相反。我们在得到sessionSecretDESPassPhraseChars时从decryptSessionSecretDESPassPhrase()(见清单 6-16 调用byteArrayToCharArray(),在加密sessionSecretDESPassPhraseChars时从getCryptSessionSecretDESPassPhrase()(见清单 6-5 )调用charArrayToByteArray()

清单 6-17。数组转换方法,byteArrayToCharArray()charArrayToByteArray()

`    static char[] byteArrayToCharArray( byte[] bytes ) {
        char[] rtrnArray = new char[bytes.length];
        for ( int i = 0; i < bytes.length; i++ ) {
            rtrnArray[i] = ( char )bytes[i];
        }
        return rtrnArray;
    }

static byte[] charArrayToByteArray( char[] chars ) {
        byte[] rtrnArray = new byte[chars.length];         for ( int i = 0; i < chars.length; i++ ) {
            rtrnArray[i] = ( byte )chars[i];
        }
        return rtrnArray;
    }`

我们通常可以通过强制转换来来回转换这些数组类型,如下例所示。然而,我们需要意识到缩小和扩大转换的含义。我们必须将数组中的char值限制为标准 ASCII 字符,而不是 16 位 Unicode 字符,以便在转换中不丢失信息。

byte[] bAr = new byte[10]; char[] cAr = (char[])bAr; bAr = (byte[])cAr;

JDeveloper IDE(可能还有其他地方)不支持这种数组转换,所以我们将依赖于我们的辅助方法。JDeveloper 很好,因为它是免费的,而且它是为使用 Oracle 数据库而高度定制的;它比任何其他 IDE 更好地处理 Oracle 视图。您可以在 Oracle 公司网站上找到 JDeveloper,网址为[www.oracle.com](http://www.oracle.com)

您可能想知道为什么我们将密码短语维护为一个 char 数组。这是我们构建PBEKeySpec时需要的格式。

用于显示实际算法的方法

清单 6-18 展示了showAlgorithm()方法。这实际上是重复的功能。看一下decryptSessionSecretDESPassPhrase()方法的代码(如前所示),您会看到我们将sessionSecretDESAlgorithm从 Oracle 数据库发送到客户端的String,我们可以简单地打印出来。

直接从 Oracle 数据库中选择(通过函数f_show_algorithm)的唯一额外保证是在传输过程中不会混淆。我们已经在app_sec_pkg中构建了调用该方法返回算法名称的函数。我们也可以从客户端调用这个方法(在调用服务器之前)并比较使用的算法。

清单 6-18。显示正在使用的密码算法名称,showAlgorithm()

    public static final String showAlgorithm() {         String rtrnString = "showAlgorithm failed";         try {             rtrnString = sessionSecretDESKey.getAlgorithm();         } catch( Exception x ) {             rtrnString = x.toString();         } finally {             return rtrnString;         }     }

这是一个临时的测试方法,我们将在后面的章节中从代码中删除它和调用它的 Oracle 函数。

仅在客户端测试 DES 加密

我们将再次通过从OracleJavaSecuremain()方法中调用我们的方法来进行我们的仅客户端测试。main()第一部分的代码如清单 6-19 中的所示。从获取我们的客户机公钥模数和指数开始,在这个过程中生成 RSA 公钥/私钥对(如果不存在的话)。

清单 6-19。仅用于客户端测试的代码,来自main()

`    String clientPubModulus = getLocRSAPubMod();
    String clientPubExponent = getLocRSAPubExp();

// Emulates server actions
    RAW mCryptSessionSecretDESPassPhrase =
        getCryptSessionSecretDESPassPhrase( clientPubModulus,
        clientPubExponent );
    RAW mCryptSessionSecretDESSalt =
        getCryptSessionSecretDESSalt( clientPubModulus,
        clientPubExponent );
    RAW mCryptSessionSecretDESAlgorithm =
        getCryptSessionSecretDESAlgorithm( clientPubModulus,
        clientPubExponent );
    RAW mCryptSessionSecretDESIterationCount =
        getCryptSessionSecretDESIterationCount( clientPubModulus,
        clientPubExponent );
    RAW cryptData = getCryptData( "Monday" );

testAsClientAndServer = true;

// As client
    System.out.println( getDecryptData( cryptData,
        mCryptSessionSecretDESPassPhrase, mCryptSessionSecretDESAlgorithm,
        mCryptSessionSecretDESSalt, mCryptSessionSecretDESIterationCount ) );
    System.out.println( showAlgorithm() );`

接下来,我们模拟服务器,接收模数和指数,从getCryptSessionSecretDESPassPhrase()和其他方法获得 DES 秘密密码密钥工件。在该过程中,从工件构建公钥的副本,生成 DES 秘密密码密钥,并且使用 RSA 公钥加密每个秘密密码密钥工件。

我们还模拟服务器,用秘密密码密钥加密一些数据:

RAW cryptData = getCryptData( "Monday" );

我们测试的下一步假设我们回到了客户端,并且已经收到了所有加密的秘密密码密钥工件,我们从这些工件中生成了一个秘密密码密钥的副本。为此,我们将testAsClientAndServer设置为true,以覆盖我们刚刚在模拟服务器时创建的秘密密码密钥(即使密钥是相同的):

testAsClientAndServer = true;

现在再次作为客户端,我们用所有的 DES 秘密密码密钥工件和加密数据调用getDecryptData()。这将基于工件创建一个新的 DES 密钥,然后使用该密钥解密数据。我们将打印出解密的数据,这些数据应该与我们之前加密的数据相同。此外,我们将打印出用于加密密码的 DES 算法名称。

运行代码

我们假设您按照上一章中的步骤运行了代码。本章将使用相同的程序。在命令提示符下,将目录切换到第六章。使用以下命令编译代码:

javac orajavsec/OracleJavaSecure.java

如果您有任何问题,请参考第三章中关于在命令提示符下编译和设置您的环境CLASSPATH以包含 ojdbc6.jar 的说明。然后使用以下命令运行同一目录中的代码:

java orajavsec.OracleJavaSecure.OracleJavaSecure

观察结果

前面部分中发出的命令将会打印出以下两行:

Monday PBEWithSHA1AndDESede

在模拟服务器时,我们使用 DES 加密密码密钥对字符串“Monday”进行了加密,并将加密的数据以及使用客户端 RSA 公钥加密的加密密码密钥工件传递回客户端。回到客户端,我们使用工件构建了一个复制的 DES 密钥,并解密了加密的数据。我们打印出解密后的数据,在命令提示符下看到了“星期一”。然后我们打印了协商好的算法。如果您在工作站上使用的是 JDK 1.6 或更高版本,您将会看到PBEWithSHA1AndDESede;但是,如果你使用的是 JDK 1.5,你会看到PBEWithMD5AndDES

编码测试客户端/服务器秘密密码加密

下一行位于我们在 OracleJavaSecure.java 的 Java 代码的顶部。取消对它的注释,并将整个代码复制到您的 Oracle 客户端。

CREATE OR REPLACE AND RESOLVE JAVA SOURCE NAMED appsec."orajavsec/OracleJavaSecure" AS

为了安全起见,向下滚动到类主体,确保在连接字符串中没有有效的密码。如果是这样,请在 Oracle 中执行该命令之前,从连接字符串中删除密码。

    private static String appsecConnString =         "jdbc:oracle:thin:AppSec/**password**@localhost:1521:Orcl";

在您的 Oracle 客户端(如 SQL*Plus)中执行脚本,将 Java 代码加载到 Oracle 数据库中。正如我们所看到的,这个命令将 Java 代码加载到 Oracle 数据库中并进行编译。

设置测试服务器和客户端的代码

为了在您的客户机上编译和执行OracleJavaSecure,我们取消注释以在 Oracle 数据库上运行的第一行需要被注释:

//CREATE OR REPLACE AND RESOLVE JAVA SOURCE NAMED appsec."orajavsec/OracleJavaSecure" AS

向下滚动到类主体,并在连接字符串中设置密码。还要更正连接字符串的任何其他地址和名称。

    private static String appsecConnString =         "jdbc:oracle:thin:AppSec/**password**@localhost:1521:Orcl";

还将testingOnServer boolean设置为true:

    private static boolean testingOnServer = **true;**

保存文件。

在本章的前面,您可能已经在 Oracle 上执行了app_sec_pkg包规范和主体。如果您还没有这样做,现在就做吧。这将创建我们需要做秘密密码加密的 Oracle 结构。

考虑 main()方法的服务器部分

这次当我们运行OracleJavaSecuremain()方法时,我们将通过testingOnServer测试,因此我们将执行main()的剩余部分,如清单 6-20 所示。我们声明了几个成员变量来保存从 Oracle、errNoerrMsg返回的错误号和错误消息。

因为我们是从客户端运行的(不是在 Oracle 数据库上),所以我们需要加载特定于 Oracle 的驱动程序(假设我们可能没有使用 JDK 1.6 或更高版本)。我们将设置 Oracle 连接以供使用:注意,我们将作为appsec用户进行连接。

我们将使用特定于 Oracle 的OracleCallableStatement,它允许我们从 Oracle 检索OUT参数,并传输特定于 Oracle 的数据类型。

清单 6-20。客户端/服务器测试代码,来自main()

    if( testingOnServer ) {         int **errNo;**         String **errMsg;**         // Since not on the Server, must load Oracle-specific Driver         Class.forName( "oracle.jdbc.driver.OracleDriver" );         // This will set the static member "conn" to a new Connection         conn = DriverManager.**getConnection**( appsecConnString );         **OracleCallableStatement stmt;**

从 Oracle 获取 DES 加密口令

在清单 6-21 的中,我们的第一个过程调用是对p_get_shared_passphrase。这将简单地测试我们的客户机和 Oracle 之间的 RSA 和 DES 密钥的交换。我们将我们的 RSA 公钥模数和指数传递给这个过程,作为回报,我们得到了由 Oracle 数据库使用公钥加密的 DES 秘密密码密钥工件。注意,我们注册了OUT参数,并设置或setNull了我们所有的参数。

清单 6-21。main() 获取共享密码

stmt = ( OracleCallableStatement )conn.prepareCall(         "CALL app_sec_pkg**.p_get_shared_passphrase**(?,?,?,?,?,?,?,?)" ); `    stmt.registerOutParameter( 3, OracleTypes.RAW );
    stmt.registerOutParameter( 4, OracleTypes.RAW );
    stmt.registerOutParameter( 5, OracleTypes.RAW );
    stmt.registerOutParameter( 6, OracleTypes.RAW );
    stmt.registerOutParameter( 7, OracleTypes.NUMBER );
    stmt.registerOutParameter( 8, OracleTypes.VARCHAR );
    stmt.setString( 1, clientPubModulus );
    stmt.setString( 2, clientPubExponent );
    stmt.setNull(   3, OracleTypes.RAW );
    stmt.setNull(   4, OracleTypes.RAW );
    stmt.setNull(   5, OracleTypes.RAW );
    stmt.setNull(   6, OracleTypes.RAW );
    stmt.setInt(    7, 0 );
    stmt.setNull(   8, OracleTypes.VARCHAR );
    stmt.executeUpdate();

errNo = stmt.getInt( 7 );
    if( errNo != 0 ) {
        errMsg = stmt.getString( 8 );
        System.out.println( "Oracle error " + errNo +
            ", " + errMsg );
        System.out.println( (stmt.getRAW( 3 )).toString() );
    } else {
        mCryptSessionSecretDESSalt = stmt.getRAW( 3 );
        mCryptSessionSecretDESIterationCount = stmt.getRAW( 4 );
        mCryptSessionSecretDESAlgorithm = stmt.getRAW( 5 );
        mCryptSessionSecretDESPassPhrase = stmt.getRAW( 6 );
    }
    if( null != stmt ) stmt.close();`

在执行该语句后,我们得到从 Oracle 数据库返回的错误号,errNo,并确保它为 0。如果没有,我们报告错误。

如果没有错误,那么我们作为客户端应用,设置一个与每个加密工件相等的方法成员,比如mCryptSessionSecretDESSalt。现在,我们拥有了构建 Oracle 数据库上生成的密码密钥的精确副本所需的一切。至此,我们已经完全交换了钥匙;但是,我们还没有在客户机上建立 DES 加密口令密钥的副本。在这段代码的后面,我们将使用我们的私钥解密每个工件,然后构建秘密的密码密钥,并使用它与 Oracle 交换加密的数据。

因为我们已经完成了这个语句,所以我们关闭它。这是一个一致性和勤奋的问题,在我们结束声明之前,我们保证它不是null。实际上,如果stmtnull,我们会在第一个registerOutParameter()调用的早期代码中抛出一个异常。

查看基于密码加密的协商算法

我们调用函数,f_show_ algorithm 来显示 Oracle 数据库选择的算法名(参见清单 6-22)。因为 Oracle 11g JVM 是基于标准 JVM 版本 1.5 的,所以我们将看到 Oracle 数据库在这方面表现出了前面提到的错误,并选择了PBEWithMD5AndDES作为协议,而不是我们所要求的PBEWithSHA1AndDESede。这将作为与客户端协商的通用算法。

清单 6-22。显示算法,来自main()

    stmt = ( OracleCallableStatement )conn.prepareCall(         "{**?** = call app_sec_pkg.**f_show_algorithm**}" );     stmt**.registerOutParameter( 1**, OracleTypes.VARCHAR );     stmt.executeUpdate();     **System.out.println( stmt.getString(1) );**     if( null != stmt ) stmt.close();

这里要注意的是调用 Oracle 函数的语法,而不是过程。该函数总是返回值。“领头的?= "代表返回值,我们的语句参数(1)就是那个值。准备好的可调用语句中的每个问号都是一个参数,无论它位于过程名或函数名之前还是之后。参数总是从左到右编号,1(一)是第一个。

此函数调用中使用的格式(带有左花括号和右花括号)称为 SQL92 语法。这是对 1992 年采用的 SQL 国际标准的引用。另一种可用于调用存储过程和函数的语法形式是 PL/SQL 块语法(带有beginend语句)。我发现,对于 Oracle 驱动程序的旧版本(例如 jdbc14.jar )(虽然没有 SQL92 旧), PL/SQL 块语法可以工作,而 SQL92 语法则不行。以下是来自清单 6-22 中的相同prepareCall()方法调用的 PL/SQL 块语法:

    stmt = ( OracleCallableStatement )conn.prepareCall(         "begin ? = call app_sec_pkg.f_show_algorithm; end;" );

调用 Oracle 数据库获取加密数据

接下来,我们要演示如何从 Oracle 数据库取回加密的数据,并在客户机上使用 DES 加密密码密钥的本地副本对其进行解密。在清单 6-23 的中,你可以看到我们称这个过程为p_get_des_crypt_test_data。同样,我们传递我们的公钥工件,并检索加密的秘密密码密钥工件。因为这个过程是我们将在本书的剩余部分看到重复的,我将称之为“密钥交换”。我们刚刚从另一个检索秘密密码密钥工件的过程中返回,所以我们不需要再次设置我们的方法成员——这些行被注释掉了。请注意,所有这些调用都发生在同一个 Oracle 会话中,因此使用现有的密钥—没有额外的密钥生成。

清单 6-23。获取 DES 的地穴测试数据,来自main()

stmt = ( OracleCallableStatement )conn.prepareCall(         "CALL app_sec_pkg**.p_get_des_crypt_test_data**(?,?,?,?,?,?,?,?,?,?)" );     stmt.registerOutParameter( 3, OracleTypes.RAW );     stmt.registerOutParameter( 4, OracleTypes.RAW );     stmt.registerOutParameter( 5, OracleTypes.RAW );     stmt.registerOutParameter( 6, OracleTypes.RAW );     stmt.registerOutParameter( 7, OracleTypes.NUMBER );     stmt.registerOutParameter( 8, OracleTypes.VARCHAR );     stmt.registerOutParameter( 10, OracleTypes.RAW );     stmt.setString( 1, clientPubModulus );     stmt.setString( 2, clientPubExponent );     stmt.setString( 9, **"Tuesday"** );     stmt.setNull(   3, OracleTypes.RAW );     stmt.setNull(   4, OracleTypes.RAW );     stmt.setNull(   5, OracleTypes.RAW );     stmt.setNull(   6, OracleTypes.RAW );     stmt.setInt(    7, 0 );     stmt.setNull(   8, OracleTypes.VARCHAR );     stmt.setNull(  10, OracleTypes.RAW );     stmt.executeUpdate();     errNo = stmt.getInt( 7 );     if( errNo != 0 ) {         errMsg = stmt.getString( 8 );         System.out.println( "Oracle error " + errNo +             ", " + errMsg );         System.out.println( (stmt.getRAW( 10 )).toString() );     } else {         **//mCryptSessionSecretDESSalt** = stmt.getRAW( 3 );         //mCryptSessionSecretDESIterationCount = stmt.getRAW( 4 );         //mCryptSessionSecretDESAlgorithm = stmt.getRAW( 5 );         //mCryptSessionSecretDESPassPhrase = stmt.getRAW( 6 );         **cryptData = stmt.getRAW( 10 );**         **System.out.println( getDecryptData( cryptData,**             mCryptSessionSecretDESPassPhrase,             mCryptSessionSecretDESAlgorithm, mCryptSessionSecretDESSalt,             mCryptSessionSecretDESIterationCount ) );     }     if( null != stmt ) stmt.close();

除了密钥交换,我们还将字符串“Tuesday”发送给p_get_des_crypt_test_data过程,以便在 Oracle 数据库上使用秘密密码密钥进行加密。因此,在执行了Statement之后,我们检索加密的数据,然后通过在客户端本地调用getDecryptData()来解密数据,并打印出解密后的String。注意,getDecryptData()方法获取所有加密的秘密密码密钥工件。

如果秘密密码密钥还没有建立,那么getDecryptData()调用makeSessionSecretDESKey()。当我们调用getDecryptData()时,我们传递足够的参数,即秘密密码密钥的工件,来构建密钥;但是,如果已经建成,我们就不重复这项工作。我们可能会多次调用getDecryptData()来获得多条加密数据,但是构建秘密密码密钥的工作将只进行一次。

测试 Oracle 数据库加密和本地解密数据

清单 6-24 中的下一个测试更加简洁,尽管不太现实。我们将调用临时函数f_get_crypt_data来获取代表明文String“星期三”的加密数据。我们将从语句中取回加密的数据RAW,并在本地调用getDecryptData()方法来解密它,打印结果。

清单 6-24。获取地穴数据,来自main()

    stmt = ( OracleCallableStatement )conn.prepareCall(         "{? = call app_sec_pkg**.f_get_crypt_data**(?) }" );     stmt.registerOutParameter( 1, OracleTypes.RAW );     stmt.setString( 2, **"Wednesday"** );     stmt.executeUpdate();     **cryptData = stmt.getRAW( 1 );**
    **System.out.println( getDecryptData( cryptData,**         mCryptSessionSecretDESPassPhrase,         mCryptSessionSecretDESAlgorithm, mCryptSessionSecretDESSalt,         mCryptSessionSecretDESIterationCount ) );     if( null != stmt ) stmt.close();

通常我们不会调用 Oracle 数据库来加密来自客户端的明文数据,然后在客户端解密它以获得明文。然而,这正是我们在这里所做的。

向 Oracle 发送加密数据

在清单 6-25 中的最后一个测试中,我们将在客户端加密明文数据“星期四”。我们用 DES 秘密密码密钥的副本加密它,基于我们从 Oracle 得到的工件。然后我们通过调用f_get_decrypt_data函数将加密的数据提交给 Oracle 数据库。Oracle 数据库将使用原始密码密钥解密数据,然后我们的客户机将读取作为参数 1 返回的明文String。我们打印结果并关闭Statement

清单 6-25。获取解密数据,来自main()

    **cryptData = getCryptData( "Thursday" );**     stmt = ( OracleCallableStatement )conn.prepareCall(         "{? = call app_sec_pkg**.f_get_decrypt_data(?)** }" );     stmt.registerOutParameter( 1, OracleTypes.VARCHAR );     **stmt.setRAW( 2, cryptData );**     stmt.executeUpdate();     **System.out.println( stmt.getString( 1 ) );**     if( null != stmt ) stmt.close();

这是另一个不太可能的场景,我们调用一个 Oracle 函数来解密数据以便在客户机上使用。不要担心向客户端暴露不需要的功能;我们将继续解决这个问题。事实上,我们已经有了!我们的客户端应用不会作为appsec用户连接,只有appsec能够执行app_sec_pkg包中的过程和功能。

测试我们的安全客户端/服务器数据传输

在命令提示符下,将目录切换到第六章。使用以下命令编译代码:

javac orajavsec/OracleJavaSecure.java

同样,如果你有任何问题,参考第三章关于在命令提示符下编译和设置你的环境CLASSPATH包括 ojdbc6.jar 的指导。然后使用以下命令运行同一目录中的代码:

java orajavsec.OracleJavaSecure

结果将打印以下六行:

Monday PBEWithSHA1AndDESede PBEWithMD5AndDES Tuesday Wednesday
Thursday

前两行显示了我们在本章前面执行的相同的仅客户端测试。之后,因为testingOnServertrue,main()方法继续从 Oracle 获取 DES secret 密码密钥。然后我们调用f_show_algorithm并在命令提示符下显示协商好的算法,很可能是PBEWithMD5AndDES

之后,我们通过p_get_des_crypt_test_data过程将字符串“星期二”发送到 Oracle 数据库进行加密。我们读取加密的数据,用秘密密码密钥的副本解密,并显示“星期二”。

我们也直接调用函数f_get_crypt_data,传递给它明文字符串“星期三”。我们读取函数返回的加密数据,用秘密密码密钥的副本再次解密,并显示“星期三”。到目前为止,很明显,我们的 Oracle 连接会话正在为多个查询保留和重用相同的 RSA 和 DES 密钥。

最后,我们在客户机上加密字符串“Thursday”,并将其提交给 Oracle 函数f_get_decrypt_data。Oracle 数据库使用原始密码密钥解密数据。我们读取返回的明文字符串并打印出来,“星期四。”

章节回顾

我们发现自己正处于另一章的末尾。回顾过去,让我们看看我们走过了哪些路。

  • 我们学习了 DES 秘密密码加密。特别是,我们了解了组成秘密密码密钥的各种工件:通行短语、salt、迭代计数和算法。我们还设计了一种方法来观察、报告和解决 JCE 的一个 bug,甚至编码来适应最终的 JVM 升级,而不会有这个 bug。
  • 我们发现了如何在 Oracle 上生成密码密钥,进行密钥交换,然后在客户机上构建一个相同的密钥。使用相同的密钥(共享密码),我们可以交换加密数据。我们还看到了如何使用 RSA 公钥来加密 DES 密钥,这样我们就可以在 Oracle 数据库和客户机之间交换它,但仍然保持它的保密性。
  • 我们广泛使用了SecureRandom类,以便为每个会话生成一个随机的密码短语、salt 和迭代计数。
  • 我们研究了 Oracle 包,这是我们在组织和安全性方面所依赖的东西。

图 6-1 和 6-2 展示了我们在本章中介绍的秘密密码加密过程。这些过程将贯穿本书的其余部分。在图 6-1 的顶部,你会看到我们指的是图 5-1 中的【A】——在那里我们看到了在客户端生成 RSA 公钥/私钥对的标准流程。当我们调用p_get_shared_pass_phrase时,我们将公钥指数和模数传递给 Oracle 数据库。您可以在图 6-1 中看到,在最右边,一个等价的公钥建立在 Oracle 数据库上,由标记为 RSA 的密钥图像表示。最右边的另一个密钥图像,标记为 DES,描述了我们在 Oracle 数据库上创建的共享密码密钥。

共享密码密钥的每个工件都在 Oracle 数据库上用公钥加密,并返回给客户机。每个加密的工件都放在p_get_shared_pass_phrase过程的一个OUT参数中。我们已经概述了整个过程,并在图 6-1 中将其标记为【B】区块。这是我们交换共享密码密钥的标准过程,我们将在以后的图中引用它。

在图 6-1 的底部,我们说明了客户端如何调用方法来解密秘密密码密钥工件并构建一个等价的秘密密码密钥。底部的密钥图像代表客户端上的密码密钥。

images

图 6-1。密钥交换

在图 6-2 中,您可以看到在客户端和 Oracle 数据库之间交换加密数据的示例流程图。在图 6-2 的前三分之一,您可以看到在 Oracle 数据库上进行密钥交换和数据加密的过程。加密的数据在p_get_des_crypt_test_data过程的OUT参数中返回给客户端。这里我们指的是图 6-1 的【B】块,在这里我们看到了在 Oracle 数据库上构建并返回给客户端的密码密钥。在图 6-2 中,除了建立密码密钥,我们还使用该密钥对以加密形式返回给客户端的数据进行加密。

在图 6-2 的中间部分,通过调用getDecryptData()方法(两个同名方法的版本 A ,我们看到了客户端如何解密来自 Oracle 数据库的数据的图示。我们概述了图 6-2 的这一部分,并将其标记为块【C】——它说明了解密数据的标准过程。您可以看到,在解密过程中,在客户机上建立了一个等价的加密密码密钥,标记为 DES(根据需要)。

图 6-2 的最后一部分,在底部显示了这个过程的镜像。在这个例子中,客户机使用已经构建好的等价密码密钥来加密数据。然后通过调用f_get_decrypt_data函数将加密的数据发送到 Oracle 数据库。然后,Oracle 数据库使用原始密码密钥解密数据。在我们的示例代码中,解密后的明文数据被返回给客户端进行显示。

images

图 6-2。加密数据交换示例

七、传输中的数据加密

在第六章中,我们为 Oracle 数据库和 Java 客户端之间的数据加密奠定了基础。我们证明了我们可以安全地交换密钥,然后来回发送加密数据,成功地在接收方和 Oracle 数据库中解密数据。

在本章中,我们将完成加密的基础,我们将继续在应用安全中构建加密,appsec Oracle 模式。然后,我们将扮演应用开发人员的角色,使用appsec结构来保护对我们数据的访问。具体来说,我们将保护对HR示例模式中数据的访问。

考虑最后一章,观察我们作为appsec用户构建和测试我们的应用安全结构和代码。并不打算让每个应用都作为appsec运行。相反,我们将允许每个需要我们安全性的应用执行我们的安全功能,我们将在本章中演示这一点。

此外,我们需要为开发人员提供对 Java 结构的访问,以包含在他们的桌面应用中。我们将在本章末尾讨论这一点。

安全管理员活动

我们的安全管理员secadm需要提供更多的权限。有些权限是系统特权,有些是授予appsecHR模式中的包的。

images 你可以在名为chapter 7/sec ADM . SQL的文件中找到以下命令的脚本。

以 SECADM 用户身份连接到 Oracle 数据库,并获得安全应用角色secadm_role

EXECUTE sys.p_check_secadm_access;

我们将在appsec模式中为记录错误创建一个表。我们还将创建一个与该表相关联的触发器。我们的触发器就像一个在特定事件发生时运行的过程——在我们的例子中,当一条记录被插入到我们的表中时,我们的触发器就会运行。

我们希望有一个应用错误的中央表,因为错误消息可能会返回给几十或几百个应用。作为应用安全管理员,我们如何从所有这些来源获得报告?如果我们的应用开发人员是认真的,他们会让我们知道他们看到了什么问题,但我们不指望这种情况会发生。我们将从我们的远程监听站——错误表——监控错误。

授予应用安全用户更多的系统权限

为了成功设置错误日志表,我们需要允许appsec将数据存储在表空间中。默认的表空间是“USERS”,这就足够了。我们需要指定appsec可以使用多少空间,一个配额。我们将开始允许两兆字节的空间。以appsec的身份执行以下操作:

ALTER USER appsec DEFAULT TABLESPACE USERS QUOTA 2M ON USERS;

此外,为了让appsec能够创建触发器,我们需要授予CREATE TRIGGER系统特权。我们将把它授予她的非默认角色(她只是偶尔需要它):

GRANT CREATE TRIGGER TO appsec_role;

允许用户在其他模式下执行包

我们希望HR执行appsec安全结构。我们希望创建一个角色,我们可以向其授予对包的执行权限,然后将该角色授予任何需要它的人。然而,让我们来看看为什么这种方法并不总是有效。请考虑以下不应该执行的语句:

--CREATE ROLE appsec_user_role NOT IDENTIFIED; --GRANT EXECUTE ON appsec.app_sec_pkg TO appsec_user_role; --GRANT appsec_user_role TO hr;

具体来说,当过程、函数和包调用其他模式中的过程、函数和包时,这里说明的方法不起作用。为了超前一点,我们将在HR模式中创建我们希望用来执行app_sec_pkg包的过程(你知道,我们希望HR调用函数来加密数据)。

问题是HR过程、函数和包不能从角色获得特权。这是一个限制(基于依赖模型),旨在防止每次我们注销或设置角色时HR过程失效。我们通过将对app_sec_pkg包的执行权直接授予HR用户来弥补这个限制。例如,执行以下代码:

GRANT EXECUTE ON appsec.app_sec_pkg TO hr;

与此形成鲜明对比的是,我们的应用用户appusr和其他应用用户将根据需要直接调用HR模式中的过程、函数和包。我们不想象appusr从他们自己的过程调用我们的过程。因此,我们可以将对我们的HR安全包hr_sec_pkg的访问权授予appusr所拥有的角色hrview_role。下面是 GRANT 语句,我们将在创建hr.hr_sec_pkg包之后执行它:

--GRANT EXECUTE ON hr.hr_sec_pkg TO hrview_role;

稍后我们将执行一个名为 HR.sql 的脚本,该脚本创建 hr.hr_sec_pkg 包,并执行前面的 GRANT 语句。

应用安全用户活动

我们将创建一个错误日志表和一个插入触发器,我们还将添加一些程序来记录到app_sec_pkg包中。

images 注意你可以在名为chapter 7/appsec . SQL的文件中找到以下命令的脚本。

appsec用户身份连接到 Oracle 数据库,并将您的角色设置为非默认角色appsec_role:

SET ROLE appsec_role;

创建错误记录表

接下来,创建一个错误日志记录表。您可能希望发挥 DBA 的技能,或者让 DBA 帮助您定义该表,设置其性能参数并估计初始存储和增长计划,这些都没有在此处定义。执行清单 7-1 中的代码,使用默认值创建表格。

清单 7-1。创建应用安全错误日志表,t_appsec_errors

CREATE TABLE appsec.t_appsec_errors (     err_no     NUMBER,     err_txt    VARCHAR2(2000),     msg_txt    VARCHAR2(4000) **DEFAULT** NULL,     update_ts  DATE **DEFAULT** SYSDATE );

我们将捕获 Oracle 错误号err_no和文本err_txt,并为自己提供另一个字段msg_txt,用于提供有用的信息(例如,方法名或堆栈跟踪)。我们还将捕获错误的时间,update_ts,这在两个方面有所帮助:首先,我们想知道事情是什么时候发生的,或者正在发生什么;第二,当日志记录太旧而不再有用时,我们希望将其丢弃。

注意,清单中最后两个列定义使用关键字DEFAULT指定默认值。要插入记录,只需插入前两个字段。第三个默认为NULL,第四个默认为 Oracle 数据库上的当前日期和时间SYSDATE。事实上,我们不想在UPDATE_TS中插入日期。我们希望接受默认设置。因为appsec是唯一一个将在该表中输入数据的人(这是一个用于应用安全的错误日志,不是用于一般用途),我们不需要实施默认的UPDATE_TS

为了按日期完成记录的排序和选择,我们将在UPDATE_TS列建立一个索引。执行清单 7-2 中的代码来创建索引。

清单 7-2。为应用安全错误日志表的索引,t_appsec_errors

CREATE INDEX i_appsec_errors00 ON appsec.t_appsec_errors (        update_ts );

索引一旦创建,就会在您插入或更新行时自动维护。此外,当您从表中选择记录时,会自动使用它们。使用哪个索引是 Oracle 数据库做出的逻辑选择,如果需要的话,可以用提示覆盖。需要注意的一点是,没有提到索引前导列的选择查询不会直接从索引中受益。例如,如果我们从t_appsec_errors表中选择所有行,其中msg_txt列包含字符串“Exception”,我们就不会直接使用我们刚刚创建的索引。如果我们在列(update_ts, msg_txt)上创建一个索引,这个索引也不会直接有利于我们的查询。当我们从msg_txt中进行选择时,为了获得索引的直接好处,我们希望创建一个索引,将msg_txt作为第一列,例如(msg_txt, update_ts)。

Oracle 数据库优化器实际上可以通过执行跳过扫描来使用任何索引来提高查询的性能。跳过扫描可以提高简单查询的性能,潜在地减少对全表扫描的依赖(全表扫描会导致非常差的性能)。还可以使用 skip scan 提示显式命名要使用的索引。从跳过扫描提示中获得最佳性能可能需要一些反复试验和一些工程设计。下面是一个摘自 Oracle 文档的示例。带有加号和提示名称的注释部分( / / )作为对优化器的提示。

SELECT **/*+ INDEX_SS(e emp_name_ix) */** last_name     FROM employees e     WHERE first_name = 'Steven';

注意 skip scan 提示,INDEX_SS指示优化器使用索引emp_name_ix进行查询。即使我们选择了first_name列为‘史蒂文’的记录,我们也要求受益于last_namefirst_name的索引。为了更好地理解跳过搜索优化,我建议做一个互联网搜索的例子。

您可能希望根据不同的列对t_appsec_errors表进行排序或选择,但是除非有一个频繁的查询需要对该列进行排序,否则您不需要索引。因为这个表实际上只用于错误后的故障排除,所以我们不期望有其他索引—我们将总是选择最近的记录(基于update_ts)。

我们将为准备授予 select 权限的表创建一个视图;但是,我们目前还没有想到任何可能需要查看它的人。也许将来我们会有一个精明的应用开发者,他想帮助调试他的应用对appsec包的使用。我们可以安排她从我们的视野中选择。执行以下操作:

CREATE OR REPLACE VIEW appsec.v_appsec_errors AS SELECT * FROM appsec.t_appsec_errors;

创建一个表格来管理我们的错误日志表

请记住,我们只给应用安全性用户appsecUSERS表空间中提供了两兆字节的空间。创建一个表,尤其是一个日志表,而不提供任何定期清理和维护,即使不是疏忽,也是不体谅人的。

现在告诉你一个秘密:我们正在制造一个机器人。不是用来取咖啡的机械装置,而是帮助我们管理错误日志表的软件哨兵,尤其是在我们不注意的时候。我们将用一个触发器自动删除表中的旧记录,每当我们向表中插入一条记录时,该触发器就会运行。

棘手的是,管理我们的表涉及到一些工作,所以我们希望最小化管理任务发生的频率。事实上,我们只想每天管理一次错误日志表。我们也不介意知道表最后一次被管理是什么时候。实现这两个目标的最好方法是,每当管理错误日志表时,创建另一个表来存储日期。执行清单 7-3 中的代码,创建错误日志维护表。

清单 7-3。创建应用安全错误日志表和索引,t_appsec_errors_maint

CREATE TABLE appsec.t_appsec_errors_maint (     update_ts DATE DEFAULT SYSDATE ); CREATE UNIQUE INDEX i_appsec_errors_maint00 ON appsec.t_appsec_errors_maint (        update_ts );

同样,按日期选择对性能很重要,所以我们将在UPDATE_TS列(唯一的一列)上创建一个索引。

这一次,我们使它成为一个UNIQUE索引,这意味着我们将只有一个带有特定时间戳的条目。在我们的错误日志表上,索引不是UNIQUE,因为我们可能同时在表中有多个错误和条目。

t_appsec_errors_maint表仅供内部使用,所以我们不会创建视图,也不会在表上授予特权。

创建错误日志管理程序

我们的表管理任务是由触发器发起的,但是在定义触发器之前,我们需要定义完成管理任务的过程。我们的管理过程将被命名为p_appsec_errors_janitor,它没有参数。

我们希望它独立运行;所以我们用修饰语PRAGMA AUTONOMOUS_TRANSACTION来定义。这允许过程执行插入、删除和提交更改,即使调用该事务的程序没有提交。如果没有这个修饰符,如果我们在这里发出一个 commit,我们就要求 Oracle 数据库提交我们在当前会话中所做的每个更新、插入或删除。当我们处理一个错误时,除了在日志中插入错误消息和清除旧条目之外,我们特别希望避免提交任何东西。执行清单 7-4 中的脚本来创建程序。

清单 7-4。管理错误日志表的程序,p_appsec_errors_janitor

CREATE OR REPLACE PROCEDURE appsec.p_appsec_errors_janitor AS     PRAGMA AUTONOMOUS_TRANSACTION;     m_err_no NUMBER;     m_err_txt VARCHAR2(2000); BEGIN     INSERT INTO t_appsec_errors_maint ( update_ts ) VALUES ( SYSDATE );     COMMIT;     -- Remove error log entries over 45 days old     DELETE FROM t_appsec_errors WHERE update_ts < ( SYSDATE - 45 );     COMMIT;     INSERT INTO t_appsec_errors         ( err_no, err_txt, msg_txt ) VALUES         ( 0, 'No Error', 'Success managing log file by Janitor' );     COMMIT; EXCEPTION     WHEN OTHERS     THEN         m_err_no := SQLCODE;         m_err_txt := SQLERRM;         INSERT INTO t_appsec_errors             ( err_no, err_txt, msg_txt ) VALUES             ( m_err_no, m_err_txt, 'Error managing log file by Janitor' );         COMMIT; END; /

我们的第一步(在BEGIN头之后)是在我们的管理表中插入当前日期。这将防止其他人也试图管理错误日志表。我们提交它,这在这里是允许的,因为我们是一个自治的事务。

第二步是删除错误日志中超过 45 天的记录。注意,我们做了一些涉及SYSDATE的日期运算,在这里是SYSDATE - 45,相当于 45 天前。我们将在触发器中使用类似的日期算法。我们也提交这一删除。

BEGIN标题下的最后一件事是在错误日志中插入一个“成功”消息,并提交它。为什么不呢?那似乎是个好地方。

正如我们到目前为止看到的其他过程一样,我们将捕捉错误。在这种情况下,我们将把错误插入到我们的错误日志表中(为什么不呢?将我们所有的故障排除信息放在一个地方会很好。)并提交。

创建触发器以维护错误日志表

我们上面定义的维护过程将在您每次调用它时工作,无论您如何调用它。你可以雇人每天手动运行一次这个过程。Oracle 数据库有一个调度器(DBMS_SCHEDULER PL/SQL包或旧的DBMS_JOB PL/SQL包),您可以使用它每天运行一次。

相反,我们将通过添加一个触发器来使表自治。触发器与过程有很多相似之处,所以它与我们一直在讨论的语法是一致的。执行清单 7-5 中的代码,在t_appsec_errors表上创建并启用一个触发器,该触发器将在每行(日志条目)插入表中后运行。

清单 7-5。在错误日志表上插入触发器,t_appsec_errors_iar

CREATE OR REPLACE TRIGGER appsec.t_appsec_errors_iar     **AFTER INSERT** ON t_appsec_errors FOR EACH ROW DECLARE     m_log_maint_dt DATE; BEGIN     **SELECT MAX( update_ts ) INTO m_log_maint_dt** FROM t_appsec_errors_maint;     -- Whenever T_APPSEC_ERRORS_MAINT is empty, M_LOG_MAINT_DT is null     IF( ( m_log_maint_dt IS NULL ) OR         ( m_log_maint_dt < ( SYSDATE - 1 ) ) )     THEN         p_appsec_errors_janitor;     END IF; END; / ALTER TRIGGER appsec.t_appsec_errors_iar ENABLE;

此触发器在每次插入后运行,AFTER INSERT;然而,我们只希望我们的过程每天运行一次。为了实现这一点,我们从t_appsec_errors_maint表中获取我们的过程最后一次运行的MAX( update_ts ),并将该日期存储在m_log_maint_dt中。(注意这个SELECT INTO语法的例子——选择一个变量的值。)然后我们检查m_log_maint_dt是否为NULL(每当t_appsec_errors_maint表为空时)或者m_log_maint_dt是否早于 24 小时前(< SYSDATE – 1)。如果是,那么我们运行我们的过程,p_appsec_errors_janitor

测试触发器

当您作为appsec用户连接到 Oracle 数据库时,您可以测试触发器。首先执行下面几行来插入一个错误日志条目并提交它:

INSERT INTO appsec.v_appsec_errors (err_no, err_txt ) VALUES (1, 'DAVE' ); COMMIT;

注意,我们的自治过程只能处理独立存在的数据。我们的插入和更新不会独立存在于数据库中,直到我们COMMIT数据。

还要注意,我们依赖于默认值msg_txtupdate_ts——这些列不是我们的 insert 语句的一部分。

查询我们的每个表、错误日志和维护记录,观察我们之前的插入是否成功,以及看门人过程是否运行。这里有一个例子:

SELECT * FROM appsec.v_appsec_errors ORDER BY update_ts; SELECT * FROM appsec.t_appsec_errors_maint ORDER BY update_ts;

现在插入一个假装 60 天的错误日志条目(注意带SYSDATE的算术):

INSERT INTO appsec.v_appsec_errors (err_no, err_txt, msg_txt, update_ts)     VALUES (2, 'DAVE', 'NONE', SYSDATE - 60 ); COMMIT;

再次查询我们的每个表,以确保我们的插入有效,并且我们的看门人过程没有再次运行(因为它已经在这一天运行过):

SELECT * FROM appsec.v_appsec_errors ORDER BY update_ts; SELECT * FROM appsec.t_appsec_errors_maint ORDER BY update_ts;

现在,将我们最后一次看门人维护运行日期的数据更改为昨天(实际上是 24 小时前),并确保更改有效(如果您在此表中有多个条目,此UPDATE将不起作用。本表中UPDATE_TS上的指数为UNIQUE指数):

UPDATE appsec.t_appsec_errors_maint SET update_ts = SYSDATE-1; COMMIT; SELECT * FROM appsec.t_appsec_errors_maint ORDER BY update_ts;

并从今天开始提交另一条记录(默认,SYSDATE):

INSERT INTO appsec.v_appsec_errors (err_no, err_txt ) VALUES (3, 'DAVE' ); COMMIT;

在这些测试中,最后一次查询我们的每个表,以确保我们的插入有效,我们的看门人过程再次运行,并且模拟旧记录(带有err_no = 2的记录)被删除:

SELECT * FROM appsec.v_appsec_errors ORDER BY update_ts; SELECT * FROM appsec.t_appsec_errors_maint ORDER BY update_ts;

更新应用安全包

在第六章的中,我们有两个过程(p_get_shared_passphrasep_get_des_crypt_test_data)和一个函数(f_show_algorithm),我们称之为“临时的”。它们仅在第六章的中用于测试,我们将在本章的app_sec_pkg中删除它们。在app_sec_pkg中的功能的剩余部分将会保留并且没有被改变。查看文件,chapter 7/appsec . SQL查看完整清单。我们在本章中引入了一个新的程序:p_log_error

创建错误日志记录过程

p_log_error程序采用一个NUMBER和一个或两个VARCHAR2(文本)参数。err_txt字段限制为 2,000 个字符,但一个VARCHAR2列最多可包含 4000 个字符;因此,如果需要,我们将m_err_txt参数截断为 2,000 个字符,以适合我们的err_txt列。

注意,这个过程(包)和被更新的表在appsec模式中,但是调用这个过程的可能是另一个模式中的应用(比如HR)。我们已经将对app_sec_pkg包的执行权授予了HR用户,我们需要将执行权授予任何其他需要我们的应用安全进程的应用用户。

如果你愿意,回想一下我们定义t_appsec_errors表的时候。回想一下,我们将msg_txtupdate_ts列设置为可空,并使用默认值(NULLSYSDATE)。这允许我们通过为前两列提供数据元素来插入数据。我们甚至可以在不提及最后两列的情况下插入数据。其实我们说过不想给update_ts插入一个值;而是允许 Oracle 数据库分配当前的默认值SYSDATE

好了,现在我们正在创建一个程序(如清单 7-6 中的所示)供各种应用调用,以便将错误记录插入到我们的表中,并且该程序考虑了那些默认值。首先,过程不接受update_ts的值;相反,将使用默认的SYSDATE。第二,msg_txt的值有一个缺省值NULL,这样应用用户可以在有或者没有msg_txt值的情况下调用这个过程。

清单 7-6。插入日志条目的程序,p_log_error

    PROCEDURE p_log_error( m_err_no NUMBER, m_err_txt VARCHAR2,         m_msg_txt VARCHAR2 DEFAULT NULL )     IS         l_err_txt VARCHAR2(2000);     BEGIN         l_err_txt := **RTRIM( SUBSTR( m_err_txt, 1, 2000 ) )**;         INSERT INTO v_appsec_errors ( err_no, err_txt, msg_txt )             VALUES ( m_err_no, l_err_txt, m_msg_txt );         COMMIT;     END p_log_error;

我们使用substring函数SUBSTR,只获取错误文本的前 2000 个字符。然后,我们使用右修剪功能,RTRIM删除任何空格在右端的剩余文本。如果m_err_txtNULLSUBSTR返回一个NULLRTRIM返回一个NULL

p_log_error过程的最后,我们简单地将错误数据插入到我们的错误日志表和COMMIT中。

执行包规格和主体

执行名为chapter 7/appsec . SQL文件中的两个块来替换app_sec_pkg包规范和主体。您可以看到这两个块都是以命令CREATE OR REPLACE开始的。因为我们已经有了一个名为app_sec_pkg的包,这个命令将替换它。这个命令最大的优点是我们可以在运行的 Oracle 数据库上执行它,并且使用这个包的应用不会失败。也就是说,如果封装规格不需要改变。考虑另一个选项:如果我们不得不DROP然后分别CREATE这些结构,我们将不得不等待直到 Oracle 数据库离线,或者至少直到依赖的应用不运行;否则,在DROPCREATE之间的过渡期间,应用将会失败。

传输中使用和测试加密的方法

我们的工作模型不会从OracleJavaSecuremain()方法进行测试;相反,我们将展示如何作为一个独立的应用使用我们的应用安全包app_sec_pkg的结构。我们将在OracleJavaSecure类中再添加两个方法:一个用于测试,resetKeys();另一个是让客户端准备加密数据以更新/插入 Oracle 数据库,makeDESKey()

我们希望能够以最少的工作量从我们的客户端应用进行数据更新。最简单的工作需要以下步骤:

  1. 在客户端生成 RSA 密钥,并将公钥传递给 Oracle。
  2. 在 Oracle 数据库上生成 DES secret 密码密钥,用 RSA 公钥加密工件,并传递回客户端。
  3. 在客户端上构建 DES 密钥的副本。
  4. 用 DES 密钥加密数据并发送到 Oracle 数据库进行解密和更新。

我们已经演示了一个 Oracle 过程p_get_shared_passphrase,它允许我们将步骤 1 和 2 合并成一个步骤。但是,第 4 步需要第二条 Oracle 语句。因此,我们至少需要两次调用 Oracle 数据库来进行第一次更新。在同一个 Oracle 会话中,我们可以进行额外的更新,每次只需一个调用。我们只需要做一次组合步骤 1、2、3(密钥交换);然后在建立了键之后,我们可以使用现有的键进行尽可能多的更新和插入。

建立秘密口令密钥的方法

在第六章中,我们使用了p_get_shared_passphrase Oracle 过程将所有的 DES secret 密码密钥工件拿到客户端;然而,直到我们从 Oracle 数据库接收到想要在客户机上解密的加密数据,我们才构建了秘密密码密钥。

在本章中,即使没有数据要解密,我们也需要 DES 密钥。我们将在客户端进行数据加密,并将其作为独立任务发送到 Oracle 数据库。因此,我们需要一个 Java 方法来独立地构建密码密钥。清单 7-7 显示了该方法的代码。

清单 7-7。方法调用建立秘密密码的密钥,makeDESKey()

    public static final void makeDESKey(         RAW cryptSecretDESPassPhrase, RAW cryptSecretDESAlgorithm,         RAW cryptSecretDESSalt, RAW cryptSecretDESIterationCount )     {         try {             decryptSessionSecretDESPassPhrase( cryptSecretDESPassPhrase,                 cryptSecretDESAlgorithm, cryptSecretDESSalt,                 cryptSecretDESIterationCount );             makeSessionSecretDESKey();         } catch( Exception x ) {             x.printStackTrace();         }     }

try块中是我们之前的getDecryptData()方法的大部分主体,没有实际解密数据的调用。这给我们提供了一个机会来做一些重构,改进我们代码的设计。因为我们的新方法完成了我们在getDecryptData()中所做的大部分工作,所以让我们重写getDecryptData()来调用新方法,如清单 7-8 所示。

清单 7-8。用秘密密码密钥解密数据,getDecryptData()

    public static final String getDecryptData( RAW cryptData,         RAW cryptSecretDESPassPhrase, RAW cryptSecretDESAlgorithm,         RAW cryptSecretDESSalt, RAW cryptSecretDESIterationCount )     {         String rtrnString = "getDecryptData() A failed";         try {             if( ( null == sessionSecretDESKey ) || testAsClientAndServer ) {                 **makeDESKey**( cryptSecretDESPassPhrase, cryptSecretDESAlgorithm,                     cryptSecretDESSalt, cryptSecretDESIterationCount );             }             rtrnString = getDecryptData( cryptData );         } catch( Exception x ) {             x.printStackTrace();         }         return rtrnString;     }

粗体文本,我们对makeDESKey()的调用,是我们之前已经移入makeDESKey()主体的代码。

重置所有键的临时方法

我们添加到OracleJavaSecure的第二个方法是resetKeys()resetKeys()方法仅用于本章中的测试(不过,我们将在第十章中再次提到它)。稍后我们将描述几个测试场景,其中一个将模拟在客户机上启动一个新的连接/会话(通过运行这个方法)并尝试使用 Oracle 数据库上的现有键。这个场景将会失败,但是我们将进行测试来演示这个场景。

resetKeys(),清单 7-9 中,我们将最初设置为null的静态成员设置回null。回想一下,它们是null,以便于为null测试那些变量和/或测试与其他成员的比较。我们需要最初将它们设置为null,以便编译通过“可能尚未初始化”错误消息。

我们还将sessionSecretDESAlgorithm的值重置为其预先协商的值。

清单 7-9。复位所有按键,resetKeys()

    public static final void resetKeys() {         locRSAPubMod = null;         saveExtRSAPubMod = null;         extRSAPubKey = null;         sessionSecretDESPassPhraseChars = null;         sessionSecretDESKey = null;         sessionSecretDESAlgorithm = "PBEWithSHA1AndDESede";     }

将更新的 OracleJavaSecure 类加载到 Oracle 中

以应用安全用户appsec、非默认角色用户appsec_role的身份连接或保持连接到 Oracle 数据库,并将文件 第七章\ orajavsec \ Oracle javasecure . Java中的代码复制/粘贴到您的 Oracle 客户端。取消第一行的注释,然后运行脚本来替换 Oracle 数据库中的 Java 类。

CREATE OR REPLACE AND RESOLVE JAVA SOURCE NAMED appsec."orajavsec/OracleJavaSecure" AS

人力资源用户的安全结构

我们的应用加密工作模型将包括从HR模式中读取数据,敏感列在通过网络传输时被加密。部分责任落在了应用开发者身上,他们必须确保敏感数据只能以加密的形式提供给客户端。我们的应用安全模式appsec可以提供这些工具,但是我们的应用开发者,比如HR,需要实现它们。

先来探究一下HR是怎么加密他的数据的。然后,我们将看看应用安全管理器可以为所有应用开发人员提供什么样的模板来实现这一点。

探索支持人力资源任务的权限

Oracle 提供的HR是一个示例模式,已经拥有各种系统特权。HR拥有默认角色RESOURCE,并通过该角色拥有以下权限列表:

CREATE SEQUENCE, CREATE TRIGGER, CREATE CLUSTER, CREATE PROCEDURE, CREATE TYPE, CREATE OPERATOR, CREATE TABLE, CREATE INDEXTYPE

所有打算实现我们的应用安全性的应用模式都需要CREATE PROCEDURE系统特权。

回想一下,要访问应用安全结构,我们还需要授予每个应用模式执行app_sec_pkg包的对象特权,就像我们对HR所做的那样(已经作为secadm):

GRANT EXECUTE ON appsec.app_sec_pkg TO hr;

创建人力资源安全包

HR将拥有自己的过程和函数包,提供对HR表的访问,但仅以加密形式返回任何敏感列。让我们检查一下这个包,然后在最后创建它。

images 你可以在名为 * Chapter7 /HR.sql* 的文件中找到以下命令的脚本。

CREATE OR REPLACE PACKAGE hr.hr_sec_pkg IS     **TYPE RESULTSET_TYPE IS REF CURSOR;**

在我们的包的规范中,我们将定义一个TYPE。我们将其命名为RESULTSET_TYPE,它将代表一个CURSOR,在 Java 中也称为ResultSet。当我们调用过程来获取我们的加密的HR数据时,我们将从 Oracle 数据库返回一些OUT参数。正如我们已经看到的,许多OUT参数将是我们的秘密密码密钥的工件,其中一个也可能是RESULTSET_TYPE,它将保存多行加密数据

从员工中选择敏感数据列

清单 7-10 中的代码是一个名为p_select_employees_sensitive的 Oracle 过程的主体。你应该非常熟悉这种格式。用于设置秘密密码密钥工件的参数列表和代码看起来就像我们之前看到的那样。我们有一个名为resultset_outOUT参数,它将保存一个RESULTSET_TYPE(数据行):

清单 7-10。从雇员表中选择敏感数据的程序,p_select_employees_sensitive

PROCEDURE p_select_employees_sensitive(         ext_modulus               VARCHAR2,         ext_exponent              VARCHAR2,         secret_pass_salt      OUT RAW,         secret_pass_count     OUT RAW,         secret_pass_algorithm OUT RAW,         secret_pass           OUT RAW,         **resultset_out         OUT RESULTSET_TYPE**,         m_err_no              OUT NUMBER,         m_err_txt             OUT VARCHAR2 )     IS BEGIN         m_err_no := 0;         secret_pass_salt :=             appsec.app_sec_pkg.f_get_crypt_secret_salt( ext_modulus, ext_exponent );         secret_pass_count :=             appsec.app_sec_pkg.f_get_crypt_secret_count( ext_modulus, ext_exponent );         secret_pass :=             appsec.app_sec_pkg.f_get_crypt_secret_pass( ext_modulus, ext_exponent );         secret_pass_algorithm :=             appsec.app_sec_pkg.f_get_crypt_secret_algorithm(ext_modulus, ext_exponent); **        OPEN resultset_out FOR SELECT**             employee_id,             first_name,             last_name,             email,             phone_number,             hire_date,             job_id,             appsec.app_sec_pkg.**f_get_crypt_data**( TO_CHAR( salary ) ),             appsec.app_sec_pkg.**f_get_crypt_data**( TO_CHAR( commission_pct ) ),             manager_id,             department_id         FROM employees;     EXCEPTION         WHEN OTHERS THEN             m_err_no := SQLCODE;             m_err_txt := SQLERRM;             appsec.app_sec_pkg.p_log_error( m_err_no, m_err_txt,                 'HR p_select_employees_sensitive' );     END p_select_employees_sensitive;

填充结果集 _ 类型

p_select_employees_sensitive过程的中间,我们打开RESULTSET_TYPE从查询中收集一个CURSOR。注意,当我们返回到客户端时,我们实际上并没有传输所有的数据;相反,我们为客户端提供了一个CURSOR句柄,这样客户端就可以收集和处理数据行,一次一行。

我们使用的查询选择了EMPLOYEES表中的所有列。注意清单 7-10 中的,我们用这些调用加密了SALARYCOMMISSION_PCT:

    appsec.app_sec_pkg.f_get_crypt_data( TO_CHAR( salary ) ),     appsec.app_sec_pkg.f_get_crypt_data( TO_CHAR( commission_pct ) ),

我们的加密方法要求我们使用String传递数据进行加密。SALARYCOMMISSION_PCT都是数字列,所以我们先把它们转换成VARCHAR2,然后传递给我们的应用安全 Java 存储过程(函数)appsec.app_sec_pkg.f_get_crypt_data

该函数返回一个保存加密数据的RAW类型。客户端将数据解密回明文String。并且我们会将数据转换回它原来的类型(Date,数字等)。),根据客户端的需要。

您可能会问,“但是我们不能加密非String数据吗?”答案是肯定的。实际上,我们可以加密任何可以表示为一个byte数组的东西,这实际上是任何东西,经过一些转换。然而,如果你能在屏幕上看到数据或者把它打印出来,那么你也可以把数据表示成一个String,当我们转换成Strings或者从Strings转换过来的时候,这通常会更清楚,而且通常情况下,我们最终无论如何都需要一个String

images 在本章结束时,你将有一个坚实的基础来扩展我们在这里建立的加密。您将能够扩展您所学的内容,以便加密对象或 BLOBS 或其他类型的数据。

记录错误信息

在过程的最后,我们捕获任何 Oracle 异常并记录错误。我们称我们的新程序为p_log_error。我们将在appsec模式中记录错误,这样我们的应用安全管理器就可以捕捉到appsec结构中的错误,并且可以帮助使用这些结构调试单个应用中的问题。我们不会孤立应用开发人员,但会在调试工作中提供帮助。

选择所有数据作为单个敏感字符串

HR实现了另一种方法,加密所有选择的数据,不是作为单独的列,而是作为每行一个长的连接的VARCHAR2。这个过程(清单 7-11 中的p_select_employees_secret)和我们上次看到的唯一区别是RESULTSET_OUT的定义。

清单 7-11。将所有数据加密为单个字符串的程序部分,p_select_employees_secret

    OPEN resultset_out FOR **SELECT** **        appsec.app_sec_pkg.f_get_crypt_data(**             TO_CHAR( employee_id ) ||', '||             first_name ||', '||             last_name ||', '||             email ||', '||             phone_number ||', '||             TO_CHAR( hire_date ) ||', '||             job_id ||', '||             TO_CHAR( salary ) ||', '||             TO_CHAR( commission_pct ) ||', '||             TO_CHAR( manager_id ) ||', '||             TO_CHAR( department_id )         )     FROM employees;

双管字符“||”是 Oracle 数据库中用来连接文本的符号。注意,我们为不属于VARCHAR2类型的列调用了TO_CHAR函数。在我们将所有这些列连接在一起之后,我们将得到的VARCHAR2传递给f_get_crypt_data函数进行加密,并在RESULTSET_OUT中为每行返回一个RAW

请注意,在客户端,我们可能必须在解密数据后解析数据,以获得各个列。我们使用逗号作为列之间的分隔符,但是解析逗号假设数据中不存在逗号。每个应用都必须规划它们对我们的应用安全结构的使用,以及以适合其客户使用的形式提供其数据的最佳方法。对于不需要单个记录元素的客户机来说,这种串联格式可能更好。

为员工 ID 选择敏感数据

我们将探索从HR中选择加密数据的程序的另一个例子。这个 Oracle 过程p_select_employee_by_id_sens与前两个过程几乎相同,除了它也采用一个表示单个EMPLOYEE_ID的参数。如清单 7-12 中的所示。

清单 7-12。通过 ID 选择敏感数据的程序部分,p_select_employee_by_id_sens

    m_employee_id    employees.employee_id%TYPE     ...     OPEN resultset_out FOR SELECT         employee_id,         ...     FROM employees     **WHERE employee_id = m_employee_id;**

resultset_out参数的查询选择了EMPLOYEE_ID等于该输入参数的数据。

这个过程应该只返回一行数据。

修改程序以获取共享密码

我们在第六章的中看到了p_get_shared_passphrase程序。在这一章中,我们用错误日志来修饰它。错误日志记录可以帮助应用安全性支持应用开发人员。

最大的变化是我们将app_sec_pkg包中的p_get_shared_passphrase放到我们自己的应用包hr_sec_pkg中。现在我们在hr_sec_pkg中有了它,这样我们的客户端应用(可能使用hr_view角色运行)就可以执行这个过程。我们允许HR执行app_sec_pkg结构,但是我们不允许hr_view这样做。因此,hr_view执行HR结构,HR结构执行appsec结构。

我们调用p_get_shared_passphrase并接着调用我们的新OracleJavaSecure.makeDESKey()方法来完成密钥交换并构建共享的秘密密码密钥。在尝试数据更新之前,我们必须这样做。

更新雇员中的敏感数据列

我们现在可以对数据实施加密更新了。我们将在包hr_sec_pkg中定义一个如清单 7-13 所示的过程p_update_employees_sensitive,以获取EMPLOYEES表中所有列的数据。对于敏感列,我们将提交封装加密数据的RAW类型。唯一的IN参数是表列数据,唯一的OUT参数是错误号和文本。注意这里缺少了什么——没有代表我们的加密密钥的参数。我们必须假设关键的交换已经发生。如果我们尚未在当前 Oracle 会话中交换密钥,则用户应用正在尝试在需要加密的字段中提交未加密的数据,或者他们正在使用不同会话中的密钥加密数据;Oracle 数据库将无法解密数据。

我们使用引用原始数据定义的锚定数据类型表单来定义参数类型。我们将这个数据类型声明锚定到前面的定义。例如,在该宣言中:

        m_employee_id        employees.employee_id%TYPE,

我们说m_employee_id参数与EMPLOYEES表中的EMPLOYEE_ID列是同一类型。我们将在适当的时候使用这种形式的“引用类型规范”来进一步建立我们正在接收的数据和它要到达的表之间的关系。这种做法至少有两个好处。第一,我们的过程将只接受适合于它将被插入或更新的字段的数据。这是对 SQL 注入攻击的进一步保护(见下一节的详细讨论)。锚定数据类型很好的第二个原因是,我们可以更改表中该列的定义,而不必同时更改这个过程。

清单 7-13。更新雇员表中的敏感数据,p_update_employees_sensitive

PROCEDURE p_update_employees_sensitive(         m_employee_id        employees.employee_id%TYPE,         m_first_name         employees.first_name%TYPE,         m_last_name          employees.last_name%TYPE,         m_email              employees.email%TYPE,         m_phone_number       employees.phone_number%TYPE,         m_hire_date          employees.hire_date%TYPE,         m_job_id             employees.job_id%TYPE,         **crypt_salary         RAW**,         **crypt_commission_pct RAW**,         m_manager_id         employees.manager_id%TYPE,         m_department_id      employees.department_id%TYPE,         m_err_no         OUT NUMBER,        m_err_txt        OUT VARCHAR2 )     IS         test_emp_ct      NUMBER(6);         v_salary         VARCHAR2(15); -- Plenty of space, eventually a NUMBER         v_commission_pct VARCHAR2(15);     BEGIN         m_err_no := 0;         v_salary := appsec.app_sec_pkg.f_get_decrypt_data( crypt_salary );         v_commission_pct :=             appsec.app_sec_pkg.f_get_decrypt_data( crypt_commission_pct );         SELECT COUNT(*) INTO test_emp_ct FROM employees WHERE             employee_id = m_employee_id;         IF test_emp_ct = 0         THEN             INSERT INTO employees                 (employees_seq.NEXTVAL, first_name, last_name, email, phone_number,                 hire_date, job_id, salary, commission_pct, manager_id, department_id)             VALUES                 (m_employee_id, m_first_name, m_last_name, m_email, m_phone_number,                 m_hire_date, m_job_id, v_salary, v_commission_pct, m_manager_id,                 m_department_id);         ELSE             -- Comment update of certain values during testing - date constraint             UPDATE employees             SET first_name = m_first_name, last_name = m_last_name, email = m_email,                 phone_number = m_phone_number,                 -- Job History Constraint -- hire_date = m_hire_date, job_id = m_job_id, `                salary = v_salary, commission_pct = v_commission_pct,
                manager_id = m_manager_id
                -- Job History Constraint -- , department_id = m_department_id
            WHERE employee_id = m_employee_id;
        END IF;
    EXCEPTION
        WHEN OTHERS THEN
            m_err_no := SQLCODE;
            m_err_txt := SQLERRM;
            appsec.app_sec_pkg.p_log_error( m_err_no, m_err_txt,
                'HR p_update_employees_sensitive' );
    END p_update_employees_sensitive;

END hr_sec_pkg;
/`

程序变量和数据解密

我们不能像在前面的示例过程中使用OUT参数那样修改IN参数,但是我们希望捕获解密的输出,因此我们建立了两个过程变量:v_salaryv_commission_pct。我们还定义了一个名为test_emp_ct的数值过程变量:

    test_emp_ct      NUMBER(6);     **v_salary**         VARCHAR2(15); -- Plenty of space, eventually a NUMBER     **v_commission_pct** VARCHAR2(15);

BEGIN标题下,我们的过程体包括对f_get_decrypt_data Oracle 函数的两次调用,该函数将返回一个代表SALARYCOMMISSION_PCTVARCHAR2数据类型。再次注意,此过程的使用假设您已经完成了密钥交换:

    m_err_no := 0;     v_salary := appsec.app_sec_pkg.**f_get_decrypt_data**( crypt_salary );     v_commission_pct :=         appsec.app_sec_pkg.**f_get_decrypt_data**( crypt_commission_pct );

插入或更新

我发现多功能过程通常是管理数据插入和更新的最佳选择。最多,我们给数据视图授予SELECT特权,给管理过程授予EXECUTE特权。我们定期传递一个事务代码(通常是 A、U 或 D ),它指示我们是否要插入(添加)、更新或删除一个记录。对于一个简单的双功能过程(插入或更新),我们不需要事务代码,但可以检查数据并 1)更新现有记录或 2)如果没有现有记录与键列匹配,则插入新记录。

在我们的管理过程主体中,我们将通过使用SELECT INTO语法来填充test_emp_ct,其中包含其EMPLOYEE_ID与被传入进行更新的m_employee_id相匹配的员工数量。不应该有超过一个,所以我们期望从计数中得到 0 或 1 的值。

    SELECT COUNT(*) INTO test_emp_ct FROM employees WHERE         employee_id = m_employee_id;

然后我们测试看test_emp_ct是否为 0——如果是,我们做一个INSERT;如果不是,一个UPDATE:

    IF test_emp_ct = 0     THEN         **INSERT** INTO employees             ...     ELSE         **UPDATE** employees         SET first_name = m_first_name, last_name = m_last_name, email = m_email,             phone_number = m_phone_number, hire_date = m_hire_date,             -- **Job History Constraint** -- job_id = m_job_id,             salary = v_salary, commission_pct = v_commission_pct,             manager_id = m_manager_id             -- **Job History Constraint** -- , department_id = m_department_id         WHERE employee_id = m_employee_id;     END IF IF;

雇员表上的完整性约束

您将在前面的代码中看到,我们跳过了更新两列:JOB_IDDEPARTMENT_ID。原因是在EMPLOYEES表上有一个现有的触发器,当EMPLOYEES记录中的这两列中的任何一列被更新时,该触发器会在JOB_HISTORY中插入一条记录。触发代码如清单 7-14 所示。

清单 7-14。对雇员表的现有完整性约束,HR.update_job_history

CREATE OR REPLACE TRIGGER HR.update_job_history   AFTER **UPDATE OF job_id, department_id ON HR.EMPLOYEES** FOR EACH ROW BEGIN   **add_job_history**(:old.employee_id, :old.hire_date, sysdate,                   :old.job_id, :old.department_id); END;

你可以在清单 7-14 的中看到触发器调用一个过程add_job_history。这个过程所做的只是将一条记录记录到JOB_HISTORY表中。然而,JOB_HISTORY表包含一个关于(EMPLOYEE_ID, 开始日期)的UNIQUE索引。

总结一下这个问题:如果您试图一天多次更新一个EMPLOYEES ' JOB_IDDEPARTMENT_ID,它会失败,因为触发器不能在同一天为同一个用户在JOB_HISTORY表中插入另一个记录。这是一个商业规则,HR示例模式的开发人员通过一个UNIQUE索引来强制执行——雇员一天内不能换工作超过一次。

更新触发器语法

我想指出触发器语法的一个方面。你看到清单 7-14 中的前缀了吗?该前缀表示我们正在使用表中已经存在的值。因为这是一个 AFTER UPDATE 触发器,所以表中存在的值与我们在更新中提交的值相同。这在数据更新后运行。

通常,触发器可用于测试、过滤和操作提交到表中的数据,然后再进行存储。例如,如果我正在更新一个雇员的姓氏,我可能会说:

update employees set last_name = ‘coffin’ where employee_id = 700;

如果我们要求所有的last_name条目都是大写字母,这可能会是个问题!我们可以用一个BEFORE UPDATE OR INSERT触发器来捕捉并纠正这个问题。在我们的触发器中间,我们可能会说:

:new.last_name := upper(:new.last_name);

这将使我们提交给姓氏的新值大写。如果我们想抱怨用户试图用已经存在的相同姓氏更新姓氏,我们可以将大写的新值与旧值进行比较,如下所示:

IF :new.last_name = :old.last_name THEN      Raise_Application_Error(-20000, 'Same last name as before!');

一个BEFORE UPDATE触发器可以访问数据库中的现有值(:old)和正在提交的新值(:new)。这种能力经常在触发器中使用。

避免 SQL 注入

如果计算机用户保存了提交数据的网页的 html 源,并且能够修改该网页以发送通常不被允许的数据,那么这将是交叉视线脚本的一个例子(用户自己的网页是一个站点,向另一个站点的 web 服务器提交数据。)例如,您可能有一个提交地址的邮政编码并且只允许数字数据的网页。我可能会恶意修改网页,使我的副本在邮政编码字段中提交一个 web 链接(URL)。对跨站点脚本的唯一真正预防是假设它总是会发生,并在服务器上采取措施来捕捉和处理它。

也许您的 web 页面向 Oracle 数据库提交数据,恶意用户修改了您的 web 页面的副本,以便在邮政编码字段中提交 Oracle SQL 或 PL/SQL 命令。黑客可能会将此代码放在字段“11111;delete from employees;--”中。如果您构建的动态查询只是将提交的数据嵌入到查询中,那么不用执行:

UPDATE EMPLOYEES SET ZIP=**11111** WHERE EMPLOYEE_ID=300;

您可以执行这组命令:

UPDATE EMPLOYEES SET ZIP=**11111;delete from employees;**-- WHERE EMPLOYEE_ID=300;

这相当于三行代码:对所有雇员的更新,删除雇员的所有记录,以及一个注释。这是 SQL 注入的一个例子。

典型的 SQL 注入攻击通过附加一个对所有数据都适用的 where 测试来修改 select 语句。例如,如果我接受用户输入姓氏来搜索雇员,并且用户键入“King”或“a”=“a”,我的动态 SQL 可能如下所示:

SELECT * FROM EMPLOYEES WHERE LAST_NAME=**'King' or 'a'='a';**

如果这是一个密码匹配 Oracle 数据库中存储的值的测试,那么 SQL 注入可能如下所示:

SELECT count(*) FROM EMPLOYEES WHERE LAST_NAME='King' and PASSWORD=**'whatever' or 'a'='a';**

select 语句将返回一个大于 0 的数字,即使用户不知道密码,他也可能获得访问权限。

在 Oracle 数据库中,您可以通过几种方法来防止 SQL 注入的发生。一种传统的方法是过滤传入的数据和/或对数据进行转义(使其成为单个字符的序列,而不是文本。)然而,更好的方法是始终使用参数化输入。我们用带参数的存储过程来实现这一点。我们不是在构建动态查询,而是将参数抽取到已经在 Oracle 数据库中暂存的 PL/SQL 中。数据库将变量绑定到我们的查询/更新框架。

我们还可以通过如下 Java 语句阻止客户端的 SQL 注入。userInputEmpID的值在问号(?).

String query = "SELECT * FROM EMPLOYEES WHERE EMPLOYEE_ID = **?** "; PreparedStatement pstmt = connection.prepareStatement( query ); pstmt.setString( 1, **userInputEmpID** );

让一个PreparedStatement接受我们的参数并用它填充查询,可以防止恶意代码被添加到查询中。再一次,PreparedStatement存放在 Oracle 数据库中,我们的参数在那里设置,Oracle 数据库将它们绑定到更新/查询。

如果您需要在应用(java 或其他)中放置 Oracle 数据库查询的代码,请使用PreparedStatement,如前所述,而不是将用户输入连接到查询字符串中。

证明存储过程中的 SQL 注入失败

我在hr_sec_pkg中增加了两个过程,它们将展示在存储过程中 SQL 注入的尝试。我不是来自密苏里州,但我来自“展示自我”的心态:信任,但要核实。让我们在对LAST_NAME进行选择查询时尝试一下 SQL 注入。在清单 7-15 、p_select_employee_by_ln_sens中部分显示的过程中,我们将传入第十个参数LAST_NAME,并修改我们在过程中的选择以使用它:

清单 7-15。按姓氏选择雇员数据并尝试 SQL 注入

    PROCEDURE p_select_employee_by_ln_sens(         ...         **m_last_name**    employees.last_name%TYPE )     IS BEGIN         ...         OPEN resultset_out FOR SELECT             ...         FROM employees         **WHERE last_name = m_last_name;**

让我们看看是否可以通过将 SQL 注入包含在RAW中(就像我们对加密数据更新所做的那样)并将RAW转换为WHERE子句中的VARCHAR2,来偷偷加入一些。我们在清单 7-16 的中的一个名为p_select_employee_by_raw_sens的测试过程中完成了这个任务。

清单 7-16。通过原始值选择员工数据并尝试 SQL 注入

PROCEDURE p_select_employee_by_raw_sens(         ...         m_last_name    RAW )     IS BEGIN         ...         OPEN resultset_out FOR SELECT             ...         FROM employees         **WHERE last_name = UTL_RAW.CAST_TO_VARCHAR2( m_last_name )**;

你会很高兴地注意到,当我们测试这一点,这些企图逃避是不成功的。在这两种情况下,正如预期的那样,Oracle 数据库会说,“给我一些东西插入测试WHERE LAST_NAME = ?;”。

我们将为 Oracle 数据库提供这个字符串:“King”或“a”=“a”,人们可能会想象用单引号将它括起来:

    WHERE LAST_NAME = 'King' or 'a'='a';

然而,Oracle 数据库将我们的字符串视为一个单个数据元素,并检查是否有人的LAST_NAME是(以转义形式):" King\ '或'a'='a "或" {King '或' a'='a} "。

执行人力资源包规范和主体

既然我们已经描述了hr_sec_pkg包中的过程,我们将继续执行包规范和包主体的CREATE语句。执行名为 Chapter7 /HR.sql 文件中的两个块,创建hr_sec_pkg包规范和主体。在您创建了hr_sec_pkg之后,您需要将包的 execute 权限授予hrview_role角色

GRANT EXECUTE ON hr.hr_sec_pkg TO hrview_role;

插入雇员记录:更新序列

为了让我们的示例代码能够工作,我们需要一个固定的EMPLOYEE_ID,数字 300 作为EMPLOYEES表中的记录。当最初安装样本EMPLOYEES表时,大约有 100 条记录,EMPLOYEE_ID从 100 到大约 200。一般来说,插入到EMPLOYEES表中会使用序列的下一个值EMPLOYEES_SEQ,如下所示(暂时不要执行,仅供参考):

    INSERT INTO employees         (employee_id, first_name, last_name, email, phone_number, hire_date,         job_id, salary, commission_pct, manager_id, department_id)     VALUES         (**employees_seq.NEXTVAL**, 'David', 'Coffin', 'DAVID.COFFIN',         '800.555.1212', SYSDATE, 'SA_REP', 5000, 0.20, 147, 80);

每次调用SEQUENCE.NEXTVAL时,该值都会递增。要查看EMPLOYEES_SEQ的当前(下一个)值,执行以下命令:

SELECT last_number FROM user_sequences WHERE sequence_name='EMPLOYEES_SEQ';

images 你可以在名为 * Chapter7 /HR.sql* 的文件中找到本节命令的脚本。

没有认可的方法来手动设置序列的LAST_NUMBER。但是,我们可以调整增量值来获得想要的效果。首先,确保上面命令中返回的电流LAST_NUMBER小于 300(我们的例子是EMPLOYEE_ID)。)如果不是,您可能需要替换一个比我们示例代码中的LAST_NUMBER大的数字,或者更新EMPLOYEE_ID 300 处的数据。

要设置在EMPLOYEE_ID = 300 处插入我们的示例EMPLOYEES记录,我们需要让EMPLOYEES_SEQLAST_NUMBER等于 300,我们将使用匿名(未命名)PL/SQL 块来完成。这不会保存到一个命名的存储过程中,而是执行一次来完成我们的计划。

images 注意我们使用user_sequences视图中的LAST_NUMBER代替序列的当前值CURRVAL。我们这样做是因为我们在这个会话中可能没有CURRVALCURRVAL仅在我们在此会话中对序列执行NEXTVAL后存在。然后我们可以得到序列的当前值。

参见清单 7-17 。我们有一个NUMBERoffset,我们从序列中选择值(300–LAST_NUMBER)放入其中。例如,如果我们的LAST_NUMBER当前是 207,offset的值将是 300–207 或 93。我们将一个命令字符串alter_command连接到ALTER序列,将INCREMENT BY值设置为那个offset。我们将那个ALTER命令传递给EXECUTE IMMEDIATE。那么下一次我们调用EMPLOYEES_SEQ.NEXTVAL的时候,就会得到 207 + 93 = 300 的值。为了完成这个计划,我们将序列的INCREMENT BY值设置回 1。

此时执行清单 7-17 中的所有命令。您将创建我们的测试用户为employee_id = 300。请随意在最后的INSERT命令中插入您自己的个人数据。

清单 7-17。匿名 PL/SQL 块重置序列

`DECLARE
    offset NUMBER;
    alter_command VARCHAR2(100);
    new_last_number NUMBER;
BEGIN
    SELECT (300 - last_number) INTO offset FROM user_sequences
        WHERE sequence_name='EMPLOYEES_SEQ';

alter_command := 'ALTER SEQUENCE employees_seq INCREMENT BY ' ||
        TO_CHAR(offset) || ' MINVALUE 0';
    EXECUTE IMMEDIATE alter_command;

SELECT employees_seq.NEXTVAL INTO new_last_number FROM DUAL;
    DBMS_OUTPUT.PUT_LINE( new_last_number );

EXECUTE IMMEDIATE 'ALTER SEQUENCE employees_seq INCREMENT BY 1';
END;
/

SELECT last_number FROM user_sequences WHERE sequence_name='EMPLOYEES_SEQ';

INSERT INTO employees
    (employee_id, first_name, last_name, email, phone_number, hire_date,
    job_id, salary, commission_pct, manager_id, department_id)
VALUES
    (employees_seq.NEXTVAL, 'David', 'Coffin', 'DAVID.COFFIN',     '800.555.1212', SYSDATE, 'SA_REP', 5000, 0.20, 147, 80);

COMMIT;`

在不插入记录的情况下递增序列的一种强力方式是将SELECT SEQUENCE.NEXTVAL足够多次。您也可以将INCREMENT BY值设置为负数,以减少序列中LAST_NUMBER的值。

我们可以通过再次选择EMPLOYEES_SEQ来查看新的LAST_NUMBER设置。确保它是 300,然后插入我们的示例记录EMPLOYEE_ID = 300。终于COMMIT更新了。通过选择查看我们的新条目:

SELECT * FROM employees WHERE employee_id=300;

我应该提到在名为SECURE_DMLEMPLOYEES表上有一个现有的INSERT / UPDATE / DELETE触发器。该触发器限制将EMPLOYEES数据更改为工作日的上午 8 点到下午 6 点。这类似于我们在第二章中实施的限制。但是,默认情况下,此触发器是禁用的。

加密数据交换的演示和测试

我们将执行一个单独的 Java 类,TestOracleJavaSecure来模拟HR模式的客户端应用。我们的客户端应用将调用我们在hr_sec_pkg中定义的存储过程,进行一些查询和一些更新。

我们将只探索应用代码的几个大小片段。当我们浏览这一部分时,您应该打开 TestOracleJavaSecure.java 文件进行参考。

images 你可以在文件chapter 7/testoraclejavasecure . Java中找到这段代码。

一些预备步骤

在我们开始大规模的个人演示和测试之前,我们想弄清楚我们的方向。接下来的几个小节为后面的演示和测试做好了准备。

main()方法和方法成员

TestOracleJavaSecure类的全部代码都驻留在main()方法中。所以,当我们从命令行用这个类调用 Java 时,我们简单地从上到下运行代码。

我们在main()方法中做的第一件事是建立一个 Oracle 连接,如清单 7-18 所示。编辑连接字符串,以使用您分配给应用用户appusr的密码,以及您的特定服务器名称和端口。

清单 7-18。测试传输中加密的代码开头,TestOracleJavaSecure

public class TestOracleJavaSecure {     public static void main( String[] args ) {         Connection conn = null;         try {             private static String appusrConnString =                 "jdbc:oracle:thin:AppUsr/password@localhost:1521:Orcl";             Class.forName( "oracle.jdbc.driver.OracleDriver" );             conn = DriverManager.getConnection( appusrConnString );

准备加密

我们不需要OracleJavaSecure中的Connection,因为我们不会直接从那个类中调用 Oracle 数据库。客户端上OracleJavaSecure的唯一功能(在本章中)是建立密钥和加密/解密数据。参见清单 7-19 。

清单 7-19。准备加密

    //OracleJavaSecure.setConnection( conn );     String locModulus = OracleJavaSecure.getLocRSAPubMod();     String locExponent = OracleJavaSecure.getLocRSAPubExp();

我们得到 RSA 密钥对,并得到公钥指数和模数以传递给 Oracle 数据库。

设置非默认角色

appusr用户有权限执行appsec.p_check_hrview_access程序(回头参考第二章,该程序将设置安全应用角色hrview_role。我们执行如清单 7-20 所示的程序。

清单 7-20。设置非默认角色

    stmt = ( OracleCallableStatement )conn.prepareCall(         "CALL appsec.**p_check_hrview_access()**" );     // Comment next line to see Exception when non-default role not set     stmt.executeUpdate();

为了获得角色,我们需要执行语句。如果您想确保没有该角色的访问将会失败,请将该行注释为executeUpdate()并运行TestOracleJavaSecure。确保在运行测试之后取消注释该行,这样您就可以运行我们的主要测试。

重用可调用语句

由于OracleCallableStatement是实现Statement的接口,我们可以像普通的Statement一样使用它。可以反复使用常规的Statement来执行查询和更新。然而,以我的经验来看,如果您有来自您的OracleCallableStatement调用的过程的OUT参数,那么您不应该重用它——只需要获得一个新的OracleCallableStatement

在清单 7-20 中设置我们角色的第一个调用,给我们留下了一个OracleCallableStatement,我们可以重用它来获得EMPLOYEES的非敏感视图中的行数。我们将通过两种方式对行进行计数,如清单 7-21 所示:一次是通过遍历所有行的ResultSet,递增我们的计数,cnt;一次通过选择所有行的count(*)。选择count(*)是一种更有效的方式:

清单 7-21。获得员工公共视图中的行数

`    rset = stmt.executeQuery(
        "SELECT * FROM hr.v_employees_public" );
    int cnt = 0;
    while( rset.next() ) cnt++;
    System.out.println( "Count data in V_EMPLOYEES_PUBLIC: " + cnt );

rset = stmt.executeQuery(
        "SELECT COUNT(*) FROM hr.v_employees_public" );
    if( rset.next() ) cnt = rset.getInt(1);
    System.out.println( "Count data in V_EMPLOYEES_PUBLIC: " + cnt );

if( null != stmt ) stmt.close();`

从员工中选择加密数据

这里是我们从 Oracle 数据库中选择加密数据的教科书过程,清单 7-22 。它类似于我们在上一章中使用的测试程序。我们将公钥模数和指数传递给 Oracle 数据库,并接收回我们的 DES 加密密码密钥的加密工件。此外,我们还有一个类型为OracleTypes.CURSOROUT参数。我们将利用OracleTypes.CURSOR(Java 中的ResultSet)来读取我们的数据。

清单 7-22。 Java 代码从员工中选择敏感数据,来自p_select_employees_sensitive

`stmt = ( OracleCallableStatement )conn.prepareCall(
        "CALL hr.hr_sec_pkg.p_select_employees_sensitive(?,?,?,?,?,?,?,?,?)" );
    stmt.registerOutParameter( 3, OracleTypes.RAW );
    stmt.registerOutParameter( 4, OracleTypes.RAW );
    stmt.registerOutParameter( 5, OracleTypes.RAW );
    stmt.registerOutParameter( 6, OracleTypes.RAW );
    stmt.registerOutParameter( 7, OracleTypes.CURSOR );
    stmt.registerOutParameter( 8, OracleTypes.NUMBER );
    stmt.registerOutParameter( 9, OracleTypes.VARCHAR );
    stmt.setString( 1, locModulus );
    stmt.setString( 2, locExponent );
    stmt.setNull(   3, OracleTypes.RAW );
    stmt.setNull(   4, OracleTypes.RAW );
    stmt.setNull(   5, OracleTypes.RAW );
    stmt.setNull(   6, OracleTypes.RAW );
    // This must go without saying - unsupported type for setNull
    //stmt.setNull( 7, OracleTypes.CURSOR );
    stmt.setInt(    8, 0 );
    stmt.setNull(   9, OracleTypes.VARCHAR );
    stmt.executeUpdate();

errNo = stmt.getInt( 8 );
    if( errNo != 0 ) {
        errMsg = stmt.getString( 9 );
        System.out.println( "Oracle error 1) " + errNo +             ", " + errMsg );
    } else {
        System.out.println( "Oracle success 1)" );
        sessionSecretDESSalt = stmt.getRAW( 3 );
        sessionSecretDESIterationCount = stmt.getRAW( 4 );
        sessionSecretDESAlgorithm = stmt.getRAW( 5 );
        sessionSecretDESPassPhrase = stmt.getRAW( 6 );
        rs = ( OracleResultSet )stmt.getCursor( 7 );
        //while( rs.next() ) {
        // Only show first row
        if( rs.next() ) {
            System.out.print( rs.getString( 1 ) );
            System.out.print( ", " );
            System.out.print( rs.getString( 2 ) );
            System.out.print( ", " );
            System.out.print( rs.getString( 3 ) );
            System.out.print( ", " );
            System.out.print( rs.getString( 4 ) );
            System.out.print( ", " );
            System.out.print( rs.getString( 5 ) );
            System.out.print( ", " );
            System.out.print( rs.getString( 6 ) );
            System.out.print( ", " );
            System.out.print( rs.getString( 7 ) );
            System.out.print( ", " );
            System.out.print( OracleJavaSecure.getDecryptData(
                rs.getRAW( 8 ), sessionSecretDESPassPhrase,
                sessionSecretDESAlgorithm, sessionSecretDESSalt,
                sessionSecretDESIterationCount ) );
            if ( null != rs.getRAW( 8 ) )
                System.out.print( " (" + rs.getRAW( 8 ).stringValue() +
                        ")" );
            System.out.print( ", " );
            // Most initial commissions in database are null
            System.out.print( OracleJavaSecure.getDecryptData(
                rs.getRAW( 9 ), sessionSecretDESPassPhrase,
                sessionSecretDESAlgorithm, sessionSecretDESSalt,
                sessionSecretDESIterationCount ) );
            if ( null != rs.getRAW( 9 ) )
                System.out.print( " (" + rs.getRAW( 9 ).stringValue() +
                        ")" );
            System.out.print( ", " );
            System.out.print( rs.getString( 10 ) );
            System.out.print( ", " );
            System.out.print( rs.getString( 11 ) );
            System.out.print( "\n" );
        }
    }
    if( null != rs ) rs.close();
    if( null != stmt ) stmt.close();`

我们的错误处理与我们在上一章中所做的相同:我们通过两个OUT参数返回错误号和消息。如果没有错误,我们继续将密码密钥工件放入本地方法成员中,我们还将CURSOR放入ResultSet中,这样我们就可以遍历数据了。由于EMPLOYEES表中大约有 100 个条目,我们将只显示第一个条目(通过读取if块中的ResultSet.next()而不是while块中的】。

大多数列都是明文,所以我们只需将它们打印出来。但是SALARYCOMMISSION_PCT值是由 Oracle 数据库作为RAW加密数据交给我们的。(注意加密的SALARYResultSet的第 8 个位置位置,而ResultSetStatement的第 7 个位置。那些元素的编号是独立的。)我们将把这些RAW值发送给getDecryptData()方法,同时发送的还有秘密密码密钥工件。如果我们还没有构建秘密密码密钥的本地副本,我们将在那里构建它;在任何情况下,我们都会解密数据,并将其作为String返回。我们也会把它打印出来。

仅出于演示目的,我们还将打印出实际的RAW(如果不是null),放在括号“()”内。如果我们想在这段代码的多次运行中跟踪它,我们会发现每次都不一样——因为在不同的 Oracle 会话中使用了不同的密码密钥。

在每次调用我们的存储过程进行数据加密时,我们将关闭OracleCallableStatement。我们也可以关闭ResultSet,尽管这是不必要的——当我们关闭Statement时,它本来就是关闭的,因为它是通过Statement获得的。然而,显式关闭每个ResultSet是一种好的做法,特别是因为重用一个Statement是一种常见的做法——在单个Statement的生命周期中,您可能会打开多个ResultSets,并且为了确保您正在释放 Oracle 资源,您应该在使用完每个ResultSet后关闭它。

选择加密字符串中的所有列

我们从 Oracle 数据库中选择加密数据的第二个示例过程p_select_employees_secret不是选择并加密单个列,而是选择所有列,并将它们连接成一个逗号分隔的要加密的VARCHAR2。在这种情况下,没有任何数据以明文形式发送。在客户端,如果需要单独的数据列,您将需要解析解密的字符串以获取单独的数据元素。

同样在这个例子中,我们使用逗号作为字段之间的分隔符。这假设数据中没有逗号——这通常不是一个有效的假设。您可以使用其他不太可能出现在数据中的分隔符,如插入符号(^)or 波浪号(~)。这只是一个示例,在为您的应用构建这样的过程之前,您需要评估您对数据的特定要求。

我们调用这个过程,解密数据并打印结果,如清单 7-23 中的部分所示。

清单 7-23。加密从员工中选择的所有数据,来自p_select_employees_secret

stmt = ( OracleCallableStatement )conn.prepareCall(         "CALL hr.hr_sec_pkg.p_select_employees_secret(?,?,?,?,?,?,?,?,?)" );         ...         if( rs.next() ) {             System.out.**print**( OracleJavaSecure.**getDecryptData( rs.getRAW( 1 )**,                 sessionSecretDESPassPhrase,                 sessionSecretDESAlgorithm, sessionSecretDESSalt,                 sessionSecretDESIterationCount ) );             if( null != rs.getRAW( 1 ) )                 System.out.**print**( " (" + **rs.getRAW( 1 ).stringValue()** +                     ")" );             System.out.print( "\n" );

同样,我们将加密的RAWString值打印在数据旁边的括号中。在这种情况下,有一个单独的RAW代表整个串联数据行。

将加密数据发送到 Oracle 数据库进行插入/更新

我不会说下一个示例过程与我们前面的示例没有相似之处,但是您会注意到在两个方向上都没有密钥交换的痕迹。这是一个更新,为了在客户端加密数据,我们已经交换了密钥。清单 7-24 显示了调用我们的加密数据更新过程p_update_employees_sensitive的代码。

清单 7-24。更新员工中的敏感数据,调用p_update_employees_sensitive

`    stmt = ( OracleCallableStatement )conn.prepareCall(
        "CALL hr.hr_sec_pkg.p_update_employees_sensitive(?,?,?,?,?,?,?,?,?,?,?,?,?)" );
    stmt.registerOutParameter( 12, OracleTypes.NUMBER );
    stmt.registerOutParameter( 13, OracleTypes.VARCHAR );
    stmt.setInt(    1, 300 );
    stmt.setString( 2, "David" );
    stmt.setString( 3, "Coffin" );
    stmt.setString( 4, "DAVID.COFFIN" );
    stmt.setString( 5, "800.555.1212" );
    stmt.setDate(   6, new Date( ( new java.util.Date() ).getTime() ) );
    stmt.setString( 7, "SA_REP" );
    // Note - may not have locModulus, locExponent,  at this time!
    stmt.setRAW(    8, OracleJavaSecure.getCryptData( "9000.25" ) );
    stmt.setRAW(    9, OracleJavaSecure.getCryptData( "0.15" ) );
    stmt.setInt(   10, 147 );
    stmt.setInt(   11, 80 );
    stmt.setInt(   12, 0 );
    stmt.setNull(  13, OracleTypes.VARCHAR );
    stmt.executeUpdate();

errNo = stmt.getInt( 12 );
    if( errNo != 0 ) {
        errMsg = stmt.getString( 13 );
        System.out.println( "Oracle error 3) " + errNo + ", " + errMsg );
    }
    else System.out.println( "Oracle success 3)" );
    if( null != stmt ) stmt.close();`

对于我们的加密数据,我们首先调用getCryptData()方法。然后,我们在 Oracle 过程中设置一个RAW参数。当我们执行此语句时,明文和加密数据值都被发送到 Oracle 数据库进行插入或更新,这由过程决定。

两个日期类的故事

和我们之前的例子一样,在这里的清单 7-24 中,我们正在设置我们的参数,除了这一次,我们正在设置数据而不是关键工件。在我们的第六个参数中,我们实例化了一个java.util.Date 类。不带参数的构造函数创建一个带有当前日期和时间的Date。对于 Oracle 数据库,我们经常会使用java.sql.Date的实例(注意包 java.sql 而不是 java.util ),它没有这样的构造函数;然而,我们可以用来自java.util.Date.getTime()方法的 long(毫秒)来构造一个java.sql.Date。我们也可以使用与java.util.Date()构造函数相同的语句来实例化一个java.sql.Date对象,如下所示:

java.sql.Date( System.currentTimeMillis() );

对于我们在代码中使用的语法,我有一个观察。我们实例化的( new java.util.Date() )周围的括号允许我们直接访问它作为一个对象,访问它的getTime()方法,就像这样:

    stmt.setDate(   6, **new Date( ( new java.util.Date() ).getTime())**);

好吧,那么为什么java.sql.Date没有一个Date()构造函数,它不是通过扩展java.util.Date继承了一个吗?哦,好问题!简单的回答是,构造函数不被认为是类的成员,所以它们不会被子类继承。然而,您可以通过调用super()方法作为任何子类构造函数的第一行来访问父构造函数。可以有多个具有不同签名的super()方法,每个方法对应父类中的一个构造函数。

我们将在第九章的中再次回到 java.sqlJava . util的主题。我们需要交换日期的标准做法,我将在那里提出一个。

从雇员中选择单个行

也许我们不想选择整个表,而只想选择满足特定条件的记录。我们可以在查询中提供标准作为WHERE子句的一部分。在这个过程中,p_select_employee_by_id_sens,清单 7-25 ,我们提供了第十个参数,EMPLOYEE_ID

清单 7-25。按 ID 选择,敏感数据来自员工,来自p_select_employee_by_id_sens

    stmt = ( OracleCallableStatement )conn.prepareCall(         "CALL hr.hr_sec_pkg.p_select_employee_by_id_sens(?,?,?,?,?,?,?,?,?,?)" );     ...     **stmt.setInt(   10, 300 )**; // Employee ID 300     stmt.executeUpdate();

过程调用的其余部分与我们之前的查询示例相同,除了我们只期望返回一条或零条记录(EMPLOYEE_ID是该表的一个UNIQUE键)。)所以我们可以在一个if( rs.next() )块中处理ResultSet,而不是while( rs.next() )

按姓氏选择雇员数据:尝试 SQL 注入

我们还可以查询满足其他一些标准的所有记录,这些标准可能不是UNIQUE。在部分清单 7-26 中,我们将展示LAST_NAME使用p_select_employee_by_ln_sens过程进行的查询EMPLOYEES的一个例子。有两个条目带有LAST_NAME“国王”。所以这将返回两行。

清单 7-26。按姓氏选择,敏感数据来自员工,来自p_select_employee_by_ln_sens

stmt = ( OracleCallableStatement )conn.prepareCall(         "CALL hr.hr_sec_pkg.p_select_employee_by_ln_sens(?,?,?,?,?,?,?,?,?,?)" );     ...     stmt.setString(   10, "King" ); // Employees Janette and Steven King     ...     while( rs.next() ) {

使用while块遍历ResultSet以查看所有返回的行。

我们可以尝试 SQL 注入,将之前的参数 10 设置替换为以下内容:

    stmt.setString(   10, "King' or 'a'='a" );

我们将看到的是没有数据返回,因为没有EMPLOYEESLAST_NAME

通过 RAW 选择员工数据:尝试 SQL 注入

也许有人会想,如果我们把数据作为一个RAW返回,并且只在做出选择的时候把它转换成一个VARCHAR2,我们就可以突破并完成 SQL 注入(参考 Oracle 过程,p_select_employee_by_raw_sens,我们在前面和清单 7-16 中描述过它)。)这个过程调用,部分清单 7-27 ,尝试了那个策略。

清单 7-27。通过原始值选择,敏感数据来自员工,来自p_select_employee_by_raw_sens

    stmt = ( OracleCallableStatement )conn.prepareCall(         "CALL hr.hr_sec_pkg.p_select_employee_by_raw_sens(?,?,?,?,?,?,?,?,?,?)" );     ...     stmt.setRAW(   10, new RAW("King' or 'a'='a".**getBytes()**) );

您将再次看到,我们在存储过程中对 SQL 注入的尝试失败了。与在动态 SQL 中嵌入用户提供的文本相反,传递参数似乎非常抵制 SQL 注入。

还要注意我们如何对待引号之间的值,就好像它已经是一个String对象,调用getBytes()方法。我们首先在《??》第六章中看到了这一点。

使用新的客户端密钥测试加密失败

也许你需要亲眼看看,也许不需要。在任何情况下,如果客户机上的键与 Oracle 数据库上的键不匹配,您都可以测试调用过程:它将失败。请注意,到目前为止,在 Java 代码的TestOracleJavaSecure.main()方法中,我们已经交换了密钥,这些密钥将继续工作。我们将在一瞬间移除或禁用客户端上的密钥。我们通过调用resetKeys()方法来做到这一点(参见清单 7-28 )。

清单 7-28。用混合密钥测试加密

    OracleJavaSecure.resetKeys(); // Method for Chapter 7 testing only     locModulus = OracleJavaSecure.getLocRSAPubMod();     locExponent = OracleJavaSecure.getLocRSAPubExp();

我们还通过调用getLocRSAPubMod()方法在客户端建立新的密钥,并获得公共模数和指数。如果我们不努力将这些公钥工件传递给 Oracle 数据库并检索新的密码密钥工件作为回报,那么更新(不交换密钥)将会失败。这就是我们将要测试的。

请注意,我们不太可能会意外重置我们的键,多次调用getLocRSAPubMod()方法或OracleJavaSecure的其他方法不会创建新的键(如果它们已经存在),而是会返回现有的键。这些键是static,所以只要 Java 虚拟机继续运行,它们就不会自己消失。所以,这个测试真的只是为了好玩:回答一个“如果”的问题。

新 Oracle 连接测试失败

我们可以演示的另一个测试是我们关闭 Oracle 数据库的Connection并建立一个新数据库的场景。服务器端密钥将会消失——客户端公钥和密码密钥都将消失。我们可以重置Connection并准备调用我们的加密程序,方法是将我们的安全应用角色设置为HRVIEW_ROLE,如清单 7-29 中的所示。

清单 7-29。重置连接并重置安全应用角色

    if ( null != conn ) conn.close();     conn = DriverManager.getConnection( appusrConnString );     OracleJavaSecure.setConnection( conn );     stmt = ( OracleCallableStatement )conn.prepareCall(         "CALL appsec.p_check_hrview_access()" );     stmt.executeUpdate();

同样,在进行加密的 Oracle 更新的情况下,我们将看到一个失败,因为 Oracle 数据库既没有公钥也没有密码密钥。这段代码最初是注释的。

一些结束语

我已经在前面的章节中介绍了所有的大型演示和测试。现在,轮到您修改测试并尝试自己的测试了。请随意评论和取消评论我们刚刚描述的那些测试代码部分。当您重新编译并运行TestOracleJavaSecure的这些部分时,您将看到一条关于尝试数据更新的消息,表明我们“在预期的地方失败了——没问题”

如果您需要关闭/打开您的连接

当我们选择加密数据时,重置您的 Oracle 连接会带来另一个潜在的问题,因为我们肯定会将我们的公钥传递给 Oracle 数据库,而数据库会构建密码密钥并将其交还。但是,我们仍然在客户机上保留一份旧的秘密密码密钥,解密数据时会遇到问题。在我们尝试解密数据之前,我们可以通过立即调用OracleJavaSecure.makeDESKey()方法,在客户机上获取并解密秘密的密码密钥工件之后进行补救。

我认为最好的经验法则,以及对代码审查清单的检查,是在查询和更新期间保持 Oracle 连接。如果有长时间的暂停,并且您需要关闭 Oracle 连接,那么当您打开一个新的连接时,通过p_get_shared_passphrase过程将您的公钥工件传递到 Oracle 数据库(如下所述),并通过调用makeDESKey()方法在客户机上构建一个替换的密码密钥。

运行无数据加密的基本密钥交换

当我们只想向 Oracle 数据库提交加密的数据更新时,或者我们想在进行任何选择之前做好更新准备时,我们需要确保我们事先已经交换了密钥。我们可以通过调用p_get_shared_passphrase过程来实现(在我们当前的设计中,这个过程必须包含在每个单独的应用包中,比如hr_sec_pkg)。清单 7-30 展示了从 Java 客户端进行基本密钥交换的基础。

清单 7-30。基本密钥交换

    stmt = ( OracleCallableStatement )conn.prepareCall(         "CALL hr.hr_sec_pkg.p_get_shared_passphrase(?,?,?,?,?,?,?,?)" ); ...     OracleJavaSecure.makeDESKey( sessionSecretDESPassPhrase,         sessionSecretDESAlgorithm, sessionSecretDESSalt,         sessionSecretDESIterationCount );

我们将获得并解密我们的秘密密码密钥的每个工件,并将其传递给makeDESKey()方法。此时,我们已经完成了密钥交换,并准备好交换加密数据,并在客户端和 Oracle 数据库上进行解密。

在我们调用了p_get_shared_passphrase过程和OracleJavaSecure.makeDESKey()之后,TestOracleJavaSecure类将再次尝试进行加密数据更新,并将成功。

执行演示和测试

我们现在将运行我们的演示和测试。为此,我们将根据需要再次编辑我们的代码,然后编译并运行它。在命令提示符下,将目录更改为 第七章 。编辑TestOracleJavaSecure.java如果还没有,将appusr的正确密码和正确的主机和端口号放入 Oracle 连接字符串,靠近顶部。

    private static String appusrConnString =         "jdbc:oracle:thin:appusr/password@localhost:1521:Orcl";

用这些命令编译代码,或者只编译第二个命令,第二个命令会自动编译第一个命令(确保对OracleJavaSecure.java的第一行进行了注释,这一行指向CREATEOracle 数据库中的 Java 结构)。

javac orajavsec/OracleJavaSecure.java javac TestOracleJavaSecure.java

然后使用以下命令运行同一目录中的代码:

java TestOracleJavaSecure

观察结果

当您执行TestOracleJavaSecure(分布式的)时,前面列出的所有测试将从上到下直接运行。结果将如下所示:

Count data in V_EMPLOYEES_PUBLIC: 108 Count data in V_EMPLOYEES_PUBLIC: 108 Oracle success 1) 198, Donald, OConnell, DOCONNEL, 650.507.9833, 2007-06-21 00:00:00, SH_CLERK, 2600 (E27811A8C7C9D9F3), null, 124, 50 Oracle success 2) 198, Donald, OConnell, DOCONNEL, 650.507.9833, 21-JUN-07, SH_CLERK, 2600, , 124, 50 (F7EA4E97B2F39E036AF6E880B2E5CA3EB78332BF8CE82B7585A4CBC7B340FEBDE4862830927 D118D27A1DDE3304478D9A463EBA9BC78E3188217884D5F5EA92F54A6EA2FB62598D1419F003295D F1C076E48BC6D07058E3B) Oracle success 3) Oracle success 4) 300, David, Coffin, DAVID.COFFIN, 800.555.1212, 2010-08-30 00:00:00, SA_REP, 9000.25, .15, 147, 80 Oracle success 5) No data on failed SQL Injection Oracle success 6) No data on failed SQL Injection Failed where expected - OK. Need key exchange. Oracle success 8) Oracle success 9)

演示场景

这里用相对简单的英语列出了我们演示过的场景。有大量的代码来完成所有这些不同的场景。每个场景的代码与其他一些场景非常相似,只是针对特定的演示进行了修改。

  • 我们查询了EMPLOYEES表,得到了加密形式的SALARYCOMMISSION_PCT列。对于这两者,我们打印出解密的String,在括号中,是加密的RAWstringValue()(除非为空)。我们只展示了ResultSet的第一排。
  • 我们查询了该表,并以加密的形式将所有列放回一个串联的String中。我们打印解密的数据,在括号中是加密的RAWstringValue()。同样,我们只显示了第一行。
  • 我们对EMPLOYEES表进行插入或更新,插入EMPLOYEE_ID = 300。如果它已经存在,我们进行更新。到那时工资是 9000.25(现在我在做梦)。
  • 我们从EMPLOYEES中选择一行,请求数据WHERE EMPLOYEE_ID = 300
  • 我们试图用一个样本 SQL 注入字符串在我们的过程中查询EMPLOYEES。这将失败,并且不会返回任何数据。
  • 我们再次尝试使用示例 SQL 注入字符串通过我们的过程查询EMPLOYEES,这一次是作为RAW传输的,只有在执行SELECT时才会被转换。这同样会失败,并且不会返回任何数据。
  • 我们也可以编译TestOracleJavaSecure来测试重置客户端密钥或重置 Oracle 连接。之后,我们向 Oracle 数据库发送加密数据进行插入/更新的尝试失败了,这是意料之中的。
  • 我们成功地调用了p_get_shared_passphrase过程并运行了makeDESKey()方法来完成密钥交换。
  • 同样,我们将加密的数据发送到 Oracle 数据库进行插入/更新,我们成功了。现在工资 9700.75(现在老婆在做梦)。

查询员工以查看更新

通过作为HR用户连接到 Oracle 数据库并执行以下命令,您可以看到我们插入/更新的记录的状态:

SELECT * FROM employees WHERE employee_id=300;

打包模板实现加密

我知道人们发现安全成本,就像质量成本一样,令人不快;但不安全的潜在成本高得离谱。我们的安全结构只有投入使用才有价值。我们将在接下来的几章中增加一些价值,但就目前的情况来看,让人们使用我们的安全结构可能就像拔牙一样困难。这不是那种“如果你建造了它,他们就会来”的时刻。

我们将通过为应用开发人员提供模板来降低进入这些安全结构的门槛,他们可以使用这些模板来快速实现其应用的后端 Oracle 结构,以及对这些结构的前端 Java 调用。

Oracle 应用安全结构模板

我们将提供给开发人员的第一个文件提供了 Oracle 安全包的代码,他们需要在他们的应用模式中实现该包。在该文件中,有应用模式、包、过程、表/视图和列的通用名称。开发人员应该搜索这些名称,并用他们将使用的实际名称替换它们。

images 你可以在 这个文件第七章 /AppPkgTemplate.sql

您的安全管理员secadm需要将appsec.app_sec_pkg上的 execute 授予开发人员的应用模式用户,这样应用就可以使用app_sec_pkg过程和函数。

为了创建应用包,开发人员将需要CREATE PROCEDURE系统权限。此外,为了让应用用户使用应用包,所有应用用户都需要被授予包的EXECUTE对象权限。

我们的软件包中有四个模板程序:

    p_get_shared_passphrase     p_select_APPTABLE_sensitive     p_select_APPTABLE_by_COLUMN1_sens     p_update_APPTABLE_sensitive

这里没有向您介绍任何新的东西——您已经看到了在HR模式中使用的所有这些结构。

【Java 调用应用安全的模板

我们还将为我们的应用开发人员提供一个模板 Java 文件。每个开发人员将搜索和替换模式、表、过程等的通用名称。应用中任何合适的名称。

images 你可以在 第七章/**AppJavaTemplate.java中找到AppAccessSecure模板类的 Java 代码。

这个文件与我们在HR模式、TestOracleJavaSecure.java中的结构测试代码非常相似。它由一个main()方法组成,该方法建立一个 Oracle 连接并调用开发人员将在应用模式中定义的应用结构。

对于应用开发人员来说,这可能是最令人生畏的代码,因为需要进行大量的密钥交换。你现在是专家了,所以你最好给你的应用开发者一些帮助。事实上,如果您能够帮助开发人员正确地实现这一点,并避免任何安全缺陷,就像我们对HR.EMPLOYEES表所做的那样,这将会为您省去一些麻烦。

应用使用的 Java 档案

除了为您的开发人员提供这两个模板文件之外,您还需要为他们提供orajavsec/Oracle javasecure . class文件。我建议你不要给开发人员 OracleJavaSecure.java 的代码文件,只是为了保证你的组织中没有使用这个类的修改版本。

可能用于分发类文件的最佳形式是 Java 归档(JAR)文件的形式。要创建合适的 jar 文件进行分发,可以运行 JDK 附带的 JAR 工具。如果你的PATHCLASSPATH仍然按照第二章中的描述设置,你可以得到一个命令提示窗口,将目录切换到第七章第七章目录。从那里,执行以下命令:

jar cvf orajavsec.jar orajavsec/OracleJavaSecure.class

这将在当前目录下创建一个名为 orajavsec.jar 的文件。分发这个文件,并指示您的应用开发人员在开发期间和运行他们的应用代码时将这个文件名放在他们的CLASSPATH中。

现在不要停止

有了模板,我们就可以让 Oracle 应用开发人员了解网络上的加密数据,而您最好选择一款应用,并将其配置为使用这些结构和方法,以开辟道路。但这只是安全喘息的中途之家。我们将在接下来的章节中介绍一些强大的概念,这些概念将会吸引你的应用开发人员继续使用这个程序。步入安全的应用开发和操作就像找回自己的生活。安全感是一种幸福感。这需要努力,但有了努力,你就能决定自己的安全计算命运。

章节回顾

我们将在第六章中学到的密钥交换和数据加密概念应用于一个特定的 Oracle 应用,即HR模式。这是科学史上的伟大时刻之一吗?也许不是,但这是一个里程碑,是我们在应用安全性方面取得更大、更好成就的跳板。我们已经成功加密了通过网络传输的敏感应用数据。

我们构建了错误日志记录的结构,专门用于跟踪涉及我们的应用安全流程的错误和事件,而不管什么客户端应用当前正在使用它们。此外,我们构建了一个触发器来自动管理日志,丢弃超过 45 天的记录。

本章介绍了许多代码,包括 PL/SQL 和 Java。这段代码的目标相当简单——当从HR模式中检索敏感数据和记录时,以及当更新敏感数据和记录时,使用我们现有的数据加密过程。

一路上,我介绍了以下概念:

  • 使用性能索引
  • 触发器、自主事务和COMMIT
  • 序列的使用和操作
  • 默认程序参数
  • CURSOR TYPE的使用
  • 通过引用使用类型规范,数据类型的锚定声明
  • 使用java.util.Datejava.sql.Date

我们花了大量时间评估和测试我们对 SQL 注入攻击的防护能力。

最后,我们展示了可以交付给应用程序员的模板,以便他们可以在自己的应用中实现这些安全结构。

请研究图 7-1 和图 7-2 以获得安全应用数据查询和更新过程的直观概述。图 7-1 展示了在 Oracle 数据库中查询敏感数据并以加密形式检索的过程。在顶部,您可以看到对图 5-1【A】的引用,这是我们看到在客户端上创建 RSA 私钥/公钥对的地方。然后在图 7-1 中,我们看到客户端调用p_check_hrview_access来设置访问敏感HR数据的角色。

此时,我们拥有调用hr_sec_pkg包的执行特权。我们调用p_select_employees_sensitiveEMPLOYEES表中获取公共和敏感数据。首要任务是完成密钥交换。您可以在图 7-1 的右侧看到,我们在 Oracle 数据库上创建了一个等效的 RSA 公钥,我们创建了将返回给客户端的加密密码(DES)密钥。我们在图 6-1【B】中看到了那个过程的细节。

Oracle 数据库上该过程的下一步是查询EMPLOYEES表并加密SALARYCOMMISSION_PCT字段。最后,该过程向客户端返回数据,包括明文字段和敏感字段的加密形式。

回到客户端(图 7-1 的左侧),我们基于 Oracle 数据库返回的工件构建一个等价的密码密钥。请参见插图底部的关键图像。然后我们显示来自ResultSet的数据,使用秘密密码密钥解密敏感数据组件。

images

图 7-1。安全应用数据查询

图 7-2 说明了敏感数据的更新过程。在这个过程中,我们使用密码密钥对客户端的敏感数据进行加密。我们通过调用getCryptData()方法来实现。然后,我们通过调用p_update_employees_sensitive过程将明文字段和加密的敏感字段提交给 Oracle 数据库。

密钥交换必须已经发生,以便 Oracle 数据库可以使用等效的(原始)密码密钥来解密数据。然后程序执行一个INSERTUPDATE命令来存储数据。

images

图 7-2。安全应用数据更新

八、单点登录

单点登录(SSO)是一个相对简单的概念。在您能够开始工作之前,当您打开计算机时,您登录了吗?如果是这样,那么我们应该能够找出你登录时的身份,并假设你在使用我们的应用时,你的身份是相同的。

这里的部分奥秘围绕着保证和信任。我能相信初始登录足够安全,只有实际用户才能验证自己身份吗?我能确定没有其他人有那个用户名和密码,没有人能冒充那个用户(欺骗用户)吗?这种保证不仅来自加密的力量和密码存储保护的强度,还来自计算机安全行为准则:密码更改的频率、密码组成和强度规则、屏幕锁定规则、禁止密码共享和发布的规则、关于恶意软件和病毒的教育以及社会工程。这种保证也来自网络访问控制(NAC)。我们是否保证每台客户端计算机都有最新的更新和防病毒软件?我们是否保证客户端计算机正确配置了安全选项,如我们的密码保护屏幕锁定?当移动计算机通过公共网络与我们公司通信时,我们是否使用硬盘加密、防火墙软件和虚拟专用网络(VPN)来保护它们?在我们允许这些计算机连接到我们的公司网络之前,我们是否使用 NAC 来做所有这些事情?

如果我确定了所有这些事情,以及对抗计算机安全威胁所需的任何其他事情,那么我可以相信原始登录对于任何进一步的身份要求都足够好。在继续使用 SSO 构建我们的应用安全性之前,我们需要一个很大的基础。

另一层认证?

我们总是可以在我们的应用中添加另一个身份验证——让用户有机会重新输入用户名和密码(可能是不同的密码),但是除了让用户感到沮丧之外,我们在安全性方面有所改进吗?(在下一章中,我们将尝试使用双因素身份验证来提高安全性。)

我并不是说额外的认证是一件坏事。有时你没有我前面列出的信任和保证,所以 SSO 是无效的。当你有十个或几十个密码时,问题就来了。此时,会发生两件事:由于丢失或忘记密码以及无数次密码重置,您的身份验证支持系统负担沉重,您的组织(和个人)的安全性降低。随着密码数量的增加,安全性会降低,这是真的吗?是的,因为现在您已经将用户置于这样的境地:他们有太多的密码,更改太频繁以至于记不住——他们必须被写下来。

也许你的用户足够精明,可以保留一个他们有密码的所有地方的列表,当他们在一个地方更改密码时,他们会在所有地方更改密码。我不确定你是否能指望这一点,即使是在一个少于 10 人的计算组织中。而且总有例外—更改更频繁的密码和具有不同组成规则的密码。

此外,您的用户是否能够区分可同步单个工作密码的安全企业网站列表和所有其他不安全或与工作无关的网站,这些网站不应该访问他们的工作密码?授予敏感信息访问权限的工作密码不应在外部系统上使用——谁知道这些外部系统维护的是什么安全?

最后需要争取 SSO。首先,我们需要与新来港儿童建立信任和保证的基础。然后,我们应该在任何可以实现 SSO 的地方推动 SSO 的采用。我们的 Java 和 Oracle 应用就是其中之一。

谁登录了客户端?

如果您使用的是 Microsoft Windows 客户端或 UNIX 客户端,操作系统(OS)会保留您在验证或登录时声明的身份信息。该身份发布在您的环境设置中,以便于从脚本访问,但是这些脚本可以更改环境,因此,仅从您的环境设置来看,您就可以欺骗不同的用户。

要查看这种欺骗,请打开一个命令提示窗口(在 Windows 上)并键入 SET 来查看您的设置。也许你在结尾有一个叫做USERNAME的设置。观察它是什么。在同一窗口中,键入:

set USERNAME=coffin

现在再次输入 SET ,观察USERNAME的值是多少。请注意,这不会改变您在操作系统中的身份,但是对于任何使用当前环境作为其源的脚本来说,这可能会改变您的身份。

因为环境是如此可塑和短暂,它不是一个值得信赖的身份来源。不要从环境中读取用户的身份,如果你这样做,改变你的方式!

寻找更好的操作系统用户身份来源

Windows 愿意告诉我们的应用登录的用户是谁。具体来说,所有知道如何阅读 Active Directory 身份验证用户 ID 的人都可以使用它。JRE for Windows 有一个可以获取用户 ID 的类。这个类被命名为NTSystem,它位于com . sun . security . auth . module包中 Windows 上的 Java 应用可以自动使用它。

有一大堆首字母缩略词可以帮助我们定义NTSystem的功能。NTSystem调用名为 jaas_nt.dll 的动态加载库(DLL)中的函数 Java 认证和授权系统(jaas)包的一部分。该 DLL 包含在 Windows 的 JRE(和 JDK)中。NTSystem使用 Java 本地接口(JNI)对 DLL 进行本地代码调用。我怀疑NTSystem是以 Active Directory 的前身 NT 目录服务命名的。对微软来说,Windows NT(1993 年作为新技术上市)服务器操作系统是许多名字的来源,包括 NT 域名。

使用 NTSystem 或 UnixSystem 获取身份

当您使用NTSystem时,您不依赖于环境或其他中介,而是直接向操作系统获取用户身份。清单 8-1 显示了该功能的代码。

清单 8-1。使用 NTSystem 获取 Windows 用户身份

`import com.sun.security.auth.module.NTSystem;

NTSystem mNTS = new NTSystem();
String name = mNTS.getName();`

这不是很容易吗?

如果你有 UNIX 客户端,你可以通过UnixSystem类使用类似的 JAAS 组件,如清单 8-2 中的所示。

清单 8-2。使用 UNIX 系统获取 UNIX 用户身份

`import com.sun.security.auth.module.UnixSystem;

UnixSystem mUX = new UnixSystem();
String name = mUX.getUsername();`

这些类在两个客户端上都不可用,您只能在存在该类的系统上编译上面显示的代码。所以你应该在你的应用代码中只包含对NTSystemUnixSystem的一个或另一个调用。您可以包含其中的每一个并注释其中一个来编译另一个,以便在适当的时候分发给那些客户端。

使用 Java 从 Windows Active Directory 读取当前用户身份还有其他方法,例如作为轻量级目录访问协议(LDAP)服务;但是,安全性和配置将因您的公司环境而异。我不会在这里讨论这种方法。

使用反射进行跨平台编码

这种“选择您的客户端平台”的方法并不令人满意,也不符合 Java“在任何地方运行”的目标。但是我们必须处理这样一个事实,即NTSystemUnixSystem类不仅是特定于平台的,而且是跨平台不可用的。有一种方法可以解决这个问题:反射。有了反思,我们可以忘记那些讨厌的非此即彼import语句和对平台不合适代码的评论。

有了反射,我们可以编写代码,用可能性而不是细节来编译。我们可能会在 Windows 平台上运行,在这种情况下,我们希望使用NTSystem。但是也有可能我们运行在 UNIX 平台上,在这种情况下我们希望运行UnixSystem

有了反射,我们将加载一个平台合适的类,看不见(就像去相亲),然后我们将使用该类的资源。反射使用运行时类型自省来查找和使用特定类的属性。

清单 8-3 显示了使用反射从 Windows 操作系统获取用户身份的代码。UNIX 的代码类似,但是使用了UnixSystem类而不是NTSystem类。

images 注意你会在chapter 8/platformreflectest . Java .文件中找到清单 8-3 中的代码

清单 8-3。使用反射获取操作系统用户身份

`//import com.sun.security.auth.module.NTSystem;
import java.lang.reflect.Method;

//NTSystem mNTS = new NTSystem();
Class mNTS = Class.forName( "com.sun.security.auth.module.NTSystem" );

//String name = mNTS.getName();
Method classMethod = mNTS.getMethod( "getName" );
String name = ( String )classMethod.invoke( mNTS.newInstance() );`

首先请注意,在这段代码中,我们不再导入特定于 Windows 的类NTSystem。我们将无法在 UNIX 平台上用那个import语句编译代码。相反,我们引入了反射类MethodMethod可以表示一个类中的任何特定方法。

接下来,注意我们没有像之前那样实例化一个NTSystem类。现在,我们通过使用Class.forName()方法并给出完全限定名NTSystem得到一个NTSystem类。我们以前在哪里见过这个?哦,没错。我们在加载OracleDriver时使用了这个语法。使用这种语法,编译器没有问题——它将完全限定名视为一个String,因此即使在 UNIX 机器上不存在NTSystem,您也可以在那里编译这段代码。

接下来,我们知道我们需要访问一个名为getName()的方法,所以我们将该方法的名称传递给Class.getMethod(),它返回一个Method类,classMethod表示getName()方法。

我们还没有一个NTSystem的实例,但是我们有一个句柄mNTS,它可能相当于一个static类。我们的下一步要求我们在NTSystem的实例上调用getName()方法。要调用方法,我们调用classMethod.invoke(),但是我们需要一个NTSystem的真实实例(对象),所以我们通过调用Class.newInstance()来实例化对象。

此时,NTSystem的实例从getName()方法返回 OS 用户名。然而,因为我们是通过Method类调用这个,我们将得到一个返回的Object类型,我们需要将它转换为String

当我们读到第十章时,我们会做更多关于反射的内容。在这里,我们使用反射从 Oracle 数据库的存储和网络传输中恢复类和对象。最后我们会读取它们的成员,调用它们的方法。

确保更严格的操作系统身份

在我们接受用户 ID 的NTSystem报告之前,我们想知道什么?我们想先知道我们是在 Windows 客户端上。欺骗我们代码的一种可能方式是在 UNIX 客户端上运行一个名为com . sun . security . auth . module . nt system的假冒类,该类在客户端CLASSPATH中找到。试图实现这一点可能会有问题,但我们会通过简单地确保我们是在 Windows 机器上来避免这个问题。参见清单 8-4 。

知道我们在一个 Windows 客户机上,还会告诉我们使用哪个 JAAS 源:NTSystem而不是 UnixSystem。

清单 8-4。获取操作系统用户身份,getOSUserID()

`    private static String expectedDomain = "ORGDOMAIN";

//System.getProperties().list(System.out);
    if( ( System.getProperty("os.arch").equals("x86") ||
        System.getProperty("os.arch").endsWith("64")) &&
        System.getProperty("os.name").startsWith("Windows") )
    {
        // Using reflection
        Class mNTS = Class.forName( "com.sun.security.auth.module.NTSystem" );

Method classMethod = mNTS.getMethod( "getDomain" );
        String domain = ( String )classMethod.invoke( mNTS.newInstance() );
        domain = domain.toUpperCase();

classMethod = mNTS.getMethod( "getName" );
        String name = ( String )classMethod.invoke( mNTS.newInstance() );
        name = name.toUpperCase();

System.out.println( "Domain: " + domain + ", Name: " + name );
        if ( ( name != null ) && ( !name.equals( "" ) ) &&
            ( domain != null ) &&
            domain.equalsIgnoreCase( expectedDomain ) )
        {
            rtrnString = name;
        } else {
            System.out.println( "Expecting domain = " + expectedDomain );
            System.out.println( "User " + name + " must exist in Oracle"  );
        }
    }`

if语句测试System的两个属性,以确保我们的操作系统架构(os.arch系统属性)和操作系统名称(os.name系统属性)与 Windows 客户端一致。

要查看System的所有属性,可以取消代码最上面一行的注释。调用一个Properties对象的list()方法会将属性“打印”到一个输出流——在我们的例子中是System.out

预期域

在清单 8-4 中的身份代码中,我们也从NTSystem.getDomain()方法中获得 Windows 域名。这必须与我们硬编码的expectedDomain相匹配。

假设我们的应用代码需要访问组织网络上的资源,比如 Oracle 数据库;我们应该有一个很高的门槛,客户机在被允许访问我们的公司网络之前必须通过这个门槛。我们用 NAC 系统来做这件事。NAC 监管的一部分是确保我们的客户连接到我们的公司域服务(Active Directory)。用户必须登录到我们的域才能访问网络。

如果我们的网络没有受到 NAC 的保护,NAC 保证我们的域,那么另一种欺骗的途径可能是可用的。黑客可能用一个假冒的用户身份建立自己的域(她可能伪装成我们中的一员),并让我们的代码在NTSystem之前从她的域中获取假冒的 ID。

我们通过要求客户端计算机连接到我们的公司域来避免潜在的问题,即使我们有 NAC。即NTSystem必须返回我们期望的域名,否则我们不接受用户身份的主张。

images 在单机系统上,域名可能只等于系统名。

使用 USING 系统

如果您打算将UnixSystem用于您的客户机,那么您将希望确保您的客户机和 Oracle 数据库使用相同的命名服务。这个信息不是由UnixSystem类提供的。

有些东西你可以用。UnixSystem提供用户标识符uid,它是代表用户的数值。uid不一定是唯一的,但是在一个单一的命名系统中,一个特定的用户会有一个特定的uid

要使用uid,您的客户端可以将它们看到的uid传输到 Oracle 数据库,数据库可以确保在其命名服务中为该用户看到相同的uid。这种检查提供的保证级别相当低,所以我不建议这样做。

相反,我建议您使用 NAC 来确保您的所有 UNIX 和 Linux 客户机在能够访问网络和 Oracle 数据库之前都在使用所需的命名服务。我喜欢 NAC!这听起来像一个竞选口号,但那是在我的时代之前。

区分大小写

你会注意到在我们的代码中,用户名和域名都是大写的。我们还使用equalsIgnoreCase()方法测试域。域的不区分大小写测试只是为了防止有人实现了这段代码,却忘记了将expectedDomain全部大写。来自NTSystem的域名无论大小写如何,如果拼写相同,就是同一个域名。

在 Java 中,我们可以进行不区分大小写的测试,但是在 Oracle 数据库中,我们总是区分大小写的。在 Windows/NT 域(Active Directory)中,根据用户 id 的输入方式,您会发现混合情况。Windows 域不区分大小写:用户 Coffin 与 coffin 或 COFFIN 相同。

在处理用户 ID 时,您或您的应用开发人员可能会在 Java 中使用equals()方法而不是equalsIgnoreCase()方法。还有一种可能性(尤其是如果您遵循这本书的话),您会将用户 ID 发送到 Oracle 数据库并保存在那里,或者测试它在数据库中的存在性。对于这些可能性,我们将确保我们的数据在区分大小写是一个问题的地方是一致的。我们将用大写字母处理用户 id。

以我们确定的用户身份访问 Oracle 数据库

Oracle JDBC 将许多身份特征从客户机传输到服务器。其中包括操作系统用户 ID、IP 地址以及在某些情况下的终端(客户端计算机)名称。我们可以查询这些项目并使用它们进行验证。此外,我们可以将身份信息传递给 Oracle 数据库,我们可以假设一个有效的备用身份,并将连接的身份用作代理。

身份的所有这些方面,在适当设置时,允许我们授权访问,同样重要的是,允许我们审计对数据的访问。我们想知道、监控和报告谁做了什么。

研究面向程序员的 Oracle SSO 选项

现在让我们检查一下我们的一些选择。我将把使用 Oracle 数据库进行单点登录的选项限制为以下几种:

  • appusr的身份与 Oracle 数据库建立标准连接,并通过 OS 用户身份进行授权(也许)和审计(当然)。
  • 与 Oracle 数据库建立代理会话。Oracle 用户将被命名为与我们的 OS 用户相同的名称,并将通过appusr进行代理。这要求每个操作系统用户至少有一个 Oracle 用户,并且与操作系统用户同名。(这将是本章之后的章节中使用的默认方法。但是,如果您目前正在使用另一种方案,您将会很高兴地知道我们也可以用这种方法实现单点登录。)
  • 为应用提供一个连接池,其中 Oracle 用户的名称与我们的操作系统用户相同,所有用户都通过appusr进行代理。我们将研究轻量级(瘦)连接池和重量级(Oracle 调用接口,或 OCI)连接池。我们还将实施 Oracle 数据库的最新连接池技术:通用连接池(UCP)。

设置客户端标识符

客户机标识符是我们可以为每个 Oracle 连接设置的身份特征。它可以用于很多事情,但是出于我们的目的,我们将把它设置为等于我们从NTSystemUnixSystem获得的用户身份。

使用一个OracleConnection类(它扩展了标准的Connection类),我们可以使用清单 8-5 中的代码来设置客户端标识符。

清单 8-5。设置客户端标识符,doTest1()

`    userName = OracleJavaSecure.getOSUserID();

String metrics[] =
        new String[OracleConnection.END_TO_END_STATE_INDEX_MAX];
    metrics[OracleConnection.END_TO_END_CLIENTID_INDEX] = userName;
    conn.setEndToEndMetrics( metrics, ( short )0 );`

最后一行是为连接设置端到端度量的调用。该调用接受一个String数组、metrics和一个类型为short(一个较小的整数)的索引,该索引等于 0—我们将值 0 转换为short。我们将String数组的大小设置为等于OracleConnection中名为END_TO_END_STATE_INDEX_MAX的常量成员,并将用户标识放在数组中的常量索引END_TO_END_CLIENTID_INDEX处。

稍后,当我们想要查看客户机标识符的设置时,我们将通过查询SYS_CONTEXT('USERENV','CLIENT_IDENTIFIER')在 Oracle 数据库上检查它。除了会话上下文之外,Oracle 数据库还提供了创建和使用应用上下文的工具。相对于将数据存储在数据库表中,上下文是在会话中存储信息的一个便利特性。应用上下文经常与细粒度访问(FGA)控制的安全主题一起被提及(参见第十二章),但是上下文本身并不提供安全性——只是信息的另一个存储位置。

准备访问人力资源数据

在所有情况下,我们都希望访问HR模式中的数据,因此我们可以为此做一些准备。首先,我们将调用appsec.p_check_hrview_access过程来获取我们的安全应用角色hrview_role。然后,我们可以将当前模式设置为HR模式。这对访问没有影响,但是允许我们调用HR模式中的视图和过程,而不用在每个调用前加上前缀“HR”。

清单 8-6。准备访问人力资源数据

    stmt.execute("CALL appsec.p_check_hrview_access()");     stmt.execute("ALTER SESSION SET **CURRENT_SCHEMA**=hr");     rs = stmt.executeQuery( "SELECT COUNT(*) FROM **v_employees_public**" );

请注意,视图名称v_employees_public上没有“HR .”前缀。

更新 p_check_hrview_access 程序,非代理会话

我们将对appsec.p_check_hrview_access过程做一些彻底的修改:一个处理常规连接,一个处理代理会话。一旦决定了要实现哪种方法,就可以注释或删除其中一个或另一个代码块。在appsec.p_check_hrview_access的主体中,我们将放置这段代码,清单 8-7 用于非代理会话。

清单 8-7。验证非代理会话

    IF( ( SYS_CONTEXT( 'USERENV', 'IP_ADDRESS' ) LIKE '192.168.%' OR         SYS_CONTEXT( 'USERENV', 'IP_ADDRESS' ) = '127.0.0.1' )     AND TO_CHAR( SYSDATE, 'HH24' ) BETWEEN 7 AND 18     AND **SYS_CONTEXT( 'USERENV', 'SESSION_USER' ) = 'APPUSR'**     AND SYS_CONTEXT( 'USERENV', **'CLIENT_IDENTIFIER' ) = just_os_user** )     THEN         --**DBMS_SESSION.SET_ROLE**('hrview_role');         EXECUTE IMMEDIATE '**SET ROLE** hrview_role';     END IF;

images 注意你可以在名为 Chapter8/AppSec.sql 的文件中找到这个脚本。

if语句中关于IP_ADDRESSSYSDATE时间限制的前两个测试与我们在第二章中实现的相同。第三个测试确保SESSION_USER是‘APPUSR’;也就是说,appusr用户连接到了 Oracle 数据库。在此之前,我们已经将appsec.p_check_hrview_access的执行权限限制到了appusr,但是现在对于代理会话,我们需要允许任何 Oracle 用户执行我们的过程,并确保他们在事后作为appusr连接。我们向PUBLIC(每个人)授予 execute,如下所示:

GRANT EXECUTE ON appsec.p_check_hrview_access TO PUBLIC;

记住,所有这些检查的目标是最终将角色设置为hrview_role,如果一切都检查通过的话。至少有以下两种方式来设置角色:

  • 呼叫DBMS_SESSION.SET_ROLE.
  • 立即执行SET ROLE命令。

两种形式都有效,但我们将继续使用第二种形式。它更通用,我们将在其他情况下使用 EXECUTE IMMEDIATE 语法。

确保客户端标识符和操作系统用户

我们过程中的第四个测试,如清单 8-7 所示,确保我们作为客户端标识符传递给 Oracle 数据库的操作系统用户身份等于 JDBC 传递给数据库的OS_USER连接特征。这只是我们的第二次检查,以确保应用设置的客户端标识符(代表操作系统用户身份)与 Oracle 数据库检测到的操作系统用户身份相同。我们保证应用没有将客户端身份设置为操作系统用户身份之外的身份。

我们通过 JDBC 客户端使用清单 8-8 中的代码获得被 Oracle 数据库just_os_user检测到的操作系统用户。真的,它只是另一个会话上下文环境设置;然而,我们需要按摩的价值。

清单 8-8。让操作系统用户被甲骨文感知

    just_os_user    VARCHAR2(40); -- Windows users are 20, allow 20 for domain     backslash_place NUMBER; BEGIN     -- Upper case OS_USER and discard prepended domain name, if exists     just_os_user := **UPPER**( SYS_CONTEXT( **'USERENV',** '**OS_USER**' ) );     -- Back slash is not an escape character in this context     -- Negative 1 indicates count left from the right end, get last backslash     backslash_place := **INSTR**( just_os_user, '\', -1 );     IF( backslash_place > 0 )     THEN         just_os_user := **SUBSTR**( just_os_user, backslash_place + 1 );     END IF;

注意,我们需要使用UPPER函数将 JDBC 发送到 Oracle 数据库的OS_USER大写,这样我们就可以匹配我们在客户端标识符中设置的大写用户标识。在某些情况下,域名被加在OS_USER的前面,带有一个反斜杠分隔符,就像这样:org domain \ o user。我们使用INSTR(在字符串中)函数来查找反斜杠的位置,如果有的话。然后我们使用SUBSTR(子串)函数删除域名和反斜杠。回头看看清单 8-7 中的,正是这个经过修改的OS_USERjust_os_user与我们在客户端标识符中设置的值进行比较。

测试所有这些用户身份特征是相同的,并不能真正为我们带来更多的安全性,但却为潜在的黑客设置了另一个障碍。您可能会惊讶地发现,要防止非法闯入,往往只需要一个额外的障碍,而一个简单的额外障碍却被当作商业安全解决方案出售。

设置了客户标识符的审计活动

以下查询将显示设置了客户端标识符的连接的审计线索条目。你可能还没有,但是我们会在测试后看到这些联系。

SELECT * FROM sys.dba_audit_trail WHERE client_id IS NOT NULL ORDER BY TIMESTAMP DESC;

images 你可以在名为 Chapter8/SecAdm.sql 的文件中找到这个脚本。

代理会话

代理会话背后的想法是,我们可以作为我们的应用用户连接,但作为一个确定的个人用户工作。这允许我们以应用用户的身份安全地连接(不使用个人密码,也确实不需要),并审计个人的活动。这些目标与我们通过将客户机标识符设置为操作系统用户身份所实现的目标没有根本的不同,如前一节所述。(我们努力的)主要区别是:

  • 使用代理会话,我们不是作为应用用户,而是作为个人用户工作。
  • 对于代理会话,每个要连接的人都必须有一个 Oracle 用户。

在 Oracle 中创建个人用户

当我们只需要应用帐户来完成工作时,我们为什么要在 Oracle 数据库中设置个人用户呢?这是一个很好的问题,答案可能会让您有理由选择我们在本节中描述的两条路线中的任何一条:

1)使用标准连接并将操作系统用户标识放入 Oracle 客户端标识符中。

2)使用代理会话。

个人用户需要一些管理活动;然而,它可以是最小的。让我们创建一个名为OSUSER的示例用户(在这些命令中,可以随意用您的用户 ID 替换OSUSER):

CREATE USER osuser **IDENTIFIED EXTERNALLY;** GRANT create_session_role TO osuser; ALTER USER **osuser GRANT CONNECT THROUGH appusr;**

对于每个用户,您只需要执行这些命令。每个用户都必须拥有 Create Session 系统权限,因为代理需要创建额外的会话。您将拥有一个连接(作为应用用户)和两个会话—一个作为应用用户,一个作为代理用户。

您可以根据组织中所有用户的列表编写用户创建脚本,并为每个人快速创建一个 Oracle 用户。创建这些用户的容易程度并不是他们的优势,因为仍然需要付出努力。当员工不在时,还需要进行管理工作来删除或禁用用户。

反对 Oracle 数据库中的单个用户的另一个理由是,为每个用户提供了一些访问和分配。每个用户都有一个关联的模式。这个模式不会包含太多内容,但它会存在。当您滚动模式列表(在 IDE 中)以找到您想要的模式时,您可能会滚动属于个人的几十个、几百个或几千个模式。

代理从外部识别的用户

也许您的组织已经使用了 Oracle Internet directory (OID)和/或企业用户安全性,如果是这样,则可以创建在每个 Oracle 数据库实例上没有模式的个人用户,并可以在代理会话中使用。这些用户将会是IDENTIFIED GLOBALLY

也许,您信任另一个目录服务或操作系统来识别您的用户。在这种情况下,每个用户仍然有一个惟一的模式,但是身份验证(ID 和密码)将在外部保留。例如,您可以为您的数据库设置一个类似“OPS()OSAUTHENTPREFIXOPSOSUSER的 Oracle 用户。OSUSER用户在连接到 Oracle 数据库时不需要提供密码,但是通过操作系统的认证OSUSER`就可以获得访问权限。设置它需要一些步骤,包括从 Oracle 数据库获得对目录或域服务器的访问权。这是单点登录的一种形式。

然而,我们正在做的是有意不同的。我们正在 Oracle 数据库中创建没有身份验证的个人用户。它们没有口令,标识的外部修饰符只是告诉 Oracle 数据库用户没有通过数据库的身份验证。

另一种方法是创建由随机密码标识的用户,管理员也不会保留该密码。如果没有人知道密码,就没有人可以用它进行身份验证。这样做的问题是,任何存在的密码都需要管理,并且至少要定期更改(更改为另一个随机密码)。

建立代理会话

要建立代理会话,我们要做两件事。首先,我们创建一个Properties类(基本上是一个带有键和值的散列表,例如,key=PROXY_USER_NAME,value=OSUSER)。然后我们将这个Properties类传递给OracleConnection类的openProxySession()方法,如清单 8-9 所示。这段代码来自于OraSSOTests类的doTest2()方法。另见main()方法。

images 注意在名为chapter 8/orassotests . Java .的文件中找到测试代码

请注意,此时我们已经有了一个现有的连接。我们作为应用用户连接到 Oracle 数据库。这里的目标是通过应用用户代理我们的操作系统用户帐户。

清单 8-9。开启代理会话,doTest2()

`    userName = OracleJavaSecure.getOSUserID();
    Properties prop = new Properties();
    prop.setProperty( OracleConnection.PROXY_USER_NAME, userName );
    conn.openProxySession (OracleConnection. PROXYTYPE_USER_NAME, prop);

String metrics[] =
        new String[OracleConnection.END_TO_END_STATE_INDEX_MAX];
    metrics[OracleConnection.END_TO_END_CLIENTID_INDEX] = userName;
    conn.setEndToEndMetrics( metrics, ( short )0 );`

注意,我们从NTSystemUnixSystem获取操作系统用户身份到userName,并将其设置为PROXY_USER_NAME。当我们打开会话时,我们告诉它我们是基于用户名PROXYTYPE_USER_NAME的代理。

我们还将客户端标识符设置为操作系统用户名——这是搜索审计日志的好方法。

此时,我们有一个代理会话,我们可以使用以下查询来验证它:

SELECT USER , SYS_CONTEXT('USERENV','PROXY_USER') , SYS_CONTEXT('USERENV','OS_USER') , SYS_CONTEXT('USERENV','SESSION_USER') , SYS_CONTEXT('USERENV','OS_USER') , SYS_CONTEXT('USERENV','IP_ADDRESS') , SYS_CONTEXT('USERENV','TERMINAL') , SYS_CONTEXT('USERENV','CLIENT_IDENTIFIER') FROM DUAL;

这将返回一系列标识值,如下所示:

user                 : OSUSER userenv proxy_user   : **APPUSR** userenv current_user : OSUSER userenv session_user : OSUSER userenv os_user      : OSUser (occasionally OrgDomain\OSUser) userenv ip_address   : 127.0.0.1 userenv terminal     : unknown userenv client_id    : OSUSER

在我们的 Oracle 代理会话中,USERCURRENT_USERSESSION_USER也被设置为OSUSER。Oracle 数据库发现我们来自 JDBC 的操作系统用户是OSUser,如OS_USER会话环境值所示。

最后,我们连接为appusr,这允许OSUSER代理通过,因此我们将APPUSR视为PROXY_USER。如果你看一下OraSSOTests的代码,你会看到我们连接为appusr:

`    private String appusrConnString =
        "jdbc:oracle:thin:appusr/password@localhost:1521:orcl";

conn = (OracleConnection) DriverManager.getConnection( appusrConnString );`

所以我们作为appusr连接,但是在建立我们的代理会话之后,您可以看到我们的用户是OSUSER

我们用稍微不同的语法关闭代理连接:

    conn.close( OracleConnection.PROXY_SESSION );

在这个上下文中(doTest2()方法),效果与标准的conn.close()相同,但是对于缓存的连接/连接池,这个新语法只关闭当前会话,但是保持连接对其他人可用。

代理用户与代理用户名

不幸的是,Oracle 在“以用户身份连接”和“通过用户代理”的关系中都使用了“代理用户”一词。代理用户proxy_user是连接到数据库的 Oracle 用户,如我们的会话环境SYS_CONTEXT( 'USERENV',、??【代理用户】 )。并且PROXY_USER_NAME是当我们建立我们的代理连接时通过代理用户获得访问的用户名, prop.setProperty( OracleConnection. 代理用户名, userName )。也许用“代理主机用户”和“代理客户机用户”,或者“代理连接用户”和“代理会话用户”来描述这些角色会更好尽管混乱,我们需要保持他们的直线。一个用户最初使用其密码连接到 Oracle 数据库(称为代理用户),另一个用户通过该用户进行代理(连接)。第二个用户拥有将完成所有工作的会话,我们将在审计日志中看到他。

更新 p_check_hrview_access 程序,代理会话

我们的安全应用角色程序appsec.p_check_hrview_access必须再次更新,以验证代理会话并根据需要授予hrview_role。为此,我们已经将清单 8-10 中所示的代码添加到程序体中(在文件 AppSec.sql 中找到)。

清单 8-10。验证代理会话

    IF( **SYS_CONTEXT( 'USERENV', 'PROXY_USER' ) = 'APPUSR'**     AND ( SYS_CONTEXT( 'USERENV', 'IP_ADDRESS' ) LIKE '192.168.%' OR         SYS_CONTEXT( 'USERENV', 'IP_ADDRESS' ) = '127.0.0.1' )     AND TO_CHAR( SYSDATE, 'HH24' ) BETWEEN 7 AND 18     AND SYS_CONTEXT( 'USERENV', 'SESSION_USER' ) =         SYS_CONTEXT( 'USERENV', 'CLIENT_IDENTIFIER' )     AND SYS_CONTEXT( 'USERENV', 'CLIENT_IDENTIFIER' ) = just_os_user )     THEN         EXECUTE IMMEDIATE 'SET ROLE hrview_role';     END IF;

if语句中的第一个测试确保我们正在处理一个代理会话,并且代理用户是appusr。如果你还记得,我们最初只允许appusr执行这个程序,但是现在我们已经授权PUBLIC执行。然而,我们仍然只允许对appusr的访问,方法是确保SESSION_USERappusr(仅设置客户端标识符时)或者PROXY_USERappusr(用于代理会话)。

接下来,我们对 IP 地址和SYSDATE时间限制进行标准测试。然后我们还有两个测试,基本上保证这三个身份特征是相同的:

SESSION_USER = CLIENT_IDENTIFIER = OS_USER

代理此会话的用户与我们从NTSystemUnixSystem获得的操作系统用户相同,并且与 JDBC 向 Oracle 数据库提供的操作系统用户相同。如果这一切都是真的,那么我们设置安全应用角色,hrview_role

审计代理会话

我们希望审计特定于代理会话的活动。我们可以使用以下命令进行配置:

AUDIT UPDATE TABLE, INSERT TABLE BY appusr ON BEHALF OF ANY; -- This would be nice, but every java class gets audited with this command --AUDIT EXECUTE PROCEDURE BY appusr ON BEHALF OF ANY; NOAUDIT EXECUTE PROCEDURE BY appusr ON BEHALF OF ANY;

因为appusr是代理用户,我们可以审计他代表别人做的任何事情。这里我们审计所有的更新和插入查询。我们决定不审计所有执行过程的调用。

这些查询将显示由代理会话生成的审计跟踪条目。第一个查询显示所有代理会话—它们有一个 PROXY_SESSIONID。

SELECT * FROM DBA_AUDIT_TRAIL WHERE PROXY_SESSIONID IS NOT NULL;

下一个查询查找与会话相关联的特定代理连接,并显示该连接的代理用户。结果可能如表 8-1 所示。我们的查询实际上返回了比表中显示的更多的列。

SELECT p.username proxy, u.os_username, u.username, u.userhost, u.terminal, u.timestamp, u.owner, u.obj_name, u.action_name, u.client_id, u.proxy_sessionid FROM sys.dba_audit_trail u, sys.dba_audit_trail p WHERE u.proxy_sessionid = p.sessionid ORDER BY u.timestamp DESC;

images

使用连接池

如果您非常关注桌面客户端应用,那么您可以跳过这一部分。连接池通常只在多线程、多用户服务器应用中需要。连接池是可供客户端或客户端线程使用的连接的集合。根据需要,客户机将从池中获取一个连接,使用它来查询或更新 Oracle 数据库,然后将它返回到池中。

连接池通常在 JVM 运行期间存在。考虑这个场景:一个 web 应用服务器(例如,Tomcat)开始运行,一个应用从一个池中请求一个连接。此时,一个连接池被建立,其中一个连接被提供给应用。当线程完成连接时,特定的应用线程(通常绑定到一个用户请求——一个寻找动态网页的浏览器)会将连接返回到池中。我们的连接池是供所有 web 应用线程(浏览动态网页的用户)使用的,直到 web 应用服务器(Tomcat)关闭。

我们将在这里花费足够的精力来证明我们的单点登录可以在任何可用的连接池方法中工作。如果您正在使用 Java Enterprise Edition (J2EE)容器(像 web 应用服务器一样),并且使用 Enterprise Java Beans(EJB),那么您很有可能通过“容器管理的连接池”来使用连接池

来自 OCI 连接池的代理连接

Oracle call interface (OCI)连接池是传统的连接池方法,我们的单点登录方法可以在其中获得成功。OCI 是一种取代 java 的技术,不是“纯 Java”的实现。我们会说,使用 ojdbc,Java 能够调用 OCI 作为外部资源。

配置池

我们必须采取的第一步是通过一个OracleOCIConnectionPool类建立连接池。我们设置 URL(连接字符串)、用户和密码(appusrConnURLappusrConnUserappusrConnPassword),池中的所有连接都将拥有它们,如清单 8-11 中的所示。他们都将连接为appusr

务必在 OraSSOTests.java 的中编辑代码,以正确识别您的主机、端口、实例和网络域(在SERVICE_NAME中)。注意,连接 URL 被指定为“jdbc:oracle:oci:”。这是一个重量级,OCI 连接类型的指定。连接字符串以 TNSNames (透明网络底层)格式appusrConnOCIURL指定。

清单 8-11。配置 OCI 连接池

`    private String appusrConnOCIURL =
        "jdbc:oracle:oci😡(description=(address=(host=" +
        "127.0.0.1)(protocol=tcp)(port=1521))(connect_data=" +
        "(INSTANCE_NAME=orcl)(SERVICE_NAME=orcl)))";
        // Or
        //"(INSTANCE_NAME=orcl)(SERVICE_NAME=orcl.org.com)))";
    private String appusrConnUser = "appusr";
    private String appusrConnPassword = "password";

OracleOCIConnectionPool cpool = new OracleOCIConnectionPool();
    cpool.setURL(appusrConnOCIURL);
    cpool.setUser(appusrConnUser);
    cpool.setPassword(appusrConnPassword);

Properties prop = new Properties();
    prop.put (OracleOCIConnectionPool.CONNPOOL_MIN_LIMIT, "2");
    prop.put (OracleOCIConnectionPool.CONNPOOL_MAX_LIMIT, "10");
    prop.put (OracleOCIConnectionPool.CONNPOOL_INCREMENT, "1");
    cpool.setPoolConfig(prop);`

接下来,我们通过构建一个具有基本参数的Properties对象来配置连接池:最小池大小(也是初始大小)、最大池大小和增长增量。我们通过setPoolConfig()方法将Properties传递给我们的连接池。请注意,这些属性适用于池本身,而不是任何特定的连接。

获取代理连接

我们向现有的Properties对象添加了一个参数——??。当我们从池中获得一个连接时,我们特别想要一个代理连接。池中的所有连接都作为appusr连接,但是来自池中的每个代理连接都可以与通过appusr连接的不同用户相关联。我们将这个连接的PROXY_USER_NAME设置为userName,这是我们从NTSystemUnixSystem获得的操作系统用户身份。当我们从cpool.getProxyConnection()方法清单 8-12 中请求一个代理连接时,我们在请求中传递带有PROXY_USER_NAMEProperties

清单 8-12。从 OCI 连接池中获取代理连接

    prop.setProperty(OracleOCIConnectionPool.PROXY_USER_NAME, userName );     conn = (OracleConnection)cpool.getProxyConnection(         OracleOCIConnectionPool.PROXYTYPE_USER_NAME, prop);

对于这个来自 OCI 池的连接,我们设置客户端标识符的方式与我们之前设置标准连接的方式相同。

查看代理会话

此时,如果查询OracleConnection.isProxySession()方法,会发现这不是一个代理会话。不要让那打扰你。如果您查询 Oracle 数据库,您会发现PROXY_USERappusrUSERCURRENT_USERSESSION_USER都被设置为我们的操作系统用户身份。这是通过getProxyConnection()方法获得的,一个代理连接和一个代理会话。

这是不必要的,但是如果您必须让isProxySession()方法返回true,您可以通过OracleConnection类生成另一个会话——此时您将有三个会话。使用此代码还会使您的客户端标识符无效,这会干扰设置我们的安全应用角色的过程。因此,如果您这样做,请修改 SSO 过程(例如,appsec.p_check_hrview_access)以跳过对客户端标识符的测试。

    //prop = new Properties();     //prop.setProperty(OracleConnection.PROXY_USER_NAME, userName );     //conn.openProxySession(OracleConnection.PROXYTYPE_USER_NAME, prop);

注意,这段代码实例化了一个新的Properties类。原因是我们之前的Properties有基于OracleOCIConnectionPool常量的数字键,而这个Properties实例需要基于OracleConnection常量的String键。

参见代理连接

下面是我们在OraSSOTests.doTest3()方法中测试查询的结果。这些结果来自 OCI 连接池代理连接。

Is proxy session: false user                 : OSUSER userenv proxy_user   : APPUSR userenv current_user : OSUSER userenv session_user : OSUSER userenv os_user      : ORGDOMAIN\OSUSER userenv ip_address   : 127.0.0.1 userenv terminal     : MYCOMPUTER userenv client_id    : OSUSER Read HR view!!!!!!!!!!!!!!!!!!!!

注意第一行。如前一节所述,OracleConnection.isProxySession()方法返回false;但是,这是通过代理连接的代理会话。你可以在PROXY_USER的身份特征中看到这一点,与其他用户特征形成对比。

重量级 OCI 连接的一个好处是它们可以报告终端名称。还要注意,在OS_USER参数中,OCI 连接报告了域名。也许您可以基于这些身份特征实现一些额外的安全检查。

关闭代理会话

在这个上下文中,我们的代理连接关闭方法关闭代理会话,并将连接返回到池中以供重用。注意在清单 8-13 中,我们将PROXY_SESSION常量值传递给了OracleConnection.close()方法。这与我们在没有连接池的情况下关闭代理连接是一样的,只是没有连接池,代理会话和 Oracle 连接都会关闭。

清单 8-13。关闭代理连接

    conn.close( OracleConnection.PROXY_SESSION );

查看所有池属性

我们可以检查连接池使用的所有属性,甚至那些我们没有显式设置的属性。清单 8-14 中的代码获取配置属性的完整列表并打印每个键/值对。

清单 8-14。显示所有 OCI 连接池属性

    prop = cpool.getPoolConfig();     Enumeration enumer = prop.propertyNames();     String key;     while( enumer.hasMoreElements() ) {         key = (String)enumer.nextElement();         System.out.println( key + ", " + prop.getProperty( key ) );     }

我们看到下面的列表是该代码的结果:

connpool_active_size, 0 connpool_pool_size, 2 connpool_max_limit, 10 connpool_min_limit, 2 connpool_timeout, 0 connpool_is_poolcreated, true connpool_increment, 1 connpool_nowait, false

OCI 连接池概要

OCI 连接池很好地处理了我们的单点登录方法。我们能够验证用户,通过我们的安全应用角色程序测试,并访问人力资源数据。

我们来自 OCI 连接池的OracleConnection类不识别代理会话,但是代理会话仍然存在。

OCI 连接池可以识别终端名称,并可以返回域名和操作系统用户名。

这里有个问题。OCI 连接池建立非常慢,连接很重,需要更多资源。设置完成后,使用池中现有的连接,性能就不是问题了。由于这个原因,OCI 连接池适合运行时间长的基于服务器的应用(例如,在 web 应用服务器中)。

瘦客户端连接池中的代理会话

虽然连接池通常只在支持多个并发用户的服务器应用中需要,但是您可能需要多线程、独立或客户端应用,这些应用可能需要重用池中的多个连接——要有创造性!瘦客户机连接池或缓存是实现这一点的好方法。我们的单点登录方法可以在 it 领域取得成功。

配置池/缓存

我们通过一个oracle.jdbc.pool.OracleDataSource类建立轻量级(瘦)连接池。设置池中所有连接将拥有的 URL(连接字符串)、用户和密码。它们都将连接为appusr

编辑连接字符串(URL),appusrConnURL,设置适当的主机、端口和实例;并在appusrConnPassword中为appusr设置正确的密码。URL 以 TNSNames 格式指定。请注意清单 8-15 中的 URL 将一个瘦(轻量级)连接指定为“jdbc:oracle:thin”。与 OCI 连接相比,瘦连接需要的资源更少,建立速度更快。瘦连接仅使用 java 通过 SQLNet 协议与 Oracle 数据库进行通信,而不是使用 OCI 作为运行 SQLNet 的外部非 java 资源。我们将我们的连接缓存(池)命名为“APP_CACHE”

清单 8-15。配置瘦客户端连接池

`    private String appusrConnThinURL =
        "jdbc:oracle:thin😡(description=(address=(host=" +
        "127.0.0.1)(protocol=tcp)(port=1521))(connect_data=" +
        "(INSTANCE_NAME=orcl)(SERVICE_NAME=orcl.org.com)))";

OracleDataSource cpool = new OracleDataSource();
    cpool.setURL(appusrConnThinURL);
    cpool.setUser(appusrConnUser);
    cpool.setPassword(appusrConnPassword);

// Enable Connection Caching
    cpool.setConnectionCachingEnabled(true);
    cpool.setConnectionCacheName("APP_CACHE");

Properties prop = new Properties();
    prop.setProperty("InitialLimit", "3");
    prop.setProperty("MinLimit", "2");
    prop.setProperty("MaxLimit", "10");
    cpool.setConnectionCacheProperties(prop);`

我们还将为连接池设置一些初始属性,并将它们传递给setConnectionCacheProperties()方法。这些类似于我们前面看到的 OCI 连接池的池属性,但是键名完全不同。

使用语句缓存

在网上研究轻量级连接池时,您会发现一些困惑。这种混乱很大程度上源于OracleDataSource类中语句缓存的可用性。将连接池称为连接缓存尤其令人困惑。为了更好的衡量,让我们启用语句缓存,如清单 8-16 中的所示。这与连接池无关。

清单 8-16。启用语句缓存

    cpool.setImplicitCachingEnabled(true);

使用语句缓存,当您调用准备好的语句时,本地连接会缓存它。如果您再次调用该语句,它会比没有缓存时执行得更快。

隐式缓存是自动发生的,我们通过调用setImplicitCachingEnabled(true)来启用它。您还可以启用显式语句缓存,这要求您为语句指定一个关键字符串(名称)并通过该名称调用它。有关更多信息,请在互联网上搜索 Oracle explicit 语句缓存。

获取代理会话

我们通过getConnection()方法从池中请求一个瘦连接,如清单 8-17 所示。获得池连接后,我们请求一个代理会话。将来自NTSystemUnixSystem的用户身份作为属性值传递给OracleConnectionopenProxySession()方法。同样,为了方便起见,我们只是在现有的 Property类的中添加了另一个值PROXY_USER_NAME

清单 8-17。从瘦客户端连接池中获取代理连接

    conn = (OracleConnection)cpool.getConnection();     prop.setProperty(OracleConnection.PROXY_USER_NAME, userName );     conn.openProxySession(OracleConnection.PROXYTYPE_USER_NAME, prop);

当我们关闭代理会话时,连接将返回到池中以供重用:

    conn.close( OracleConnection.PROXY_SESSION );

参见代理会话

我们在瘦客户机代理会话上的查询结果与我们预期的一样。我们已经通过代理用户appusr设置了我们的客户端标识符和我们的代理用户osuser。该连接通过了我们的安全应用角色的测试,并且能够从 HR 模式中读取数据。

Is proxy session: true user                 : OSUSER userenv proxy_user   : APPUSR userenv current_user : OSUSER userenv session_user : OSUSER userenv os_user      : OSUSER userenv ip_address   : 127.0.0.1 userenv terminal     : unknown userenv client_id    : OSUSER Read HR view!!!!!!!!!!!!!!!!!!!!

查看所有池属性

我们还可以检索和查看所有连接池属性:

`    prop = cpool.getConnectionCacheProperties();

MaxStatementsLimit, 0
AbandonedConnectionTimeout, 0
MinLimit, 2
TimeToLiveTimeout, 0
LowerThresholdLimit, 20
InitialLimit, 3
ValidateConnection, false
ConnectionWaitTimeout, 0
PropertyCheckInterval, 900
InactivityTimeout, 0
LocalTransactionCommitOnClose, false
MaxLimit, 10
ClosestConnectionMatch, false
AttributeWeights, NULL`

编译、弃用的方法和注释

当你编译 OraSSOTests.java 时,你会收到两个警告。我们在这里调用的两个方法已被否决;也就是说,它们不再作为当前的编程实践来推广。您可以通过在命令行上提供一个参数来查看这些方法,如下所示:

`javac -Xlint:deprecation OraSSOTests.java

OraSSOTests.java:265: warning: [deprecation] setConnectionCachingEnabled(boolean
) in oracle.jdbc.pool.OracleDataSource has been deprecated
                        cpool.setConnectionCachingEnabled(true);
                             ^
OraSSOTests.java:274: warning: [deprecation] setConnectionCacheProperties(java.u
til.Properties) in oracle.jdbc.pool.OracleDataSource has been deprecated
            cpool.setConnectionCacheProperties(prop);
                 ^
2 warnings`

难道你不知道吗,我们调用这两个方法来启用和配置我们的连接池(缓存)!?!为什么它们被弃用?即使有这些警告,代码仍然可以正确编译并正常运行。以我的经验来看,被否决的方法很少会真的消失——有时它们真的会复活。不要害怕!

这些方法最近才被弃用,我怀疑原因是 Oracle 开发了一个新的包来实现瘦客户端连接池,即通用连接池(UCP)。我们将在本章的后面实现 UCP。

当我们编译时,我们可以通过在方法上放置一个@SuppressWarnings 注释来避免这些不推荐使用的警告,如下所示。请注意,注释的末尾没有分号;它适用于以下方法,doTest4()

    @SuppressWarnings("deprecation")     void doTest4() {

注释不是 Java 语法的一部分,但它们包含在 Java 代码和字节码(编译后)中,以便指导 Java 实用程序和工具(如java.exejavac.exe)。注释的用途多种多样,您可以通过定义自己的注释类型以及编写 Java 代码供 Java 工具在响应注释时使用来扩展它们。

Oracle JVM 不接受许多这样的注释,所以在 Oracle 数据库中创建 Java 结构之前,我们将在代码中对它们进行注释。

瘦客户端连接池摘要

瘦客户端连接池快速而方便。它支持我们通过代理和客户端标识符进行单点登录的方法。

看到用于连接池的关键方法被标记为“已弃用”有点令人不安但这不是路障。

通用连接池

UCP 是连接池领域的最新成员。因为它太新了,所以你应该关注并实现 UCP 的任何更新。

images 注意在名为chapter 8/orassotests 2 . Java .的文件中找到 UCP 的测试代码

与 UCP 一起编译/运行

到目前为止,UCP 包还没有合并到 Oracle 驱动程序 jar (ojdbc6.jar)中。你需要在 www.oracle.com/technetwork/indexes/downloads 从甲骨文下载一个单独的 ucp.jar 文件。向下滚动并在【驱动程序】部分找到该文件。

我们针对 UCP 的测试代码在一个单独的文件中,OraSSOTests2.java。当您编译并运行这段代码时,您将需要在您的CLASSPATH中包含 ucp.jar 文件,如清单 8-18 所示。

清单 8-18。用 UCP.jar 编译运行的命令

javac -classpath "%CLASSPATH%";ucp.jar OraSSOTests2.java java -classpath "%CLASSPATH%";ucp.jar OraSSOTests2

如果你打算使用 UCP,一个更好的方法是在操作系统环境中把它添加到你的CLASSPATH中,如第三章中所述。

使用连接池工厂

UCP 使用一个PoolDataSourceFactory类来实例化连接池。我们为PoolDataSource提供用于每个连接的类的全限定名称“Oracle . JDBC . pool . Oracle data source”,并设置用于连接的 URL(连接字符串)。参见清单 8-19 。

清单 8-19。配置 UCP 连接池

`    private String appusrConnString =
        "jdbc:oracle:thin:appusr/password@localhost:1521:orcl";

PoolDataSource cpool = PoolDataSourceFactory.getPoolDataSource();
    cpool.setConnectionFactoryClassName("oracle.jdbc.pool.OracleDataSource");
    cpool.setURL( appusrConnString );

cpool.setInitialPoolSize(5);
    cpool.setMinPoolSize(2);
    cpool.setMaxPoolSize(10);`

我们调用PoolDataSource类的方法cpool来设置连接池的一些属性。

请注意,UCP 是一个瘦客户端实现

请注意,我们指定的 URL(如上)是一个轻量级的瘦连接。我们也能够用简单的连接字符串语法指定 URL,如清单 8-20 所示。如果需要,我们可以用 TNSNames 格式指定 URL,并在单独的方法调用中提供用户名和密码。我们还可以指定简单的连接字符串语法,省去用户和密码,并在单独的方法调用中提供用户和密码。

清单 8-20。替代 UCP 连接池规范

    cpool.setURL(appusrConnURL);     cpool.setUser(appusrConnUser);     cpool.setPassword(appusrConnPassword);     // Or     cpool.setURL("jdbc:oracle:**thin**:@localhost:1521:Orcl" );     cpool.setUser(appusrConnUser);     cpool.setPassword(appusrConnPassword);

我非常喜欢连接字符串语法(包括用户名和密码)的一个原因是,我可以从一个安全的外部源以单个字符串的形式向他们提供所有的连接细节,包括密码。我们将在第十章和第十一章中深入探讨这个话题。从安全的外部源提供连接字符串意味着我们可以做到以下几点:

  • 避免在我们的应用代码中嵌入密码。
  • 集中存储、维护和分发我们的连接字符串(从而允许我们在一个地方更改连接字符串的参数,并在我们使用连接的任何地方应用更改)。
获得并使用 UCP 连接

这会看起来很眼熟。我们从池中获得一个连接,获得一个代理会话,并设置客户端标识符,就像我们对非 UCP 精简连接池所做的那样。结果也是一样的。

通用连接池概述

如果你不介意生活在前沿,那么 UCP 就是你要去的地方。它将要求您关注 UCP 包的更新,以及最终将 UCP 包含在 Oracle 驱动程序中, ojdbc6.jar 。你还需要将 ucp.jar 合并到你的CLASSPATH中(参见第三章)。就我们的 Oracle SSO 目标而言,UCP 的工作没有任何问题。

Oracle 单点登录的应用使用

我们在本章中提出的假设要求是,用户已经在客户端进行了身份验证。我们在代码中的目标是利用现有的身份验证,并将其提供给应用开发人员,这样他们就可以利用 Oracle SSO,而不用承担内部工作的负担。

我将只实施我们已经检查过的 Oracle SSO 的五个选项中的一个。我们将选择一个带有代理会话 n 的非池连接(参考OraSSOTests.java中的doTest2()方法)。如果您想实现非代理连接或池连接,那么基于OraSSOTests.java代码,您应该能够轻松实现。

我们的例子应用 Oracle SSO

我们将从外到内检查这一点;也就是说,首先从应用开发人员的角度来看。在我们探索了开发人员需要做什么之后,我们将讨论为了支持开发人员,我们需要对OracleJavaSecure类做什么改变。

使用应用 Oracle 连接

每个应用将作为不同的应用用户帐户连接到不同的 Oracle 实例。该逻辑必须存在于应用中。我们的示例应用以appusr用户的身份从HR模式获取数据,因此我们以清单 8-21 中的用户身份进行连接。

清单 8-21。应用 Oracle 连接规范

    String urlString = "jdbc:oracle:thin:**appusr**/password@localhost:1521:orcl";     Class.forName( "oracle.jdbc.driver.OracleDriver" );     OracleConnection conn =         (OracleConnection)DriverManager.getConnection( urlString );

images 注意你可以在名为 Chapter8/AppOraSSO.java. 的文件中找到这段代码

另一种方法,如果你在OracleJavaSecure类中而不是在客户端应用中实现连接池,你会使用这种方法,不会在应用中实例化一个连接;相反,您只需将特定于应用的 URLurlString传递给OracleJavaSecure,以便配置连接池。您将从池中取回一个OracleConnection供您的应用使用。至少,我会这么做。

获取 SSO 的代理连接

优选地,开发人员可以进行单个方法调用来获取代理连接,这将成功地通过我们的安全应用角色过程中的测试。让我们称这个方法为setConnection()。应用开发人员会这样称呼它:

    conn = OracleJavaSecure.setConnection( conn );

这将用该方法返回的OracleConnection覆盖现有的conn。实际上,记住这些只是内存中对象的引用(指针),没有创建这个对象的新实例,所以对象指针没有改变。我们从应用向OracleJavaSecure传递了对OracleConnection的引用(所有东西都驻留在一个 JVM 中。)然后OracleJavaSecure在那个OracleConnection上设置代理会话和客户端标识符。当我们在应用中使用它时,那些特性现在是我们最初的OracleConnection的一部分。我们可以也将会使用清单 8-22 中的语法。

清单 8-22。向现有连接添加代理功能,setProxyConnection()

    OracleJavaSecure.setProxyConnection( conn );

结果是一样的——原来的OracleConnectionconn现在有了代理特征。

如果我们只传递 URL,我们将调用这个方法,它返回一个带有代理会话和客户端标识符的OracleConnection:

    OracleConnection conn = OracleJavaSecure.setConnection( urlString );

关闭代理连接

我们希望考虑到这些连接可能来自连接池的可能性,并且我们希望确保在这种情况下关闭代理会话,因此我们将指示开发人员调用一个方法来关闭连接,如下所示:

    OracleJavaSecure.closeConnection();

Oracle javasecure 的更新

通用的Connection类不支持代理连接,也不支持设置客户端标识符。从现在开始,我们将使用OracleConnection的实例。我们的静态类成员,conn现在是一个OracleConnection,参见清单 8-23 。同样,我们的conn静态初始化器将Connection转换为OracleConnection

清单 8-23。 OracleJavaSecure 静态 OracleConnection

`    private static OracleConnection conn;

static {
        try {
            // The following throws an exception when not running within an Oracle Database
            conn = (OracleConnection)(new OracleDriver().defaultConnection());
        } catch( Exception x ) {}
    }`

更新 setConnection()方法

我们将重载setConnection()方法(参见清单 8-24 ),保留一个带Connection参数的方法,并添加一个带OracleConnection的方法。第一个将调用第二个,以便两者都配置带有客户端标识符集的代理连接。来自NTSystemUnixSystem的操作系统用户身份用于代理和客户端标识符:

清单 8-24。在 OracleJavaSecure 中设置内部连接并配置,setConnection()

`    public static final OracleConnection setConnection( Connection c ) {
        return setConnection( (OracleConnection)c );
    }
    public static final OracleConnection setConnection( OracleConnection c ) {
        conn = null;
        // We are going to require that only we will set up initial proxy connections
        if( c == null || c.isProxySession() ) return null;
        else try {
            // Set up a non-pooled proxy connection with Client Identifier
            // To use an alternate solution, refer to code in OraSSOTests.java
            String userName = getOSUserID();
            if ( ( userName != null ) && ( !userName.equals( "" ) ) ) {
                Properties prop = new Properties();
                prop.setProperty( OracleConnection.PROXY_USER_NAME, userName );
                c.openProxySession(OracleConnection.PROXYTYPE_USER_NAME, prop);

String metrics[] =
                    new String[OracleConnection.END_TO_END_STATE_INDEX_MAX];
                metrics[OracleConnection.END_TO_END_CLIENTID_INDEX] = userName;
                c.setEndToEndMetrics( metrics, ( short )0 );

// If we don't get here, no Connection will be available
                conn = c;
            } else {
                // This is not a valid user
            }
        } catch ( Exception x ) {
            x.printStackTrace();
        }
        return conn;
    }`

这段代码正是我们一直在讨论的 Oracle SSO。请注意,如果传递给我们的连接已经是一个代理连接,我们将丢弃它并使我们的Connection无效。我们不会识别在其他地方配置的任何代理连接——出于安全考虑,我们嫉妒并保护我们在其中的角色。

添加一个重载的 setConnection()方法

我们从 8-24 中列出的setConnection()方法返回结果,配置OracleConnection,这些将支持一个额外的setConnection()方法。额外的方法(清单 8-25 )将 URL 作为一个String,实例化一个Connection并调用核心的 setConnection()方法,该方法使用代理会话和客户端标识符配置连接。然后返回已配置的OracleConnection。通过调用其他setConnection()方法并返回它们返回的内容,这变得相对容易。

清单 8-25。交替设置内部连接,setConnection()

    public static final OracleConnection **setConnection**( String URL ) {         **Connection c** = null;         try {             Class.forName( "oracle.jdbc.driver.OracleDriver" );             c = DriverManager.getConnection( URL );         } catch ( Exception x ) {             x.printStackTrace();         }         **return setConnection( c );**     }

images 注意如果你要在OracleJavaSecure类中实现一个连接池,这就是你要做的。您可能希望确保 URL 字符串不会随着每个后续调用而改变。

关闭代理连接

让开发人员在OracleJavaSecure上调用一个方法来关闭已配置的代理连接,以确保调用是适当的(清单 8-26 )。我们必须确保关闭代理会话,这可能是单个连接上的附加会话。

清单 8-26。关闭内部连接,closeConnection ()

    public static final void closeConnection() {         try {             conn.close( OracleConnection.**PROXY_SESSION** );         } catch( Exception x ) {}     }

给开发者的代码模板

清单 8-27 中的代码行是应用开发人员使用OracleJavaSecure类进行 Oracle SSO 所需的所有 Java 代码。

清单 8-27。方法调用应用开发者

    OracleConnection conn = OracleJavaSecure.setConnection( connectionString );     // Do Oracle queries here     OracleJavaSecure.closeConnection();

参考第七章的末尾的说明,创建一个包含OracleJavaSecure的 jar 文件,提供给应用开发人员。

请注意,还需要一个安全的应用角色,由类似于p_check_hrview_access的程序保护,以便完成我们数据的 SSO 保护。

在我们进行代理连接的情况下,我们希望每个 OS 用户身份都有一个 Oracle 用户来访问我们的应用。这些 Oracle 个人用户需要被授予对我们的应用用户的“代理权限”。

章节回顾

在本章中,我们讨论了如何使用 JAAS 类NTSystemUnixSystem来识别 Windows 或 UNIX 用户。因为这些类不是跨平台提供的,所以我们深入研究了如何使用反射来实例化和调用这些类中的方法。

建立操作系统身份后,我们研究了在向 Oracle 数据库进行身份验证时使用该身份所需的代码。最终目标是我们的 Oracle 应用用户无需输入密码就能使用我们的应用。事实上,他们在 Oracle 数据库上根本不需要密码,但是我们将能够通过以下两种方法之一来跟踪每个用户的操作:

1)我们将连接客户端标识符设置为等于用户 ID,然后在授权访问之前确保它存在于连接中。然后,我们可以在审计跟踪日志中找到该客户端 ID。

2)我们为每个 Windows/UNIX 用户创建一个 Oracle 用户,尽管 Oracle 用户除了CONNECT之外不需要密码或任何特权。然后,我们通过我们的应用用户代理这些个人用户。我们可以查询代理用户身份的审计日志。

我们研究了可能建立的几种 Oracle 连接池。我们可以使用三种连接池技术:

1)轻量级瘦客户端连接池

2)重量级 OCI 连接池

3)通用连接池(UCP)

最后,我们在OracleJavaSecure中构建了一些setConnection()方法,使得 Oracle 应用开发人员能够轻松利用这项技术。

图 8-1 展示了基本的单点登录流程。客户端应用使用适当的连接字符串调用OracleJavaSecure.setConnection()方法。该方法进一步调用getOSUserID(),它使用NTSystemUnixSystem从操作系统获取用户 ID(用户名),视情况而定。使用这个操作系统用户 ID,我们通过一个同名的 Oracle 用户为我们的Connection打开一个代理会话。

使用我们的代理会话,我们调用p_check_hrview_access过程,确保我们的 SSO 凭证是正确的,然后设置安全应用角色hrview_role。此时,我们已经完成了对 Oracle 数据库的 SSO 请注意,我们没有为特定的 Oracle 用户输入密码。然后,我们可以使用授予hrview_role的特权从EMPLOYEES表中选择敏感数据。

images

图 8-1。单点登录程序

九、双因素认证

如果没有冒名顶替者,没有江湖骗子,没有小偷,生活会是什么样子?抱歉,这个反问句不能提供任何保障。戴上一副玫瑰色的眼镜,仅仅因为我们实施了实质性的安全措施,就认为我们是安全的,这也是不行的。我们总是容易受到诡计和粗心大意的影响。即使是正直的同事,最薄弱的环节也总是走捷径的人。社会工程学和对我们的计算机安全行为准则(比如,不写下您的密码,不共享您的密码,使用复杂的密码,定期更改您的密码)的缺乏关注,给了窃贼进入我们最安全的系统的入口。

因此,我们正在寻找对身份的进一步限制,以确保坐在键盘前的人是他们声称的那个人。为了实现这一点,计算机安全领域正在做许多事情,例如:

  • 需要第二个密码或 PIN 码。
  • 通过让一个人输入一个非计算机可读的单词图形表示(称为 CAPTCHA ),确保计算机旁边有一个人而不是一个自动程序,CAPTCHA 代表完全自动化的公共图灵测试,以区分计算机和人类(部分以计算机科学和人工智能之父艾伦·图灵命名)。
  • 要求用户回答个人问题,比如给出他们第一只宠物的名字。
  • 拥有生物扫描仪,如指纹、视网膜或面部识别。
  • 具有安全 ID 令牌,该令牌将代码与服务器同步,以提供一次性密码以及 PIN 代码。
  • 到单独帐户或设备的带外通信,例如,发送到您的电子邮件、寻呼机或手机的密码。

这些努力中的一些可以被认为是双因素认证。将它们结合起来,甚至可以实现三因素认证。例如:

1)您知道的(密码和 PIN)

2)你是谁(人类和生物特征)

3)您拥有的东西(安全 ID 令牌或手机)

也许第二个密码或额外的 PIN 也可以被认为是双因素身份验证,但不是那么重要。这仍然只是你所知道的。

我们将实施双因素身份验证,使用第八章中的单点登录和一个代码,我们会将该代码发送到一个单独的帐户,最好是在单独的设备上。我们将向传呼机、手机发送密码,如果前两者都不可用,作为最后的手段,我们还会向电子邮件帐户发送密码。

让 Oracle 数据库发送电子邮件

我们的双因素身份验证将主要联系寻呼机和手机,而不会将密码发送到用户的电子邮件帐户。但是,我们将有电子邮件选项。此外,向商用手机发送消息的最简单方法是使用手机提供商的短消息服务(SMS ),即经常接受发往手机的电子邮件消息的短信主机。因此,我们将从 Oracle 数据库实现电子邮件。

Oracle 数据库提供了一个名为UTL_MAIL的包,使我们能够从数据库发送电子邮件。我们将落实这一点;不过,我们也可以加载一个 Java 类来发送电子邮件,并将其配置为从 Java 存储过程执行。也许我们会使用 JavaMail API,或者我们可能会打开一个普通的 Java Socket并向其中写入简单邮件传输协议(SMTP)命令(这样我们就不必将 JavaMail mail.jar 文件加载到 Oracle 数据库中)。在这一章的后面,我们将调用一个 Java 存储过程来读取一个网页,发送电子邮件也可以类似地完成。

安装 UTL 邮件

默认情况下,Oracle 数据库中不会安装UTL_MAIL包。我们必须手动安装它。其实我们会让SYS用户帮我们安装。该包驻留在两个文件中,这两个文件位于一个类似于以下路径的服务器目录中:Oracle \ product \ 11 . 2 . 0 \ dbhome _ 1 \ RDBMS \ ADMIN。这些文件名为 utlmail.sqlprvtmail.plb

一个。plb 文件是一个包装的 PL/SQL 文件。包装的文件可以被认为是混淆的(不容易阅读),但不是加密的。包装格式给逆向工程带来了严重的困难,但并没有阻止它;尽管如此,你需要成为一名黑客来获得资源来打开文件。要创建包装的过程、函数、包或类型,您需要传递一个。sql 文件,包含 wrap 实用程序的那些结构的CREATE语句(随 Oracle 数据库软件提供)。这就产生了一个不可逆的。plb 文件。请确保您保存了原始文件的存档。sql 文件放在一个安全的位置,以防您需要编辑它。包装的包文件用于隐藏 PL/SQL 代码,可能是为了保护知识产权或增加安全性。

作为SYS,你或者你的 DBA 朋友需要依次打开这些文件中的每一个: utlmail.sql 然后prvtmail . plb;并在数据库中执行它们。这可以通过 TOAD 完成,例如,将文件加载到 SQL 编辑器中,并作为脚本执行。从 SQL*Plus 中,SYS可以运行:

`@C:\app\oracle\product\11.2.0\dbhome_1\RDBMS\ADMIN\utlmail.sql

@C:\app\oracle\product\11.2.0\dbhome_1\RDBMS\ADMIN\prvtmail.plb`

images 注意你可以在名为 的文件中找到这些命令第九章 /Sys.sql.

授予访问 UTL _ 邮件的权限

我们只允许一个 Oracle 用户访问UTL_MAIL包:用户appsec。我们将在app_sec_pkg包中包含代码,以使用UTL_MAIL发送双因素密码。首先,让我们的安全管理员,secadm用户在数据库访问控制列表(ACL)中创建条目。让SYS进行清单 9-1 中所示的授权,包括对UTL_MAIL包到appsec_role的授权执行。

清单 9-1。授权访问 UTL 邮件,作为系统用户

GRANT EXECUTE ON sys.dbms_network_acl_admin TO secadm_role; GRANT EXECUTE ON sys.utl_mail TO appsec_role;

然后作为secadm用户,执行清单 9-2 中的命令来建立访问控制列表(ACL)条目,这将允许appsec打开端口 25 并发送电子邮件。确保编辑清单中的第二个命令,将您公司的 SMTP 服务器的名称插入到“host”字段中。

清单 9-2。 ACL 条目发送电子邮件,作为 Secadm 用户

`BEGIN
  DBMS_NETWORK_ACL_ADMIN.CREATE_ACL (

acl          => 'smtp_acl_file.xml',
    description  => 'Using SMTP server',
    principal    => 'APPSEC',
    is_grant     => TRUE,
    privilege    => 'connect',
    start_date   => SYSTIMESTAMP,
    end_date     => NULL);

COMMIT;

END;
/

BEGIN
  DBMS_NETWORK_ACL_ADMIN.ASSIGN_ACL (

acl         => 'smtp_acl_file.xml',
    host        => 'smtp.org.com',
    lower_port  => 25,
    upper_port  => NULL);
  COMMIT;

END;
/`

images 注意你可以在名为chapter 9/sec ADM . SQL .的文件中找到清单 9-2 中命令的脚本

你会注意到这种语法与我们在调用过程时所习惯的完全不同。毕竟,这些只是对默认安装在 Oracle 数据库中的DBMS_NETWORK_ACL_ADMIN包中的CREATE_ACLASSIGN_ACL过程的调用。在清单 9-2 中,我们使用命名符号指定这些程序的参数,与我们通常的位置符号相反。(我在这里给出了命名符号,因为我看到的每个例子都使用了那个符号;我们可能都只是复制 Oracle 文档中给出的例子)。

在命名符号中,分配给每个参数的值在定义的参数名称和赋值运算符=>后给出,例如acl => 'smtp_acl_file.xml'。当我们使用位置符号调用一个过程时,我们按照参数在过程中定义的顺序列出参数(因此得名位置);但是在命名符号中,顺序并不重要。

位置表示法和命名表示法之间的另一个区别是如何处理可选参数。您可能还记得,我们的错误记录表t_appsec_errors,有两个可选参数msg_txtupdate_ts。它们是可选的,因为我们用默认值配置了它们。update_ts的默认值为SYSDATE。在我们的p_log_error过程中,我们从不为update_ts提供值。我们需要默认的系统日期。

回到当前的主题:在位置表示法中,只有在所有必需参数之后定义的可选参数可以被忽略。使用命名符号,我们可以跳过提供可选参数,不管它们出现在参数定义中的什么位置,只要我们通过名称为所有必需的参数提供值。

在清单 9-2 中调用的两个过程、CREATE_ACLASSIGN_ACL中,可选参数是为每个过程定义的最后两个,因此没有令人信服的理由使用命名符号调用这些过程。此外,我列出的两个调用为每个参数提供了一个值(或 NULL ),包括可选参数。

再次以SYS用户或DBA的身份,您可以列出 ACL 以确保您的条目已经完成。执行这些查询:

SELECT * FROM sys.dba_network_acls; SELECT * FROM sys.dba_network_acl_privileges;

第一个命令的结果将如下所示:

"HOST"         "LOWER_PORT" "UPPER_PORT" "ACL"                         "ACLID" "smtp.org.com" "25"         "25"         "/sys/acls/smtp_acl_file.xml" "004B...

第二个命令的结果将如下所示:

"ACL"                         "ACLID"  "PRINCIPAL" "PRIVILEGE" "IS_GRANT" ... "/sys/acls/smtp_acl_file.xml" "004B... "APPSEC"    "connect"   "true"     ...

测试发送电子邮件

当我们在这里时,我们应该执行一个测试,以确保我们可以发送电子邮件。我们将作为我们的应用安全用户来执行测试。我很抱歉在这些账户之间跳来跳去,但我们正在委派任务,并在每一步检查我们的工作。

images 注意你可以在名为chapter 9/app sec . SQL的文件中找到本节要遵循的命令的脚本。

我们已经为appsec配置了网络 ACL,以便能够在您的 SMTP 邮件主机上打开端口 25,但是对于每个会话,我们还需要告诉UTL_MAIL包使用该服务器作为我们的 SMTP 主机。作为appsec,我们向 Oracle 会话添加了一个属性,UTL_MAIL包将读取该属性:

ALTER SESSION SET SMTP_OUT_SERVER = 'smtp.org.com';

然后我们发送一封电子邮件。我们提供的参数是我们的电子邮件地址、收件人地址、消息标题和消息文本。

CALL UTL_MAIL.SEND( 'myname@org.com', 'myname@org.com', '', '',     'Response','2FactorCode' );

让 Oracle 数据库浏览网页

除了通过电子邮件/SMS 向手机(可能还有电子邮件帐户)发送双因素身份验证密码之外,我们还将向寻呼机发送密码。在我们公司,我们通过网页界面向公司寻呼机发送文本消息。这可能是也可能不是您向寻呼机分发双因素身份验证密码时需要采用的方法;但是,它与任何消息分发都相关,因为电子邮件和 web 服务是用户应用中文本消息分发的主要模式。

将 Java 策略委托给安全管理员

我们已经看到了如何添加 ACL 来允许用户打开端口。现在,为了将端口作为 Java 存储过程打开,我们需要授予 Java 安全权限。实际上,我们允许 Java 执行通常被 Oracle JVM 安全沙箱拒绝的活动。

首先,我们将让SYSDBA将管理特定 Java 沙箱特权的策略许可委托给我们的安全管理员secadm用户。作为SYS,用清单 9-3 中的代码做这件事。

清单 9-3。授予安全管理员授予套接字权限的策略

`CALL DBMS_JAVA.GRANT_POLICY_PERMISSION(
    'SECADM_ROLE', 'SYS',
    'java.net.SocketPermission',

'*');

COMMIT;`

DBMS_JAVA.GRANT_POLICY_PERMISSION命令将secadm_role指定为许可的接收者。SYS是授权有效的模式。被授予的许可类型是SocketPermission。并且secadm_role由此可以管理任何插座(*)。

使用以下命令确保 Java 策略权限已被授予。这些策略被授予一个被授予者编号GRANTEE#。我们在用户编号USER#与被授权者编号匹配的USER$表中查找该用户的名称。

`SELECT u.user#, u.name, p.name, p.type_name, p.action
FROM sys.user$ u, sys.javapolicy p
WHERE p.name LIKE '%java.net.SocketPermission%'

AND p.grantee# = u.user#;`

这个查询的结果将类似如下。JAVA_ADMIN拥有对SocketPermission的安装授权。

`"USER#" "NAME"        "NAME_1"                        "TYPE_NAME"
"40"    "JAVA_ADMIN"  "0:java.net.SocketPermission#*" "oracle.aurora.rdbms...

"93"    "SECADM_ROLE" "0:java.net.SocketPermission#*" "oracle.aurora.rdbms...`

我们将管理关于打开套接字(网络端口)的策略的有限权限委托给了secadm_role。我们可能会授予其他策略,比如关于在 Oracle 服务器文件系统上打开文件的策略,但是我们在这里不需要它。

允许应用安全用户阅读网页

现在,作为secadm用户,让我们为我们的应用安全性授予许可,即appsec用户,实际打开一个到 web 服务器的端口,该服务器向我们公司的寻呼机发送文本消息。根据需要更改 web 服务器的名称和端口号,然后以secadm用户的身份执行清单 9-4 中的代码。

清单 9-4。授予应用安全用户套接字权限

`CALL DBMS_JAVA.GRANT_PERMISSION(
    'APPSEC',
    'java.net.SocketPermission',

'www.org.com:80',
    'connect, resolve'
);`

通过这个 Java 许可授权,我们极大地限制了实际可以做的事情。

  • 我们将只允许在特定端口连接到特定服务器,例如【www.org.com:80】
  • 我们将只允许一个用户打开连接,appsec.
  • 我们将只允许“连接”和“解析”操作,这些操作足以读取一个网页(并通过GET方法在 URL 中提交数据)。我们需要resolve操作,以便我们(在 Oracle 数据库中)可以对(例如)[www.org.com](http://www.org.com)进行 DNS 查找/名称解析,找到 IP 地址。我们需要connect动作,这样我们就可以在网络端口上建立连接。这些是 Oracle JVM 安全沙箱默认不允许的操作。

GRANT_PERMISSION的调用可能会抛出“未捕获的 Java 异常”错误。我们无法解决这个问题,没什么好担心的。也许 Oracle 不希望我们从 SQL 命令行调用DBMS_JAVA包中的过程。

测试我们在 Oracle 数据库上从 Java 读取 web 页面的能力需要我们配置一个 Java 存储过程并更新我们的 Java 代码。在我们写完代码之后,让我们等待并测试功能完整的代码。

双因素认证流程

我们将让 Oracle 应用尝试读取我们保护的敏感数据。我们将要求这些应用首先进行数据库身份验证;他们需要通过我们的单点登录测试,这在第八章中有描述。然后,他们需要请求并接收我们将从 Oracle 数据库发送给他们的双因素密码。我们将在本章中实现请求和接收双因素密码的过程。

一旦用户收到双因素密码,他们将提交该密码以及数据请求(并进行我们在第七章中讨论过的加密密钥交换)。此时,如果用户并拢脚跟说“没有什么地方比得上家”,他将回到堪萨斯州,按照授权读取和更新数据。

这里有一个问题:我们不能假设这种双因素代码交换是即时的;否则,我们只需要双因素代码交换和数据请求发生在同一个会话中。相反,我们需要在 Oracle 数据库中缓存双因素代码一段时间,以确保用户和代码的一致性。关于用户的某些其他事实也需要关联,比如发出请求的计算机的地址。

你能想象我们需要什么来实现这一切吗?首先,我们需要一些关于我们的应用用户的数据。我们需要他们公司的呼机号码、手机号码和电子邮件地址。电子邮件地址已经存储在HR.EMPLOYEES表中。我们将在HR模式中构建另一个表来保存手机和寻呼机号码。

除了手机号码,我们还需要指定一个运营商。每个运营商(如美国电话电报公司)都有一个不同的地址,我们将向该地址发送手机消息。对于美国电话电报公司手机,我们将向10-digit-phone-number@txt.att.net发送短信。有短信聚合器将短信发送到由许多运营商处理的手机上。如果你已经付费使用了一个聚合器,那么你可以用它作为所有手机的运营商。

双因素分销渠道的安全考虑

双因素身份验证消息可以通过各种设备交付给我们的应用用户。我们将考虑通过手机、寻呼机和电子邮件传递信息。对于每一种设备,我们都必须考虑其安全隐患。我们倾向于将双因素代码发送到手机或寻呼机。只有当这些失败时,我们才会将代码发送到一个电子邮件地址。

电子邮件双因素传递的安全问题

电子邮件本身是一个相当安全的应用。它有密码保护,通常管理良好。但是,数据在传输过程中通常不受加密保护,而且冒充电子邮件发件人并以他们的身份发送邮件非常容易。只要你愿意过滤掉垃圾邮件,并根据需要通过其他方式验证发件人,使用电子邮件交换非敏感数据就不会有什么固有的问题。

images 注意我喜欢电子邮件,因为它让我们以一种非并发的、非同处一地的、人对人的或广播的方式进行交流。它还可以作为对话的战术记录和通信档案。

然而,双因素身份认证的整体理念是,我们要求用户拥有两种不同且独立的身份特征。是的,电子邮件和我们的 Oracle 应用是不同的代码片段,但它们可能运行在同一台计算机上,因此它们可能不是独立的。

如果黑客闯入我的电脑,我的电子邮件正在运行,或者她也闯入我电脑上的电子邮件,然后如果我向电子邮件发送双因子代码,她也可以作为我运行 Oracle 应用;即使 Oracle 应用应该受到双因素身份认证的保护。

寻呼机双因素交付的安全问题

我不能谈论所有的寻呼机,因为有些可能操作不同,更像手机;然而,我所熟悉的寻呼机是相当简单的设备,就像调幅收音机一样。双向寻呼机通常使用寻呼广播信号来传递寻呼,但响应是通过蜂窝电话信号返回的。不要假设寻呼消息是单独发送到您的特定寻呼机的。这不是这个系统的运作方式。

寻呼消息被传送到无线电塔并广播,有时是在许多消息的数据块中。每条消息都有一个代码前缀。您的传呼机和系统中的所有传呼机,收听从无线电天线广播的所有内容。传呼机编程有一个代码或一系列要监听的代码。如果寻呼机“看到”它识别的代码,它会在屏幕上显示相关的消息。如果你的传呼机关闭了,或者无线电波的物理特性不正确(例如,你在一个布满管道的地下室里),那么你就错过了广播,你的传呼机以后就没有办法检索信息了。

现在,想象一个屋檐下的落水者坐在附近的酒店房间里,带着扫描仪和打印机。他正在收听寻呼机天线发射的无线电频率,并且他正在打印出要发送到目标代码的消息,或者他正在打印出(或保存到文件中)所有消息。再想象一下,黑客带着他自己的无线电和天线,在拐角处,向目标寻呼机代码发送他自己的迂回寻呼消息。

现在您已经知道了安全问题,我要赶紧说,寻呼机是向正在旅行或远离计算机和电话的人发送即时、简单、非敏感消息的好方法。寻呼也是同时向许多人(通过寻呼组)广播消息的好方法。只是不要发送任何你不想让世界看到的东西。如果你对信息的真实性有所怀疑,请向发件人核实。此外,如果你是一个发送者,不要假设收件人收到了网页。

寻呼机可能是一项很快就会消失的技术,因为手机短信已经变得如此流行。寻呼机的优点是价格便宜,服务计划也不贵。他们还被允许进入一些不允许使用手机的安全设施,因此可能需要寻呼机来联系那些设施中没有固定电话或计算机的人。

手机双因素交付的安全问题

与寻呼机不同,手机信息是从特定的天线传送到特定的手机。当你从一个天线(手机信号发射塔)覆盖的区域到达另一个天线覆盖的区域时,你的通信被切换到另一个手机。

一般来说,这种通信是相当安全的,通过加密。读取和发送数据的密钥储存在手机的用户识别模块(SIM)卡中。我们都看过描述 SIM 卡如何被克隆的电视节目和电影,一部假冒的手机可以在打给原手机的电话中掉线。我认为这在现实生活中并不多见,但要意识到这种可能性。

有一次,我儿子弄坏了他手机的键盘,换了一个同样号码的手机。我想他们在新手机里克隆了他的 SIM 卡。有一段时间,两部手机都收到了发给他的所有短信,所以我可以向你保证克隆(手机)是可行的。

我喜欢提醒我的孩子们,他们发送的短信,即使只发给另外一部手机,也会进入一个系统,许多他们甚至不认识的人都可以阅读这个系统:服务提供商的技术人员和可能要求访问的执法人员。这还没提到父母、兄弟姐妹、朋友和其他可能在某个时候浏览手机的窥探者或小偷。

我们需要记住,任何被制成信号的数据(包括声音、照片和视频)都应该被认为是可以公开获取的。这包括普通老式电话系统(POTS)、手机、互联网等发送的信号。这包括电、无线电波、光和声音传输。

首选双因素分娩

手机是传递双重密码的最安全的途径。然而,密码并不敏感,所以几乎任何递送路线都可以。如果代码被窃听者读取,要使用该代码,窃听者必须坐在启动 Oracle 应用的计算机前,以启动用户的身份登录,并且在代码缓存超时期限内(10 分钟)。如果有人发送欺诈性代码,除了让试图使用该代码进行双因素身份验证的用户感到沮丧之外,不会产生其他影响。

我们唯一的偏好是,如果手机或寻呼机交付可用,我们不想发送代码到电子邮件。因为电子邮件可能与 Oracle 应用运行在同一台计算机上,所以将代码发送到电子邮件不符合我们的双因素身份验证目标,即独立和不同的身份特征。具体来说,我们所依赖的两个因素是您所知道的(SSO 初始登录密码)和您所拥有的(单独的手机或寻呼机)。

支持双因素认证的 Oracle 结构

为了完成双因素身份验证,我们将创建一些新的表,并将它们合并到我们的安全流程中。我们将创建一个表来保存用户的手机和寻呼机号码。此外,我们将创建一个表来保存我们需要支持其电话的每个手机运营商的 SMS 网关地址。在我们 10 分钟的缓存超时期间,当我们等待双因素代码被发送到用户的手机、寻呼机或电子邮件时,我们将把双因素代码存储在 Oracle 数据库中我们将创建的另一个新表中。

除了新表之外,我们还将创建函数来发送和测试双因子代码。我们还将修改p_check_hrview_access过程,以便在设置安全应用角色hrview_role之前接受并测试双因素代码。

创建短信运营商主机表

大多数(如果不是全部的话)移动电话服务提供商(运营商)提供通过电子邮件服务器(SMTP)向他们的电话发送文本的接入。)例如,如果您有美国电话电报公司提供的个人手机,您可以向您在美国电话电报公司 SMTP 服务器上的电话号码发送电子邮件(例如 8005551212@txt.att.net),他们会将您的文本消息作为 SMS 消息发送到该手机。

每个运营商都有自己的 SMTP 到 SMS 网关和地址;例如,txt.att.net 在美国电话电报公司。我们需要一个表来存储这些地址,这样我们就可以将双因素认证码发送到各种运营商提供的手机上。我们将创建一个表sms_carrier_host,来保存这些地址。参见清单 9-5 。我们还基于被指定为主键的sms_carrier_cd创建了一个惟一的索引。并且我们创建了一个视图(v_sms_carrier_host)的表格。在人力资源模式中创建此表。

images 注意你可以在名为 * Chapter9 /HR.sql* 的文件中找到清单 9-5 中的命令。

清单 9-5。创建短信运营商主机表

`CREATE TABLE hr.sms_carrier_host
(
    sms_carrier_cd  VARCHAR2(32 BYTE) NOT NULL,

sms_carrier_url VARCHAR2(256 BYTE)

);

CREATE UNIQUE INDEX sms_carrier_host_cd_pk ON hr.sms_carrier_host

(sms_carrier_cd);

ALTER TABLE hr.sms_carrier_host ADD (

CONSTRAINT sms_carrier_host_cd_pk

PRIMARY KEY
    (sms_carrier_cd)

USING INDEX sms_carrier_host_cd_pk

);

CREATE OR REPLACE VIEW hr.v_sms_carrier_host AS SELECT * FROM hr.sms_carrier_host;`

创建该表后,您需要用主要承运人的地址以及受您的员工欢迎的任何其他承运人的地址预先填充该表。要知道,一些较小的手机提供商会利用主要运营商的系统,尤其是那些严格按照预付费手机提供服务的提供商的手机。

您可以在互联网上找到提供商的短信网关的完整列表。一个相当全面的列表可以在[en.wikipedia.org/wiki/List_of_SMS_gateways](http://en.wikipedia.org/wiki/List_of_SMS_gateways)找到。我不想推广任何特定的运营商,但在列表 9-6 中提供了这些示例地址(URL ),您可能想插入供您使用。

清单 9-6。插入短信运营商主机条目样本

`INSERT INTO hr.sms_carrier_host
    ( sms_carrier_cd, sms_carrier_url ) VALUES

( 'Alltel', 'message.alltel.com' );

INSERT INTO hr.sms_carrier_host

( sms_carrier_cd, sms_carrier_url ) VALUES

( 'AT_T', 'txt.att.net' ); INSERT INTO hr.sms_carrier_host

( sms_carrier_cd, sms_carrier_url ) VALUES

( 'Sprint', 'messaging.sprintpcs.com' );

INSERT INTO hr.sms_carrier_host

( sms_carrier_cd, sms_carrier_url ) VALUES

( 'Verizon', 'vtext.com' );`

创建员工手机号码表

我们在HR模式中已经有了一个包含员工电子邮件地址的表:EMPLOYEES表。然而,我们还需要存储员工的寻呼机号码和手机号码,所以我们将创建一个表来保存这些数据值,即emp_mobile_nos表。我们还需要一个数据值来将所有这些联系在一起:当用户登录时,我们通过 SSO 过程识别她,我们需要一种方法将HR EMPLOYEESemp_mobile_nos数据与用户相关联。我们将在emp_mobile_nos表中添加一个名为user_id的列来建立关联。创建命令见列表 9-7 。

HR.EMPLOYEES表包含一个关于EMPLOYEE_ID的主键索引,它是一个独立的数值:每个被雇佣的雇员都被依次分配下一个数值。无论该值是多少,我们都将使用为同一员工分配手机号码。我们进一步将数字EMPLOYEE_ID与 SSO 登录用户user_id列关联起来。

清单 9-7。创建员工手机号码表

`-- Adjust length of Pager_No and Phone_No as needed
CREATE TABLE hr.emp_mobile_nos

(
    employee_id     NUMBER (6) NOT NULL,

user_id         VARCHAR2(20 BYTE) NOT NULL,

com_pager_no    VARCHAR2(32 BYTE),

sms_phone_no    VARCHAR2(32 BYTE),

sms_carrier_cd  VARCHAR2(32 BYTE)

);`

在这一点上,我会指出一个你需要考虑的重要事实。随着我们在安全结构和程序方面的进步,我们变得依赖于我们以前建立的安全性。我们的双因素认证依赖于 SSO 我们需要识别请求双因素身份认证的用户,以便将双因素代码发送到正确的设备。

另外,我们需要知道什么EMPLOYEE_ID与一个user_id相关联。只有在emp_mobile_nos 或替代表中有条目的员工才能通过双因素认证。考虑我们可能已经将这些列直接添加到了HR.EMPLOYEES表中。就数据标准化标准而言,这样做是正确的;除非可以证明并非所有员工都需要访问我们的应用。

将计算机的user_id字段放在手机号码表中,而不是放在主EMPLOYEES表中,这似乎有点落后,但是我们正在将这一功能添加到现有的HR结构中,并且我们决定将这一切都放在一个地方,即emp_mobile_nos表中。

通过唯一索引的方式对数据实施完整性约束是一个好主意。我们用清单 9-8 中的命令来做这件事。我们只允许每个employee_id在此表中有一条记录。我们也只允许一个user_id与一个记录相关联,因此与一个employee_id相关联。注意在表定义中employee_iduser_id都不允许为空。

清单 9-8。为员工手机号码表创建索引

`CREATE UNIQUE INDEX emp_mob_nos_emp_id_pk ON hr.emp_mobile_nos
    (employee_id);

CREATE UNIQUE INDEX emp_mob_nos_usr_id_ui ON hr.emp_mobile_nos

(user_id);

ALTER TABLE hr.emp_mobile_nos ADD (

CONSTRAINT emp_mob_nos_emp_id_pk
**    PRIMARY KEY**

(employee_id)
    USING INDEX emp_mob_nos_emp_id_pk,
    CONSTRAINT emp_mob_nos_usr_id_ui
    UNIQUE (user_id)

USING INDEX emp_mob_nos_usr_id_ui
);

ALTER TABLE hr.emp_mobile_nos ADD (

CONSTRAINT employee_id_fk

**  FOREIGN KEY (employee_id)**

**  REFERENCES employees (employee_id)**,
  CONSTRAINT sms_carrier_cd_fk

**  FOREIGN KEY** (sms_carrier_cd)

REFERENCES sms_carrier_host (sms_carrier_cd));`

employee_id上的惟一索引既是这个表上的主键,也是外键。参考列表 9-8 中的命令。通过外键关系,我们建立了一个约束条件,即为了在emp_mobile_nos表中为某个employee_id创建一个记录,在EMPLOYEES表中必须已经存在一个带有那个EMPLOYEE_ID的记录。

类似地,我们在表上有一个外键约束,sms_carrier_cd_fk。(同样,参见清单 9-8 )该约束限制我们只在sms_carrier_host表中已经存在sms_carrier_cds的情况下才在emp_mobile_nos表中写入(插入或更新)sms_carrier_cds

您需要注意外键在两个表上都是绑定的。一旦我们在emp_mobile_nos表中插入了带有特定employee_id的记录,我们就不能从EMPLOYEES表中删除那个EMPLOYEE_ID的记录,直到我们已经从emp_mobile_nos中删除了相关的记录。我们对规则进行了编码,我们不能在emp_mobile_nos中为employee_id创建记录,除非在EMPLOYEES中存在相关记录;如果我们从EMPLOYEES中删除相关记录,我们将不再满足该要求。Oracle 数据库不允许我们这样做。

我们总是希望有一个表的视图,它是我们对表的主要引用,所以我们用清单 9-9 中的代码创建了一个。如果我们正在授予和使用一个视图,那么我们可以在维护视图的同时改变它所引用的表(或多个表),并且避免破坏任何代码。

清单 9-9。创建员工手机号码表视图

`CREATE OR REPLACE VIEW v_emp_mobile_nos AS SELECT * FROM hr.emp_mobile_nos;

INSERT INTO hr.v_emp_mobile_nos

( employee_id, user_id, com_pager_no, sms_phone_no, sms_carrier_cd )

VALUES ( 300, 'OSUSER', '12345', '8005551212', 'Verizon' );

COMMIT;`

将我们的示例用户 ID OS usr 的手机号码插入表中。请注意,employee_id 300 是我们在更新序列并在第七章的中插入我们的示例用户时强制使用的。既然我们在这里,让我们也为你插入一个记录。使用刚才给出的 insert 语句,替换您用来登录(可能是登录到 Windows)的user_id、您的寻呼机号码、手机号码和运营商代码(您将对来自同一提供商的所有手机使用的任何名称)。您还需要在HR.EMPLOYEES表中插入一条记录(名、姓和电子邮件地址。)使用清单 9-10 中的命令作为模板。

清单 9-10。创建员工并添加其手机号码的模板命令

`INSERT INTO hr.employees
    (employee_id, first_name, last_name, email, phone_number, hire_date,

job_id, salary, commission_pct, manager_id, department_id)
VALUES
    (hr.employees_seq.NEXTVAL, 'First', 'Last', 'EMAddress',
    '800.555.1212', SYSDATE, 'SA_REP', 5000, 0.20, 147, 80);

INSERT INTO hr.v_emp_mobile_nos

( employee_id, user_id, com_pager_no, sms_phone_no, sms_carrier_cd )

VALUES ( (
        select employee_id from hr.employees where

first_name = 'First' and last_name = 'Last'
    ), 'UserID', '12345', '8005551212', 'Verizon' );

COMMIT;`

请务必COMMIT您所做的插入和更新,以便使它们对其他会话和其他用户可见。

从应用安全程序访问人力资源表

我们将继续让我们的应用安全用户appsec运行所有的安全程序。为了完成我们的双因素认证,她需要从HR读取EMPLOYEES表,以及我们刚刚创建的表。我们授予她访问权限,如清单 9-11 中的所示。

清单 9-11。授予应用安全用户查看我们视图的权限

`GRANT SELECT ON hr.v_employees_public TO appsec;
GRANT SELECT ON hr.v_sms_carrier_host TO appsec;

GRANT SELECT ON hr.v_emp_mobile_nos TO appsec;`

创建双因子代码缓存表

现在我们已经在HR中定义了保存地址和号码的表,我们要将双因素认证码发送到这些地址和号码,我们需要考虑如何在发送代码和让用户在我们的应用中输入代码之间进行过渡。双因素认证码可能需要几分钟才能通过互联网和手机系统到达用户的手机。我们不希望在整个带外通信期间保持与 Oracle 数据库的连接开放,因此我们需要考虑如何存储双因素代码以供以后比较,以及如何确保我们向其发出和发送双因素代码的同一用户正在输入它。

通过我们的 SSO 流程以及登录用户(user_id)和employee_id(在emp_mobile_nos表中)之间的映射,我们可以为登录的特定用户保存一个双因素身份验证代码。在这一点上,我们将允许每个用户有一个双因素代码(尽管我们将在下一章中修改它),所以我们通过employee_id进行索引。

我们创建一个表来暂时缓存(保留)双因素身份验证代码。这显示在清单 9-12 中。我们将在appsec模式中创建它,因此如果还没有连接到 Oracle 数据库,请以appsec的身份连接到 Oracle 数据库,并执行SET ROLE appsec_role来获得应用安全角色,以便完成这一步。

清单 9-12。创建一个表来缓存双因素认证码

`CREATE TABLE appsec.t_two_fact_cd_cache
(
    employee_id   NUMBER(6) NOT NULL,

two_factor_cd VARCHAR2(24 BYTE),

ip_address    VARCHAR2(45 BYTE) DEFAULT SYS_CONTEXT( 'USERENV', 'IP_ADDRESS' ),

distrib_cd    NUMBER(2),
    cache_ts      DATE DEFAULT SYSDATE
);`

现在这纯粹是猜测和创造性的想象,但我相信以下格式的代码将足够复杂的安全和易于输入。一组 12 个随机数字字符,每组 44 个,用破折号分隔,例如 1234-5678-9012。也许您不同意,并且认为另一种格式更好,这没关系——您有 PL/SQL 代码,并且可以根据需要更改它。请注意,这个表是用 24 个字符的最大长度two_factor_cd创建的。

还要注意,我们将ip_address的列大小调整为 45 个字符。我可能有点过度指定了,但是 IPv4 地址限制为 15 个字符,而 IPv6 地址最多可以有 39 个字符。此外,映射到 IPv6 的 IPv4 地址最多可由 45 个字符表示。

我们在t_two_factor_cd_cache表中还有一个字段用于时间戳cache_ts。它被默认设置为SYSDATE,但这只在我们插入时有效。我们更新的时候会“手动”设置为SYSDATE。为什么我们需要缓存时间戳?这里有一些更有创造性的想象:你会在代码中发现,我们认为双因素验证码在 10 分钟内是有效的。在该时间段内,我们不会向同一用户发送另一个双因素身份验证代码,10 分钟后,我们发送的代码不再有效。

这个表中我们需要研究的最后一列distrib_cd,是一个数值,它表示双因子代码是如何分布的。表 9-1 显示了潜在值。

images

我不会在这里展示它,但是在这个表上,employee_id既是唯一索引又是主键。我们还创建了视图,v_two_fact_cd_cache。创建索引的代码可以在文件chapter 9/appsec . SQL中找到。

测试缓存老化

让我们插入一条记录,并使用我们将要使用的老化算法。

`INSERT INTO appsec.v_two_fact_cd_cache
( employee_id ,two_factor_cd )

VALUES
(300,'FAKE');`

现在,选择记录以查看时间戳的设置。

SELECT * FROM appsec.v_two_fact_cd_cache;

执行以下命令几次。它将显示自时间戳设置以来经过的时间,并且应该在您运行它时计数。

SELECT (SYSDATE-cache_ts)*24*60 FROM appsec.v_two_fact_cd_cache WHERE employee_id=300;

这个命令使用了我们以前见过的日期算法。我们从SYSDATE中减去cache_ts。通常你从SYSDATE中加减所看到的是一些天数。幕后总是有更多的精度,用天数的分数来表示。在 10 分钟的时间跨度内,一天只有一小部分时间。我们把它乘以 24 得到一小时的分数。我们把它乘以 60 得到分钟数。

验证当前缓存的双因素密码

特定用户(由 SSO 确定)将向该过程提交双因素认证码。我们需要问关于代码的什么问题来确定它是否可接受?首先,我们询问用户是否有现有的代码。其次,我们询问现有代码是否是从当前用户正在使用的同一个地址请求的。第三,我们询问现有代码是否不到 10 分钟。我们在函数f_is_cur_cached_cd中的SELECT查询中询问所有这些问题,如清单 9-13 所示。如果满足这些要求的代码存在,它将在cached_two_factor_cd变量中返回。

清单 9-13。对照缓存版本测试用户输入的双因素代码,f_is_cur_cached_cd

`CREATE OR REPLACE FUNCTION appsec.f_is_cur_cached_cd( just_os_user VARCHAR2,
    two_factor_cd t_two_fact_cd_cache.two_factor_cd%TYPE )

RETURN VARCHAR2

AS
    cache_timeout_mins NUMBER := 10;
    return_char VARCHAR2(1) := 'N';

cached_two_factor_cd v_two_fact_cd_cache.two_factor_cd%TYPE;

BEGIN
    SELECT c.two_factor_cd INTO cached_two_factor_cd

FROM v_two_fact_cd_cache c, hr.v_emp_mobile_nos m

WHERE m.employee_id = c.employee_id

AND m.user_id = just_os_user

AND c.ip_address = SYS_CONTEXT( 'USERENV', 'IP_ADDRESS' )

AND ( SYSDATE - c.cache_ts )2460 < cache_timeout_mins;

**    IF cached_two_factor_cd = two_factor_cd**
**    THEN**

return_char := 'Y';
    END IF;
    RETURN return_char;
END f_is_cur_cached_cd;

/`

我们做的最后一件事是将我们找到的cached_two_factor_cd与用户交给我们的two_factor_cd进行比较。如果它们相等,我们返回一个“Y”(相当于一个boolean true,但是更容易。)否则,我们返回默认的“N”。

请注意,如果 10 分钟不适合您的应用,这是需要设置不同缓存持续时间的两个地方之一。另一个地方是在OracleJavaSecure.distribute2Factor()方法中。

与本章中定义的其他过程和函数一样,我们不将它们放在 Oracle 包中。由于对这些结构的不同授权(none 或PUBLIC),我们需要不同的包,每个包只有一个或两个过程或函数。在下一章中,我们将添加许多类似的过程和函数,届时我们将把它们组织成包。

发送双因素密码

如果我们已经确定需要向用户发送双因素代码,我们将调用 Oracle 数据库上的函数f_send_2_factor。它是一个 Java 存储过程,调用 Oracle 数据库上的 Java 代码来完成我们在 PL/SQL 中无法(轻松)完成的任务。

在这种情况下,我们调用distribute2Factor()方法(参见清单 9-14 ),该方法生成双因子代码,试图将其发送到寻呼机和手机,并将代码存储在缓存表中。

清单 9-14。发送双因子码给用户,f_send_2_factor

`CREATE OR REPLACE FUNCTION appsec.f_send_2_factor( just_os_user VARCHAR2 )
RETURN VARCHAR2

AS LANGUAGE JAVA

NAME 'orajavsec.OracleJavaSecure.distribute2Factor( java.lang.String ) return
java.lang.String';

/`

更新安全应用角色,HRVIEW_ROLE 程序

回想一下我们的安全应用角色,hrview_role。没有它,我们就无法读取我们在HR模式中认为敏感的数据。我们最初在第二章的中创建了过程p_check_hrview_access,以提供对安全应用角色的访问。当时我们只是查了 IP 地址和时间。

在上一章中,我们更新了进行单点登录的过程。如果用户连接通过了我们的 SSO 要求,我们设置角色;否则,不会。

在本章中,我们将添加另一项测试—用户是否通过了有效的双因素身份认证代码?参见清单 9-15 。为了适应这种测试,我们必须修改过程以接受一个参数:用户输入的双因素代码。我们还返回错误代码,就像我们在其他过程中所做的那样。在这种情况下,我们将返回我们的分配代码(记住 1 如果通过寻呼机?如果没有实际错误(否err_no),则在err_txt字段中。

清单 9-15。安全应用角色过程头,p_check_hrview_access

`CREATE OR REPLACE PROCEDURE appsec.p_check_hrview_access(
    two_factor_cd t_two_fact_cd_cache.two_factor_cd%TYPE,

m_err_no  OUT NUMBER,
    m_err_txt OUT VARCHAR2 ) AUTHID CURRENT_USER
AS
    just_os_user    VARCHAR2(40);

backslash_place NUMBER;
BEGIN`

我在这里省略了这个过程的主体,但是我想指出专门用于双因素认证的部分,如清单 9-16 所示。如果我们已经通过了 SSO 和其他连接测试,那么我们输入这个代码。如果用户没有给我们一个双因素认证码,那么我们调用f_send_2_factor函数。否则,我们通过调用f_is_cur_cached_cd函数来测试双因素代码,看看它是否符合要求。如果双因素代码好,我们设置安全应用角色;然而,如果没有,我们通过引发NO_DATA_FOUND异常让他们知道:他们输入了错误的代码,或者可能只是一个旧的(超过 10 分钟)代码。

清单 9-16。安全应用角色过程体,p_check_hrview_access

`    THEN
        IF( two_factor_cd IS NULL OR two_factor_cd = '' )

THEN
            m_err_txt := f_send_2_factor( just_os_user );

ELSIF( f_is_cur_cached_cd( just_os_user, two_factor_cd ) = 'Y' )

THEN
            EXECUTE IMMEDIATE 'SET ROLE hrview_role';

ELSE
            -- Wrong or Old 2_factor code. Could return message in M_ERR_TXT,
            -- or this will get their attention.
**            RAISE NO_DATA_FOUND;**

END IF;
    END IF;`

注意,当我们调用f_send_2_factor时,我们将m_err_txt设置为返回值。这样我们就可以将来自f_send_2_factor的分发代码(双因子代码被发送到什么设备的摘要)传递回客户端。

更新双因素认证的 OracleJavaSecurity.java

我们将对OracleJavaSecure.java进行几项更新和补充,以支持双因素认证。最大的增加将是一种新的分发双因素认证码的方法distribute2Factor()。我们将详细讨论这种方法。

我们将看到如何设置一些静态成员变量来保存我们打算使用的特定寻址数据。我们还探索了向 SMS 设备、寻呼机和电子邮件发送双因素代码的各种方法。

设置一些公司特定的地址

OracleJavaSecure.java中有几项设置是特定于贵公司双因素认证的实现的:贵公司内部的 DNS 域名,为贵公司处理邮件路由的主机名,也许还有一个 web 应用 URL,通过它可以发送文本寻呼机消息。我们会将这些内容添加到我们为单点登录设置的贵组织的 Windows 域名中,作为您需要在编译前在OracleJavaSecure.java中编辑的项目。参见清单 9-17 。

images 编辑在名为 的文件中找到的代码第九章/orajavsec/Oracle javasecure . Java .

清单 9-17。设置公司特定的邮件和寻呼地址

`    private static String expectedDomain = "ORGDOMAIN";
    private static String comDomain = "org.com";
    private static String smtpHost = "smtp." + comDomain;
    private static String baseURL =
        "http://www.org.com/servlet/textpage.PageServlet?ACTION=2&PAGERID=";

private static String msgURL = "&MESSAGE=";`

编译双因素递送路线代码:二进制数学

我们汇编一个代码来表示为传输双因素认证码而选择的递送路线的汇编。对于所选的每条路线,我们将一个常数值(静态最终值)添加到我们的累积分配代码中。在处理结束时,单个代码表示使用的所有路线。就像一个字节中的位(8 位,每一位的值是前一位的两倍,代表 256 个唯一值),我们使用二进制数学来累积我们的分布代码。初始常数在列表 9-18 中列出。

清单 9-18。交货路线常数

`    private static final int USE_PAGER = 1;
    private static final int USE_SMS = 2;

private static final int USE_EMAIL = 4;`

表 9-1 列出了总和如何代表所有不同的交货路线组合。请注意,最大值比最大值的两倍小一(7 是最大值,比最大常数的两倍 4 小一)。

如果你还没有猜到,下一个常量的值应该是 8,然后是 16 和 32。我们只对t_two_fact_cd_cache表中的distrib_cd列进行了大小调整,以容纳 2 位数,因此我们被限制为这 6 个常量值。这六个常数的最大和是 63。接下来的第七个常数将是 64,但包括该值的最大和将是 127,一个三位数的值。

探索二维码的分发方法

当我们在前面定义f_send_2_factor Oracle 函数时,我们注意到它只是调用了distribute2Factor() Java 方法:它是一个直通式 Oracle 函数,就像大多数 Java 存储过程一样。推理有点深奥。你看,我们已经在 Oracle 数据库上执行了,当我们调用f_send_2_factor时,正在运行p_check_hrview_access过程。为什么不直接从p_check_hrview_access调用 Java 的distribute2Factor()方法呢?这将是一个很好的功能,但它不可用。我们需要通过一个声明为AS LANGUAGE JAVA的专用 Java 存储过程来访问 Oracle JVM。我们不能在同一个函数或过程中混合使用 PL/SQL 和 Java 调用;因此,我们调用一个单独的 PL/SQL 函数作为 Java 存储过程来访问 Java 方法。

当我们使用distribute2Factor()方法时,我们已经从 SSO 处理中知道了用户是谁。我们将用户 ID 传递给该方法,这样我们就可以将我们的双因素代码发送给目标接收者拥有的设备。方法的设置和结束是熟悉的,正如你在清单 9-19 中看到的。此方法返回一个表示分配代码的字符串(发送双因子代码的路由摘要)。

清单 9-19。分配双因子码的方法:Framework,distribute2Factor()

`    public static final String distribute2Factor( String osUser ) throws Exception {
        // Do not resend this two-factor authentication code,
        //  nor a new one using this session
**        if ( twoFactorAuthChars != null ) return "0"**;

int distribCode = 0;

Statement stmt = null;
        try {
**            ...**

} catch( Exception x ) {
            java.io.CharArrayWriter errorText = new java.io.CharArrayWriter( 4000 );
            x.printStackTrace( new java.io.PrintWriter( errorText ) );
            stmt.executeUpdate( "CALL app_sec_pkg.p_log_error( 0, '" +

errorText.toString() + "', '')" );
        } finally {
            try {
                if( stmt != null ) stmt.close();
            } catch( Exception y ) {}
        }
        return String.valueOf( distribCode );

}`

注意清单 9-19 中间的省略号(…)。这就是我们在接下来的小节中讨论的代码所在的位置。

创建双因子代码

distribute2Factor()方法中,我们生成符合我们规定格式的双因素认证码:12 个数字字符分成三组,每组四个,用破折号隔开(例如,1234-5678-9012)。我建议这种格式便于用户阅读和输入。此外,这一点很重要,非常古老的纯数字寻呼机只能显示有限数量的字符;通常只有数字字符和破折号。规定的格式符合最低共同标准。

我们将双因子代码构建成一个 14 个字符的字符数组,如清单 9-20 所示。我们在每个地方放一个随机的数字字符。在 ASCII 字符集中,数值从 48 到 58。这个跨度的大小是 10,我们基本上选择 0 到 9 之间的下一个随机整数。我们为数字字符“0”添加了第一个 ASCII 值,( 48),所以本质上我们得到了一个介于 48 和 58 之间的随机值,我们将其转换为char并设置在twoFactorAuthChars数组中。

当我们行进通过for循环时,我们观察我们的位置。在每四个字符之后,我们要放置一个破折号。我们做一个模数 5 (% 5)来测试我们的位置。当下一位,模数 5 等于 0 时,我们需要设置下一位为破折号字符。第一个数组位置是 0,所以我们加一个,我们想测试我们的下一个位置,所以我们再加一个。我们测试(i+2)%5

当我们的测试是肯定的,我们继续并增加它,这样我们可以设置下一个地方为破折号并继续;然而,我们不想在数组的末尾添加另一个破折号字符,所以我们只设置了破折号if ( i < twoFactorLength )

清单 9-20。构建双因子代码

`    private static int twoFactorLength = 14;
    private static char[] twoFactorAuthChars = null;

twoFactorAuthChars = new char[twoFactorLength];

for ( int i = 0; i < twoFactorLength; i++ ) {
        // Use numeric only to accommodate old pagers
        twoFactorAuthChars[i] = ( char )( random.nextInt( 58 - 48 ) + 48 );

// Insert dashes (after every 4 characters) for readability
        if( 0 == ( ( i + 2 ) % 5 ) ) {
            i++;
**            if ( i < twoFactorLength )**
**                twoFactorAuthChars[i] = '-';**

}
    }
    String twoFactorAuth = new String( twoFactorAuthChars );`

在这个方法中,我们只使用双因素验证码的String表示,所以我们设置了一个方法成员。在其他地方,双因素验证码被称为一个char数组。当我们进入这个方法时,我们测试twoFactorAuthChars数组的存在(见清单 9-19 ,我们将它作为静态类成员保留。

处理 Oracle 和 Java 日期:标准实践

你熟悉吉米·克罗斯的歌曲“瓶中时间”吗?那首歌中我最喜欢的一句是,“我经历的够多了,知道你是我想与之共度时光的人。”一些旧的编码实践就是这样。我维护的一个特别的标准实践是 Oracle 数据库和 Java 之间的日期交换。当我遵守这个计划时,我从来没有遇到过麻烦,这是我将要描述的。

我将向你们展示的唯一潜在问题是,精度仅限于秒。如果您需要毫秒级的精度,您将需要稍微修改这种方法。嗯,还有时区的问题,我们已经讨论过了;如果你的公司分布在多个时区,你可能不得不处理这个问题。

我提出的第一个标准是永远用java.util.Date,千万不要用java.sql.Date。除了由于它们具有相同的名称(但相互排斥)而引起的混淆之外,还有它们操作不同的问题。学习java.util.Date。总是导入,独占使用。你可以同时使用两个Date类,但是需要非常小心地将它们分别称为java.util.Datejava.sql.Date,并且正确地引用它们。但更合理的是,你将不得不选择其中之一。选择java.util.Date

我们可能从使用java.sql.Date(不是我们的首选方法)中获得的一个好处是,我们可以调用ResultSet.getDate()并直接获得一个java.sql.Date对象,而无需强制转换。然而,java.sql.Date缺少的最重要的一个功能是获取当前时间和日期的能力。这个话题出现在第七章的侧栏“两个日期类的故事”中另一方面,使用java.util.Date,我们简单地得到一个new Date(),默认的构造函数基于当前的毫秒值构建一个Date。避免使用java.sql.Date的第二个原因是,如果您决定在应用中对那个Date进行标准化,那么即使在一些与 SQL 数据库没有交互的类中,您也必须从java.sql包中导入它。在那些情况下,我会争辩说,java.sql.Date是不合适的。

清单 9-21 中的所示的格式StringsSimpleDateFormat类是我们在 Oracle 数据库和 Java 之间进行数据交换所需要的。这就是我们使用的distribute2Factor()方法。

清单 9-21。Oracle 数据库和 Java 之间的标准数据交换

`import java.text.SimpleDateFormat;
import java.util.Date;

String oraFmtSt = "YYYY-MM-DD HH24:MI:SS"; // use with to_char()
    String javaFmtSt = "yyyy-MM-d HⓂ️s";
    SimpleDateFormat ora2JavaDtFmt = new SimpleDateFormat( javaFmtSt );`

我建议的下一个标准实践是,您总是以Strings / VARCHAR2的形式交换日期。我们将为日期定义类似的格式,允许我们以字符串的形式交换它们,并在 Java 和 Oracle 数据库上重建有代表性的日期。在 Oracle 数据库上,我们使用TO_CHARTO_DATE函数;在 Java 中,我们使用SimpleDateFormat.parse()SimpleDateFormat.format()方法。在 Oracle 数据库和 Java 之间交换日期的标准方法如表 9-2 所示。

images

用户有哪些路线?

distribute2Factor()方法的下一步是查询HR数据,看看用户有什么设备:寻呼机、手机等等。,我们可以向其发送双因素身份验证代码。在这个过程中,我们将一下子获得缓存的双因素代码(如果有的话),缓存时的时间戳和请求它的 IP 地址。

注意在我们的查询(清单 9-22 )中,我们使用 Oracle TO_CHAR方法获取时间戳,并使用我们从 Java 传递的格式字符串。

清单 9-22。查询可用配送路线

`    stmt = conn.createStatement();
    ResultSet rs = stmt.executeQuery(
"SELECT m.employee_id, m.com_pager_no, m.sms_phone_no, s.sms_carrier_url, e.email, " +

"SYS_CONTEXT( 'USERENV', 'IP_ADDRESS' ), " +
"TO_CHAR( c.cache_ts, '" + oraFmtSt + "' ), c.ip_address " +

"FROM hr.v_emp_mobile_nos m, hr.v_employees_public e, hr.v_sms_carrier_host s, " +

"v_two_fact_cd_cache c WHERE m.user_id = '" + osUser + "' " +

"AND e.employee_id =  m.employee_id " +

"AND s.sms_carrier_cd (+)= m.sms_carrier_cd " +

"AND c.employee_id (+)= m.employee_id " );

if ( rs.next() ) {
        String empID     = rs.getString( 1 );
        String pagerNo   = rs.getString( 2 );
        String smsNo     = rs.getString( 3 );
        String smsURL    = rs.getString( 4 );
        String eMail     = rs.getString( 5 );
        String ipAddress = rs.getString( 6 );`

这个查询内置了一点容错功能。你看到(+)=符号了吗?这些表示所谓的外部连接。注意,我们从v_sms_carrier_host视图中获得了sms_carrier_url,其中用户的sms_carrier_cdv_sms_carrier_host中的相匹配。现在,如果用户没有手机或者sms_carrier_cd怎么办?如果我们将该查询作为一个直接连接(s.sms_carrier_cd = m.sms_carrier_cd),我们将不会获得该用户的记录。但是,通过添加外部连接指示符(+),我们要求查询返回包含主数据的结果,即使这个辅助数据不存在。我们对双因素缓存数据执行另一个外部连接,因为用户可能在缓存中还没有双因素身份验证代码。

我想重申,如果用户在t_emp_mobile_nos表中没有条目,他将无法进行双因素认证。他不一定需要有手机或寻呼机,但是 SSO 用户 ID ( user_id)和employee_id的关联要求在t_emp_mobile_nos中有用户的条目。如果用户在t_emp_mobile_nos中的记录没有 SMS 电话号码或寻呼机号码,那么 distribute2Factor()方法将消息发送到用户的电子邮件中。

现在的通行码还有效吗?

distribute2Factor()中,我们希望确定用户是否会在现有双因素代码的 10 分钟超时到期之前再次请求另一个双因素身份验证代码。用户可能有也可能没有现有的、缓存的双因素身份验证代码:我们可能从该表中获取值nulls。因此,我们将下面的代码(清单 9-23 )放在一个 try/catch 块中。将nulls加入到我们的String成员中没有问题,但是将null解析成Date并将nullsString.equals()方法进行比较会抛出异常。

清单 9-23。测试缓存的双因素认证码的有效性

`    try{
        String cTimeStamp = rs.getString( 7 );
        String cIPAddr    = rs.getString( 8 );
        // Ten minutes ago Date
        Date tmaDate = new Date( (new Date()).getTime() - 10601000 );

Date cacheDate = ora2JavaDtFmt.parse( cTimeStamp );
        // If user coming from same IP Address within 10 minutes
        // do not distribute Code (will overwrite code from new IP Addr)
**        if( ipAddress.equals( cIPAddr ) && cacheDate.after( tmaDate ) )**
**            return "0";**

} catch( Exception z ) {}`

这段代码的核心是最后一行,在这里我们测试用户是否来自我们为其生成双因素代码的同一个 IP 地址,以及日期是否不到 10 分钟。如果是这种情况,我们不生成新的双因素代码,也不重新发送现有的代码;我们只是返回。

在这里,花一分钟时间通读我们十分钟前用来计算的代码行,tmaDate。我们得到当前日期的毫秒数,然后用 1000 毫秒减去 60 秒的 10 分钟。

将通行码分发给路线

distribute2Factor()方法中,我们的下一个议程是将生成的双因素代码发送到首选和/或现有设备。根据偏好,我们会将双因素代码发送到用户的寻呼机和手机。如果两者都不可用,我们会将代码发送到用户的电子邮件中。见清单 9-24 。

要将代码发送到手机,用户必须同时拥有电话号码和运营商代码。如果他这样做了,我们就调用distribToSMS()方法。注意,我们将返回值int添加到我们的累积值distribCode中。类似地,如果用户有寻呼机号码,我们就将双因素代码发送给寻呼机。在以下部分中,我们将探讨向特定器件发送双因素代码的各种方法。

清单 9-24。通过手机短信、寻呼机和/或电子邮件分发的通话方式

`if( ( smsNo != null ) && ( !smsNo.equals( "" ) ) &&
        ( smsURL != null ) && ( !smsURL.equals( "" ) )
    )
        distribCode += distribToSMS( twoFactorAuth, smsNo, smsURL );

if( ( pagerNo != null ) && ( !pagerNo.equals( "" ) ) )
        distribCode += distribToPagerURL( twoFactorAuth, pagerNo );     // Recommend not send to e-mail unless no other distrib option succeeds
    // !Uncomment code in next line!
    if( //( distribCode == 0 ) &&

( eMail != null ) && ( !eMail.equals( "" ) )
    )
        distribCode += distribToEMail( twoFactorAuth, eMail );`

目前,我们已经注释了一个测试,如果我们成功地将代码发送到寻呼机或手机,我们将希望实现该测试来避免将双因素代码发送到电子邮件。我们已经对该测试进行了注释,这样您也可以看到实际的电子邮件路由,但是您可能希望在生产中取消对该测试的注释。此外,如果用户有电子邮件地址,我们只能将代码发送到电子邮件。

在 Oracle 中缓存密码

一旦我们分发了双因素身份验证代码,如果我们真的找到了传递它的路径(distribCode > 0 ),我们希望缓存它,以便与用户输入的任何代码进行比较。下面来自distribute2Factor()方法的语句(清单 9-25 )更新条目,如果它存在的话。update 语句的一个特性是它返回一个整数来指示更新了多少行。如果我们看到少于 1 行被更新,我们假设我们需要插入一行来缓存这个特定用户的代码。换句话说,我们尝试一个更新,它也作为一个测试,看看我们是否需要插入一行。一旦我们为大多数应用输入了大多数用户,我们几乎总是想要进行更新,所以这个顺序,更新然后插入,是最有效的。

清单 9-25。缓存双因素认证码

`    if( distribCode > 0 || isTesting ) {
        int cnt = stmt.executeUpdate(
"UPDATE v_two_fact_cd_cache SET two_factor_cd = '" + twoFactorAuth +

"', ip_address = '" + ipAddress + "', distrib_cd = " +

String.valueOf( distribCode ) + ", cache_ts=SYSDATE " +

"WHERE employee_id = " + empID );

**        if( cnt < 1 )**

stmt.executeUpdate(
"INSERT INTO v_two_fact_cd_cache( employee_id ,two_factor_cd, distrib_cd ) VALUES " +

"( " + empID + ", '" + twoFactorAuth +"', " + String.valueOf( distribCode ) + " )" );
    }`

将代码分发给短信

我们在本章前面的测试中看到,我们需要为我们的 SMTP 服务器设置一个会话属性。这是我们在清单 9-26 中执行的第一条语句。之后,我们调用UTL_MAIL包向用户手机发送消息。send 函数的参数是发件人的电子邮件、收件人的电子邮件、我们不关心的另外两个分发、消息标题(“响应”)和消息文本(我们的双因素身份验证代码)。

我们从用户的手机号码smsNo和他的运营商的短信网关地址smsURL构建收件人的电子邮件地址。

清单 9-26。将代码分发到 SMS,distribToSMS()

`    private static final int distribToSMS( String twoFactorAuth, String smsNo,
        String smsURL )
    {
        int distribCode = 0;

Statement stmt = null;
        try {
            stmt = conn.createStatement();
            stmt.executeUpdate( "ALTER SESSION SET SMTP_OUT_SERVER = '" +
                smtpHost + "'" );
            stmt.executeUpdate( "CALL UTL_MAIL.SEND( 'response@" +

comDomain + "', '" + smsNo + "@" + smsURL +
                "', '', '', 'Response','" + twoFactorAuth + "' )" );
            distribCode += USE_SMS;

} catch ( Exception x ) {
        } finally {
            try {
                if( stmt != null ) stmt.close();
            } catch( Exception y ) {}
        }
        return distribCode;

}`

例如,此消息看起来像下面这样。

From: response@org.com To: 8005551212@txt.att.net Subject: Response 1234-5678-9012

还要注意,在这条消息中,我们使用了try / catch / finally块来结束语句。我们一开始将方法成员distribCode设置为 0。如果我们成功地将代码发送到手机,我们将把USE_SMS常量的值加到distribCode。最后我们返回distribCode

将代码分配给寻呼机 URL

distribToPagerURL()方法与我们刚刚讨论的distribToSMS()有许多相似之处。主要区别在于,我们不是使用 Oracle 包来发送电子邮件,而是使用纯 Java URL类来读取网页。参见清单 9-27 。实际上,我们并不关心浏览器在访问这个网页时会看到什么。用户在浏览器窗口中看到的条目表单永远不会被加载。它被绕过,因为我们将输入字段数据作为 URL 地址的一部分提交。我们使用所谓的GET方法在 URL 行上将值传递给 web 服务器。我们的 URL 将如下所示:

www.org.com/servlet/textpage.PageServlet?PAGERID=12345&MESSAGE=1234-5678-9012

我们在PAGERID参数中指明要发送的寻呼机,在MESSAGE参数中指明消息的内容。在我们用这个地址创建了一个新的URL实例之后,我们调用它的getContent()方法来“浏览”这个网址。此时,带有分页应用的 web 服务器已经响应了GET方法,如果没有抛出异常,我们可以继续返回USE_PAGER常量的值。

清单 9-27。将代码分发到寻呼机,distribtopageurl()

`    private static final int distribToPagerURL( String twoFactorAuth,
        String pagerNo )
    {
        int distribCode = 0;

try {
            URL u = new URL( baseURL + pagerNo + msgURL + twoFactorAuth );
            u.getContent();
            distribCode += USE_PAGER;

} catch ( Exception x ) {}
        return distribCode;

}`

这种方法最有可能需要特殊编辑才能在您的组织中发挥作用。您必须研究寻呼消息是如何发送到您公司的寻呼机的,如果有的话。即使您没有寻呼机,该代码也可以作为一个示例,说明如何将双因素代码发送到其他 web 服务。

将代码分发到电子邮件

我们的distribToEMail()方法不仅相似,而且几乎与distribToSMS()方法相同,只是这里的收件人是用户的电子邮件地址。我省略了代码,但是消息是这样的:

From: response@org.com To: OSUSER@org.com Subject: Response 1234-5678-9012

如果成功,我们返回常量USE_EMAIL的值。

测试双因素认证

在本节中,我们再次利用我们所有的资源来演示和测试双因素身份认证以及我们到目前为止讨论过的所有其他内容。我们将使用我们一直在检查的代码来测试我们的双因素身份验证,以生成和传输双因素身份验证代码。为了充分体验这一点,您需要在数据库中输入您的寻呼机、手机和/或电子邮件地址,正如我们所描述的。或者,您可以查询数据库,从appsec.v_two_fact_cd_cache视图中选择生成的双因子代码。

如果您还没有执行我们在本章中讨论的 SQL 命令,您需要打开 第九章 文件夹和文件 Sys.sqlSecAdm.sqlAppSec.sqlHR.sql ,并以适当的用户身份执行这些命令。您可以在中执行它们,但是当您在 AppSec.sql 中执行到一半时,您需要在继续执行appsec之前停止并执行 HR.sql

您还需要在 Oracle 数据库中执行OracleJavaSecure.java来创建 Java 结构。然后我们将在客户机上运行类TestOracleJavaSecure来完成我们所有的演示和测试。

在 Oracle 中更新 OracleJavaSecure Java

如果您还没有,请编辑OracleJavaSecure.java的代码,以提供我们公司特定的地址用于双重身份验证。我们在之前的清单 9-17 中看到了这一点。编辑在名为chapter 9/orajavsec/Oracle javasecure . Java的文件中找到的代码。

`    private static String expectedDomain = "ORGDOMAIN";
    private static String comDomain = "org.com";
    private static String smtpHost = "smtp." + comDomain;
    private static String baseURL =
        "http://www.org.com/servlet/textpage.PageServlet?ACTION=2&PAGERID=";

private static String msgURL = "&MESSAGE=";`

将新的orajavsec/Oracle javasecure . Java代码加载到 Oracle 数据库中。您将把它加载到appsec模式中,因此您应该以该用户的身份连接到 Oracle 数据库,并且不要忘记将您的角色设置为appsec_role。再次取消以CREATE OR REPLACE AND RESOLVE JAVA…开头的第一行的注释,并在您的 SQL 客户端(SQL*Plus、SQL Developer、JDeveloper 或 TOAD)中执行它。记得先设定好角色。(这个文件也将在客户机上编译和执行,所以在保存文件之前重新注释第一行。)

您可能还需要修改 SQL 客户机的环境,以便在 URL 字符串中包含一个&符号,如前面的示例baseURL所示。注意参数“ACTION=2”和“PAGERID=”之间的&符号。当一些 SQL 客户机看到这样的&符号时,它们会认为这是一个标记,表示在执行时要被替换的变量,在本例中是“& PAGERID”。大多数情况下,这是一个很好的假设,但不是在这种情况下,也不是在我们加载到 Oracle 数据库的 Java 代码中。我们可以用两种方法之一来补救这种情况:我们可以使用不同的 SQL 客户端,或者甚至求助于使用 loadjava 实用程序,如第四章中所述。然而,一个更简单、更直接的解决方法是告诉我们的 SQL 客户机不要用这个简单的命令进行变量替换。

SET DEFINE OFF;

编辑测试代码

我们将执行一个单独的 Java 类TestOracleJavaSecure,来测试我们的双因素认证。编辑靠近顶部的代码,为appusr用户设置合适的密码和连接字符串。

images 注意你可以在文件chapter 9/testoraclejavasecure . Java中找到TestOracleJavaSecure类的代码。

计划将双因子代码作为参数传递给 Main

TestOracleJavaSecure类的全部代码都驻留在main()方法中。所以,当我们从命令行用这个类调用 Java 时,我们简单地从上到下运行代码。这类似于我们在第七章中的测试。然而,在这里,我们必须调用这个类两次。第一次,我们毫无争议地称之为。在通话结束时,如果一切按计划进行,双因素身份验证代码将被发送到我们的手机、寻呼机和电子邮件中。

一旦我们在任何设备上收到代码,在 10 分钟内,我们可以再次调用TestOracleJavaSecure执行 Java,除了这一次我们将包含双因素代码作为参数。我们稍后执行的两个命令如下所示:

java TestOracleJavaSecure java TestOracleJavaSecure 1234-5678-9012

如果您没有任何设备或电子邮件来接收双因素授权码,您可以在OracleJavaSecure.java中将isTesting boolean设置为true,并在 Oracle 数据库中重新加载。然后重新编译并运行上面给出的命令。这将在t_two_fact_cd_cache表中放置一个双因素代码,即使没有找到分配设备。然后,您可以从该查询中获得生成的双因素代码:

SELECT * FROM appsec.v_two_fact_cd_cache;

命令行上的参数作为一个Strings数组传递给main()方法。我们可以通过测试String数组长度是否大于 0 来测试双因子代码的存在。我们也向自己保证数组的第一个元素不是null,如清单 9-28 所示。

清单 9-28。将双因素代码传递给 TestOracleJavaSecure main()方法

    public static void main( String[] args ) {         try {             // Passing 2-factor code in as argument on command line             String args0 = "";             if( **args.length != 0 && args[0] != null** ) args0 = args[0];             args0 = OracleJavaSecure.checkFormat2Factor( args0 );

当这个人输入他在手机上收到的代码时,我们希望确保他理解我们的格式规则的负担最小。如果他只输入数字字符,不输入破折号,我们愿意接受。如果他附加了其他字符,比如时间和日期或者他手机上显示的任何内容,我们应该挑选出我们的双因素代码,如果容易得到的话。我们将他在命令提示符下提供的任何内容发送给OracleJavaSecure.checkFormat2Factor()方法。如果您发现经常出现其他打字错误,那么您可以在这种方法中加入更多的智能。

计划获得安全应用角色

无论我们是否有双因素代码,我们都将我们的 Oracle 过程称为“??”,用于安全应用角色“??”。如果我们还没有一个双因子代码,我们将一个空字符串传递给过程;否则,我们传递代码。你现在已经熟悉了像这样的阅读程序,在清单 9-29 中。

清单 9-29。从 TestOracleJavaSecure 设置安全应用角色

`    stmt = ( OracleCallableStatement )conn.prepareCall(
        "CALL appsec.p_check_hrview_access(?,?,?)" );

stmt.registerOutParameter( 2, OracleTypes.NUMBER );
    stmt.registerOutParameter( 3, OracleTypes.VARCHAR );
**    stmt.setString( 1, args0 );
    stmt.setInt(    2, 0 );
    stmt.setNull(   3, OracleTypes.VARCHAR );
    stmt.executeUpdate();
    errNo = stmt.getInt( 2 );
    errMsg = stmt.getString( 3 );
    if( errNo != 0 ) {
        System.out.println( "Oracle error 1) " + errNo + ", " + errMsg );
    } else if( args0.equals( "" ) ) {
        System.out.println( "DistribCd = " + errMsg );
        System.out.println( "Call again with 2-Factor code parameter" );
    } else {
        if( null != stmt ) stmt.close();
        System.out.println( "
Oracle success** 1)" );

OracleResultSet rs = null;
        RAW sessionSecretDESPassPhrase = null;
        RAW sessionSecretDESAlgorithm = null;
        RAW sessionSecretDESSalt = null;
        RAW sessionSecretDESIterationCount = null;

String locModulus = OracleJavaSecure.getLocRSAPubMod();

String locExponent = OracleJavaSecure.getLocRSAPubExp();

stmt = ( OracleCallableStatement )conn.prepareCall(
            "CALL hr.hr_sec_pkg.p_select_employees_sensitive(?,?,?,?,?,?,?,?,?)" );`

我们报告从程序中返回的任何错误。一个明确的潜在错误是用户可能输入了错误或旧的代码,在这种情况下,将返回“DATA NOT FOUND”错误。

如果没有错误,并且我们在调用时没有双因素代码,那么我们假定双因素代码已分发,我们显示在errMsg中返回的分发代码,并要求用户带着他们的双因素代码再次访问。如果他们调用时有一个双因素代码,并且没有错误,那么我们假设p_check_hrview_access成功了,用户的连接被授予了hrview_role,可以继续读取数据。

我们的TestOracleJavaSecure类将执行p_select_employees_sensitive过程来证明访问已经成功。毫无疑问,你对这个程序太熟悉了。

运行测试并观察结果

重述一下需求:要运行测试,您必须已经创建了我们在本章中描述的表、过程和授权。您将在 HR.emp_mobile_nos表中为您的用户 ID 插入一条记录。您还将在 第九章/orajavsec/Oracle javasecure . Java的顶部编辑特定于公司的值。您还应该取消对OracleJavaSecure.java的第一行的注释,并在 Oracle 数据库中执行它来加载 Java 结构。

OracleJavaSecure加载到 Oracle 数据库后,恢复最上面一行的注释并保存文件。在第九章 目录的 命令提示符下,用这些命令编译OracleJavaSecure.javaTestOracleJavaSecure.java

javac orajavsec/OracleJavaSecure.java javac TestOracleJavaSecure.java

然后通过执行第一行java TestOracleJavaSecure来运行测试,并查看结果。再次执行相同的命令,查看双因子代码是否不再发送(DistribCd = 0)。收到双因素身份验证代码后,执行最后一个命令,在命令行上传递双因素代码。

`java TestOracleJavaSecure
    DistribCd = 5
    Call again with 2-Factor code parameter
java TestOracleJavaSecure

**    DistribCd = 0**
java TestOracleJavaSecure 1234-5678-9012

DistribCd = null
    Oracle success 1)
    Oracle success 2)
    198, Donald, OConnell, DOCONNEL, 650.507.9833, 2007-06-21 00:00:00, SH_CLERK, 26
    00 (AD6E5035FAB394A8), null, 124, 50`

10 分钟后,双因素代码将不再能够成功访问。10 分钟后,使用相同的代码再次尝试执行TestOracleJavaSecure,并观察“未发现数据”错误消息。

java TestOracleJavaSecure 1234-5678-9012     DistribCd = ORA-01403: **no data found**     Oracle error 1) 100, ORA-01403: no data found

章节回顾

我们使 Oracle 数据库能够发送电子邮件和浏览网页。利用这些能力,我们开发了一个流程,向我们的应用用户的手机、寻呼机和电子邮件帐户发送双因素身份验证代码。用户必须输入这些双因素代码,才能访问我们的应用数据。

我们构建了一个表来保存用户的手机号码,另一个表保存每个手机运营商的短信网关的网址。我们还添加了一个表来保存缓存的 2 因子代码。

我们实现了外键来维护表之间的引用完整性。我们还讨论了在 Oracle 数据库和 Java 之间交换数据的编码标准。

在图 9-1 中提供了 2 因子代码分配过程的概述。注意该图中对图 8-1 和【D】模块的引用。在那里,我们看到了获取 SSO 代理连接的过程的图示。使用这个连接,我们调用两次p_check_hrview_access过程。第一次,我们没有双因素认证码。因此,我们执行图 9-1 中块【E1】所示的过程。在该模块中,我们可以看到用于将双因素代码分配给与用户相关的各个设备的流程图。

第二次调用p_check_hrview_access时,我们提供双因素认证码作为参数。因为我们已经返回该代码,所以我们执行图 9-1 的的【E2】块中描述的过程。在那里,我们可以看到对双因子代码有效性的测试,如果一切顺利,就可以使用SET ROLE命令来获取hrview_role

有了这个角色,我们的用户可以继续使用加密来选择和更新 HR 模式中的敏感数据。这些工序参照图 7-1 和图 7-2 包含在图 9-1 中。

images

图 9-1。双因素认证码流程

十、应用授权

每个 Oracle 应用都连接到一个或多个 Oracle 数据库实例,可能作为一个或多个用户。我们已经看到了如何通过一次一个应用的安全性和加密来实现这一点。我们的第一个应用作为appusr帐户连接,并通过hr_view角色访问HR模式中的数据。

对于该应用以及任何类似的应用,开发人员将为安全应用角色开发一个过程,就像我们的p_check_hrview_access。但是对于我们所有的安全性,有一个方面我们还没有解决:开发人员仍然需要将应用用户密码硬编码到他们的代码中(或者找到另一种机制。)如果我们为他们提供一个安全的密码存储库,一个不像其他解决方案那样容易被冒名顶替的应用攻击的存储库,会怎么样?

在本章中,我们将构建一个动态的 Oracle 过程,用于验证所有的安全应用角色。个人开发者不必为他们的应用提供这样的过程。相反,他们将提供与他们的安全应用角色相关的三个唯一项目的列表:应用 ID(名称)、应用用户和安全应用角色名称。我们将把这些元素存储在一个表中。然后,我们将为所有安全应用角色提供动态过程,并围绕它组织我们的代码。

接下来,我们将构建一些其他表来处理每个用户的多个应用,将用户的特定于应用的安全数据与该用户的其他应用的安全数据分开。

我们还将让每个应用向我们发送一段有代表性的代码——一个特定于应用的类,以字节数组表示的对象的形式提供。(细节将变得更清楚。)通过检查该对象,我们将知道哪个应用正在请求服务,作为回报,我们将向该应用提供该应用所需的特定连接字符串的列表。当然,所有这些都是在考虑加密和安全性的情况下完成的。

我们将把应用连接字符串存储在 Oracle 表中。需要一些过程、函数和 Java 代码来完成这个任务,并且需要更多的代码来更新存储的数据。我们还将支持一个应用的多个版本。

我们将所有数据存储在 Oracle 数据库中,并使用 Oracle 数据库的安全性,但是请记住,在通过安全检查之前,我们没有应用 Oracle 连接。这是一个关于“先有鸡还是先有蛋”的两难问题或者在我们的例子中,“Oracle 连接和我们的 Oracle 连接字符串(密码)列表哪个先出现?”答案是,我们首先需要一个到 Oracle 数据库的连接,因此我们将使用另一个 Oracle 应用验证用户appver为所有应用提供必要的初始连接。appver用户将会是一个顽固的用户,我们可以设置尽可能多的限制。他的唯一目的是保护和提供特定于应用的 Oracle 连接字符串。

因为我们希望在分发连接字符串之前集合所有的安全力量,所以我们将让appver决定具体的应用,如前所述。他还将提供我们在前面章节中讨论过的单点登录(SSO)和双因素身份验证。并且他将建立用于分发和解密连接字符串的初始加密密钥集。

注意,为应用建立的每个新连接可能需要一组新的不同的加密密钥。在本书提供的代码中,我们将为并发使用保留多少组键是有限制的。我们将保留appver连接的加密密钥,以便继续解密该应用的连接字符串,并且我们将允许每个应用用户一次使用一组额外的加密密钥。可以同时使用其他不加密的 Oracle 连接。然而,一个勤奋的程序员可以克服这些限制。

多个应用的安全应用角色程序

回想一下我们的安全应用角色— hrview_role和设置角色的过程—它实现了哪些特定于应用的功能?它测试了许多非特定于应用的东西:IP 地址、时间、双因素身份验证,以及最重要的 SSO 身份。然而,有些事情是特定于应用的:寻找角色的用户(appusr)和角色本身(hrview_role)。

我们此时的目标是构建一个适用于任何应用的单一安全应用角色过程,强制执行我们所有的连接安全要求,但将所需的特定角色授予特定的应用用户。我们可以构建一个过程来处理这个问题,但是首先我们需要一个这些应用特定特性的注册表。我们将在appsec模式中创建t_application_registry表来保存数据。创建这个表的代码在清单 10-1 中。

images注意你可以在名为 Chapter10/AppSec.sql 的文件中找到清单 10-1 中的脚本。

清单 10-1。应用注册表显著特征表

CREATE TABLE appsec.t_application_registry (     application_id VARCHAR2(24 BYTE) NOT NULL,     app_user       VARCHAR2(20 BYTE) NOT NULL,     app_role       VARCHAR2(20 BYTE) NOT NULL );

我们还将创建一个通用的表视图。尽管这里没有显示,我们将使application_idapp_user列成为唯一的索引和主键。在我们到达第十二章之前,我们不会依赖那个键。现在,可以说每个应用可以使用多个安全应用角色。我们将通过代理各种应用用户来获得这些角色。所以一对application_idapp_user是获得一把app_role的唯一钥匙。

现在,让我们插入一个带有已知标签的数据记录:用户APPUSR和角色HRVIEW_ROLE。我们将这些设置赋予HRVIEWapplication_id,如下所示:

INSERT INTO appsec.v_application_registry ( application_id, app_user, app_role ) VALUES ( 'HRVIEW', 'APPUSR', 'HRVIEW_ROLE' );

我们在这里引入application_id列作为获取所需角色的句柄。每个应用都需要一个唯一的application_id,再增加几个,就可以让我们现有的代码为多个应用提供双因素身份验证、SSO 和安全应用角色。

为多个应用重建双因子缓存表

用户可能希望同时使用多个应用,我们希望它们独立运行。这些应用将拥有对不同数据的授权,并将使用不同的 Oracle 实例集。它们也可以在十分钟内从客户端开始,两个因素相互依存。

为了提供这些应用的独立操作,我们将添加application_id列作为t_two_fact_cd_cache表主键的一部分。参见清单 10-2 。这样,我们可以为用户使用的每个应用创建和分配一个双因素身份验证代码。这清楚地表明了这样一个事实:应用的双因素身份验证已经离单点登录只有一步之遥了。如果您的公司计算环境对初始登录强制实施双因素身份认证(例如,Windows 密码和安全 ID 令牌),则不需要对应用进行双因素身份认证。然而,用于基本计算机访问的双因素身份验证,无论是安全令牌、生物扫描仪还是电子徽章,似乎在电影中仍比在企业环境中更普遍。如果您有一台带有生物识别接口(指纹或面部识别)或安全卡插槽的计算机,它就不能作为输入密码的替代品进行双因素身份验证。只有当您除了输入密码之外还使用它时,它才是双重身份验证。

因此,我们将构建双因素身份认证,独立于每个应用。我们首先删除以前的t_two_fact_cd_cache表,并创建一个带有application_id列的新表。

清单 10-2。重做双因子代码缓存表

`DROP TABLE appsec.t_two_fact_cd_cache CASCADE CONSTRAINTS;

CREATE TABLE appsec.t_two_fact_cd_cache
(
    employee_id    NUMBER(6) NOT NULL,
    application_id VARCHAR2(24 BYTE) NOT NULL,
    two_factor_cd  VARCHAR2(24 BYTE),
    ip_address     VARCHAR2(45 BYTE) DEFAULT SYS_CONTEXT( 'USERENV', 'IP_ADDRESS' ),
    distrib_cd     NUMBER(1),
    cache_ts       DATE DEFAULT SYSDATE
);

CREATE UNIQUE INDEX two_fact_cd_emp_id_pk ON appsec.t_two_fact_cd_cache
    (employee_id,application_id);`

我们还重新创建了该表的一个视图(代码未显示),用于一般参考。在文件 Chapter10/AppSec.sql 中有一个示例 insert 和一些缓存老化测试代码。

更新双因子代码函数以使用应用 ID

请参考 AppSec.sql 文件中的完整代码列表。我们修改现有的f_is_cur_cached_cd函数,将application_id作为参数,并基于application_idv_two_fact_cd_cache中进行选择。我们还更新了现有的f_send_2_factor函数,将application_id作为参数,并将其传递给distribute2Factor()方法。

将 SSO 测试移至独立功能

为了在我们的代码片段之间进行分工,我们将把 SSO 过程分离到一个单独的函数中,f_is_sso。在这个函数中,我们传递应用用户的值。传统上,appusr是我们的应用用户,但是我们将传递我们在t_application_registry表中找到的任何注册应用的用户。回想一下,对于 SSO,应用用户必须要么是我们会话的连接用户,要么是代理用户。f_is_sso函数将返回经过验证的 SSO 用户 ID,如果无效,则返回空字符串。清单 10-3 只显示了f_is_sso的签名。

清单 10-3。测试用户是否通过 SSO 要求的函数,f_is_sso

CREATE OR REPLACE FUNCTION appsec.f_is_sso( m_app_user VARCHAR2 ) RETURN VARCHAR2 AUTHID CURRENT_USER AS     return_user     VARCHAR2(40) := ''; ...

images注意作为AUTHID CURRENT_USER执行的过程和函数从不放在包中,它们通常被授予EXECUTEPUBLIC

添加仅供应用安全使用的 Oracle 包

我们将增加严格由我们的应用安全用户appsec使用的函数和过程的数量,几乎增加一个数量级,因此我们将使用一个包来分组和组织它们。我们将这个包称为appsec_only_pkg包。该包还允许我们在一个位置保护代码——在这种情况下,我们不会授权任何人在appsec_only_pkg上执行。我们将删除函数f_is_cur_cached_cdf_send_2_factor,并将它们移到我们的包中,如清单 10-4 中的所示。

清单 10-4。仅供应用安全使用的包

`DROP FUNCTION appsec.f_is_cur_cached_cd;
DROP FUNCTION appsec.f_send_2_factor;

CREATE OR REPLACE PACKAGE appsec.appsec_only_pkg IS

FUNCTION f_is_cur_cached_cd(
        just_os_user     VARCHAR2,         m_application_id v_two_fact_cd_cache.application_id%TYPE,
        m_two_factor_cd  v_two_fact_cd_cache.two_factor_cd%TYPE )
    RETURN VARCHAR2;

FUNCTION f_send_2_factor(
        just_os_user     VARCHAR2,
        m_application_id v_two_fact_cd_cache.application_id%TYPE )
    RETURN VARCHAR2;`

添加助手函数获取 APP_ROLE

当我们运行设置这些角色的过程时,我们将需要来自t_application_registry表的一段数据,即安全应用角色名称。我们已经讨论了作为AUTHID CURRENT_USER运行我们的安全应用角色过程的需要。我们这样做是为了确保当前用户的有效性,而不是模式所有者appsec。这叫调用者的权利。此外,这是由CURRENT_USER设定角色的唯一方式。

为了执行安全应用角色过程,我们需要授予PUBLIC对过程的执行权限。然而,我们不想在v_application_registry视图上授予PUBLIC数据特权。如果我们有一个助手函数,它与v_application_registry和安全应用角色过程属于同一个模式,那么助手函数可以由过程执行,并代表它读取表中的数据。PUBLIC 在过程上有 execute,但在视图或函数上没有 grants 然而,该过程可以访问这些函数,以便在视图上进行选择。这样做的结果是,我们可以运行一个过程来授予角色,但不公开用于评估访问的数据。

我们根据来自f_get_app_role函数的application_idapp_user读取应用角色名称。我们将把这个功能添加到我们的appsec_only_pkg包中。参见清单 10-5 。

清单 10-5。获取应用角色名称的辅助函数

    FUNCTION f_get_app_role(         m_application_id v_two_fact_cd_cache.application_id%TYPE,         m_app_user       v_application_registry.app_user%TYPE )     RETURN VARCHAR2     AS         m_app_role v_application_registry.app_role%TYPE;     BEGIN         SELECT app_role INTO m_app_role         FROM v_application_registry         WHERE application_id = m_application_id         AND app_user = m_app_user;         RETURN m_app_role;     END f_get_app_role;

用动态程序替换 hrview_role 访问程序

我们将用一个通用程序p_check_role_access来替换设置hrview_role的应用专用程序p_check_hrview_access,该通用程序将为任何应用设置安全应用角色。每个应用都需要在t_application_registry表中有一个(一个或多个)条目。借助这一新流程,我们可以轻松地将单点登录和双因素身份认证应用于多个应用。

新程序的代码

通过清理,我们将放弃旧的p_check_hrview_access程序。这将确保我们使用新的程序,即使是设置hrview_role。参见清单 10-6 。

我们的新过程p_check_role_access看起来与我们之前的安全应用角色过程非常相似。我们使用一个已经通过我们的应用 Oracle 用户代理的连接进入这个过程,我们将它放入app_user变量中。新的过程采用额外的application_id参数,它又将这个参数和app_user标识一起传递给我们新的f_get_app_role助手函数,以便从v_application_registry中读取角色名。此外,我们将app_user传递给新的f_is_sso函数来取回经过验证的用户,而不是在这个方法中拥有 SSO 的代码。

清单 10-6。动态安全应用角色过程,p_check_role_access

`DROP PROCEDURE appsec.p_check_hrview_access;

CREATE OR REPLACE PROCEDURE appsec.p_check_role_access(
    --m_two_factor_cd      v_two_fact_cd_cache.two_factor_cd%TYPE,
    m_application_id     v_two_fact_cd_cache.application_id%TYPE,
    m_err_no         OUT NUMBER,
    m_err_txt        OUT VARCHAR2 )
AUTHID CURRENT_USER
AS
    return_user VARCHAR2(40);
    m_app_user  v_application_registry.app_user%TYPE;
    m_app_role  v_application_registry.app_role%TYPE;
BEGIN
    m_err_no    := 0;
**    m_app_user  := SYS_CONTEXT('USERENV','PROXY_USER');**
    m_app_role  := appsec_only_pkg.f_get_app_role( m_application_id, m_app_user );
    return_user := f_is_sso( m_app_user );
    IF( return_user IS NOT NULL )
    THEN
    -- Code for two-factor Auth moved to appver login process
    --    IF( m_two_factor_cd IS NULL OR m_two_factor_cd = '' )
    --    THEN
    --        m_err_txt := appsec_only_pkg.f_send_2_factor( return_user, m_application_id );
    --    ELSIF( appsec_only_pkg.f_is_cur_cached_cd( return_user, m_application_id,
    --        m_two_factor_cd ) = 'Y' )
    --    THEN
            EXECUTE IMMEDIATE 'SET ROLE ' || m_app_role;
    --    ELSE
    --        RAISE NO_DATA_FOUND;
    --    END IF;
        app_sec_pkg.p_log_error( 0, 'Success getting SSO and setting role, ' ||
            SYS_CONTEXT( 'USERENV', 'OS_USER' ) );
    ELSE
        app_sec_pkg.p_log_error( 0, 'Problem getting SSO, ' ||
            SYS_CONTEXT( 'USERENV', 'OS_USER' ) );
    END IF;
EXCEPTION
    WHEN OTHERS THEN
        m_err_no := SQLCODE;
        m_err_txt := SQLERRM;         app_sec_pkg.p_log_error( m_err_no, m_err_txt,
            'APPSEC p_check_role_access' );
END p_check_role_access;
/`

请注意,不再需要双因素代码,所有与处理双因素身份验证相关的逻辑都已被注释。我们将该逻辑转移到应用验证后立即发生,即appver用户连接。在控制初始应用访问之后,appver的工作是向应用提供一个连接字符串列表。正是在这个过程中,调用一个新的过程,P_GET_APP_CONNS,我们将做双因素认证。

如果我们通过了 SSO,那么我们继续将角色设置为我们在v_application_registry中查找的值。如果用户的连接/会话未能通过我们的 SSO 要求,那么我们将记录错误“获取 SSO 时出现问题”,并在没有设置角色的情况下返回。那个用户有严重问题,我们根本不想和他打交道。

将动态安全应用角色过程投入使用

作为所有应用获得操作所需权限的一站式服务,p_check_role_access需要所有应用都可以执行。我们将授权PUBLICp_check_role_access执行。这个过程用AUTHID CURRENT_USER运行,所以它不能直接访问appsec模式中的数据;然而,这个过程凭借其在appsec模式中的定义,可以执行appsec模式中的其他函数和过程,比如提供所需数据的f_get_app_role辅助函数。以下是我们授予的执行权限:

GRANT EXECUTE ON appsec.p_check_role_access TO PUBLIC;

我们将添加一些额外的审计,因为我们想看看在这个过程中是否有错误的趋势。我们可以将这个审计信息与v_appsec_errors日志视图中的信息结合起来。例如:

AUDIT EXECUTE ON appsec.p_check_role_access     BY ACCESS WHENEVER NOT SUCCESSFUL;

images 注意您可以在名为 Chapter10/SecAdm.sql 的文件中找到包含上述语句的脚本。

你可能还记得,当我们最初在第二章的中创建hrview_role时,我们指定它将是IDENTIFIED USING appsec.p_check_hrview_access。我们将不得不改变它的方向,通过新的程序来识别它。我们将删除该角色并重新创建它。此外,我们需要重复我们对该角色的授权,如清单 10-7 所示。

清单 10-7。重新创建由新程序确定的人力资源视图角色

`DROP ROLE hrview_role;
CREATE ROLE hrview_role IDENTIFIED USING appsec.p_check_role_access;

GRANT EXECUTE ON hr.hr_sec_pkg TO hrview_role;`

重写和重构方法来分发双因子代码

我们将使用distribute2Factor()方法再进行一次传递。我们需要在几个地方合并application_id。当我们在这里的时候,我们也将重构代码,使之更安全、更有条理。

如果你回顾上一章中的这个方法,你会看到我们有两个动态查询:一个查询我们构建来从HR.emp_mobile_nos表和其他表中获取数据,另一个查询我们构建来更新v_two_fact_cd_cache视图。为了安全起见,我们更喜欢参数化的过程和函数,而不是动态查询。这种方法和这些动态查询在 Oracle 数据库中运行,不太可能受到 SQL 注入的影响,但是我们应该考虑这种可能性。在这些查询中执行 SQL 注入需要什么?

第一个查询有两个参数。它使用oraFmtSt字符串来格式化日期,这是本地定义的——这是防篡改的。它还采用了从f_send_two_factor传递来的osUser名称,而这个名称又是从我们的安全应用角色过程传递来的,并且是从我们的 SSO 进程中派生出来的。我可以推测,为了在动态查询中实现 SQL 注入,用户必须在操作系统中有一个极其奇怪的用户名——这不太可能。

第二个查询(更新或插入)采用我们在本地生成的双因素代码(防篡改)、Oracle 数据库感知的 IP 地址(在我们最疯狂的梦想中只是怀疑)和雇员 ID,后者是我们从HR表中获得的,必须满足严格的类型约束NUMBER(6)。再说一次,这不是 SQL 注入的候选人。

因此,我们将这些查询从 Java 代码中转移到存储过程中的动机,并不能作为反对 SQL 注入的理由。在任何情况下我们都会这样做,因为在存储过程中包含数据库逻辑使得我们的 Java 代码对数据组织的依赖性更小,对数据库更改的容忍度更高。如果 DBA 或我们的应用安全经理要求更改或移动数据表,我们可以修改过程来适应这些更改,而不需要更改 Java 代码。我们希望数据库更改只影响本机数据库结构,而不是 Java。

获取双因子代码交付的员工地址的程序

我们构建一个过程来获取寻呼机、电话和其他号码,我们将使用这些号码来分发我们的双因素身份验证代码。同时,通过一个简单的查询,我们可以获得员工的电子邮件地址和会话的 IP 地址。我们还将获得这个应用上这个用户的缓存的双因素身份验证代码,以及缓存的时间戳。根据用户 ID 和应用 ID 一次获得所有这些数据元素,为我们所有的双因素代码分发测试和交付提供了足够的数据。

看看清单 10-8 中的参数列表。你会看到它们大部分是OUT参数——我们正在返回大量数据。我们只向这个过程传递三个参数:用户 ID、我们在上一章讨论的日期格式字符串和我们在本章介绍的应用 ID。我们将把p_get_emp_2fact_nos放在appsec_only_pkg包中。

清单 10-8。获取双因子码分配地址的过程

PROCEDURE p_get_emp_2fact_nos(         os_user               hr.v_emp_mobile_nos.user_id%TYPE,         fmt_string            VARCHAR2,         m_employee_id     OUT hr.v_emp_mobile_nos.employee_id%TYPE,         m_com_pager_no    OUT hr.v_emp_mobile_nos.com_pager_no%TYPE,         m_sms_phone_no    OUT hr.v_emp_mobile_nos.sms_phone_no%TYPE,         m_sms_carrier_url OUT hr.v_sms_carrier_host.sms_carrier_url%TYPE,         m_email           OUT hr.v_employees_public.email%TYPE,         m_ip_address      OUT v_two_fact_cd_cache.ip_address%TYPE,         m_cache_ts        OUT VARCHAR2,         m_cache_addr      OUT v_two_fact_cd_cache.ip_address%TYPE,         **m_application_id**      v_two_fact_cd_cache.application_id%TYPE,         m_err_no          OUT NUMBER,         m_err_txt         OUT VARCHAR2 )     IS BEGIN         m_err_no := 0;         SELECT e.employee_id, m.com_pager_no, m.sms_phone_no, s.sms_carrier_url,             e.email, SYS_CONTEXT( 'USERENV', 'IP_ADDRESS' ),             TO_CHAR( c.cache_ts, fmt_string ), c.ip_address         INTO m_employee_id, m_com_pager_no, m_sms_phone_no, m_sms_carrier_url,             m_email, m_ip_address, m_cache_ts, m_cache_addr         FROM hr.v_emp_mobile_nos m, hr.v_employees_public e,             hr.v_sms_carrier_host s, v_two_fact_cd_cache c         WHERE m.user_id = os_user         AND e.employee_id =  m.employee_id         AND s.sms_carrier_cd (+)=  m.sms_carrier_cd         AND c.employee_id (+)= m.employee_id **        AND c.application_id (+)= m_application_id;**     EXCEPTION         -- User must exist in HR.V_EMP_MOBILE_NOS to send 2Factor, even to email         WHEN OTHERS THEN             m_err_no := SQLCODE;             m_err_txt := SQLERRM;             appsec.app_sec_pkg.p_log_error( m_err_no, m_err_txt,                 'app_sec_pkg.p_get_emp_2fact_nos' );     END p_get_emp_2fact_nos;

清单 10-8 过程中的查询与我们在distribute2Factor()方法的前一版本中的选择查询几乎相同。唯一不同的是增加了最后一行:AND c.application_id (+)= m_application_id。添加后,我们将从双因素缓存中为该用户进行选择,并且仅针对特定的应用 ID。同样,(+)符号表示一个外部连接,所以我们将返回主要数据(例如,寻呼机号码),即使缓存中还没有这个用户和这个应用的双因素代码。

更新双因子代码缓存的存储过程

第二个 Oracle 语句是我们在distribute2factor()中用于更新双因素缓存的语句,我们将动态 SQL 从 Java 迁移到 Oracle 存储过程中。本程序的参数主要是IN参数。我们传入为其生成双因子代码的用户 ID 和应用 ID。我们还传入了双因子代码和分发代码,这是一个数值,表示哪些路由用于分发双因子代码。清单 10-9 展示了这个程序。我们将把p_update_2fact_cache放在appsec_only_pkg包中。

清单 10-9。更新双因子代码缓存的程序

    PROCEDURE p_update_2fact_cache(         m_employee_id        v_two_fact_cd_cache.employee_id%TYPE,         m_application_id     v_two_fact_cd_cache.application_id%TYPE,         m_two_factor_cd      v_two_fact_cd_cache.two_factor_cd%TYPE,         m_distrib_cd         v_two_fact_cd_cache.distrib_cd%TYPE,         m_err_no         OUT NUMBER,         m_err_txt        OUT VARCHAR2 )     IS         v_count          INTEGER;     BEGIN         m_err_no := 0;         **SELECT COUNT**(*) INTO v_count             FROM v_two_fact_cd_cache             WHERE employee_id = m_employee_id             AND application_id = m_application_id;         IF v_count = 0 THEN             **INSERT** INTO v_two_fact_cd_cache( employee_id, application_id,                 two_factor_cd, distrib_cd ) VALUES             ( m_employee_id, m_application_id, m_two_factor_cd, m_distrib_cd );         ELSE             **UPDATE** v_two_fact_cd_cache SET two_factor_cd = m_two_factor_cd,                 ip_address = SYS_CONTEXT( 'USERENV', 'IP_ADDRESS' ),                 distrib_cd = m_distrib_cd, cache_ts=SYSDATE             WHERE employee_id = m_employee_id             AND application_id = m_application_id;         END IF;     EXCEPTION         WHEN OTHERS THEN             m_err_no := SQLCODE;             m_err_txt := SQLERRM;             appsec.app_sec_pkg.p_log_error( m_err_no, m_err_txt,                 'app_sec_pkg.p_update_2fact_cache' );     END p_update_2fact_cache;

您可以看到在这个过程的主体中有三个 Oracle 语句:一个SELECT、一个INSERT和一个UPDATE。如果你还记得,在上一章的distribute2factor()方法中,我们只需要两条语句。在那里,我们试图更新并读取返回的整数的值。如果返回值为 0,那么没有记录受到更新的影响,我们进行了插入。但是,在这里,我们必须手动执行SELECT COUNT,看看是否有记录需要更新。如果计数为 0,我们执行INSERT,否则我们执行UPDATE

在这些语句中,您可以看到我们正在处理与用户(雇员)id 和应用 ID 相关联的记录。对于多个应用,用户可能在缓存中有多个双因素代码。

更改分配双因子代码的方法

OracleJavaSecure.distribute2Factor()方法的变化主要与调用和接收来自p_get_emp_2fact_nosp_update_2fact_cache的输出参数有关。另一个增加的是对applicationID参数的接收和使用,如清单 10-10 所示。我们不打算详细讨论我们对新程序的调用—您以前已经看到过类似的调用。

清单 10-10。双因子码分配方法的标题

`    private static String applicationID = null;

public static final String distribute2Factor( String osUser, String applicationID )
        throws Exception
    {
        // Set class static member equal to what passed here from Oracle
        OracleJavaSecure.applicationID = applicationID;`

images 注意你可以在名为chapter 10/orajavsec/Oracle javasecure . Java .的文件中找到刚才描述的代码,你可以在清单 10-10 中看到

我们在这个过程中收到的静态类成员applicationID的设置暗示了一个可能没有被注意到的变化。在上一章中,我们对twoFactorAuth成员有一个小问题:我们在distribute2Factor()方法中生成它,在那里使用它,并把它交给distribToSMS()distribToPagerURL()distribToEMail()方法。此外,我们在客户机的命令行上输入它,把它交给我们的安全应用角色过程,并最终把它交给f_is_cur_cached_cd函数。因此,我们有两个不同的切入点——这将继续下去。问题是在OracleJavaSecure中,我们有两个不同的引用;这是不必要的,因为无论是在 Oracle 数据库上生成以供分发,还是在客户机上输入以返回 Oracle 数据库进行测试,我们都可以称之为同一件事。将该成员移动到静态类成员并有一个标准的引用位置对我们有利。然后我们可以停止在方法间传递它,只在每个方法中局部引用它。

我们还有几个静态类成员,我们将在本章中使用,第一个是在这里的distribute2Factor()方法中使用的。你可以看到我们将applicationID传递给这个方法,并用它来设置一个静态类成员。我们会在很多地方提到它,感谢我们不必在方法参数中列出它,并在方法间传递它。

我们将applicationID传递给distribute2Factor()方法,因为这是在 Oracle 数据库上调用的第一个 Java 方法。在客户端,applicationID由用户输入,并从使用我们服务的特定应用传递给OracleJavaSecure。我们将在本章后面讨论这个过程。

更新为双因子分配格式

现在,因为我们将为多个应用向用户发送双因素身份验证代码,所以我们必须在发送的消息中识别它们。我们将使用应用 ID 来识别代码用于什么应用。我们将在主题行和邮件正文中放置“for APPID”字样,其中“APPID”是我们引用的特定应用 ID。这里有一个例子。

From: response@org.com To: 8005551212@txt.att.net Subject: Response **for HRVIEW** 1234-5678-9012 **for HRVIEW**

申请授权概述

我们将完全使用 Oracle 数据库作为后端来实现这一点,当然是使用 Oracle JVM 来运行。这意味着我们必须处理一个重要的安全问题。为了与 Oracle 数据库进行对话,我们需要使用用户名和密码进行连接。我们将花大量时间解决这个问题。

简而言之,我们将如何进行应用授权,我们也称之为应用验证:

  1. 我们将首先通过新用户appver.代理连接到 Oracle
  2. 与过去一样,我们需要通过 SSO 和双因素身份认证要求。
  3. 一旦我们获得了双因素认证,我们就交换加密密钥。
  4. 我们还检索了一个加密的连接字符串列表,可以在当前的应用中使用。
  5. 当我们使用其中一个应用连接字符串时,我们再次保证我们的 SSO,并且我们交换额外的加密密钥来查询加密的数据,就像以前一样;但是,我们不会重新进行双因素身份认证。

所有这些看起来都很好,但是问题出现了:我们如何知道正在使用什么应用?嗯,我们当然不想仅仅相信应用的话。安全计算的一个标准假设是,只要有机会,黑客的代码就会说谎——一种应用身份盗窃。我们正在向已知的应用分发连接字符串列表,包括用户名和密码。它们是加密的,但我们不想把它们交给任何人。

为了确保一个应用是它所说的那个人,我们将要求应用给我们一个它自己的片段,一个我们已经收到并注册的片段。我们将比较应用提供的内容和我们注册的内容,如果它们“相等”,那么我们就认可该应用。

我们要求呈现的应用部分是一个内部类。这个类必须与我们注册的类同名。我们可能会注册同一个内部类的多个版本,以处理应用升级,但是连接字符串需要重新创建或复制到新版本。

当我说我们保证内部的类是“平等的”时,这就是试金石。默认的相等测试是类是等价的,我们没有覆盖这个测试;它们驻留在相同的内存地址,并且是同一个类。我们也可以说class1 == class2

在 JVM 中,类是在内存中建模的,但是只需要内存中的一个地方来建模一个特定的类。可以有多个实例,但它们都使用相同的模型。当我们说class1.equals( class2 )的时候,类必须是同一个模型。我将给你更多的启示:如果这些类(应用提供的和我们已经注册的)声称是相同的——相同的包和相同的类名——但不是,那么甚至在我们到达.equals()方法之前,我们将看到一个异常。当我们试图基于手头的序列化对象反序列化一个Class时,如果它与我们已经实例化的对象不同,那么它看起来就像一个试图装进圆孔的方栓。具体来说,抛出一个InvalidClassException。一个 JVM 只能容纳一个名称到类实例的映像关系。尝试引入另一个不同的同名类,JVM 会拒绝它。

让我委婉地告诉你,你可能会发现需要改进的地方。改进它的一个显而易见的方法是在服务器上运行我们的应用,也许是作为 web 应用,这样我们就不需要处理客户端应用认证。但是,即使在这种情况下,您可能仍然希望对服务器应用进行应用身份验证,以便将 Oracle 连接字符串限制在特定的应用中。

用户申请授权

我们需要一个看门人。他将是我们的保镖,拒绝痞子和麻烦制造者。我们将称他为我们的应用验证,appver用户。在这个类比中,应用(不是人)是我们的客户。一些应用是允许的,因为它们已经预先注册并出示了身份,提交并通过了身份检查。

除了守卫入口之外,appver还为每一位成功的参赛者提供进入允许他们进入的内门所需的钥匙。同样,在我们的类比中,键是 Oracle 连接字符串,其中包含连接到 Oracle 数据库的用户 id 和密码。这些不是个人用户和密码,而是应用密码,允许安全的应用角色访问应用数据。

就像看门人一样,appver对所有人都是可用的,并且有足够的信息来给予通行和方向。必要时,他也有足够的权力阻止通行。

有限制和无限制的新档案

通常,出于安全考虑,我们会限制用户帐户,但是在设置应用授权时,我们需要一些灵活性。我们将通过一个独特的概要文件appver_prof为这个账户分配限制和权限,如清单 10-11 中的所示。

清单 10-11。申请授权概要

CREATE PROFILE appver_prof LIMIT     CONNECT_TIME          1     IDLE_TIME             1     SESSIONS_PER_USER     UNLIMITED     PASSWORD_LIFE_TIME    UNLIMITED     FAILED_LOGIN_ATTEMPTS UNLIMITED;

images 注意你可以在名为 Chapter10/SecAdm.sql 的文件中找到这个脚本。

与我们的普通标准相反,我们的应用验证用户将拥有一个永不过期的密码。我们还将允许该用户输入错误的密码不限次数,而不锁定它(防止它进一步尝试登录,即使使用正确的密码)。此外,我们将允许该帐户无限数量的并发会话。

我知道这听起来很不安全,但我们有可能有数百人试图访问应用,这一点得到了该用户的证实。我们需要严格控制该密码何时过期—我们将定期手动重置密码—可能需要向客户端分发一些文件。但是,我们不希望它自动过期。此外,我们不知道有多少人将同时授权申请,因此无限的会话。也许以后,一旦我们有了一些历史,我们就会知道正确的会议次数是多少。

当然,我们可以说无限制的会话,但实际上是有限制的。硬限制是创建数据库所服务的进程的数量。默认的进程数是 150。展望未来,当我们在第十一章中为应用认证创建一个专用数据库时,我们将把进程的数量增加到 500。

无限制的失败登录尝试有点难以证明,因为这给了黑客使用暴力攻击来猜测密码的机会。然而,另一种选择是,可能会出现一系列中断的应用。如果黑客或错误的应用多次尝试使用错误的密码登录并锁定用户,所有依赖该用户进行授权的应用都会失败,直到帐户被重置。

我们将尽可能地限制这个账户。我们只会给它一些特权。它需要通过单点登录、双因素身份认证、加密和应用授权,这也是它存在的原因。我们将为此用户设置的几个限制将通过appver_prof配置文件中的参数来设置。我们将只允许这个帐户有一分钟的连接时间,和一分钟的空闲时间(最小值)。

申请验证用户

是时候创建我们的应用验证用户了,我们将其命名为appver。清单 10-12 显示了创建 appver 用户的命令。我们给这个用户分配了一个密码,但是现在,我们把它看得更像一行代码或者一个地址。它把人们带到工作场所,但本身并不做任何工作。现在,这个密码将被硬编码到客户端的OracleJavaSecure类中。在第十一章中,我们将对密码进行混淆和加密。无论如何,请给appver分配一个复杂的密码。

清单 10-12。创建申请验证用户

`CREATE USER appver
    IDENTIFIED BY password
    QUOTA 0 ON SYSTEM
    PROFILE appver_prof;

GRANT create_session_role TO appver;`

请注意,我们将appver_prof配置文件分配给了appver。我们也没有给appver空间,0 个存储配额。最后,我们将create_session_role授予appver,这样他就可以连接到 Oracle 数据库。

应用验证登录触发器

我们将为appver帐户创建一个登录触发器。我们已经看到了数据库触发器,但这一个是不同的——它定义了每当用户登录到appver模式时我们将让 Oracle 数据库采取的操作。清单 10-13 显示我们的登录触发器简单地调用了一个过程p_appver_logon

清单 10-13。应用验证登录触发

CREATE OR REPLACE TRIGGER secadm.t_screen_appver_access AFTER LOGON ON appver.SCHEMA BEGIN     appsec.**p_appver_logon;** END; /

申请验证登录流程

我们的appver登录触发器p_appver_logon的程序如清单 10-14 所示。如果我们能在登录触发期间完成完整的 SSO 检查就好了,但是可惜的是,代理会话和USERENV中的CLIENT_IDENTIFIER设置在登录时不可用。但是,我们仍然可以保证会话用户是appver(还能是谁?),并且我们的 IP 地址是可以接受的。我们还调用函数f_is_user来确保os_user也是数据库用户。这可能是最重要的测试,因为这也是我们将确保我们的代理登录。

清单 10-14。应用验证登录流程

CREATE OR REPLACE PROCEDURE appsec.p_appver_logon AUTHID CURRENT_USER AS     just_os_user    VARCHAR2(40);     backslash_place NUMBER; BEGIN     just_os_user := UPPER( SYS_CONTEXT( 'USERENV', 'OS_USER' ) );     backslash_place := INSTR( just_os_user, '\', -1 );     IF( backslash_place > 0 )     THEN         just_os_user := SUBSTR( just_os_user, backslash_place + 1 );     END IF;     -- For logon trigger - **limited SSO**, no PROXY_USER and no CLIENT_IDENTIFIER     IF( SYS_CONTEXT( 'USERENV', 'SESSION_USER' ) = 'APPVER'     AND( SYS_CONTEXT( 'USERENV', 'IP_ADDRESS' ) LIKE '192.168.%' OR         SYS_CONTEXT( 'USERENV', 'IP_ADDRESS' ) = '127.0.0.1' )     -- Requirements must be applicable to all applications - time may not be     --AND TO_CHAR( SYSDATE, 'HH24' ) BETWEEN 7 AND 18     -- Assure that OS_USER is a database user     AND( appsec_only_pkg.**f_is_user( just_os_user )** = 'Y' ) )     THEN         app_sec_pkg.p_log_error( 0, 'Success APPVER logon, ' || just_os_user );     ELSE         app_sec_pkg.p_log_error( 0, 'Problem getting APPVER logon, ' || just_os_user );         --**just_os_user := sys.f_get_off;**         -- This causes logon trigger to fail -- so not connected to Oracle         **RAISE_APPLICATION_ERROR**(-20003,'You are not allowed to connect to the database');     END IF; END p_appver_logon; /

我们以AUTHID CURRENT_USER的身份运行这个登录触发过程;这是发票人的权利。这是我们能够准确衡量用户身份的唯一方法——类似于安全应用角色过程的方法。因此,我们需要授权PUBLIC执行这个过程:

GRANT EXECUTE ON appsec.p_appver_logon TO PUBLIC;

下车功能

总有一种想要完全掌控的渴望。如果能够在我们的登录触发器中发现问题并立即终止会话,那就太好了。你看到清单 10-14 中的p_appver_logon、登录触发程序中的注释行just_os_user := sys.f_get_off了吗?这是一个行不通的好主意。清单 10-15 中的函数可以为我们完成任务;但是,Oracle 数据库不允许我们终止当前会话。

清单 10-15。失效的断路开关,f_get_off

`CREATE OR REPLACE FUNCTION sys.f_get_off
RETURN VARCHAR2
AS
    PRAGMA AUTONOMOUS_TRANSACTION;
    p_sid vsession.SIDpserialvsession.serial#%TYPE;
BEGIN
    p_sid := SYS_CONTEXT( 'USERENV', 'SID' );
    SELECT serial# INTO p_serial
    FROM v$session
    WHERE sid = p_sid;
    EXECUTE IMMEDIATE 'ALTER SYSTEM KILL SESSION ''' ||
                 p_sid || ',' || p_serial || '''';
    RETURN 'OFF';
END f_get_off;

GRANT EXECUTE ON sys.f_get_off TO appsec;`

images 注意你可以在名为 Chapter10/Sys.sql 的文件中找到清单 10-15 的函数。

该功能的核心在EXECUTE IMMEDIATE命令中。按照这些思路,另一个可行的方法是,我们将被杀死的SIDSERIAL#插入到一个表中。然后使用一个独立的预定任务,读取表并终止会话,然后从表中删除记录。

反正用最直接的方式粗暴对待是行不通的。这样也好,因为我们对p_appver_logonRAISE_APPLICATION_ERROR的调用做了同样的事情。因为我们在登录触发器中,所以当我们引发异常时,登录会失败。

查找数据库用户的功能

我们有一个用于appver登录触发过程的函数f_is_user,它测试 OS 用户是否也是数据库用户。这是一个重要的测试,因为它强制执行了我们的一部分 SSO 要求——如果我们没有创建一个与此人的操作系统用户名同名的 Oracle 用户,那么他就不能使用我们的应用。清单 10-16 中的函数将被添加到包appsec_only_pkg中。

清单 10-16。查找数据库用户的功能

    FUNCTION f_is_user( just_os_user VARCHAR2 )     RETURN VARCHAR2     AS         return_char VARCHAR2(1) := 'N';         v_count     INTEGER;     BEGIN         SELECT COUNT(*) INTO v_count         FROM **sys.all_users**         WHERE username = just_os_user;         IF v_count > 0 THEN             return_char := 'Y';         END IF;         RETURN return_char;     END f_is_user;

images 返回文件 Chapter10/SecAdm.sql 供本讨论参考。

请注意,我们从何处获得用户是 Oracle 用户的指示符。我们从 Oracle 数据字典中读取视图,SYS.ALL_USERS。该视图被授予对PUBLIC的选择。我相信这是一个安全问题。如果黑客获得了任何 Oracle 用户帐户的访问权限,他可以读取ALL_USERS视图和获得他可能试图访问的所有用户名的列表。数据字典还有其他视图同样被PUBLIC授予 select,我认为这超越了安全性。另一个特殊的视图是SYS.ALL_SOURCE,它列出了每个模式中每个存储过程的整个主体,以及用户被授权执行的其他代码。让一个黑客看到我们的代码(不管他是一个合法的流氓用户还是一个入侵者)会招致进一步的危害。

当我们在第十一章的中为应用认证创建一个专用数据库时,我们将为这些特别敏感的视图撤销 select by PUBLIC。我们还将从公共访问中删除一些附加视图。

通过应用验证的代理和其他代理

最后,关于我们的应用验证用户,我们需要允许每个用户通过appver进行代理。我们的登录触发器不会看到代理,但是当我们执行过程来获取与当前应用相关的安全应用角色时,我们将使用代理测试作为 SSO 的一部分。例如:

ALTER USER osuser GRANT CONNECT THROUGH appver;

不要忘记为您希望授予应用访问权限的每个操作系统用户创建一个 Oracle 用户。并授权每个用户可以通过appver进行代理。此外,对于每个操作系统用户,授予其帐户可以通过与特定应用相关联的角色进行代理。

例如,要让名为“coffin”的操作系统用户访问hrview应用,您需要执行以下命令:

CREATE USER coffin IDENTIFIED EXTERNALLY; GRANT CREATE_SESSION_ROLE TO coffin; ALTER USER coffin GRANT CONNECT THROUGH APPVER; -- APPUSR is the account that gets access to HRVIEW_ROLE ALTER USER coffin GRANT CONNECT THROUGH APPUSR;

此外,为了让用户 coffin 通过双因素认证,他需要在HR.EMPLOYEESHR.emp_mobile_nos中输入条目。

审核应用验证

知道用户做的每一件事是很好的——应该只有他做的一些声明。然而,我们预计他会被呼叫很多很多次,我们不想审计他做出的所有合法呼叫。当他试图选择数据时,第一个线索就是appver正在做一些非官方的事情。因此,我们将使用审计日志,通过清单 10-17 中未注释的命令来监控这一点。

清单 10-1。,审核申请验证

`--AUDIT ALL STATEMENTS BY appver BY ACCESS; -- WHENEVER SUCCESSFUL;
AUDIT SELECT TABLE BY appver BY ACCESS;

AUDIT EXECUTE PROCEDURE
    BY appver
    BY ACCESS
    WHENEVER NOT SUCCESSFUL;`

此外,当appver未能执行过程时,我们可能会通过审计捕捉到一些应用错误和企图滥用。所以我们在这些调用是NOT SUCCESSFUL的时候进行审计。我们不想审计成功的过程调用,因为我们知道他将调用过程,并且我们希望他成功。

申请授权的结构

我们讨论了appver完成的任务之一——将应用提供的内部类对象与已经注册的对象进行比较。当第一眼看到内部类被插入时(当应用没有现有的注册表项时),就会发生注册。没错:你(一个新应用)第一次出现的时候,我们把你的名字写在留言簿上,保存你提供的内部类。您的应用必须跨越几个障碍才能走到这一步,我们为此给予您信任。然而,在这一点上,它只不过是一张大头照。

为了注册连接字符串,您的应用必须返回相同的标识。从现在开始,您还必须以相同的身份出现,以便将这些连接字符串返回到您的应用。

更大的应用安全空间

因为我们承诺为每个使用这些服务的应用存储几个对象,所以我们应该给appsec模式多一点空间用于存储。执行这个ALTER USER命令来完成:

-- Increase quota to hold app verification data ALTER USER appsec DEFAULT TABLESPACE USERS QUOTA 10M ON USERS;

应用连接注册表

我们将创建一个表(见清单 10-18 )来保存每个应用提供的对象,作为RAW数据类型。它需要小于 2K 才能存储为一个RAW,所以开发人员不应该扩展我们提供给他们的模板类。除了每个对象之外,我们还将存储一个二进制大对象(BLOB)数据类型,其中包含一个相关连接字符串的列表。

正如您可能想象的那样,很难对RAWBLOB类型进行索引和选择。所以我们将类名和类版本作为VARCHAR2数据类型进行索引。这些标识符可以从对象中获得,所以我们从不传递它们:传递对象就足以让我们发现应用声称是谁。

设置 Oracle 连接字符串表(BLOB)不是一次性的;它们可以被更新。所以我们在表中包含了一个update_dt列来跟踪它。默认情况下,我们使用 empty_blob()指令分配 blob 定位器地址,不指向特定的 BLOB,但仍然指向一个地址——而不是 null。

清单 10-18。应用连接注册表

CREATE TABLE appsec.t_app_conn_registry (     class_name      VARCHAR2(2000) NOT NULL,     class_version   VARCHAR2(200) NOT NULL,     class_instance  RAW(2000),     update_dt       DATE DEFAULT SYSDATE,     connections     BLOB DEFAULT EMPTY_BLOB() );

images 注意你可以在名为 Chapter10/AppSec.sql 的文件中找到这个脚本。

我们在class_nameclass_version上创建索引和主键,未显示。我们还创建了该表的一个视图,用于常规参考。

应用的一组连接字符串

在客户端,我们将把连接字符串表作为HashMap来处理,我们称之为connsHash。你必须确保它被标记为private,这样只有OracleJavaSecure级才能看到它。以下是声明:

    private static **HashMap**<String, RAW> connsHash = null;

我们将与 Oracle 数据库交换密钥,因此我们可以接收这个用共享密码密钥加密的连接字符串表。我们将只在需要时解密它们,创建一个连接,然后释放连接字符串用于垃圾收集(不保留对它的类成员引用)。)请注意,在垃圾收集器运行(自动、自动调度)之前,明文连接字符串将在机器内存中,但不容易检索,即使使用调试器也是如此。

一个HashMap是一个Collection类,它有一个键和值的关系。它就像一个包含唯一键和相关值的两列表。键和值都是 Java 对象,而不是原语。在这种情况下,我们的键是Strings,我们的值是RAWs。具体来说,我们的RAW值是应用使用的每个连接字符串的加密形式。这个connsHash的声明使用泛型来要求键是Strings,值是RAW,语法是<String, RAW>

HashMaps可以不用泛型创建;也就是说,不指定键和值的对象类型,但是每次检索键或值时,都需要将其转换为适当的类型。此外,通过指定对象类型,我们确信我们的应用将只把该类型的对象放在我们的键和值字段中。否则,HashMaps可以一次容纳多种Object类型。

代表应用的内部类

我们的应用将通过传递一个实现特定接口的对象向这个应用认证过程标识自己:接口RevLvlClassIntfc。一个接口就像一个类,但是它是空心的——它没有内部结构。一个接口可以指定一系列方法,所有实现该接口的类都需要实现这些方法;也就是说,它们需要具有相同签名的方法(相同的名称、参数和返回类型)。希望他们也能提供完成任何需要的胆量或功能代码。

RevLvlClassIntfc接口在 orajavsec 包中,就像OracleJavaSecure一样。也像OracleJavaSecure一样,接口需要存在于 Oracle 数据库和客户机中。RevLvlClassIntfc需要加载到 Oracle 中,如清单 10-19 的第一行所示,并且需要与 OracleJavaSecure.class 一起分发给开发人员,很可能在同一个 jar 文件中。

清单 10-19。修改等级类接口

`//CREATE OR REPLACE AND RESOLVE JAVA SOURCE NAMED appsec."orajavsec/RevLvlClassIntfc" AS
package orajavsec;

public interface RevLvlClassIntfc {
    //private static final long serialVersionUID = 2011010100L;
    //private String innerClassRevLvl = " 20110101a";
    public String getRevLvl();
}`

images 注意你可以在名为chapter 10/orajavsec/revlvlclassintfc . Java .的文件中找到这段代码

接口中有一个方法,getRevLvl()。这将返回一个修订级别,innerClassRevLvl,我们可以用它来支持一个应用的多个版本。例如,如果您将应用数据移动到新的 Oracle 表或新的 Oracle 数据库,您可以更新内部类中的innerClassRevLvl,并使用我们的进程注册更新的应用,并生成一组与之相关的新连接字符串。旧的应用/版本和新的应用/版本将能够同时获得它们各自的连接。此外,为了强制每个人迁移到应用的新版本并禁用旧版本,我们可以从应用注册表中删除旧版本的内部类,或者只删除相关的连接字符串列表。

我们将要求该接口的实现者(开发人员)也提供一个名为serialVersionUIDstatic long,用于对象序列化(打包以存储在数据库中并通过网络传输)。)我们稍后将对此进行更多讨论。

在 OracleJavaSecure 中实现内部类

出于本章测试的目的,我们将在实现RevLvlClassIntfcOracleJavaSecure类中定义一个内部类InnerRevLvlClass。要生成内部类,只需将类定义包含在现有的类定义中。在我们的例子中,它看起来像清单 10-20 中的。在OracleJavaSecure的定义体中,我们声明了内部类InnerRevLvlClass

清单 10-20。用于身份和版本控制的 OracleJavaSecure 内部类,InnerRevLvlClass

**public class OracleJavaSecure** { … **    public static class InnerRevLvlClass**         implements **Serializable**, RevLvlClassIntfc     {         private static final long serialVersionUID = 2011010100L;         private String innerClassRevLvl = "20110101a";         public String getRevLvl() {             return innerClassRevLvl;         }     }

images 注意在本章余下的大部分时间里,我们将反复引用名为chapter 10/orajavsec/Oracle javasecure . Java、的文件中的 Java 代码,以及名为 Chapter10/AppSec.sql 的文件中的 SQL 和 PL/SQL 代码。

注意,我们的内部类被指定为public staticstatic不是必需的,但是它向我们保证当父类OracleJavaSecure被实例化时,这个类是存在的。另一方面,从安全角度来看,public标志意义重大。我们更愿意将这个内部类私有,我们可以为OracleJavaSecure.InnerRevLvlClass这样做。但是对于其他独立的应用,内部类需要是公共的,以便 Oracle 数据库上的OracleJavaSecure可以实例化它们。

我们的内部类也必须实现Serializable接口(我们提供给开发人员的模板的另一个必需元素。)通过实现Serializable(不需要任何方法),我们确保可以获取内部类,获取对象的字节,并将它们传递给 Oracle 数据库进行存储。如果对象实现了Serializable接口,我们只能用那种方式处理对象。

应用交给我们的类的serialVersionUID必须与 Oracle 数据库中的类定义的serialVersionUID相同(这个讨论将在下一节继续)。我们为serialVersionUID设置了一个静态成员变量,但是如果我们没有这个成员,JVM 会计算一个值。如果该值不相同,则类无法实现,对象无法实例化,并将引发异常。大多数情况下,除非我们对类进行结构性修改,否则计算出的serialVersionUID将是相同的,但是“大多数情况下”的统计数据会带来我们不愿意承担的风险。

注意,我们为innerClassRevLvlserialVersionUID提供的值几乎是我们想要的任何值;然而,我认为对于任何一个日期具有多值灵活性的日期形式将会是一个好的值。如果我在 2012 年 2 月 14 日发布一个新的应用,我可能会给出这些值:

        private static final long **serialVersionUID** = 2012021400L;         private String innerClassRevLvl = "20120214a";

如果我在当天晚些时候发布了一个修订版,它可能会有这些更改的值(仅作为示例,这些值通常会因为不同的原因而被单独修改):

        private static final long **serialVersionUID** = 2012021401L;         private String innerClassRevLvl = "20120214b";

这可能是显而易见的,但是serialVersionUID的值末尾的L是数字的一部分,表明它是一个long值。这是一种将默认情况下被解释为整数的值转换为另一种类型的基元值的方法。使用大写L避免将小写l (ell)与1 (one)混淆。还有其他的文字值转换,由后缀(例如f表示浮点)和前缀(例如0x表示十六进制)组成。需要的话拿一个。

反序列化和版本 UID

在序列化之后恢复对象的生命需要反序列化。但是要做到这一点,有一个很大的要求 JVM 中必须存在对象的类定义。这是序列化文档中经常忽略或假定的一个要求。通常,应用负责序列化对象,等效的应用处理反序列化。应用有一个类定义,可能在一个单独的中。class 文件,或者作为内部类,因此它已经知道如何构建被反序列化的类型的对象。序列化对象不包含类定义的所有细节;相反,应该考虑在序列化时只保留对象的数据或状态。为了在 JVM 中反序列化对象,现有的类定义必须提供一个框架,在这个框架上构建对象的主体。

现在这些对我们来说都有意义了,但是考虑一下这个变化:我们将在 Oracle 数据库中反序列化对象,在一个应用不运行的地方。要做到这一点,我们必须有类定义;因此,对于每个应用,我们将存储内部类定义(我们将在后面看到如何做)。对于内部类OracleJavaSecureInnerRevLvlClass,类定义在 Oracle 数据库中,因为我们在服务器上创建了OracleJavaSecure。对于其他应用,我们必须在 Oracle 数据库上创建内部类。

在对象序列化和反序列化之间的过渡阶段,可能会对类定义进行一些非结构化的更改,Java 编译器和运行时版本可能会继续发展;但是我们应该能够,甚至十年后,反序列化我们的对象。这就是serialVersionUID成员变量发挥作用的地方。在开始使用我们的序列化对象时,根据 suid (当serialVersionUID成员不存在时,该值的另一个名称)的运行时计算,我们可能没有问题。)然而,随着时间的推移和事物的变化,我们可能会开始经历我们序列化的内容和我们试图反序列化的内容之间的不匹配。

我们可以通过简单地编码一个serialVersionUID成员,而不是依赖于运行时计算,来避免串行版本不匹配的问题(由技术进步而不是我们的类的变化引起的)。

如果我们对我们的类定义做了结构性的修改,我们将需要做下面两件事:

  1. 更改我们的类定义中的serialVersionUID值。
  2. 在 Oracle 中存储新版本的类定义。

注意,这种情况不同于我们只在类定义中编码一个新的innerClassRevLvl的情况。我们将这样做来处理应用版本的更改(并提供 Oracle 连接字符串的更新列表。)

设置应用上下文

您可能还记得,在上一节的讨论中,我们将所有特定于应用的元素都集中在一个地方。我们将把applicationIDappClasstwoFactorAuth代码作为静态类成员存储在OracleJavaSecure中,所以我们不需要将它们传递给所有引用它们的方法。

当一个应用最初到达OracleJavaSecure时,它可能做的第一件事就是通过调用setAppContext()方法来标识自己。setAppContext()applicationID、内部类和twoFactorAuth代码作为参数。我们第一次运行客户端应用时,没有提供twoFactorAuth代码。直到用户在他们的手机或其他设备上接收到双因素代码,他们才能够返回并重新进行该识别并继续进行。

为了请求连接字符串,我们必须通过应用验证,appver security guard。这意味着我们必须向应用授权过程展示我们的内部类。在setAppContext()方法中,我们保证我们将要呈现的内部类通过调用instanceof操作符实现了RevLvlClassIntfc接口和Serializable接口。

清单 10-21。设置应用上下文,setAppContext()

`    private static String applicationID = null;
    private static Object appClass      = null;
    private static String twoFactorAuth = null;

public static final void setAppContext( String applicationID,
        Object appClass, String twoFactorAuth )
    {
        twoFactorAuth = checkFormat2Factor( twoFactorAuth );
        if( null == applicationID || null == appClass ) {
            System.out.println( "Must have an application ID and Class" );
            return;
        }
        // Assure the app class has implemented our interface
        if ( !( ( appClass instanceof RevLvlClassIntfc ) &&
            ( appClass instanceof Serializable ) ) )
        {
            System.out.println(
                "Application ID Class must implement RevLvlClassIntfc" );
            return;
        }
        // Set class static member equal to what passed here at outset
        OracleJavaSecure.applicationID = applicationID;
        OracleJavaSecure.appClass      = appClass;
        OracleJavaSecure.twoFactorAuth = twoFactorAuth;
    }`

顺便说一句,我们以前见过这样的方法,其中我们传入与类成员名相同的引用。我们通常用这样的语句设置类成员:

this.varName = varName;

注意这个案例的不同之处。因为我们设置的是静态类成员,所以没有this可以引用——this是对当前实例的引用,但是静态类没有被实例化。所以我们使用这个语法来设置静态类成员变量的方法参数:

Class.varName = varName;

格式化用户输入的双因素代码

我们在上一章介绍了checkFormat2Factor()方法,但是没有讨论它的实现。我们希望确保努力输入双因素代码的用户在格式方面得到一些宽容。这在本章中尤其重要,从现在开始,因为我们将应用 ID 和双因素代码一起发送到用户的手机和其他设备。

我们希望确保我们总是将双因素代码作为我们信息的第一项。如果用户在其后包含应用名称,我们会发现在适当的位置截断双因素代码很容易。如果额外的数据作为单独的参数出现在命令行上,我们会忽略这些额外的参数。

有时,旧式寻呼机可能会从消息中删除非数字字符。作为一般规则,破折号总是包括在内。也许分页器还可以去掉空格并包含下划线字符。为了应对这些紧急情况,并且不增加用户格式需求的负担,我们将在setAppContext()方法中得到的双因素代码传递给清单 10-22 中的checkFormat2Factor()方法。

清单 10-22。格式化用户提供的二元验证码,checkFormat2Factor()

    public static String checkFormat2Factor( String twoFactor ) {         String rtrnString = "";         if( null == twoFactor ) return rtrnString;         // Use only numeric values and insert dash after every 4 chars         StringBuffer sB = new StringBuffer();         int used = 0;         char testChar;         int twoFactLen = twoFactor.length();         for( int i = 0; i < twoFactLen; i++ ) {             **testChar = twoFactor.charAt( i );**             **if( Character.isDigit( testChar ) )** {                 sB.append( testChar );                 **if( sB.length() == twoFactorLength )** {                     rtrnString = sB.toString();                     break;                 }                 // Insert dash if we have accepted a multiple of 4 chars                 used++;                 **if( 0 == ( used % 4 ) ) sB.append( "-" );**             }         }         return rtrnString;     }

该方法读取用户输入的双因子码的每个字符。如果字符不是数字,它将被丢弃。数字字符被附加到一个StringBuffer,并且在每四个字符之后,一个破折号被附加。这一直持续到我们用完输入或者达到所需的双因子码长度twoFactorLength。如果我们有足够的数字字符,我们返回StringBuffer作为String,否则我们返回一个空字符串。

程序员中有一种看法,特别是那些(像我一样)广泛使用 perl 的人,认为检查字符串的格式最好通过使用正则表达式来完成。正则表达式是表达字符模式的简洁方式,包括可选的格式。例如,这个正则表达式表示一个 SSN 模式:“^\\d{3}[- ]?\\d{2}[- ]?\\d{4}$”。我认为模式匹配有其局限性,需要得到承认和尊重。例如,在checkFormat2Factor()中,我们将丢弃用户可能在手机或寻呼机屏幕上看到并输入的无关数据(单词或空格)。模式匹配对于这种自由形式的用户输入不太适用。我对复杂正则表达式的第二个问题是,阅读它们通常需要一个解码环和一张废纸。(一般来说,perl 代码也是如此。)

从客户端角度保存连接字符串

我们将探索如何保存连接字符串,并将它们与客户端和服务器端的应用相关联。在某些情况下,开发人员可能会选择使用除应用授权之外的所有安全性,因此他们将受益于在客户端使用未存储在 Oracle 数据库中的连接字符串的独立方法。这些甚至可以与一组由我们的应用授权维护的连接字符串相结合。

然而,如果开发者选择不在appver的监督下存储他的连接字符串,他将需要找到另一种方法来保护密码。此外,他将失去双因素身份验证,我们已经将这种身份验证委托给了应用授权流程。

将连接字符串放入应用列表的方法

在客户端,我们有一个方法putAppConnString(),我们可以用它来添加连接字符串到connsHash。它有 5 个参数(参见清单 10-23 ):实例名、用户名、密码、主机名和端口(作为一个String)。通过将这些组件分开,它将连接字符串的组装留给了我们。我们可以保证连接字符串格式是可以接受的。我们确实花了一些时间从每个参数的头部和尾部去掉任何空白,调用了String.trim()方法。

作为一个可选参数,通过一个重载方法,我们接受一个boolean值,它可以指导我们在将连接字符串保存到connsHash之前测试它。

我们假设已经格式化的连接字符串可以添加到connsHash;但是,如果我们被指示测试连接字符串,我们可能会更改评估。如果我们正在测试连接字符串,我们简单地基于连接字符串实例化一个新的Connection,并使用它来查询数据库。如果失败,我们确定连接字符串不好。

清单 10-23。将连接字符串放入列表,putAppConnString()**

public static void **putAppConnString**( String instance, String user,         String password, String host, String port )     {         putAppConnString( instance, user, password, host, port, **false** );     }     public static void **putAppConnString**( String instance, String user,         String password, String host, String port, **boolean testFirst** )     {         instance = instance.trim();         user = user.trim();         password = password.trim();         host = host.trim();         port = port.trim();         String **key = (instance + "/" + user).toUpperCase();**         String connS = "jdbc:oracle:thin:" + user + "/" + password + "@" +             host + ":" + port + ":" + instance;         boolean **testSuccess = true;**         **if( testFirst )** {             Connection mConn = null;             try {                 mConn = DriverManager.getConnection( connS );                 Statement stmt = mConn.createStatement();                 ResultSet rs = stmt.executeQuery(                     "SELECT SYSDATE FROM DUAL" );                 System.out.println( "Connection string successful" );             } catch( Exception x ) {                 System.out.println( "Connection string failed!" );                 **testSuccess = false;**             } finally {                 try {                     if( null != mConn ) mConn.close();                 } catch( Exception x ) {}             }         }         **if( testSuccess )** {             try {                 appAuthCipherDES.init( Cipher.**ENCRYPT_MODE**,                     appAuthSessionSecretDESKey, appAuthParamSpec );                 byte[] bA = appAuthCipherDES.doFinal( connS.getBytes() );                 **connsHash.put(key, new RAW( bA ) );**             } catch( Exception x ) {}         }     }

最后,如果我们决定喜欢这个连接字符串(testSuccesstrue,我们就把它添加到connsHash。并且,就像我们(可能)从 Oracle 收到的连接字符串一样,我们基于从 Oracle 数据库获得的共享密码密钥,以加密的形式将其存储在这里。

HashMapconnsHash的关键字是字符串“INSTANCE/USER ”,这是我们根据提供给我们的实例名和用户名组合而成的。如果我们使用与connsHash中现有条目相同的实例和用户调用这个方法,新的连接字符串将覆盖旧的(假设它是可接受的)。

在 Oracle 上存储连接字符串列表的客户端调用

一旦我们填写了我们的应用想要使用的一个或多个连接字符串的等级,我们就可以调用putAppConnections()方法将connsHash提交给 Oracle 数据库。我们在的清单 10-24 中看到了这种方法。

再次注意,我们需要已经与 Oracle 数据库交换了密钥。当然,我们有,否则我们将无法从 Oracle 获得先前存在的connsHash,也无法向connsHash添加新的连接字符串——我们将没有任何东西要提交。

我们将确保与appver有一个我们可以使用的连接。我们测试appVerConn是否是null,一个我们用来与appver对话的连接的引用。如果是null,则存在两个问题之一:要么我们还没有连接为appver(调用getAppConnections()),要么我们已经覆盖了我们到appver的连接,因此它不再可用于执行更新putAppConnections()。解决方案是,如果需要,在从列表中获取一个连接字符串用于应用之前,总是调用putAppConnections()

把一个 Java 对象转换成字节数组是小事一桩,只要它实现了Serializable。然而,它一开始看起来确实有点令人生畏。深入这段代码,我们将我们的对象写入一个ObjectOutputStreamooutObjectOutputStream直接连接到ByteArrayOutputStreambaos。在我们写完我们的对象后,我们刷新oout并关闭它,确保我们的整个对象都被提交到baos。此时,我们调用baostoByteArray()方法,以字节数组的形式获取对象。

清单 10-24。在 Oracle 中存储连接字符串列表,putAppConnections()

`public static void putAppConnections(){
        OracleCallableStatement stmt = null;
        try {
            if( null == appVerConn ) {
                if( null == conn ) {
                    System.out.println( "Call getAppConnections to establish " +
                        "connection to AppVer first, " +
                        "else can not putAppConnections!" );
                } else {
                    System.out.println( "Connection to AppVer overwritten - " +
                        "can not putAppConnections!" );
                }
                return;
            }

ByteArrayOutputStream baos = new ByteArrayOutputStream();
            ObjectOutputStream oout = new ObjectOutputStream( baos );
            oout.writeObject( appClass );
            oout.flush();
            oout.close();
            byte[] appClassBytes = baos.toByteArray();
            baos.close();

baos = new ByteArrayOutputStream();
            oout = new ObjectOutputStream( baos );
            oout.writeObject( connsHash );
            oout.flush();
            oout.close();
            byte[] connsHashBytes = baos.toByteArray();
            baos.close();

stmt = ( OracleCallableStatement )conn.prepareCall(
                "{? = call appsec.appsec_public_pkg.f_set_decrypt_conns(?,?)}" );
            stmt.registerOutParameter( 1, OracleTypes.VARCHAR );
            stmt.setBytes( 2, appClassBytes );
            stmt.setBytes( 3, connsHashBytes );
            stmt.executeUpdate();             String checkReturn = stmt.getString( 1 );
            if( ! checkReturn.equals( okReturnS ) )
                System.out.println( checkReturn );
        } catch ( Exception x ) {
            x.printStackTrace();
        } finally {
            try {
                if ( null != stmt )
                    stmt.close();
            } catch ( Exception y ) {}
        }
    }`

我们在应用内部类appClassconnsHash HashMap对象上执行同样的操作,后者也实现了Serializable(就像connsHash保存的String键和RAW值一样)。

我们将应用类和connsHash作为字节数组提交给 Oracle 数据库。我们将它们发送给f_set_decrypt_conns Java 存储过程(函数)。该函数仅仅调用 Oracle 数据库端的 Java,将这些对象传递给 Oracle 数据库上的setDecryptConns()方法,这将在接下来的小节中详细讨论。

从服务器角度保存连接字符串

在 Oracle 数据库方面,我们经历了一个相当复杂的过程,以便在存储连接字符串之前将连接字符串的connsHash表变成我们想要的形状。您可以看到,当我们将它们提交给 Oracle 时,它们是用该会话独有的密码密钥加密的。如果我们按原样存储它们,我们将永远无法在会话结束后解密它们;因此,我们将在存储到 Oracle 数据库之前对它们进行解密。当我们到达第十一章时,我们将探索并应用加密到磁盘上的数据。然而,解密过程之前有一个同样复杂的过程,以确保提交的应用类适合于覆盖注册表中的现有条目或插入新条目。

调用 Java 解密连接字符串列表的函数

从客户端,我们调用 Oracle 数据库上的 Java 存储过程f_set_decrypt_conns来传递连接HashMapf_set_decrypt_conns简单地将对象传递给 Oracle 数据库上的 Java 代码进行处理。Java 存储过程可以简单地视为传递数据的入口,以及 Oracle 数据库上 Java 方法的调用。我们将这个函数添加到一个新的包中,appsec_public_pkg,见清单 10-25 。

清单 10-25。解密连接字符串列表的函数调用,f_set_decrypt_conns

`CREATE OR REPLACE PACKAGE BODY appsec.appsec_public_pkg IS

FUNCTION f_set_decrypt_conns(
        class_instance RAW, connections RAW )
    RETURN VARCHAR2
    AS LANGUAGE JAVA     NAME 'orajavsec.OracleJavaSecure.setDecryptConns( oracle.sql.RAW, oracle.sql.RAW ) return
java.lang.String';

...

GRANT EXECUTE ON appsec.appsec_public_pkg TO PUBLIC;`

现在,我们将通过appver代理各种用户向f_set_decrypt_conns提交connsHash,因此我们将在包appsec_public_pkg上向PUBLIC授予 execute。

显然,我们不希望任何人都向此函数提交对象,因此我们需要通过以下一种或多种方式来保护它:

  1. PUBLIC中撤销f_set_decrypt_conns上的GRANT EXECUTE,仅向需要访问的用户(操作系统用户名)添加授权。
  2. 保护用于完成此功能的代码—将其分离到一个管理应用中。
  3. 在这段代码中实现一些额外的测试,可能是在一个 Oracle 表中检查用户的存在(列表),这个表就是为此目的而构建的。

我们将在第十一章和第十二章做所有这些事情。

存储应用连接字符串列表的方法

在 Java 中,我们可能采取的某些行动被认为是有风险的或不确定的。Java 是一种强类型语言,所以它不能容忍对象类型标识的不确定性。然而,在我们同时控制对象的发起和接收的情况下,我们可以忽略这方面的任何警告并继续进行。

在清单 10-26 到 10-35 中给出的setDecryptConns()方法中,我们从事这样的努力。我们将从一个ObjectInputStream中读取一个对象,然后对待它,就好像我们知道它是哪种对象一样。我们这样做两次。首先,我们读入应用的内部类对象,然后调用它的getRevLvl()方法,假设它有必要的资源来响应。第二种情况是当我们读入connsHash对象并将其转换为HashMap时。

编译时, javac 会向我们报告有“未检查或不安全的操作”。我们可以通过@SuppressWarnings( "unchecked" )注释,要求 javac 不要打扰我们。该注释直接应用于后面的方法,并且只应用于该方法。注意清单 10-26 中的注释和方法声明之间没有标点符号。不幸的是,Oracle JVM 不接受这些注释,所以我们需要注释掉它们,忍受编译时警告。

清单 10-26。 SuppressWarnings()批注,setDecryptConns()

**    @SuppressWarnings( "unchecked" )**     public static String setDecryptConns( RAW classInstance, RAW connections ) {         String rtrnString = "function";         OracleCallableStatement stmt = null;         try {

从一个字节数组构建一个类

从 Oracle 数据库上接收到的RAW数据类型中获取一个类是我们之前将类对象转换成字节数组时所看到的另一面。在清单 10-27 中的第一步,setDecryptConns()的一部分是从我们称之为classInstanceRAW中获取一个byte数组。我们将那个byte数组输入到一个ByteArrayInputStreambAIS。然后我们实例化一个ObjectInputStreamoins,耦合到bAIS。我们调用oinsreadObject()方法来重建一个名为classObjectObject。关闭流之后,我们得到了Class的一个实例,对于classObject来说是providedClass

清单 10-27。从一个字节数组构建一个类

    byte[] appClassBytes = classInstance.**getBytes()**;     ByteArrayInputStream bAIS = new ByteArrayInputStream( appClassBytes );     ObjectInputStream oins =         new ObjectInputStream( bAIS );     Object **classObject = oins.readObject()**;     oins.close();     Class **providedClass = classObject.getClass()**;

使用 Java 反射来调用方法

有了类对象,我们可以使用反射来获取类名,甚至调用它的方法。这也是setDecryptConns()方法的一部分。获取类名只需调用一次getName()方法。为了调用内部类中的getRevLvl()方法,我们通过调用getMethod()方法从providedClass获得一个Method对象classMethod,如清单 10-28 所示。然后我们调用classMethodinvoke()方法,传递我们在清单 10-27 中得到的classObject,从getRevLvl()中检索实际返回值。将返回值Object转换为String

清单 10-28。通过反思调用方法

`    String className = providedClass.getName();
    Method classMethod = providedClass.getMethod( "getRevLvl" );
    String classVersion = ( String )classMethod.invoke( classObject );

// Do this once we get to Oracle
    // Before we store any class, let's assure it has a package (.)
    // noted before being an inner class ()ourplannedrequirementsif(1==className.indexOf(".")||className.indexOf("" ) < className.indexOf( "." ) )
        return "App class must be in a package and be an inner class!";`

我们将对开发人员的应用内部类提出一些要求。首先,我们将要求它们的内部类被声明为public。我们也希望它们被声明为static,但不要求这样(然而,它将包含在我们提供给它们的模板代码中)。接下来,我们要求它们是内部类,并且它们的包含类包含在一个包中。我们可以通过确保内部类的名称包含句点“.”来确保满足这些要求字符,表示一个包,并且它还包括一个美元符号,“$”字符,表示包中某个类的内部类。javac 编译器连接类名和内部类名,在它们之间添加一个美元符号,以形成完全限定的内部类名。

了解该班级是否已注册

如果我们以前从未见过这个类,那么我们将假设这是初始化,我们将通过在v_app_conn_registry视图中插入这个类来注册一个新的应用。我们通过从视图中选择使用相同名称和版本注册的类来找出我们以前是否见过这个类。Oracle 程序为我们完成了这一任务。参见清单 10-29 。

清单 10-29。确定应用类是否已经注册

    stmt = ( OracleCallableStatement )conn.prepareCall(         "CALL appsec.appsec_only_pkg.**p_count_class_conns**(?,?,?)" );     stmt.registerOutParameter( 3, OracleTypes.NUMBER );     stmt.setString( 1, className );     stmt.setString( 2, classVersion );     stmt.setInt(    3, 0 );     stmt.executeUpdate();

如果结果是p_count_class_conns告诉我们没有以那个名称/版本注册的类,那么我们继续插入;否则,我们需要检查我们刚刚收到的类是否等于我们以该名称注册的类。如果“相等”,我们将覆盖现有的存储的connsHash;,但是如果不相等,我们将处理一个冒名顶替者。

一些开发人员的困惑和解决方案

不幸的是,我们的应用开发人员可能成为他们自己行为的受害者。如果开发人员改变了内部类的代码而没有改变版本号,这将导致我们的相等性测试失败。在这种情况下,开发人员应该更改他的内部类中的serialVersionUIDinnerClassRevLvl,通过我们的进程注册它,并创建一个新的连接字符串列表或从以前的版本中复制连接字符串。

可以说,应用开发人员可能搬起石头砸自己的脚的另一种方式是在代码中移动他的内部类。例如,如果他把他的内部类移到公共类定义之外,它就变成了外部类,或者如果他把它从类的主体移到一个方法中(从技术上来说,这是一个完全可以接受的移动),那么包和类名就会改变以反映这种移动。在这些情况下,内部类将被视为一个新的实体并被注册,但在开发人员为新版本重建列表之前,它不会有任何关联的连接字符串。在这种情况下,他不能从以前的版本中复制他的连接字符串列表,因为这被认为是一个新的类,在一个新的路径中被发现(它甚至可能与以前的类具有相同的版本号)。

获取应用 ID 类和连接的 HashMap 列表

回到setDecryptConns()中手头的任务,我们调用p_get_class_conns存储过程来获取我们注册的类以及与这个类名和版本相关联的connsHash。在清单 10-30 中,我们将connsHash作为BLOB来处理。您会记得,在t_app_conn_registry表定义中,我们将其定义为BLOB;这允许我们存储一个大于 2K 字节的connsHash对象。当我们定义过程p_get_class_conns时,我们还通过引用其在表上的定义将第四个参数指定为BLOB,如下所示:

    m_connections    OUT v_app_conn_registry.connections%TYPE

在我们代码的其他地方,我们将connsHash作为RAW来处理。有趣的是,在 PL/SQL 中,也就是在代码中,一个RAW的大小可以达到 32K,但是在 Oracle 表的存储中,只能是 2K。在代码中很容易将BLOBs作为RAWs来处理,但是为什么不总是将BLOB称为BLOB?与数据库表或连接紧密耦合。它们在传输中表现不佳,所以我们依赖于RAW数据类型。

清单 10-30。以 BLOB 形式从 Oracle 数据库获取连接列表

`    if( stmt.getInt( 3 ) == 0 ) {
        // Do insert!
    } else {
        // Assure provided instance and cached, if same version, are equal
        // NOTE: handling BLOBs with getBytes and setBytes is new to 11g
        stmt = ( OracleCallableStatement )conn.prepareCall(
            "CALL appsec.appsec_only_pkg.p_get_class_conns(?,?,?,?)" );
        stmt.registerOutParameter( 3, OracleTypes.RAW );
        stmt.registerOutParameter( 4, OracleTypes.BLOB );
        stmt.setString( 1, className );
        stmt.setString( 2, classVersion );
        stmt.setNull(   3, OracleTypes.RAW );
        stmt.setNull(   4, OracleTypes.BLOB );
        stmt.executeUpdate();

...

byte[] cachedBytes = stmt.getBytes(3);
    oins = new ObjectInputStream( new ByteArrayInputStream(
        cachedBytes ) );
    classObject = oins.readObject();
    oins.close();`

从 Oracle 数据库中提取内部类对象和connsHash是一个两步过程。我们分别得到了RAWBLOB的字节。然后我们在流中移动字节,就像我们之前看到的那样,来重组我们的对象。清单 10-30 中的显示了内部类的流程。

类相等性测试

我们到达一个十字路口。我们已经实例化了一个对象(来自客户端提供的字节数组),从中我们获得了名称和版本号。现在,我们将从注册表中存储为RAW的字节中实例化另一个类。如果这两个对象不相同,Oracle JVM 会大加抱怨:它会抛出一个InvalidClassException

我们将继续通过调用equals()方法来确保客户端传递给我们的对象和存储在我们的注册表中的对象是相等的,如清单 10-31 所示。在 Java 中,这不仅仅意味着对象的变量值相等,还意味着它们在内存中基于相同的模型。他们本质上来自同一个阶层。

清单 10-31。阶级平等测试

`    Class testClass = classObject.getClass();

if( testClass.equals( providedClass ) ) {
            // further tests are unnecessary
        } else return "Failed to setDecryptConns()";
    }`

解密连接字符串以便存储和重用

一旦我们解决了身份问题(仍然在setDecryptConns()方法中),无论我们准备插入还是覆盖一个注册条目,我们都要处理从客户端收到的connsHash。目前,连接字符串是用我们的共享会话 DES 密钥加密的,当当前会话关闭时,该密钥将消失。这将是一个不可用的状态来存储它们。我们应用中的下一个用户将不能阅读它们,我们自己的下一个会话也不能。

因此,我们将解密所有连接字符串,并以不加密的方式存储它们。当下一个会话来获取这个应用的连接字符串时,我们将在交付之前用那个会话的密钥对它们进行加密。注意在清单 10-32 中,当我们将cryptConnsHash成员设置为HashMap<String, RAW>时,没有问题被询问。这种盲目的信任,在这一点上是恰当的,是 Java 编译器用“未检查的”警告所警告的。还要注意,我们将从一个HashMap<String, RAW>过渡到一个新的HashMap<String, String>

清单 10-32。施放加密的连接列表,准备解密

`    oins = new ObjectInputStream( new ByteArrayInputStream(
        connections.getBytes() ) );
    classObject = oins.readObject();
    oins.close();
    HashMap<String, RAW> cryptConnsHash =
        (HashMap<String, RAW>)classObject;

HashMap<String, String> clearConnsHash =
        new HashMap<String, String>();
    oins.close();`

我们初始化我们的共享秘密密码进行解密(见清单 10-33 ,然后遍历cryptConnsHash HashMap来解密每个值。我们在新的clearConnsHash中使用相同的密钥存储每个解密的值。有了来自集合类的HashMap类,我们可以使用 for each 语法遍历它们的成员。你可以把我们的for语句理解为“对于cryptConnsHash组合键中的每个键”

清单 10-33。解密每个连接字符串并保存到新列表

cipherDES.init( Cipher**.DECRYPT_MODE**, sessionSecretDESKey, paramSpec );     **for( String key : cryptConnsHash.keySet() )** {         // Decrypt each one         **clearConnsHash.put**( key,             new String(                 **cipherDES.doFinal**(                     (cryptConnsHash.get( key )).getBytes()                 )             )         );     }

存储我们解密的连接密钥的语法看起来有点复杂,但是我们只是简单地在clearConnsHash中放入一个新条目,使用我们在 for each 循环中获得的密钥和一个新的String。新的String是从我们解密与前一个cryptConnsHash中相同密钥相关的值得到的字节中得到的。

存储该应用的连接名称

我们将使用一个现在熟悉的过程从clearConnsHash HashMap中获取一个字节数组。这显示在清单 10-34 中。

清单 10-34。获取连接字符串列表的字节数组

    ByteArrayOutputStream baos = new ByteArrayOutputStream();     ObjectOutputStream oout = new ObjectOutputStream( baos );     **oout.writeObject( clearConnsHash )**;     oout.flush();     oout.close();     byte[] **connsHashBytes = baos.toByteArray()**;     baos.close();

setDecryptConns()的最后一步是在v_app_conn_registry视图中存储新的connsHash。我们通过将应用 ID 类的字节数组和clearConnsHash传递给清单 10-35 中的p_set_class_conns过程来做到这一点。这是我们将connsHash作为BLOB处理的另一个过程。在 Oracle Database 11g 中,我们能够使用Statement.getBytes().setBytes()方法从 Java 中获取和设置BLOBs

清单 10-35。在 Oracle 中存储连接字符串列表

    // NOTE: handling BLOBs with getBytes and setBytes is new to Oracle Database 11g     stmt = ( OracleCallableStatement )conn.prepareCall(         "CALL appsec.appsec_only_pkg.p_set_class_conns(?,?,?,?)" );     stmt.setString( 1, className );     stmt.setString( 2, classVersion );     **stmt.setBytes(  3, appClassBytes )**;     **stmt.setBytes(  4, connsHashBytes )**;     stmt.executeUpdate();

在应用注册表中设置值的 Oracle 过程

p_set_class_conns程序有三个部分。这显示在清单 10-36 中。第一个获取已经存在的具有特定类名和版本号的记录的计数。如果有 0,我们将在第二部分插入一条新记录;如果为 1,我们将更新第三个中的现有记录。和p_get_class_conns一样,你可以看到我们通过引用v_app_conn_registry.connections%TYPEm_connections作为BLOB处理。

清单 10-36。在应用注册表中设置值的过程,p_set_class_conns

    PROCEDURE p_set_class_conns(         m_class_name     v_app_conn_registry.class_name%TYPE,         m_class_version  v_app_conn_registry.class_version%TYPE,         m_class_instance v_app_conn_registry.class_instance%TYPE,         m_connections    **v_app_conn_registry.connections%TYPE** )     IS         v_count INTEGER;     BEGIN         **SELECT COUNT(*) INTO v_count**             FROM v_app_conn_registry             WHERE class_name = m_class_name             AND class_version = m_class_version;         **IF v_count = 0 THEN**             **INSERT** INTO v_app_conn_registry ( class_name, class_version,                 class_instance, connections ) VALUES                 ( m_class_name, m_class_version, m_class_instance, m_connections );         **ELSE**             **UPDATE** v_app_conn_registry SET class_instance = m_class_instance,                 connections = m_connections, update_dt = SYSDATE             WHERE class_name = m_class_name             AND class_version = m_class_version;         END IF;     END p_set_class_conns;

从应用注册表获取条目的 Oracle 过程

我们将简要地看一下我们用来从v_app_conn_registry获取数据的 Oracle 存储过程。这里没有惊喜。我们已经建立了这些过程,因此在 Java 代码中不需要任何动态 SQL 查询。

查找此应用是否存在注册表项

p_count_class_conns返回一个整数,表示在v_app_conn_registry中有多少具有这个特定类名和版本号的记录。因为该视图中的记录以这两列为关键字,所以我们期望计数仅为 0 或 1。代码在清单 10-37 中。

在回顾中,我们使用它来确定我们是否需要根据一个注册的类来检查客户提供的类的身份,或者我们是否可以简单地将它作为一个新的类插入。如果应用类别符合我们的标准,我们对注册新应用没有任何偏见。

清单 10-37。查找现有的应用注册表条目

    PROCEDURE p_count_class_conns(         m_class_name         v_app_conn_registry.class_name%TYPE,         m_class_version      v_app_conn_registry.class_version%TYPE,         m_count          OUT NUMBER )     IS BEGIN         SELECT COUNT(*)         INTO m_count         FROM v_app_conn_registry         WHERE class_name = m_class_name         AND class_version = m_class_version;     END p_count_class_conns;

事后看来,这是 Oracle 存储函数的一个很好的候选对象。它返回一个值。作为一个过程,我们通过一个OUT参数返回值。

获取注册应用的连接字符串列表

p_get_class_connsv_app_conn_registry获取应用 ID 类和相关联的connsHash,以获得特定的类名和版本号。这显示在清单 10-38 中。不明显,但是再次通过将m_connections称为类型v_app_conn_registry.connections% TYPE,我们将该列作为其原生类型 a BLOB来处理。

清单 10-38。从注册表中获取连接字符串列表

    PROCEDURE p_get_class_conns(         m_class_name         v_app_conn_registry.class_name%TYPE,         m_class_version      v_app_conn_registry.class_version%TYPE,         m_class_instance OUT v_app_conn_registry.class_instance%TYPE,         **m_connections    OUT v_app_conn_registry.connections%TYPE** )     IS BEGIN         **SELECT class_instance, connections**         **INTO m_class_instance, m_connections**         FROM v_app_conn_registry         WHERE class_name = m_class_name         AND class_version = m_class_version;     END p_get_class_conns;

获取应用连接字符串:Java 客户端

这段代码需要处理的管理任务太多了,以至于实际应用很容易被遗忘。我们现在转向实际应用。一个客户端应用希望使用 Oracle 数据库进行工作。该应用使用我们的代码来实现安全性——SSO、双因素身份验证和加密,以及良好的 Oracle 和 Java 安全编程实践。

为了让开发人员更容易使用该应用,我们给了开发人员一些模板,让他们可以遵循这些模板来完成所有工作。我们让他做以下四件事:

  1. 在他的CLASSPATH中包含 OracleJavaSecure.classRevLvlClassIntfc.class 文件(我们提供一个 jar 文件)。
  2. 编写实现RevLvlClassIntfcSerializable的应用 ID 内部类。
  3. 调用OracleJavaSecure.setAppContext()方法,传递他的应用名、ID 类和双因素身份验证代码(是的,他必须处理从第一个请求到使用双因素代码进行第二次调用的循环)。
  4. 通过调用getAppAuthConn()方法向OracleJavaSecure请求他需要的 Oracle 连接。

最后,我们能够为开发人员的应用提供一些 Oracle 连接。这些连接基于以加密形式安全传输和维护的连接字符串(包括密码)。

从应用列表中获取 Oracle 连接

我们的getAppAuthConn()方法基于内存中已经存在的连接字符串列表提供连接。然而,这是一个很好的起点,因为如果内存中不存在连接字符串,getAppAuthConn()调用getAppConnections()方法,根据应用 ID 类从 Oracle 数据库中检索它们。这显示在清单 10-39 中。

我们也测试一下appAuthSessionSecretDESKey is null. This will be the case when we call this method for the first time—we will not have generated or received our two-factor authentication code. If it is null, we return a null, which lets the application know it, needs to loop and let the user come again with a two-factor code. When we have provided the two-factor code and we call this method again, then we will have exchanged keys and appAuthSessionSecretDESKey will not be null

清单 10-39。从列表中获取一个 Oracle 连接,getAppAuthConn()

    public static **OracleConnection getAppAuthConn**( String instance, String userName ) {         OracleConnection mConn = null;         try {             if( null == connsHash ) **getAppConnections()**;             // If we entered without twoFactorAuth, apAuth...DESKey is null             **if( null == appAuthSessionSecretDESKey )** return mConn;             instance = instance.trim();             userName = userName.trim();             String **key = ( instance + "/" + userName ).toUpperCase()**;             appAuthCipherDES.init( Cipher.**DECRYPT_MODE**, appAuthSessionSecretDESKey,                 appAuthParamSpec );             mConn = setConnection( new String( appAuthCipherDES.doFinal(                 connsHash.get( key ).getBytes() ) ) );

关于这种方法,我们需要观察两个事实。首先,它返回一个OracleConnection,而不是一个连接字符串。事实上,我们在相对较短的时间内丢弃了连接字符串,以减少暴露明文密码的可能性。其次,我们返回的连接被配置为通过应用用户的代理,以便通过进一步的 SSO 测试。

我们通过连接该方法调用的参数中请求的实例名和用户名来获取请求的特定连接字符串。与来自其他地方的所有数据一样,我们需要在使用前对其进行整理(验证和格式化)。我们删除了每个参数开头和结尾的空格,并将连接键大写。

有了这个密钥,我们就可以从connsHash获取所需的连接字符串了,但是回想一下,这些字符串是加密的。因此,我们使用共享密码密钥将appAuthCipherDES设置为解密模式。

然后,我们将几个调用堆叠在一起。我们根据我们的密钥从connsHash获得加密的连接字符串。然后我们获取字节数组,并将其传递给Cipher进行解密。我们基于解密的字节创建一个新的String,并将明文连接字符串传递给setConnection()方法,后者返回一个OracleConnection

通过以这种方式堆叠我们的方法调用,我们不需要为过程中的每一步识别方法成员变量。在这种情况下,我们可以有一个额外的RAW、两个byte数组和一个String成员变量。

获取从 Oracle 数据库到客户端应用的连接字符串列表

尽管我们可能会指示开发人员调用getAppAuthConn()方法,但是getAppConnections()方法是在幕后调用的。在后台,我们从 Oracle 数据库获取连接字符串列表,然后应用调用getAppAuthConn()来获取基于这些字符串的各个 Oracle 连接。OracleJavaSecure在将连接交给应用之前,完成所有繁重的工作,解密字符串并连接到 Oracle(清单 10-40 到 10-44 )。

开始我们从 Oracle 获取连接字符串的方法

这里,我们再次控制了一个过程,Java 编译器警告我们这个过程是未检查的和/或不安全的。我们将从 Oracle 数据库获得的对象转换为connsHash HashMap,而不检查它是否符合要求——如果我们错了,就会抛出一个ClassCastException。因为我们控制这个对象的来源和接收,所以我们有理由忽略这个警告。我们可以使用SuppressWarnings()注释(参见清单 10-40 中的注释)来阻止编译器抱怨,但是 Oracle JVM 编译器不接受这一点,所以我们将忍受编译时警告。

清单 10-40。从 Oracle 获取连接字符串,getAppConnections()

    //@SuppressWarnings( "unchecked" )     public static void **getAppConnections()** {         OracleCallableStatement stmt = null;         try {             **if( null == appVerConn ) setAppVerConnection()**;

在我们继续之前,我们检查是否已经连接到了appver,如果需要,调用setAppVerConnection()来创建一个。

调用存储过程获取连接字符串的应用列表

getAppConnections()p_get_app_conns的调用无疑是我们进行的最复杂的 Oracle 存储过程调用之一,但这仅仅是因为我们交换的数据的种类和范围。在列出 10-41 中真的没有什么新东西。这个过程有十几个论点:五个IN和七个OUT。但是这些OUT参数中的两个是针对错误处理的。

IN参数之一是代表应用 ID 类的字节数组。我们通过两个 Streams 类得到这个字节数组,正如我们在本章前面所看到的。其他的IN参数是我们本地 RSA 公钥的工件、模数和指数,以及双因素认证码(如果提供的话)。

除了错误消息之外,我们的OUT参数是我们的共享密码密钥的四个加密工件和与我们提交的应用 ID 对象相关联的connsHash对象。

列举 10-41。获取应用连接字符串列表的过程调用,p_get_app_conns

    stmt = ( OracleCallableStatement )conn.prepareCall(         "CALL appsec.appsec_public_pkg.p_get_app_conns(?,?,?,?,?,?,?,?,?,?,?,?)" );     stmt.registerOutParameter( 5, OracleTypes.RAW );     stmt.registerOutParameter( 6, OracleTypes.RAW );     stmt.registerOutParameter( 7, OracleTypes.RAW );     stmt.registerOutParameter( 8, OracleTypes.RAW );     stmt.registerOutParameter( 9, OracleTypes.RAW );     stmt.registerOutParameter(11, OracleTypes.NUMBER );     stmt.registerOutParameter(12, OracleTypes.VARCHAR );     stmt.setString( 1, locModulus );     stmt.setString( 2, locExponent );     stmt.**setString( 3, twoFactorAuth )**;     stmt.**setBytes(  4, appClassBytes )**;     stmt.setNull(   5, OracleTypes.RAW );     stmt.setNull(   6, OracleTypes.RAW );     stmt.setNull(   7, OracleTypes.RAW );     stmt.setNull(   8, OracleTypes.RAW );     stmt.setNull(   9, OracleTypes.RAW );     stmt.**setString(10, applicationID** );     stmt.setInt(   11, 0 );     stmt.setNull(  12, OracleTypes.VARCHAR );     stmt.executeUpdate(); ...         if( null == stmt.getRAW( 9 ) ) {             System.out.println( "Please rerun with two-factor Auth Code!" );             **return**;         }         if( null == sessionSecretDESKey ) {             **makeDESKey**( stmt.getRAW( 9 ), stmt.getRAW( 8 ),                 stmt.getRAW( 6 ), stmt.getRAW( 7 ) );

我们检查是否报告了任何错误。如果没有,我们测试作为共享密码密钥工件返回的值之一,stmt.getRAW( 9 )。如果为空,我们假设 Oracle 数据库刚刚发送了一个双因素代码,必须等到客户端应用返回双因素代码后才能继续。我们要求客户端用一个双因子代码重新运行这个方法,并退出这个方法。

基于共享密码密钥的工件,我们通过调用makeDESKey()方法来构建密钥。测试sessionSecretDESKey当前是否为null在正常操作中是不必要的,但感觉更完整。我想不出有哪一次我们会到达这里,而sessionSecretDESKey不是null

使用静态类成员来保留 APPVER 连接和键

我们在清单 10-42 中建立了几个静态类成员来保留appver连接和特定于会话的键。这些为会话生成的供appver使用的密钥,在应用稍后尝试解密connsHash中的连接字符串供应用使用时将继续需要。这些连接字符串是用与appver会话相关的共享密码密钥加密的。

清单 10-42。静态类成员保留应用验证解密密钥

    private static OracleConnection appVerConn = null;     private static byte[] appAuthSalt;     private static int appAuthIterationCount;     private static char[] appAuthDESPassPhraseChars;     private static AlgorithmParameterSpec appAuthParamSpec;     private static String appAuthSessionSecretDESAlgorithm;     private static SecretKey appAuthSessionSecretDESKey;     private static Cipher appAuthCipherDES;

你可能想参考第三章,在那里我们讨论了对象、静态成员、指针和引用。因为我们的主要加密密钥和所有工件以及相关成员都是static,所以我们不能在将主要引用指向一个新实例时,仅仅指定一个新的成员名称来引用和保留它们。我们需要将我们的新静态成员,即那些保留appver会话数据的成员,设置为一个新值,引用内存中不同的位置。在清单 10-43 中的getAppConnections()方法中,我们将那些保持器成员设置为当前关键工件的副本或克隆。我们创建支持该流程的新实例。

清单 10-43。为应用验证密钥设置静态类成员

    // Cant just set new pointers to existing members     // Since static, updates to one will update both     // Must instantiate, clone or copy values     appAuthSalt = **salt.clone()**;     appAuthIterationCount =         (**new** Integer( iterationCount )).intValue();     appAuthDESPassPhraseChars =         sessionSecretDESPassPhraseChars**.clone()**;     appAuthParamSpec = **new** PBEParameterSpec( appAuthSalt,         appAuthIterationCount );     KeySpec keySpec = **new** PBEKeySpec( appAuthDESPassPhraseChars,         appAuthSalt, appAuthIterationCount );     appAuthSessionSecretDESAlgorithm =         **new** String( sessionSecretDESAlgorithm );     appAuthSessionSecretDESKey = SecretKeyFactory.**getInstance**(         appAuthSessionSecretDESAlgorithm ).generateSecret( keySpec );     appAuthCipherDES = Cipher.**getInstance**(         appAuthSessionSecretDESKey.getAlgorithm() );     **resetKeys()**;

在我们努力保留这些成员以供appver在更新该应用的connsHash时进一步使用,并用于解密connsHash中的连接字符串的最后,我们调用resetKeys()方法,该方法将我们所有的主键和工件指向 null。我们第一次看到resetKeys()是在第七章中,我们在测试中使用了它。这里是相同的,除了一个例外。我们没有将现有的 sessionSecretDESKey设置为null,因为通过实验,我们已经确定这样做会使appAuthSessionSecretDESKey无效。出于这个原因,我们将修改几个方法来测试nullsessionSecretDESPassPhraseChars,而不是测试nullsessionSecretDESKey:getCryptData()getDecryptData()和方法集getCryptSessionSecretDESPassPhrase() / Algorithm / Salt / IterationCount。我知道有些人希望我不要详细描述陷阱、测试和其他场景,但是这里的目标不仅仅是开发一个应用,还包括开发一个理解。如果我们不在这里探讨这些问题,您将不得不自己去发现它们,这并不坏,但会很费时间。

获取连接字符串列表

从作为应用connsHash对象返回的RAW字节中,我们通过将字节传递给ByteArrayInputStreamObjectInputStream来生成对象。如果结果对象不为空,我们将该对象转换为一个HashMap<String, RAW>,如清单 10-44 所示。这是我们的编译器报告“未检查”警告的地方。然而,如果我们从 Oracle 数据库获得的对象为 null,我们假设还没有为这个注册的应用存储connsHash,我们将connsHash设置为一个新的空的HashMap<String, RAW>。在其中一个场景之后,我们可以通过putAppConnString()方法将新的连接字符串放入connsHash中,并通过putAppConnections()方法将它们存储在v_app_conn_registry视图中。

清单 10-44。将 Oracle 数据库中的连接列表对象转换为散列表

    if( classObject != null ) {         connsHash = (**HashMap<String, RAW>)classObject**;     } else {         connsHash = new HashMap<String, RAW>();     }

建立应用验证流程的连接

我对本章中的setAppVerConnection()方法的形式有所保留——参见清单 10-45 。我已经暗示过,我们的计划是让所有人都可以使用这种连接,就像夜总会的保镖一样,将他的用户名和密码视为数据,但这不是我想要的方式。

只是这一章在范围上已经相当重要了,在这方面我有一些相当长的考虑,我觉得最好推迟到下一章。请继续下一章的讨论。

清单 10-45。设置应用验证连接的方法,setAppVerConnection()

    private static void setAppVerConnection() {         setConnection( "jdbc:oracle:thin:appver/password@localhost:1521:orcl" );         appVerConn = conn;     }

现在,忽略幕后的人。这让人想起嵌入在我们的应用代码中的密码;一些我们希望远离的东西。

还要注意清单 10-45 的最后一行。我们设置了一个静态类成员(参见清单 10-42 )来保留应用验证连接。

获取应用连接字符串列表:服务器端

在这里,我们看到了带有十几个INOUT参数、p_get_app_conns的预言性 Oracle 存储过程。但这并不是这个程序值得注意的地方。相反,内部工作是我们需要注意的。然而,即使是那些也是熟悉的。

如果我们通过了 SSO,那么我们就处理双因素认证。如果用户没有提交双因素代码,我们调用f_send_2_factor函数,传递我们验证过的用户名和application_id,以便在该应用中为该用户创建、分发和缓存(存储在表中)双因素代码。

然而,如果用户确实提交了一个双因素验证码,那么我们调用f_is_cur_cached_cd函数,传递经过验证的用户、application_id和双因素验证码。如果这个双因素代码等于为这个用户和这个应用缓存的代码,那么我们继续为密码密钥设置返回值,并且我们调用f_get_crypt_conns来返回连接字符串的加密列表。

我们将把p_get_app_conns添加到appsec_public_pkg包中,因为任何使用我们的代理连接的用户都会调用它。该程序的核心代码显示在清单 10-46 中。

清单 10-46。获取从 Oracle 返回的连接字符串列表,p_get_app_conns

`    return_user := f_is_sso( m_app_user );
    IF( return_user IS NOT NULL )
    THEN

**        IF( m_two_factor_cd IS NULL )**
        THEN
            m_err_txt := appsec_only_pkg.f_send_2_factor( return_user, m_application_id );
        ELSIF( appsec_only_pkg.f_is_cur_cached_cd( return_user, m_application_id,
    m_two_factor_cd ) = 'Y' )
        THEN
            secret_pass_salt :=
                app_sec_pkg.f_get_crypt_secret_salt( ext_modulus, ext_exponent );
            secret_pass_count :=
                app_sec_pkg.f_get_crypt_secret_count( ext_modulus, ext_exponent );
            secret_pass :=
                app_sec_pkg.f_get_crypt_secret_pass( ext_modulus, ext_exponent );
            secret_pass_algorithm :=
                app_sec_pkg.f_get_crypt_secret_algorithm(ext_modulus, ext_exponent);
            m_crypt_connections := appsec_only_pkg.f_get_crypt_conns( m_class_instance );
        ELSE
            -- Wrong two-factor code entered
            RAISE NO_DATA_FOUND;
        END IF;
        app_sec_pkg.p_log_error( 0, 'Success getting App Conns, ' || return_user );
    ELSE
        app_sec_pkg.p_log_error( 0, 'Problem getting App Conns, ' || return_user );
    END IF;`

有几种方法可以让这个过程失败退出。如果用户的连接/会话未能通过我们的 SSO 要求,那么我们记录错误“获取应用连接时出现问题”并返回,而不发送双因素代码。那个用户有严重问题,我们根本不想和他打交道。然而,如果用户是好的(通过了 SSO ),但是他提交了一个坏的或旧的双因素代码,那么我们就抛出一个NO_DATA_FOUND异常,我们在这里记录这个异常,并报告给应用。在这两种错误情况下,我们都退出而不返回连接字符串列表。

f_get_crypt_conns Java 存储过程调用getCryptConns()方法。f_get_crypt_conns将应用 ID 对象从p_get_app_conns ( 清单 10-46 )传递给 Oracle 数据库上的 Java。

getCryptConns()方法中,我们将从v_app_conn_registry视图中获取与应用 ID 对象相关联的connsHash对象。然后,我们将加密connsHash中的每个明文连接字符串,然后通过p_get_app_conns过程将其传递给客户端。

这是我们讨论的引起 Java 编译器“未检查”警告的三种方法中的第三种。在这里,我们把从v_app_conn_registry出来的所谓的connsHash物体,看不见,作为一个HashMap<String, String>。我们还假设提供给我们的应用 ID 对象是RevLvlClassIntfc的一个实现,在这个实现上我们调用了getRevLvl()方法。从编译器的角度来看,这两种行为都是有问题的,但是我们知道所有参与的各方,并且正在做我们真正想要的事情。

这个方法,getCryptConns()反映了我们已经在setDecryptConns()中讨论过的功能。我们将跳过内部工作的描述,除了我们将指出用于加密清单 10-47 中每个连接字符串的代码。回想一下,Oracle 数据库中存储的connsHash是以明文形式存储的,作为HashMap<String, String>clearConnsHash。我们使用会话秘密密码密钥来加密连接字符串,并将它们放在新的HashMap<String, RAW>cryptConnsHash中。Oracle 数据库将加密的连接字符串返回给客户端应用。

我们的密码设置为加密模式。然后我们使用 for each 语法遍历clearConnsHash中的所有键。

清单 10-47。加密列表中的每个连接字符串

    cipherDES.init( Cipher.**ENCRYPT_MODE**, sessionSecretDESKey, paramSpec );     **for( String key : clearConnsHash.keySet() )** {          // Encrypt each one         **cryptConnsHash.put( key**,             **new RAW**(                 cipherDES.doFinal(                     (**clearConnsHash.get( key )**).getBytes()                 )             )         );     }

堆栈方法调用从clearConnsHash获取连接字符串,该字符串与我们在 for each 循环中获取的键相关联。我们将这个String传递给cipherDES使用秘密密码密钥进行加密。然后我们从这些加密的字节中创建一个新的RAW,并将RAW放入cryptConnsHash,用相同的密钥值加密。在这个方法的最后,我们将从 Oracle 数据库向客户端应用返回cryptConnsHash

测试应用认证,第 1 阶段

和上一章一样,这里我们再次需要编辑代码,为双因素身份验证提供我们公司特定的地址。

images 注意编辑在名为chapter 10/orajavsec/Oracle javasecure . Java的文件中找到的代码。

    private static String expectedDomain = "ORGDOMAIN" ; // All Caps     private static String comDomain = "org.com";     private static String smtpHost = "smtp." + comDomain;     private static String baseURL =         "http://www.org.com/servlet/textpage.PageServlet?ACTION=O&NEWPID=";

我们在setAppVerConnection()方法中还有一个嵌入的密码,在main()方法中还有几个。为appver和其他用户更改相应的密码。还要更改setAppVerConnection()和每个putAppConnString()方法调用中的其他连接字符串组件:服务器名、端口号和实例名。

将新结构导入 Oracle

将新的orajavsec/Oracle javasecure . Java代码加载到 Oracle 数据库中。再次取消以“创建或替换并解析 JAVA…”开头的第一行的注释,并在您的 SQL 客户端(SQL*Plus、SQL Developer、JDeveloper 或 TOAD)中执行它。如果使用试图进行变量替换的 SQL 客户端,请记住设置角色并设置 define off,如注释中所述:

// First //      SET ROLE APPSEC_ROLE; // Also having ampersands in the code without substitution variables //      SET DEFINE OFF; // To run in Oracle, search for and comment @Suppress

接下来,执行 AppSec.sqlSecAdm.sql 文件中的所有命令。按照这个顺序执行命令,因为 SecAdm.sql 中存在依赖关系。

images 注意你可以在名为 Chapter10/AppSec.sqlSecAdm.sql 的文件中找到这些脚本。

检查测试步骤

为了测试我们的应用身份验证,我们将采取以下步骤:

  • 设置我们的应用上下文:应用 ID、内部类和双因素代码。
  • 调用getAppConnections()来获取这个应用的连接字符串列表——第一次将注册我们的应用。
  • 调用putAppConnections()将我们的连接字符串列表上传到 Oracle。
  • 调用getAppAuthConn()以获得在该应用中使用的特定连接。
  • 使用连接从 Oracle 获取数据。

对于测试的第一阶段,我们将在OracleJavaSecuremain()方法中完成所有这些步骤。

我们将至少运行这个测试代码两次。第一次,我们将没有一个有效的双因素认证码,所以我们将在某一点退出程序。简而言之,我们将在得知一个双因素代码被发送给我们之后,并且在我们尝试使用与该应用相关联的连接字符串之前退出。

设置应用上下文

如果我们有一个双因素身份验证代码,我们应该在命令行上将它作为应用的参数传递给应用。或者,如果您从 IDE 中运行这个测试,那么将收到的双因素代码嵌入到这个类中可能更容易。您可以这样做,然后重新编译并执行它。参见清单 10-48 。记住每个二次元码只能用 10 分钟!

清单 10-48。应用认证测试,第 1 阶段,main()

`    public static void main( String[] args ) {
        OracleCallableStatement stmt = null;
        Statement mStmt = null;
        ResultSet rSet;

try {
            // Submit two-factor auth code on command line, once received
            String twoFactorAuth = "";
            if( args.length != 0 && args[0] != null ) twoFactorAuth = args[0];
            // You may place two-factor auth code here for testing from IDE
            // Remember, it's only good for 10 minutes from creation
            //twoFactorAuth = "1234-5678-9012";`

我们调用清单 10-49 中的setAppContext()方法来设置这个应用的上下文。在本例中,我们的应用 ID 是HRVIEW,这是我们到目前为止配置的唯一应用;可以从HR模式中获取加密数据的那个。回想一下,有一个用户appusr和一个安全应用角色hrview_role与这个应用 ID 相关联。

清单 10-49。测试对 setAppContext() 的调用

    String applicationID = "HRVIEW";     Object appClass = new InnerRevLvlClass();     **setAppContext**( applicationID, appClass, twoFactorAuth );

多个 Java 应用可以通过该应用 ID 获得访问权。他们都将获得相同的应用用户和角色。每个 Java 应用将由一个特定的、有代表性的应用内部类来标识,它将把这个内部类交给 Oracle 数据库进行验证。如果当前通过身份验证的用户(为其分发了双因素代码)已经被授权通过应用验证代理连接,appver用户,并且他交给 Oracle 数据库的应用类是有效的,那么他将接收与应用类相关联的连接字符串。如果当前用户还被授予了通过连接字符串中嵌入的 Oracle 应用用户进行代理连接的权限,那么他将可以自由地使用该连接来处理 Oracle 数据。

您可能会考虑应用另一层控制,但这似乎是多余的。您可以控制哪些用户可以访问哪些应用。在我看来,您已经通过与安全应用角色相关联的应用用户授予用户代理连接权限,从而授予用户通过他们愿意使用的任何应用接口访问数据的权限。如果他们可以访问数据,我们真的想控制他们如何访问吗?我不这么认为。

如果两个应用看到不同的数据,那么它们应该有不同的应用 id 和不同的应用用户和角色。

调用以获取应用连接

我们第一次从一个特定的应用调用getAppConnections()时,Oracle 数据库中没有存储可供我们检索的连接字符串列表。这几乎是一个次要问题,因为当一个双因素代码生成并发送给我们时,我们将在过程的早期返回。

因此,即使我们第二次调用getAppConnections(),我们也将从 Oracle 得到一个null,并且我们将把我们的连接字符串列表设置为一个新的空的HashMap。在我们为此应用调用putAppConnections()之前,情况一直如此。然而,我们可以在这种状态下使用应用,方法是将连接字符串推入列表供我们自己本地使用,如清单 10-50 所示。

清单 10-50。测试对 getAppConnections()的调用,并将连接放入本地列表

`    getAppConnections();
    // Go no further until we have a two-factor Auth Code
    if( twoFactorAuth == null || twoFactorAuth.equals( "" ) ) return;
    System.out.println( "connsHash.size = " + connsHash.size() );

putAppConnString( "Orcl", "hr",
        "password", "localhost", String.valueOf( 1521 ) );
    putAppConnString( "Orcl", "appusr",
        "password", "localhost", String.valueOf( 1521 ) );`

将连接字符串列表发送到 Oracle 数据库进行存储

在 Oracle 数据库中存储我们的连接字符串列表是一项我们只需要定期执行的任务,因为我们需要更改我们的应用密码。我们将在清单 10-51 中通过调用putAppConnections()来保存连接字符串的初始集合(来自清单 10-50 )。

清单 10-51。向 Oracle 发送连接字符串列表

    putAppConnections();

获得在此应用中使用的唯一连接

如果我们之前没有调用getAppConnections(),当我们调用getAppAuthConn()时,它将被自动调用(参见清单 10-52 )。这将是我们对希望使用我们的安全结构的应用开发人员的指导——只需打电话给getAppAuthConn()。注意,通过获得这个特定于应用的连接,我们将不再能够使用原来的appver连接来执行putAppConnections()

清单 10-52。获取并使用该应用的特定 Oracle 连接

getAppAuthConn( "orcl", "appusr" );     mStmt = conn.createStatement();     rSet = mStmt.executeQuery( "SELECT SYSDATE FROM DUAL" );     if ( rSet.next() )         System.out.println( rSet.getString( 1 ) );

注意,我们用来选择特定应用连接的关键值是实例名和用户名。这是我们调用putAppConnString()时提供的两个相同的值。一旦我们获得了连接,我们就可以用它来查询 Oracle 数据库。

使用或失去初始应用验证连接

getAppConnections()putAppConnections()都使用appver用户连接来工作。这是很重要的一点。在建立appver连接的过程中,我们交换了特定于该连接的加密密钥。我们将保留这些密钥的足够痕迹,以便继续解密connsHash中的连接字符串;然而,一旦我们为这个特定的应用连接到不同的 Oracle 用户,并为该连接交换了密钥,我们将不再能够使用先前的appver连接来调用putAppConnections()。换句话说,在使用由getAppConnections()返回的任何连接之前,我们需要运行putAppConnections()

我们在测试代码中再次调用putAppConnections(),它将失败,因为我们已经构建并使用了一个应用连接。作为用户appver到 Oracle 数据库的连接不再可用——我们只保留了与该会话相关的解密密钥。

获取应用连接和相关的安全应用角色

在我们从对getAppAuthConn()的调用中获得应用连接之后,我们希望获得与该应用相关联的安全应用角色。我们调用通用的p_check_role_access过程,而不是调用p_check_hrview_access来获取特定的应用角色,该过程授予我们与应用 ID 相关联的安全应用角色。在清单 10-53 中,注意我们将应用 ID 作为参数 1 传递。

清单 10-53。获取和应用连接并设置应用角色

int errNo;     String errMsg;     **getAppAuthConn( "orcl", "appusr" )**;     stmt = ( OracleCallableStatement )conn.prepareCall(         "CALL appsec.**p_check_role_access**(?,?,?)" );         stmt.registerOutParameter( 2, OracleTypes.NUMBER );         stmt.registerOutParameter( 3, OracleTypes.VARCHAR );         **stmt.setString( 1, OracleJavaSecure.applicationID )**;         stmt.setInt(    2, 0 );         stmt.setNull(   3, OracleTypes.VARCHAR );     stmt.executeUpdate();     errNo = stmt.getInt( 2 );     errMsg = stmt.getString( 3 );     System.out.println( "DistribCd = " + errMsg );     if( errNo != 0 ) {         System.out.println( "Oracle error 1) " + errNo + ", " + errMsg );     } **else if( twoFactorAuth.equals( "" ) )** {         System.out.println( "Call again with two-factor code parameter" );     } else {         if( null != stmt ) stmt.close();         System.out.println( "Oracle success 1)" );

在我们对 p_check_role_access 的调用中,我们不必测试双因素身份验证代码是否存在,尽管我们出于习惯在这里这样做。您应该记得,我们只是在初始应用验证连接中进行双因素身份验证,而不是在每个特定的应用连接中。我们从p_check_role_access中删除了双因素认证,并将其添加到我们的应用验证过程中,以获得应用p_get_app_conns的连接字符串列表。

将安全链重新散列到这一点;必须通过应用用户帐户授予用户代理连接权限,并且通过与此应用连接相关联的安全应用角色授予用户访问权限。

通过应用连接获取加密数据

当我们从应用连接中获得加密数据时,我们的应用认证的完整演示被提供,如清单 10-54 中的所示。

清单 10-54。测试从 Oracle 获取加密数据

`    ...
    String locModulus = OracleJavaSecure.getLocRSAPubMod();
    String locExponent = OracleJavaSecure.getLocRSAPubExp();

stmt = ( OracleCallableStatement )conn.prepareCall(
        "CALL hr.hr_sec_pkg.p_select_employees_sensitive(?,?,?,?,?,?,?,?,?)" );
    ...
    OracleJavaSecure.closeConnection();`

我们以前多次看到过这个过程调用。这里唯一的区别是,我们使用了从应用验证列表中获得的连接

添加更多应用连接字符串

开发人员可能会调用其应用中未存储在 Oracle 数据库中的connsHash列表中的连接。这些应用字符串可以通过调用putAppConnString()添加到本地应用连接列表中。该调用甚至可以覆盖来自 Oracle 数据库存储(表)的现有连接字符串,可能是为了测试新的 Oracle 实例或开发或验收实例:

    putAppConnString( "Orcl", "appusr",         "password", "localhost", String.valueOf( 1521 ), true );

如果没有进行对putAppConnections()的后续调用,那么修改后的连接字符串列表不会存储在 Oracle 中,它们只被本地客户端应用看到和使用。计划是调用一次putAppConnections(),将字符串存储在 Oracle 数据库中,然后从应用中删除连接字符串,只使用存储在数据库中的字符串。我们将完全避免把我们的连接字符串放入应用中,并将通过我们在第十二章中构建的管理接口使这个过程变得容易。

测试第二个应用

回想一下我们在之前的测试中所做的事情。我们在OracleJavaSecure类中有一个实现RevLvlClassIntfc的内部类,我们将它传递给 Oracle 数据库进行应用验证。Oracle JVM 如何处理这个内部类?它是在CLASSPATH上还是对 Oracle JVM 已知?

在这个特定的例子中,我们实际上是在加载外部类OracleJavaSecure时将该类加载到 Oracle 数据库中的。如果您有一个允许您浏览 Oracle 中的结构的 SQL 客户端应用,您可以看到 orajavsec。Oracle javasecure . InnerRevLvlClass在数据库中列出。您也可以执行以下查询来查看它:

SELECT * FROM SYS.ALL_OBJECTS WHERE OBJECT_TYPE = 'JAVA CLASS' AND OWNER = 'APPSEC';

不幸的是,Oracle 偶尔会修改对象名,您可能会看到类似“/fddfb98e_OracleJavaSecureInne”的内容。除了预期的内部类之外,对于OracleJavaSecure,您可能还会看到一个名为 OracleJavaSecure$1 的内部类——这个类代表了我们使用反射来生成一个迄今为止未知的类。

这可能是唯一一个用于验证的应用内部类,它的外部类被加载到 Oracle 中,所以我们不得不问,“Oracle 数据库将如何实例化来自其他应用的类?”

我们从未见过的物体

我已经多次说过,应用将把它的内部类交给我们,我们将验证它,以便授权一个应用。我让它听起来像是 Oracle JVM 能够仅使用我们从应用提供的类字节,或存储在 Oracle 数据库中用于该应用的字节,凭空创建类和对象。然而,事实并非如此。

我并不是说仅仅基于一个字节数组生成类和对象是不可能的——通过定义一个BytesClassLoader类,基于一个采用字节数组而不是 URL 目录或 jar 文件的URLClassLoader,这当然是可能的。然而,我们将采取简单的方法。

采取简单的方法有两个原因:为了避免由另一个ClassLoader引起的安全问题,并且因为存在一个简单的解决方案。

我们的方法与远程方法调用(RMI)有一些相似之处。在 RMI 中,存在一个表示远程对象的本地存根类。在 RMI 中,本地存根类和RMIClassLoader处理对 RMI 服务器上远程运行的实际方法的调用。这不是我们将要做的。

我说我们的方法与 RMI 相似,因为我们将在 Oracle JVM 中预加载一个应用内部类的表示。Oracle JVM 中的类需要与客户端应用传递给我们的类非常相似,但不需要完全匹配。特别是,我们将使用潜在的不同修订标签来实例化客户端类。

将存根类放在 Oracle 上

这就是为什么我们可以对每一个打算使用我们的安全结构的应用至少执行部分代码审查。这将是一件好事,因为您现在是专家了,已经回顾了第二章和第三章中讨论的所有安全编程概念。

我们需要提取用于验证每个应用本质结构的内部类。这包括外部的包含类结构。我们将把这些要素加载到 Oracle 数据库中,作为一个存根,在 Oracle 数据库中实现这种类型的类和对象。

花一点时间将原始内部类与我们的第二个应用 testojs/TestOracleJavaSecure 的存根进行比较。

images 比较名为chapter 10/testojs/testoraclejavasecure . Java(原文)和 TestOracleJavaSecure.sql (存根)的文件。

看看我们存根的某些方面,我想指出什么是必需的。首先在清单 10-55 中,注意包名, testojs 。我们将要求应用内部类存在于一个包中,以确保应用甚至可以将其内部类命名为相同的名称,但基于包前缀具有唯一的名称。其次注意外层,包含对TestOracleJavaSecure的类定义。这个外部类定义需要与我们在原始代码中看到的完全匹配。内部类命名为AnyNameWeWant

清单 10-55。第二个测试应用的存根类

CREATE OR REPLACE AND RESOLVE JAVA SOURCE NAMED appsec."testojs/TestOracleJavaSecure" AS **package testojs**; import java.io.Serializable; import orajavsec.RevLvlClassIntfc; **public class TestOracleJavaSecure {**     public static class **AnyNameWeWant**         implements Serializable, RevLvlClassIntfc     {         private static final long serialVersionUID = 2011013100L;         private String innerClassRevLvl = "20110131a";         public String getRevLvl() {             return innerClassRevLvl;         }     } }

内部类本身的定义应该与原始代码中的完全相同,除了innerClassRevLvl字符串(无论您是在类中还是在getRevLvl()方法中定义它)。注意,在任何情况下,都应该保留publicprivate修改器。我们希望内部类被声明为static,这样我们就可以处理单个对象,而不是潜在的多个实例。

除了在一个包中,内部类需要被声明为public,这样OracleJavaSecure类可以生成对象,即使它在不同的包中。因此,包含它的外部类也应该是公共的。

在包含类中放置内部类有很大的灵活性。首先,包含类不必是应用的顶级类,它可以是一个辅助类。这样,一个勤奋的程序员可能会阻止生成存根的应用安全人员阅读敏感代码。

我们可能希望应用内部类位于辅助外部类中还有另一个原因。有些担心是因为内部类可以访问其外部包含类的私有成员。因此,将应用内部类放在辅助外部类中可能不如将内部类放在核心应用类中敏感。

内部类也可以放在方法中,而不是放在主类体中。通常,在方法中定义的内部类在其类名中间有一个“$1”样式的标记。您可以在检查编译后的类名时看到这一点。

获取应用认证连接和角色

我们应该始终保持警惕,让开发人员更容易使用安全性。考虑到这一点,我们将把以前要求开发人员分别实现的几个步骤合并成一个步骤。到目前为止,我们已经要求开发人员至少设置他们的应用上下文,然后获取应用连接,最后调用 Oracle 数据库来设置他们的安全应用角色。我们仍将让开发人员设置他们的应用上下文,但是我们将把对应用连接的请求与获得安全应用角色的请求结合起来。我们将用一种新的方法来做这件事,如清单 10-56 中的所示。

当我们调用getAAConnRole()时,会有一个OracleConnection返回给我们,并且连接已经设置了安全应用角色。这个新方法采用了我们提供给getAppAuthConn()方法的相同参数。

清单 10-56。获取应用认证连接和角色,getaaconrole()

public static OracleConnection getAAConnRole( String instance, String userName ) {         OracleConnection mConn = null;         OracleCallableStatement stmt = null;         try {             mConn = **getAppAuthConn**( instance, userName );             // If mConn is null, probably did not send twoFactorAuth             if( null == mConn ) return mConn;             int errNo;             String errMsg;             stmt = ( OracleCallableStatement )mConn.prepareCall(                 "CALL appsec.**p_check_role_access**(?,?,?)" );                 stmt.registerOutParameter( 2, OracleTypes.NUMBER );                 stmt.registerOutParameter( 3, OracleTypes.VARCHAR );                 stmt.setString( 1, **applicationID** );                 stmt.setInt(    2, 0 );                 stmt.setNull(   3, OracleTypes.VARCHAR );             stmt.executeUpdate();             errNo = stmt.getInt( 2 );             errMsg = stmt.getString( 3 );             //System.out.println( "DistribCd = " + errMsg );             if( errNo != 0 ) {                 System.out.println( "Oracle error 1) " + errNo + ", " + errMsg );             } else if( twoFactorAuth.equals( "" ) ) {                 System.out.println( "Call again with two-factor code parameter" );             }         } catch ( Exception x ) {             x.printStackTrace();         } finally {             try {                 if( null != stmt ) stmt.close();             } catch( Exception y ) {}         }         return mConn;     }

请注意,对p_check_role_access的调用与我们一直要求开发人员自己做的是一样的。我们在调用中使用的applicationID是我们在调用setAppContext()时设置静态成员的值。

测试应用认证,第 2 阶段

如果还没有,执行脚本在 Oracle 数据库中为TestOracleJavaSecure创建存根内部类。

images 注意在 Oracle 数据库上执行 TestOracleJavaSecure.sql 中的脚本。

您可以查询 Oracle 数据库来查看刚刚创建的 Java 结构。将有一个 Java 结构用于TestOracleJavaSecure外部类,一个用于内部类,类似于这个列表。下面是要执行的查询:

SELECT * FROM SYS.ALL_OBJECTS WHERE OBJECT_TYPE = 'JAVA CLASS' AND OWNER = 'APPSEC'; testojs/TestOracleJavaSecure /545c0b44_TestOracleJavaSecure

接下来,在客户端编辑并编译TestOracleJavaSecure类。用您的 Oracle 实例的口令、服务器名、实例名和端口号修改对putAppConnString()的调用。

images 注意编辑在名为chapter 10/testojs/testoraclejavasecure . Java .的文件中找到的代码

这里的测试相对简单,但是在我们显示数据库中的一行加密数据时,我们在本书中描述的所有内容都已经发生了。这也可以作为应用开发人员使用我们的安全结构时必须做的一个例子。

首先,您会注意到这个应用在中实现了一个内部应用类,列出了 10-57 。

列举 10-57。第二个测试应用内部类

`package testojs;

public class TestOracleJavaSecure {
    public static class AnyNameWeWant         implements Serializable, RevLvlClassIntfc
    {
...`

设置应用上下文

测试代码的主体驻留在 main 方法中。在这种情况下,我们假设客户端用户将从命令行调用应用。第一次调用后,他将需要再次调用,包括命令行上的双因素验证码。

参见清单 10-58 中应用如何通过调用setAppContext()方法在OracleJavaSecure中设置应用上下文。

清单 10-58。设置第二个测试应用上下文

    public static void main( String[] args ) {         OracleCallableStatement stmt = null;         Statement mStmt = null;         ResultSet rSet;         try {             // Submit two-factor auth code on command line, once received             String twoFactorAuth = "";             if( args.length != 0 && args[0] != null ) twoFactorAuth = args[0];             String **applicationID = "HRVIEW"**;             Object **appClass = new AnyNameWeWant()**;             OracleJavaSecure.**setAppContext**( applicationID, appClass, twoFactorAuth );

在 Oracle 中存储连接字符串

如果这是我们第一次运行TestOracleJavaSecure类,无论是在我们拥有双因素身份验证代码之前还是之后,那么我们可以运行几行来填充连接字符串的connsHash列表,并将它们存储在 Oracle 中,如列表中的 10-59 所示。

列举 10-59。存储第二个测试应用的连接

    // Only do these lines once     // If we provided an old twoFactorAuth, will not have connHash -     // null pointer exception here     OracleJavaSecure.getAppConnections();     OracleJavaSecure.putAppConnString( "Orcl", "appusr",         "password", "localhost", String.valueOf( 1521 ) );     OracleJavaSecure.putAppConnections();

在我们成功地将特定于应用的连接字符串提交给 Oracle 之后,我们可以对这些代码行进行注释,并简单地使用我们存储在 Oracle 数据库中并从其中检索的连接字符串。

获取与角色的应用连接

对于这个测试,我们将调用我们的新方法getAAConnRole(),正如您所记得的,它获取应用连接并设置安全应用角色。这个调用显示在清单 10-60 中。

清单 10-60。调用以获得应用与角色的连接

    OracleConnection conn =         OracleJavaSecure.getAAConnRole( "orcl", "appusr" );

参见代理连接

现在我们已经建立了连接,让我们看看 Oracle 数据库是如何识别我们的。我们可以查询许多 Oracle SYS_CONTEXT设置。我们将在清单 10-61 中查询并显示那些与我们身份相关的内容。

清单 10-61。查看代理连接设置

`    mStmt = conn.createStatement();

rSet = mStmt.executeQuery( "SELECT SYS_CONTEXT( 'USERENV', 'OS_USER' )," +
    "SYS_CONTEXT( 'USERENV', 'PROXY_USER' ),SYS_CONTEXT( 'USERENV', 'IP_ADDRESS' ),"+
    "SYS_CONTEXT( 'USERENV', 'SESSION_USER' ), "+
    "SYS_CONTEXT( 'USERENV', 'CLIENT_IDENTIFIER' ) " +
    "FROM DUAL" );
    if ( rSet.next() ) {
        System.out.println( rSet.getString( 1 ) );
        System.out.println( rSet.getString( 2 ) );
        System.out.println( rSet.getString( 3 ) );
        System.out.println( rSet.getString( 4 ) );
        System.out.println( rSet.getString( 5 ) );
    }
    rSet = mStmt.executeQuery( "SELECT * FROM sys.session_roles" );
    if ( rSet.next() ) {
        System.out.println( rSet.getString( 1 ) );
    }`

从 Oracle 获取加密数据

最后,我们将调用我们的标准演示存储过程,p_select_employees_sensitive。这需要与connsHash中列出的连接进行额外的加密密钥交换。这个调用如清单 10-62 所示。我们为appver连接交换的第一个加密密钥不能在不同的 Oracle 连接中重用,例如appusr

清单 10-62。打电话获取敏感员工数据

    String locModulus = OracleJavaSecure.getLocRSAPubMod();     String locExponent = OracleJavaSecure.getLocRSAPubExp();     stmt = ( OracleCallableStatement )conn.prepareCall(         "CALL hr.hr_sec_pkg.**p_select_employees_sensitive**(?,?,?,?,?,?,?,?,?)" );     ...

章节回顾

那么,到目前为止我们都做了些什么?这个旋风式的描述将试图涵盖这个测试应用所经历的安全编程领域。

我们调用没有双因素认证码的TestOracleJavaSecure类。测试应用通过传递其内部类和应用 ID 在OracleJavaSecure中设置其应用上下文。然后(在标准运行模式下),测试应用调用OracleJavaSecure.getAAConnRole()

在后台,在OracleJavaSecure中,我们作为操作系统用户代理连接到 Oracle 数据库,通过 Oracle appver用户进行代理。最初,appver模式的登录触发器检查以确保 OS 用户是有效的 Oracle 用户。

一旦作为appver连接到 Oracle 数据库,我们就测试用户是否通过了我们的 SSO 测试。然后,由于我们没有提供双因素身份验证代码,Oracle 数据库会生成一个代码并发送给为操作系统用户注册的设备和帐户。

一旦我们收到双因素认证码,我们再次调用TestOracleJavaSecure,传递双因素代码。当TestOracleJavaSecure设置应用上下文时,双因素代码被传递给OracleJavaSecure。现在当我们调用 Oracle 数据库为appver并通过 SSO 时,我们检查两因子代码;如果它通过了,并且我们的应用上下文中的内部类与 Oracle 中的某个类匹配,我们将返回为该应用存储的加密连接字符串列表。

另一个幕后流程是我们针对应用验证流程的密钥交换。客户端将自己的 RSA 公钥发送给 Oracle,Oracle 数据库生成一个共享的口令密钥,用 RSA 密钥加密,然后返回给客户端。连接字符串用共享密码密钥加密。

继续getAAConnRole()方法的幕后活动,我们解密所请求的连接字符串,并使用它连接到 Oracle 数据库。我们连接是为了请求应用所需的安全应用角色,以便从 Oracle 数据库中读取数据。为了获得安全应用角色,我们再次确保我们通过了 SSO(可能是在与appver不同的 Oracle 实例上),但是我们不会为此连接再次进行双因素身份验证。

OracleJavaSecure.getAAConnRole()将返回一个OracleConnection,开发者可以用它来调用他的一个应用过程,以便获得加密的应用数据。当他进行呼叫时,他将为新的连接交换加密密钥,并将检索用新的共享密码密钥加密的数据。我们解密共享密码密钥的各个方面并构建该密钥,然后使用该密钥解密数据。

图 10-1 和 10-2 说明了这一过程。在图 10-1 中,我们看到了从 Oracle 数据库获取应用 Oracle 连接字符串列表的过程。这是一个两阶段的过程,第一阶段是请求双因素身份验证代码时的第一次连接,第二阶段是用户手里拿着双因素身份验证代码返回。两个方框说明了每个阶段的活动。

在图 10-1 的的开头(顶部),我们调用了setAppContext()方法,它只是将特定客户端应用的数据存放在OracleJavaSecure的静态类成员变量中。之后,客户端应用对getAppConnections()进行一次额外的调用。在该过程的第二阶段快结束时,注意连接字符串列表没有返回到客户端应用,而是保存在客户端OracleJavaSecure类的托管中。名单是OracleJavaSecure的私人类成员。在第二阶段中,您还可以看到,在返回连接字符串之前,它们是用秘密密码密钥加密的,并且是以加密的形式返回的。

除了应用的加密 Oracle 连接字符串列表,DES 共享密码密钥的加密工件也从 Oracle 数据库返回。OracleJavaSecure在客户端构建一个等效的共享密码密钥,用于解密应用连接字符串。在构建 DES 共享密码密钥并存储该密钥以供以后在克隆或新的静态成员中使用之后,调用resetKeys()方法,以便可以作为应用用户建立进一步的 Oracle 连接。

images

图 10-1。获取应用 Oracle 连接字符串列表

图 10-2 的上半部分展示了将 Oracle 连接字符串存储在客户端应用列表中的过程。客户端上列表中的所有连接字符串都以加密形式维护。在图 10-2 顶部的处,客户端应用可能进行的第一个调用是将新的连接字符串放入本地列表。这种添加仅在客户端本地进行,并不存在于存储在 Oracle 数据库上的列表中。注意,我们调用一个加密函数,使用秘密密码密钥来加密存储的连接字符串。

在图 10-2 中,客户端应用可能进行的下一个调用是对putAppConnections()的调用,它将连接字符串的完整列表存储在 Oracle 数据库中。这个过程很好地说明了我们为不同的目的使用不同的处理环境。我们的插图中的各个列提供了丰富的信息。客户端应用(最左边一列)调用OracleJavaSecur中的函数——这是 Java 调用 Java。OracleJavaSecure调用 Oracle 数据库中的 Java 存储过程,后者将调用传递给 Oracle JVM——运行在 Oracle 数据库上的 Java。我们还需要从 Oracle JVM 调用 Oracle 中的存储过程和函数(最右边一列。)标有“Oracle 数据库”的两列是同一个 Oracle 实例,只是从不同的方向调用。

在 Oracle 数据库中存储应用的连接字符串列表的过程中,您可以看到我们解密了每个连接字符串。我们构建了一个新的解密连接字符串HashMap,并将它们不加密地存储在 Oracle 数据库中。

图 10-2 的下半部分说明了应用使用的特定 Oracle 连接的获取。客户端应用向OracleJavaSecure.getAAConnRole()请求使用特定 Oracle 实例和用户的连接。注意,连接字符串只是在创建新的OracleConnection时被短暂地解密。getAAConnRole()方法不仅创建了OracleConnection,还将角色设置为访问敏感应用数据所需的安全应用角色。将设置了安全应用角色的这个OracleConnection返回给客户端应用使用。

images

图 10-2。将应用连接字符串列表存储在 Oracle 数据库中,并获取一个特定的应用 Oracle 连接以供使用。

十一、增强安全性

每年,我们公司的总裁都会给我们做动员讲话。总的来说,他会谈论我们作为一个公司在这一年中所做的好事,并分享我们失败或不太成功的时刻的清单。在演讲的最后,总裁认可了员工的技能和承诺,并鼓励我们不仅要继续做我们正在做的事情,还要做得更多,接受每一个新的挑战。

我们需要这是关于计算机安全的相同的鼓励讲话。我们做得很好,也取得了成功,但总会有新的挑战和我们可以做得更多的领域。

我们有一个特定的领域需要解决,我在前面的章节中提到过。我们所有的 Oracle 应用都有一个弱点:我们嵌入了密码。这个问题不会消失。我们已经从代码中删除了所有密码,除了应用验证用户appver的密码。现在,我们需要采取额外的步骤来保护该密码。

我们的另一个弱点是特权用户攻击的风险。目前,如果一个流氓 DBA(或具有 DBA 权限的黑客)愿意,他可以读取我们在t_app_conn_registry表中的所有密码。当然,他必须知道一些 Java 来重组HashMap。任何能够访问备份磁带或档案的人也可以打开表格,访问我们所有的应用密码。我们需要的是静态数据加密;即 Oracle 表中和磁盘上的数据加密。

在前一章中,我们生成了代码,它将存储来自以任何本地用户身份运行的任何应用的应用连接字符串。这很方便,但是我们需要将这个过程形式化,并将其委托给应用验证管理员。为此,我们将引入appver_admin角色,并讨论管理应用连接字符串的过程。

如果攻击者成功获得了appver密码,我们希望严格限制他在数据库中可以做什么和看到什么。我们已经有了登录触发器,而appver只能访问我们应用安全流程的一些程序和有限数据。但是我们将会看到,Oracle 数据库中的每个用户都可以访问我们不希望公开的PUBLIC数据,因此我们将尝试加强我们一般 Oracle 应用的安全性。

隐藏 APPVER 连接字符串

我们不会解决嵌入密码的问题,你可能想知道为什么。一言以蔽之,这是一个“先有鸡还是先有蛋”的问题。也许我们可以在 Oracle 数据库中隐藏我们的密码,但是我们需要一个 Oracle 密码来从数据库中请求它们。

没有用户/口令,您无法与 Oracle 数据库对话,因此会出现口令存储在哪里的问题。我们已经解决了所有应用密码的问题,除了网守帐户appver。因此,让我们来看看隐藏密码的一些可能的解决方案。

从第二个来源/服务器获取

好吧,如果我们不想在代码中使用密码,并且我们不能从 Oracle 数据库中获得它(没有密码),也许我们需要存储它并从辅助服务器中检索它。今天结束时,我将宣传坚持使用 Oracle 数据库作为我们的服务器的想法。支持和保护一台服务器远胜于将您的安全角色分布在多个平台上,主要是因为这样更容易考虑和监控。

从端口服务器

在我工作的地方,我们有一个端口服务器(一个打开ServerSocket并监听网络端口的多线程应用),它执行一些与我们在第十章中描述的appver结构相同的活动,并返回连接字符串。这是一个很棒的解决方案,但这不是我的功劳——是我的同事想出来的。该服务器只做它应该做的事情,并安全地传递连接字符串。在 J2EE 之前,它是土生土长的。

我们可以在我们的端口服务器中实现一些额外的安全性,比如 SSO 和双因素身份验证,这些都是目前没有的。然而,该服务器的主要缺点有两个:首先,连接字符串存储的安全性低于 Oracle 质量。现在,不是通过 Oracle 帐户获得访问权限,而是通过操作系统帐户或组来提供访问权限。其次,管理另一种具有独特配置和代码的服务器意味着潜在的更大的支持需求和独特的知识。

来自马绍尔群岛共和国

远程方法调用(RMI)是一个将 Java 功能分成两半的系统。一些 Java 代码运行在客户机上,一些运行在 RMI 服务器上。使用 RMI,您可以从 Java RMI 服务中检索连接字符串。

这种方法与前面描述的端口服务器具有非常相似的优点和缺点。它的一个优点是,它是一种行业标准的方法,而不是一种自己开发的端口服务器,因此可能不太关心对支持的独特要求。

从 URLClassLoader

如果你曾经在浏览器中运行过 Applet,你会看到URLClassLoader在运行。使用这种方法,您的一些应用代码(可能是检索和传递连接字符串的代码)不会存储在客户端计算机上。这是代码的分离,但不是处理的分离。需要时,本地应用从 HTTP (web)服务器下载 jar 文件或类文件,并运行这些文件中的代码。您的应用在客户端计算机上独立运行,不提供服务器端身份验证。此外,如果应用可以从 URL 读取文件,那么本地机器上的任何其他 web 客户端也可以。

简而言之,如果我们在浏览器中运行,我们可能有服务器端认证。可能存在从浏览器到某些文档/应用领域的领域认证,这通常意味着访问 URL 的另一个用户名/密码提示。或者在浏览器中,如果网页或服务器指示浏览器在返回 jar 文件(仍在讨论 URLClassLoader)之前向 active directory(或类似的)验证登录用户,也可能有 SSO。但是在目前的讨论中,让我们坚持客户端应用。

也许我们可以修改URLClassLoader的标准行为,从安全的存储库中读取代码,从而安全地获得我们的连接字符串。嗯,我们正在谈论一个 RMI 服务器(带有唯一的类加载器)和一个端口服务器的组合,同样有同样的弱点。

所有这些方法的缺点

电脑黑客攻击客户端比攻击服务器更好。在 Java 中,最终所有这些连接字符串都在内存中被转换成人类可读的形式。无论多么短暂,这是我们最大的安全弱点,也是我们尚未解决的问题。我们在 utility OracleJavaSecure类中所做的最大努力只成功地将连接字符串保留在应用代码之外,并且在需要它们的时候将它们加密,然后丢弃未加密的字符串。

查看针对客户端攻击的明显保护措施(即仅从服务器连接到 Oracle 数据库)超出了本书的范围。Web 应用就是这样一个例子:用户可以看到网页,但是网页是由 web 服务器查询的数据生成的。我们提到了马绍尔群岛共和国,它可以在这方面发挥潜在的作用。使用 RMI,客户机可以从 RMI 服务器请求数据,而 RMI 服务器将负责查询 Oracle 数据库。

如果您决定从 web 服务、HTTP 或 RMI 服务器上运行所有 Oracle 查询,那么您需要将注意力转向在服务器上保护 Oracle 密码的获取和使用,这与我们讨论的客户端应用安全性是相同的,只是更加集中。

从一个当地人那里得到:JNI

也许我们可以通过使用不同于 Java 的编程语言来存储和读取我们的密码,甚至可能用于查询 Oracle 数据库,从而保护我们的 Oracle 密码。我们可以用 c++编写一个过程,并使用 Java 本地接口(JNI)从我们的 Java 应用中调用它。

当然,因为 Java 被编译成由 JVM 解释的字节码,字节码比编译的 C+代码更容易反编译回源代码。但是用任何语言编写的代码都可以反编译或反汇编。动态链接库(dll)和可执行程序同样容易受到攻击。

我建议坚持使用 Java。为了安全起见,维护和应用一套安全的编程技能要比使用多种语言时半流利半可信要好。

从加密的 Java 类中获取

好了,我们回到我们的客户端 Java 代码,我们正在尝试保护我们的嵌入式 Oracle 密码。如果我们能对我们的密码进行编码,这样只有我们的程序才能读取它,那就太好了。但是我们必须隐藏我们的程序是如何读取它的,以防止黑客复制这个过程并读取密码。

这个问题已经被问过了,我们在这里再问一次,我们可以通过加密我们的类文件来隐藏我们在 Java 中所做的事情吗?答案是肯定的,Vladimir Roubtsov 在他 2003 年 5 月的文章“破解 Java 字节码加密”中对这个过程和结果做了精彩的描述。他给自己的文章加了副标题“为什么基于字节码加密的 Java 混淆方案不起作用。”

Roubtsov 文章的核心描述了他的EncryptedClassLoader。如果您还记得,我们在上一章中讨论了关于在 Oracle 数据库中实现应用对象和类的类装入器。我们说过ClassLoader不能实现他没有先验知识的类,所以我们在 Oracle 数据库上放了一个存根类。

RMI 有一个RMIClassLoader,它可以从存根或框架类中加载一个类,并将其与 RMI 服务器上的实现类进行匹配。URLClassLoader可以从远程的 jar 文件或类文件中加载类,例如在 web 服务器上。

在 Roubtsov 的EncryptedClassLoader的情况下,需要在实现对象或类之前解密字节码。这很好,我们都在考虑开绿灯!

然而,Roubtsov 指出了信任这种安全的谬误。在每个类装入器中的某个时刻,字节码被传递给defineClass()方法,该方法必须以 JVM 可读的形式呈现类。正是在这一点上,我们的类的形式和功能暴露给了任何 Java 黑客。

我不是说加密字节码是个坏主意,只是说我们没有实现有意义的加密——只是一个复杂的混淆。也许所有的加密都是如此:只有加密到了需要解密才能使用的程度。

实现我们自己的类装入器有一些问题:它如此接近 JVM 的核心,以至于我们需要小心,并且可能需要在每次 Java 更新时重新访问它。ClassLoader中的一个错误对我们的应用来说可能是毁灭性的。仅仅这一点就让我们对字节码加密望而却步。

从加密的字符串中获取

为什么我们不加密我们的密码,只在需要的时候解密?这是个好主意,但是加密密钥呢,我们如何保护它呢?这又是一个“鸡和蛋”的问题吗?我也这么认为暴露密钥就等于暴露密码。

从编码字符串中获取

如果我们对字符串进行编码,使其无法识别,会怎么样?这也许和加密一样好,但是它不需要加密密钥。我们将在一个名为OJSCode的类中查看一些示例 Java 代码,以对我们的密码进行编码。

使用编码方法

简而言之,我们的OJSCode.encode()方法将获取连接字符串,并通过与其他字节进行二进制异或(XOR)来对其进行逐字节编码。XOR 转换可以执行两次,以返回到原始字节。在 XOR 中,只有当一个或另一个原始字节中的类似位为 1,而不是两个原始字节都为 1 时,结果位才为 1。这个过程是这样的:

Original Byte   0 1 0 0 1 1 0 1 Other Byte      1 1 1 0 0 1 0 1 Result of XOR   1 0 1 0 1 0 0 0 (resultant bit is 1 where only one of bits is 1) Other Byte      1 1 1 0 0 1 0 1 Result 2<sup>nd</sup> XOR  0 1 0 0 1 1 0 1 (notice this is the same as our original Byte)

出于我们将在后面讨论的安全原因,我们将在OJSCode类之外获取我们的“其他”字节。我们对OJSCode的意图只是从OracleJavaSecure类中使用它,那么我们回到那个期望的类来获取我们的其他字节怎么样?我们将在OracleJavaSecure中从名为“location”的静态字符串中组装我们的其他字节。

因为我们知道我们将对原始字节进行 XOR 运算,所以我们需要与原始字节数量相同的其他字节。清单 11-1 展示了我们如何得到两个字节数组来表示我们的连接字符串(encodeThis)和一个相同长度的其他字节数组。

清单 11-1。得到两个相同长度的字节数组进行异或编码

`{ static String location = "in setAppVerConnection method."; }

String location = OracleJavaSecure.location;
    byte[] eTBytes = encodeThis.getBytes();
    int eTLength = eTBytes.length;
    while( eTLength > location.length() ) location += location;
    String xString = location.substring( 0, eTLength );
    byte[] xBytes = xString.getBytes();`

首先,我们从OracleJavaSecure获得一个location字符串的本地副本。它是用默认(包)范围定义的,所以orajavsec包之外的代码看不到它。我们将它自身连接足够多次,得到一个长度等于或长于encodeThis的字符串。然后我们得到与encodeThis长度相等的子串。

要进行字节异或运算,我们只需使用“^”运算符。在清单 11-2 中,我们遍历原始和其他字节数组,并对每个字节对进行异或运算,将bytes转换为这个过程的ints。我们将结果转换为一个byte,并将其存储在code[]字节数组中。

清单 11-2。逐字节异或编码

    byte[] code = new byte[eTLength];     for( int i = 0; i < eTLength ; i++ ) {         code[i] = (byte)( (int)eTBytes[i] ^ (int)xBytes[i] );     }

我们的连接字符串如下所示:

“jdbc:oracle:thin:appver/password@localhost:1521:orcl”

我们希望在OracleJavaSecure类中存储一个编码的替换——可以放在引号之间并作为字符串处理的东西。所以OJSCode.encode()方法将返回一个字符串,但不是任何一个字符串。字符串可以包含不可打印的字符,如回车(字符 13,十进制)和哔哔声(字符 7)。这些很难用引号之间的符号来表示。

当我们对原始字节进行 XOR 运算时,我们最终会得到一些不可打印的字符,因此我们需要一个过程来将它们全部转换为可读的字符,而不会损失任何保真度,也不会丢失信息。我们不需要从头开始解决这个问题——我们不是第一个有这种担忧的人。

将不可打印的字符表示为字符串的行业标准方法是对字符进行 Base64 编码。在 Base64 编码中,来自一个字节串的位被连接和分解成 6 位序列,这些序列被翻译成来自 64 个可打印字符的表中的字符。一个或多个“=”符号通常作为填充字符出现在 Base64 编码字符串的末尾(当 6 位值跨越 8 位边界时)。当看到一串可打印的字符末尾带有等号时,这清楚地表明是 Base64 编码,并且给出了用于编码的过程,如果这是一个问题的话。这不一定是一个问题,因为 Base64 编码只是为了使扩展字符更适合 web 浏览器,而常规的字母数字字符就是所期望的。但是,如果我们还试图伪装文本,那么 Base64 编码就会失败。令人惊讶的是,Base64 编码经常被用于向代理服务器和 web 服务器领域传递基本身份验证的密码,就像加密一样。清单 11-3 显示了我们进行 Base64 编码的 Java 调用。

清单 11-3。 Base64 编码

    String decodeThis = (new BASE64Encoder()).encode( code );

Oracle 已经在 JVM 中包含了 Base64 编码;不过,用起来比较麻烦。我们可以调用**sun.misc.**BASE64Encoder来完成我们的编码,但是看看它所在的包。这是 JVM 的一部分,但却是专有的。当我们编译使用该类的代码时,我们会得到以下警告消息:

sun.misc.BASE64Encoder is Sun proprietary API and may be removed in a future release

如果我们愿意,我们可以从许多免费来源获得 Base64 算法。但幸运的是,Base64 编码并不是唯一的游戏。我们可以创建自己的可打印编码。看清单 11-4 。让我们取异或运算得到的每个int,并通过调用Integer.toHexString()得到十六进制格式的String表示。我们的十六进制字符串的长度将是一个或两个字符,范围从十六进制的0 (0x00)到fe (0xfe)(十进制的 0 到 254)。当数字小于 0x10 时,它将只有一个字符,但是我们想知道如何分解我们的编码字符串,所以我们将在那些十六进制字符串前面加上一个“0”字符。这样,每个字节由两个字符表示,我们知道如何解析和解码每个字符。我们可以称之为填充十六进制异或编码。

清单 11-4。填充十六进制异或编码

    StringBuffer sBuf = new StringBuffer();     String oneByte;     for( int i = 0; i < eTLength ; i++ ) {         oneByte = **Integer.toHexString**( (int)eTBytes[i] ^ (int)xBytes[i] );         if( oneByte.length() == 1 ) sBuf.append( "0" + oneByte );         else sBuf.append( oneByte );     }     String decodeThis = sBuf.toString();

解码这个OJSCode连接字符串的过程与此相反。我们在每对字符上调用Integer.parseInt(),使用 16 的基数,因此我们从十六进制转换为十进制。然后,我们将这个整数与我们之前使用的“location”字符串中的另一个字节进行异或运算。这使我们回到最初的字符,我们从这些字符开始重建连接字符串。

使用 Base64 编码,编码字符串会更短。下面是一个例子(注意末尾的“=”符号填充):

IE5DEgtTNVAUOUUGKw4aQkUnFR8KQA==

我们自己的编码产生了更长的字符串。这是用我们的算法编码的同一个字符串。

204e43120b533550143945062b0e1a424527151f0a40

混淆算法/代码

我们对OJSCode的计划是对我们的appver密码进行编码。在这种情况下,我们的 Java 代码类似于一个加密密钥。有了密钥,我们加密的东西就能被解密。有了OJSCode,我们的编码密码可以被任何能够访问OJSCode类的人解码和读取。

如果一个黑客拥有这个类,但是看不到OJSCode Java 代码,那么她将无法复制这个逻辑,并且有一个隐藏的方面是他无法复制的:我们调用这个类来获取OracleJavaSecure类中的location字符串。这算不上什么安全功能,但如果我们隐藏它,它将成为黑客的主要障碍。

我在这里建议的是代码混淆。逻辑隐藏在明显的地方。很难知道这是否会对阅读我们的代码造成很大的障碍,但这需要几个转换步骤——与我们混淆代码的步骤相反。

混淆逻辑

我最喜欢 Java 代码的一个方面是它的可读性。我确实用堆栈方法调用来压缩我的代码,Java 代码的面向对象方面是必须理解的,但总的来说,它非常可读。如果我们把降低可读性作为我们的目标呢?我们可以采取的第一步是使用我们的十六进制编码,而不是标准的 Base64。这里的其他想法旨在让您了解如何混淆代码。请注意,使您的代码更难被黑客理解的东西也会使您的代码更难理解和维护。因此,请在安全的地方保存一份原始代码的副本,以供自己参考。

我们有一个名为eTLength的计算字段,它是我们初始连接字符串的长度,但是如果我们愿意在每次需要时获取字符串的长度,我们就不需要方法成员。我们可以通过串联Strings而不是使用StringBuffer来增加复杂性。它的效率并不高——事实上,它用瞬态字符串塞满了内存,但是逻辑不太明显。让我们也将for循环转换成一个永恒的do while true循环。我们将通过转到标签GT来打破循环:我们正在创建一个相当于GOTO语句的 Java。我们有一些关于预期输入/输出的内部知识;编码字符串(使用我们的填充十六进制编码)的长度将是连接字符串的两倍。利用这些知识,当编码的字符串足够长时,我们将会跳出循环;也就是说,连接字符串长度的两倍,连接字符串长度的模等于 0。我们知道,我们需要处理的原始字节数组中的下一个位置是编码字符串长度的一半,因为它被连接在一起。我们的其他字节数组中可能没有足够的字符来与连接字符串字节进行 XOR 运算,所以让我们通过使用 modulus 绕到“其他”字节数组的前面。我们这样做,而不是将location连接到它自己,直到足够长。到目前为止,我们的混淆代码如清单 11-5 所示。

清单 11-5。混淆逻辑,第 1 步

`    static String encode( String encodeThis ) {
        byte[] eTBytes = encodeThis.getBytes();
        byte[] xBytes = OracleJavaSecure.location.getBytes();
        String decodeThis = "";
        String oneByte;

**        GT: do {**

oneByte = Integer.toHexString(
                (int)eTBytes[decodeThis.length()/2] ^
                (int)xBytes[( decodeThis.length()/2 ) % xBytes.length] );
            if( oneByte.length() == 1 ) decodeThis += "0";
            decodeThis += oneByte;
            if( ( ( decodeThis.length()/2 ) % eTBytes.length ) == 0 )
                break GT;
        } while( true );
        return decodeThis;
    }`

查看我们的代码,我们看到许多硬编码的整数,其中大多数等于 2。让我们创建一个成员整数,通过复杂的计算得出是 2,并在这些整数的步调中使用它。我们也将利用这样一个事实:我们的整数除以自身是 1,减去自身是 0。一旦我们获得了原始连接字符串的字节,我们就可以重用该原始连接字符串作为我们的返回字符串。在OJSCode的几个地方,我们通过调用String.getBytes()方法从一个字符串中获得一个字节数组。让我们把它重写为一个叫做traverse()的方法来代替String.getBytes()。在这种情况下,我们将允许traverse()获取一个null并得到location字符串的字节。此外,我们将在OracleJavaSecure中创建一个额外的成员,命名为小写l (ell),我们将设置它等于location,我们将指向它。现在我们的代码看起来像清单 11-6 中的。

清单 11-6。混淆逻辑,步骤 2

`    static String encode( String encodeThis ) {
        byte[] eTBytes = traverse( encodeThis );
        byte[] xBytes = traverse();
        encodeThis = "";
        String oneByte = "*";
        int twoI = Integer.parseInt(
            String.valueOf( Integer.toHexString(
            (int)(oneByte.charAt(twoI - twoI))).charAt(twoI - twoI)));
        GT: do {
            oneByte = Integer.toHexString(
                (int)eTBytes[encodeThis.length()/twoI] ^
                (int)xBytes[( encodeThis.length()/twoI ) %
                xBytes.length] );
            if( oneByte.length() == ( twoI/twoI ) )
                encodeThis += "0";
            encodeThis += oneByte;
            if( ( ( encodeThis.length()/twoI ) % eTBytes.length )
                == ( twoI - twoI ) )
            {
                System.arraycopy( xBytes, twoI - twoI,
                    eTBytes, twoI * 0, twoI );
                break GT;
            }
        } while( true );
        return decodeThis;
    }

static byte[] traverse( String encodeThis ) {
        int twoI = 0;
        if( encodeThis == null )
            encodeThis = OracleJavaSecure.l;
        byte[] eTBytes = new byte[encodeThis.length()];
        do eTBytes[twoI] = (byte)(encodeThis.charAt(twoI++));
        while( twoI < eTBytes.length );
        return eTBytes;
    }`

我们也增加了一些误导。现在,在我们的do while循环的最后一个if语句中,在我们中断到GT标签之前,我们执行一个arraycopy()。我们将“其他”字节数组中的前两个字节复制到连接字符串字节的开头。这本质上是没有意义的,因为我们已经处理完那些数组了;然而,误导的策略经常被用于混淆代码。

混淆命名

让我们再多走一步——尽管还可以完成许多其他的混淆过程。这一步仅仅是用难以阅读的无意义的名称替换我们的成员名称。让我们根据表 11-1 交换名字。数字 1 (one)、大写 I 和小写 l (ell)的这些序列被选择来混淆。它们都以一个字母字符开始,以符合编译器。这一步模糊处理的最大损失是这些方法和成员的有意义的名称的损失。

Images

经过这些翻译,我们的代码几乎无法辨认。只是代码结构看起来像 Java,还有几个通用的方法引用。清单 11-7 显示了 encode() [x()]和 traverse()[lI1ll()]方法。

清单 11-7 。模糊命名

public class OJSC {     static String x( String I1ll1 ) {         byte[] lII1l = lI1ll( I1ll1 );         byte[] ll1I1 = lI1ll( null );         I1ll1 = "";         String ll11I = "*";         int IlIl1 = Integer.parseInt(             String.valueOf( Integer.toHexString(             (int)(ll11I.charAt(0))).charAt(0)));         I11lI: do {             ll11I = Integer.toHexString( `                (int)lII1l[I1ll1.length()/IlIl1] ^
                (int)ll1I1[( I1ll1.length()/IlIl1 ) %
                ll1I1.length] );
            if( ll11I.length() == ( IlIl1/IlIl1 ) )
                I1ll1 += "0";
            I1ll1 += ll11I;
            if( ( ( I1ll1.length()/IlIl1 ) % lII1l.length )
                == ( IlIl1 - IlIl1 ) )
            {
                System.arraycopy( ll1I1, IlIl1 - IlIl1,
                    lII1l, IlIl1 * 0, IlIl1/IlIl1 );
                break I11lI;
            }
        } while( true );
        return I1ll1;
    }

static byte[] lI1ll( String I1ll1 ) {
        int IlIl1 = 0;
        if( I1ll1 == null )
            I1ll1 = OracleJavaSecure.l;
        byte[] lII1l = new byte[I1ll1.length()];
        do lII1l[IlIl1] = (byte)(I1ll1.charAt(IlIl1++));
        while( IlIl1 < lII1l.length );
        return lII1l;
    }`

生成编码的 APPVER 连接字符串

我们将向OracleJavaSecuremain()方法添加代码,如清单 11-8 所示,以接受appver的密码作为命令行参数,并加密appver的连接字符串。我们检查参数的格式,看它是否可能是一个双因素身份验证代码。如果不是,那么我们调用OJSC中的混淆代码进行加密和解密以供显示。这使用了这里定义的附加的appver连接字符串属性。

清单 11-8。app ver 密码和连接字符串的实用编码

`if( args.length != 0 && args[0] != null ) {
        String encodeThis = args[0];
        if( ! encodeThis.equals(checkFormat2Factor(encodeThis)) ) {

**            encodeThis = "jdbc:oracle:thin:appver/" + encodeThis +**

**                "@localhost:1521:orcl";**

//"@localhost:1521:apver"; // for use later in Chapter 11
            String encoded = OJSC.x( encodeThis );
            System.out.println( encoded );
            encodeThis = OJSC.y( encoded );
            System.out.println( encodeThis );
        }
    } else System.out.println(         "You may enter APPVER password on command line." );`

这里的意图是,您可以用您想要的appver密码调用一次OracleJavaSecure,然后在setAppVerConnection()方法中将编码的连接字符串复制到OracleJavaSecure.java的代码中。

对编码的 APPVER 连接字符串进行硬编码

我们将通过硬编码我们编码的appver连接字符串来修改setAppVerConnection()方法,并通过在调用setConnection()时调用OJSC.y()方法来解码它。(参见清单 11-9 。)通过在调用setConnection()的最后时刻解码连接字符串,并且不将成员变量设置为解码值,当黑客可能查看明文密码时,我们最小化了攻击模式和持续时间。

清单 11-9。对编码的 appver 连接字符串进行硬编码

    String prime = "030a42105f1b3311133a0048370707005f020419190b524215151b1c13411b0a601f0a17201c18391606795e5b5c54591b1b0c02"     setConnection( OJSC.y( prime ) );     appVerConn = conn;

创建 Oracle 客户端钱包

Oracle 客户端提供了一种对存储在客户端计算机上的密码进行混淆/加密的标准方法。它被称为安全外部密码存储,但它更好地被称为钱包。因为钱包可以在服务器和客户机上使用,我们应该称之为客户机钱包。我希望向您全面介绍 Oracle 客户端钱包,但我们最终不会在我们的安全基础架构中使用它们。

关于客户机钱包,您应该知道的第一件事是,默认情况下,它们必须存在于每个操作系统用户的主目录中。客户机钱包被视为 Oracle 凭据的个人存储。这与我想要实现的目标完全相反,那就是桌面上没有 Oracle 用户凭据。现在,如果我们将我们的appver用户密码与其他潜在的额外用户密码混合在一起,我们就无法集中管理它,例如更新密码。

关于 Oracle 客户机钱包,您应该知道的第二件事是,任何拥有它们的人都可以复制和使用它们。我可以创建一个客户端钱包并将appver密码放入其中,然后将其复制到每个计算机用户的主目录,他们都可以使用这个钱包以appver的身份登录。这似乎是集中分发密码更新的一种方式。也许吧,但这也是有问题的。这些钱包文件可以像糖果一样被送出,通过电子邮件发送给好友,从备份磁带中被盗——无论如何。这不是保护。假设操作系统正在提供对用户主目录的访问控制,并且可以保护文件。

第三件要知道的事情是,客户端钱包通过钱包密码来防止修改和查看,但是如果您有文件,则使用客户端钱包不需要认证。如果我们将我们的appver密码放在客户端钱包中,那么任何拥有钱包文件的人都可以作为appver连接到 Oracle 数据库。

安装 Oracle 客户端

Oracle 客户端包含在 Oracle 数据库安装中,但是您也可以单独下载并安装 Oracle 客户端。将 Oracle 客户端安装在单独的计算机上是测试客户端应用的最佳方式,而不是在 Oracle 数据库上运行它们,这也是可以做到的。在下面的讨论中,我们将假设您已经将您的ORACLE_HOME环境变量设置为类似于以下之一的值:

`SET ORACLE_HOME=D:\app\oracle\product\11.2.0\dbhome_1

SET ORACLE_HOME=C:\app\oracle\product\11.2.0\client_1`

创建钱包

当您决定将连接密码放入客户端钱包时,您要做的第一件事是为钱包选择一个位置。虽然对创建 wallet 的位置没有限制,但是当您使用 wallet 连接 Oracle 数据库时,假定的位置是在当前操作系统用户的主目录中。您可以在任何安全的位置创建您的客户端钱包,但是您也可以在您自己需要的地方创建它。否则,您需要将它复制到您的主目录中以供使用和测试。

预期的钱包位置位于用户主目录中与操作系统用户 ID 同名的子目录中。例如,如果用户的 OS 用户 ID 是 FredF,那么我们将在他的主目录中创建一个fredf目录,并在那里创建或复制 wallet 文件。在我们的示例中,我们将使用 FredF 作为操作系统用户名。为您的客户端钱包创建一个目录。

mkdir C:\Users\FredF\fredf

images 注意在 Windows XP 中,你会用文档和设置代替这些命令中的用户

为了创建您的密码存储,您将向 mkstore 实用程序发出命令。一个命令将创建存储,另一个命令将创建密码凭证。在每个命令中,您将指向钱包文件的目录位置。第一个 mkstore 命令创建钱包。它会提示您输入符合特定复杂性规则的钱包密码。

%ORACLE_HOME%\bin\mkstore -wrl C:\Users\FredF\fredf –create

第二个命令创建加密的密码/凭证。该命令将提示您输入两次appver用户的密码;然后它会提示你输入钱包的密码——这个密码就是你在第一个 mkstore 命令中输入的密码。现在,让我们在客户端钱包中为appusr用户创建一个条目:

mkstore -wrl C:\Users\FredF\fredf –createCredential **orcl_appver** appver mkstore -wrl C:\Users\FredF\fredf –createCredential **orcl_appusr** appusr

您可以使用几个命令来查看客户端钱包的内容。您需要输入钱包密码才能使用这些命令:

mkstore -wrl C:\Users\FredF\fredf -list mkstore -wrl C:\Users\FredF\fredf –listCredential mkstore -wrl C:\Users\FredF\fredf –viewEntry oracle.security.client.connect_string1 mkstore -wrl C:\Users\FredF\fredf –viewEntry oracle.security.client.username1 mkstore -wrl C:\Users\FredF\fredf –viewEntry oracle.security.client.password1

注意,我们将第一个凭证的名称命名为orcl_appver——这代表了 orcl Oracle 数据库实例上的appver用户。我们需要对配置数据库实例的 TNSNames (透明网络底层)搜索的 sqlnet.oratnsnames.ora 文件进行一些补充。TNS 之于 SQLnet (Oracle 的数据库网络通信协议)如同域名服务(DNS)之于 TCP/IP。TNS 允许我们为单个 Oracle 数据库实例使用多个名称(别名),并在编写应用时引用别名,这些别名可以在不同的时间指向不同的 Oracle 实例。这种灵活性是命名服务的主要原因;另一个主要原因是进行远程查找(不在本地存储所有的姓名和地址)及其必然的原因:集中管理姓名/地址关联。当然,我们需要为 TNSNames 服务使用 LDAP 或类似的东西来实现第二个目标。

TNSNames 服务有许多我们在本书中没有涉及的特性。有关更多信息,请参考 Oracle 数据库网络服务参考手册。

sqlnet.oratnsnames.ora 文件存在或需要在客户端的特定目录下创建。根据您的安装,这两个文件都在类似于% ORACLE _ HOME % \ network \ admin的目录中。将清单 11-10 中的行添加到您的 sqlnet.ora 文件中。对于基本的客户端钱包安装,您只需要指定WALLET_OVERRIDE指令。您也可以指定WALLET_LOCATION指令,但是它很可能没有被使用。我发现WALLET_LOCATION指令的格式有点敏感;虽然(对于特定的 Oracle 客户端版本)允许使用驱动器号,但不允许使用引号和尾随的“\”字符。还要注意,由 Oracle 11g 客户机创建的 wallet 不能用于 Oracle 10g 客户机,但是 10g wallets 可以用于 11g 客户机。

清单 11-10。添加到钱包客户端 sqlnet.ora 文件

SQLNET.WALLET_OVERRIDE=TRUE

images 注意WALLET_OVERRIDE指令放在服务器 sqlnet.ora 文件中(例如% ORACLE _ HOME % \ NETWORK \ ADMIN \ sqlnet . ora)可以阻止 ORACLE 数据库响应客户端连接。我的建议是,如果您在与 Oracle 数据库相同的计算机上测试客户机 wallet,那么您可以在没有 sqlnet.ora 文件中的WALLET_OVERRIDE的情况下启动数据库,然后在测试客户机 wallet 时临时添加该指令

*将清单 11-11 中的行添加到您的客户端 tnsnames.ora 文件中。第一部分是 Oracle 实例的标准 TNSNames 条目。对于我们在钱包中输入的每个密码,我们将需要在 tnsnames.ora 中输入一个额外的条目。如果你以前在 tnsnames.ora 中做过条目,但从未使用过钱包,这可能对你来说有点奇怪。但是考虑到您正在为钱包中的每个凭证的特定用户提供密码,因此您正在将该密码与 tnsnames.ora 中的条目进行协调。例如,orcl_appvertnsnames.ora 中专门供appver用户使用的条目。

清单 11-11。添加到钱包客户端 tnsnames.ora 文件

`orcl =
        (DESCRIPTION=
                (SOURCE_ROUTE=YES)
                (ADDRESS=(PROTOCOL=tcp)(HOST=orcl.org.com)(PORT=1521))
                (CONNECT_DATA=(SERVICE_NAME=ORCL)))

orcl_appver =
        (DESCRIPTION=
                (ADDRESS=(PROTOCOL=tcp)(HOST= orcl.org.com)(PORT=1521))
                (CONNECT_DATA=
                        (SERVER=DEDICATED)
                        (SERVICE_NAME=ORCL)
                        (SID=ORCL)))

orcl_appusr =
        (DESCRIPTION=
                (ADDRESS=(PROTOCOL=tcp)(HOST= orcl.org.com)(PORT=1521))
                (CONNECT_DATA=
                        (SERVER=DEDICATED)
                        (SERVICE_NAME=ORCL)
                        (SID=ORCL)))`

images 注意至少主机名在你的情况下会有所不同,所以修改那些设置。

使用 SQL*Plus 中的钱包

这是使用钱包最巧妙的地方。假设 wallet 已创建并放置在需要的位置,并且您的配置文件是正确的,则无需输入口令就可以连接到 Oracle 数据库。只需输入以下 SQL*Plus 命令进行连接:

%ORACLE_HOME%\bin\sqlplus /@orcl_appusr

请记住,密码是通过钱包以某种方式获得的;因此,它仍然存在于客户端。当从钱包中获取明文密码时,可以在处理过程中检查计算机存储器以捕获明文密码。人们还可以检查 Oracle 数据库用来维护和使用 wallet 的 Java 代码、混淆的 Java 代码、dll 和混淆的 DLLs,从而揭示独立解密 wallet 口令的过程。我不想假装知道这有多难。

使用 Java 的钱包

在 Java 应用中使用客户机钱包进行身份验证需要在您的CLASSPATH中添加一个 jar 文件。读取钱包文件需要 oraclepki.jar 文件。在命令行上,你可以在 Chapter11/wallet 文件夹中运行一个测试。

java -cp %CLASSPATH%;%ORACLE_HOME%\jlib\oraclepki.jar TestWallet

TestWallet.java文件中,您会发现一个main()方法,其代码如清单 11-12 所示。您可能需要修改此代码来运行测试。

清单 11-12。配置 Java 使用客户端钱包

    System.setProperty("oracle.net.tns_admin",         "C:/app/oracle/product/11.2.0/client_1/NETWORK/ADMIN");     Properties info = new Properties();     String username = System.getProperty( "user.name" );     info.put("oracle.net.wallet_location",         "(SOURCE=(METHOD=file)(METHOD_DATA=(DIRECTORY=C:/Users/" +         username + "/" + username + ")))");

我们设置了tns_admin系统属性,这样我们就可以找到 tnsnames.ora 文件,以及该文件中的连接标识符orcl_appusr。我们实例化了一个Properties对象,当我们得到Connection时将传递这个对象。我们需要设置的属性之一是wallet_location属性。您还记得,我们将钱包文件创建或复制到用户的主目录中,该目录与用户同名。为了设置 wallet 属性,我们获取了user.name System属性,并从该值连接了一个目录位置。

我们使用一个OracleDataSource类来获取我们的连接,如清单 11-13 中的所示。我们在第八章中看到了OracleDataSource,当时我们看到了一种池连接的单点登录。在这种情况下,我们将连接 URL 设置为使用我们的钱包连接标识符orcl_appusr。我们还传入了之前实例化的Properties类,并调用了getConnection()方法。

清单 11-13。使用客户端钱包获得 Java 连接

    OracleDataSource ds = new OracleDataSource();     ds.setURL("jdbc:oracle:thin:@**orcl_appusr**");     ds.setConnectionProperties(info);     Connection c = ds.getConnection();

在 TestWallet.java 的,我们也建立了一个代理连接,和我们在OracleJavaSecure中使用的一样。因此,只有能够进行 SSO 的操作系统用户(拥有匹配的 Oracle 用户)才能运行TestWallet

*#### 管理钱包安全

客户端钱包可能是保护我们的应用验证用户密码的完美解决方案。我鼓励你在这种情况下使用它,除了我的保留意见。

诚然,钱包可以保护密码不被黑客读取,这可能比我们迄今为止在本书中开发的任何东西都要好。一旦你让它工作起来,它就会像描述的那样工作。很容易复制到每个人的主目录中供他或她使用。此外,我们已经将appver密码描述为数据:真实应用帐户的看门人或保镖。

然而,钱包的一些积极方面也有其黑暗的一面。任何拥有 wallet 文件的人都可以按照用户的指定连接到 Oracle 数据库。这种联系不一定要通过你的应用来实现;正如我们所看到的,它可以通过 SQL*Plus 会话发生。对于appver用户来说,这不是什么大问题——但是对于大多数其他 Oracle 数据库用户来说,这是个大问题。不要只考虑合法的计算机用户,也要考虑(例如)那些访问您的异地备份存储库并从那里收集钱包文件的用户。人们认为保护钱包文件是操作系统权限的责任,但这并不可靠——如果一个合法用户通过电子邮件将文件发送给一个伪装成计算机支持技术人员的黑客呢?

另一个问题是一个与文件分发、管理和更新有关的实际问题。可能您已经有了一个系统,但是将文件放入每个用户的主目录并保持更新并不是一件小事。

最后,在客户端钱包和安全性方面没有提到的一个问题是密码与主机/Oracle 数据库实例规范的脱节,这是对用于配置钱包身份验证的文件进行检查后得出的逻辑结论。用户可能会尝试用其他 Oracle 实例替换您在 tnsnames.ora 文件中指定的实例;从而针对每个实例测试特定的钱包用户和密码。例如,如果一个黑客修改了他机器上的 tnsnames.ora 文件,使得orcl_appusr条目指向(SID=TestOrcl),他可以尝试使用同样的命令以appusr的身份连接到该实例:

sqlplus /@orcl_appusr

images 警告对于 Oracle client wallet 文件,攻击者可以使用您在任何实例上配置的指定用户进行连接,只要该用户存在并具有相同的密码。

也许您的用户只存在于一个实例上,或者在每个实例上只有完全相同的特权;然而,更有可能的是,用户在较低优先级(沙盒、开发或验收)区域上比在生产区域上具有扩展特权。任何实例上的这些扩展权限都可能提供额外的攻击媒介。

跟踪 Oracle 客户端代码

在开发过程中,您经常会有这样的经历:事情不顺利,您的应用无法告诉您问题出在哪里。错误发生在底层协议的某个地方,并且是隐藏的或模糊的。您甚至可能会看到误导性的错误消息。

在这种情况下,您可能需要调用后备资源,要求您的网络管理员在您的子网上放置一个网络嗅探器,并捕获数据包进行分析;希望你能发现问题。

但是,在此之前,在处理 Oracle 数据库时,您自己也有一些选择。您可以在客户端打开跟踪,查看客户端和 Oracle 数据库之间底层协议对话。只需在客户端 sqlnet.ora 文件中设置跟踪级别。值 16 是最大细节追踪;您也可以在级别 8、4、2 和 1 中选择较少的细节。

TRACE_LEVEL_CLIENT=4

images 警告当您完成当前问题的故障排除后,请确保禁用跟踪日志记录。它会生成大量文件,这些文件可能包含大量数据,这些数据不仅会占用大量磁盘空间,还会带来安全隐患。在更高级别的跟踪日志记录中,从查询返回的数据也会显示在跟踪文件中。

跟踪文件(有几个)的默认位置之一是一个如下命名的文件夹:

%ORACLE_HOME%\log\diag\clients\user_UserID\host_##########_##\trace

在 Oracle 数据库上,您可能必须创建基本目录树,并授予所有用户对其进行写入的权限。

mkdir %ORACLE_HOME%\log\diag\clients

下面是启用了第 4 级跟踪的示例(只显示了前几行):

Trace file D:\app\oracle\product\11.2.0\dbhome_1\log\diag\clients\ user_UserID\host_##########_##\trace\ora_2988_5952.trc 2011-07-09 07:22:35.826970 : --- TRACE CONFIGURATION INFORMATION FOLLOWS --- 2011-07-09 07:22:36.058905 : New trace stream is D:\app\oracle\product\11.2.0\dbhome_1\log\diag\clients\ user_UserID\host_##########_##\trace\ora_2988_5952.trc 2011-07-09 07:22:36.058968 : New trace level is 4 2011-07-09 07:22:36.059012 : --- TRACE CONFIGURATION INFORMATION ENDS --- 2011-07-09 07:22:36.059057 : --- PARAMETER SOURCE INFORMATION FOLLOWS --- 2011-07-09 07:22:36.059105 : Attempted load of system pfile source D:\app\oracle\product\11.2.0\dbhome_1\network\admin\sqlnet.ora 2011-07-09 07:22:36.059203 : Parameter source loaded successfully 2011-07-09 07:22:36.059241 : 2011-07-09 07:22:36.059275 : Attempted load of local pfile source C:\OraJavSecure\Chapter11\wallet\sqlnet.ora 2011-07-09 07:22:36.059308 : Parameter source was not loaded 2011-07-09 07:22:36.059337 : 2011-07-09 07:22:36.059367 : -> PARAMETER TABLE LOAD RESULTS FOLLOW <- 2011-07-09 07:22:36.059402 : Successful parameter table load 2011-07-09 07:22:36.059435 : -> PARAMETER TABLE HAS THE FOLLOWING CONTENTS <- 2011-07-09 07:22:36.059472 :  TRACE_LEVEL_CLIENT = 4 2011-07-09 07:22:36.059504 :  SQLNET.WALLET_OVERRIDE = TRUE ...

记录 Oracle 瘦客户机跟踪数据

当您使用 Java 瘦 ojdbc 驱动程序时,您不能通过sqlnet.ora中的设置来配置跟踪。您将需要使用 ojdbc 驱动程序的日志功能。为此,您需要在您的CLASSPATH上放置一个不同的 ojdbc 驱动程序 jar, ojbdc6_g.jar (在 ojdbc6.jar 之前或作为其替换)。这个备用的日志 jar 文件位于 ORACLE 客户机目录%ORACLE_HOME%\jdbc\lib 中,或者位于 Oracle 下载网站:

www . Oracle . com/tech network/indexes/downloads/index . html

将目录更改为 wallet。要使用这个驱动程序文件(和钱包)运行TestWallet类,您可以运行一个 Java 命令行。跟踪日志记录和客户端 wallet 是不相关的;我们只是用TestWallet作为一个方便的例子来展示跟踪日志。

`cd Chapter11/wallet

java -Doracle.jdbc.Trace=true
    -cp .;%ORACLE_HOME%\jdbc\lib\ojdbc6_g.jar;%ORACLE_HOME%\jlib\oraclepki.jar
    -Djava.util.logging.config.file=OracleLog.properties TestWallet > temp.txt 2> temp2.txt`

关于这个命令行有几件事值得一提。指令-Doracle.jdbc.Trace=true打开跟踪记录。注意CLASSPATH, -cp指令中的 ojdbc6_g.jar 。使用这个 jar 文件可以启用日志记录。我们在CLASSPATH上还有 oraclepki.jar 文件,用于启用钱包。

-Djava.util.logging.config.file=OracleLog.properties指令告诉日志记录类在名为 OracleLog.properties 的文件中查找它们的属性设置。用于日志记录的示例属性文件可能包含清单 11-14 中的条目。

清单 11-14。配置 Java (ojdbc)跟踪日志,OracleLog.properties

handlers = java.util.logging.ConsoleHandler java.util.logging.ConsoleHandler.level = ALL java.util.logging.ConsoleHandler.formatter = java.util.logging.SimpleFormatter .level=CONFIG oracle.jdbc.level = FINE oracle.jdbc.connector.level = FINE oracle.jdbc.driver.level = FINE oracle.jdbc.pool.level = FINE oracle.net.ns.level = TRACE_20

images 注意你会在chapter 11/wallet/Oracle log . properties .找到一个完整的配置属性文件

前三个属性行将控制台(命令提示符窗口)配置为日志处理程序。ConsoleHandler.level = ALL指令表示我们希望生成的所有内容都发送到控制台。这是我的偏好。然后,如果有太多的日志数据向我袭来,我可以将其重定向到一个文件。命令行的最后一部分显示了两个标准输出数据流的重定向,它们通常出现在命令提示符窗口> temp.txt 2> temp2.txt。第一个大于号将“标准输出”流重定向到当前目录中名为 temp.txt 的文件。带有前缀“2”的第二个大于号将“标准错误”流重定向到一个名为 temp2.txt 的文件。您可以在命令的末尾使用这个指令将这两个流发送到同一个文件:> temp.txt 2> &1.

属性文件的最后几行配置跟踪日志记录的详细程度。第一个属性.level=CONFIG为 ojdbc 类的所有方面设置默认的日志记录级别。CONFIG级别为中等细节。其他级别设置在FINETRACE_20,是更详细的级别。示例 OracleLog.properties 文件列出了日志记录适用的所有方面,以及可以设置的所有级别。

使用 OracleLog.properties 中提供的设置,temp2.txt 中会生成一个非常大的跟踪输出。以下几行只是所生成输出的一部分:

Jul 9, 2011 8:32:37 AM oracle.jdbc.pool.OracleDataSource <init> TRACE_1: Public Enter: Jul 9, 2011 8:32:37 AM oracle.jdbc.pool.OracleDataSource <init> TRACE_1: Exit Jul 9, 2011 8:32:37 AM oracle.jdbc.pool.OracleDataSource setURL TRACE_1: Public Enter: "jdbc:oracle:thin:@orcl_appusr" Jul 9, 2011 8:32:37 AM oracle.jdbc.pool.OracleDataSource setURL TRACE_1: Exit Jul 9, 2011 8:32:37 AM oracle.jdbc.pool.OracleDataSource setConnectionProperties TRACE_1: Public Enter: {oracle.net.wallet_location=(SOURCE=(METHOD=file) (METHOD_DATA=(DIRECTORY=C:/Users/OSUSER/OSUSER))), oracle.net.encryption_types_client=AES192} Jul 9, 2011 8:32:37 AM oracle.jdbc.pool.OracleDataSource setConnectionProperties TRACE_1: Exit ... 00 00 00 00 00 53 45 4C     |.....SEL| 45 43 54 20 55 53 45 52     |ECT.USER| 20 46 52 4F 4D 20 44 55     |.FROM.DU| 41 4C 01 01 00 00 00 00     |AL......|

最后几行是发送到 Oracle 数据库的数据包的一部分,显示了所使用的查询。两个方向上每个数据包的全部内容都包含在跟踪日志中。没有使用数据加密。使用跟踪日志记录是在故障排除时要做的事情,但不应该在生产中进行。

加密存储在 Oracle 数据库中的数据

到目前为止,我们已经加密了客户端和 Oracle 数据库之间的网络数据。我们还加密了内存中的数据,但没有在客户端使用(连接字符串列表。)但是,我们以明文形式将数据存储在 Oracle 数据库中。我们特别关注以明文形式存储在 Oracle 上的连接字符串列表。我们将在这里解决这个问题。

DBMS_CRYPTO 包

Oracle 数据库提供了一个 PL/SQL 包,可以帮助我们进行静态数据加密(当数据存储在数据库中时)。通过使用DBMS_CRYPTO包,我们可以有选择地加密特定的数据列。还有其他选择,包括全表加密。Oracle Advanced Security 产品可从 Oracle 单独购买,可用于实现这一目标。

默认情况下,DBMS_CRYPTO包安装在 Oracle Database 11g 中,但默认情况下不启用。我们将为我们的应用安全用户appsec启用它。我们将使用一个由appsec拥有,但由appver执行的存储过程来完成加密/解密。我告诉你这些是为了解释为什么我们在DBMS_CRYPTO上给appsec授权执行,而不是给app_sec_role。原因是,除非您在会话中,否则不存在角色,在我刚刚描述的场景中,appsec不存在会话。在 Oracle SQL 命令提示符下,我们将执行这个命令。

GRANT EXECUTE ON sys.dbms_crypto TO **appsec**;

密码和密钥

现在,作为一个完全公开的问题,我赶紧补充说,DBMS_CRYPTO可以用于各种加密/解密任务。理论上,我们可以使用 Java 和 JCE 在客户端进行加密,使用DBMS_CRPTO在 Oracle 数据库上进行解密。这个提议有两个问题。首先,DBMS_CRYPTO的灵活性不足以让我们达到与OracleJavaSecure流程相同的安全级别。第二,DBMS_CRYPTO不像加密/解密通信的单方面;更确切地说,它假定它是既进行加密又进行解密的一方。它不会轻易交换加密密钥,也不会公开它的算法。

为了满足加密静态数据的要求,我们需要能够由不同的用户在不同的时间、不同的位置解密数据。也就是说,很明显,我们不能在需要的时候“协商”一个或一组密钥。我们需要在某个地方存储加密密钥,并在未来某个不确定的时间检索它来解密数据。

开发人员有责任决定在哪里存储用于DBMS_CRYPTO的加密密钥。将密码与加密数据一起存储在 Oracle 数据库中有很多好处。一个好处是,当我们恢复加密数据的备份时,我们也恢复了解密密钥。这里的风险是,如果有人获得了加密数据,他们也可能获得解密密钥。

一种可行的替代方法是将解密密钥存储在客户端应用中,并在我们需要解密数据时将其传递给 Oracle 数据库;然而,这似乎并不更安全。不,我们将坚持把加密密钥存储在数据库中,并且我们将采取一些步骤来确保它不能被我们的应用之外的任何人使用。在我告诉你我是怎么做的之前,想想你会怎么做。

静态加密密钥存储

我们将把我们的加密/解密密钥,至少是密钥的起源,存储在一个普通的 Oracle 表中,t_application_key—参见清单 11-15 。我们可能希望将这些键存储在一个单独的 Oracle 实例上,并通过一个数据库链接获取它们。这样,加密数据和加密密钥将分别备份。我们将提供一个版本号列,以防我们希望每个数据库有多个密钥,可能是为了不同的应用。你会明白为什么这可能是不必要的。我们还创建了这个表的索引和视图v_application_key

images 注意你会在一个名为 Chapter11/AppSec.sql 的文件中找到这个 SQL 脚本。

清单 11-15。静态密钥加密表

CREATE TABLE appsec.t_application_key (     key_version NUMBER(3) NOT NULL,     -- Max Key size 1024 bits (128 Bytes)     key_bytes   RAW(128) NOT NULL,     create_ts   DATE DEFAULT SYSDATE );

我们要保证的一点是,密钥永远不会改变。我们将使用 on update/delete 触发器来实现这一点。这个触发器,如清单 11-16 中的所示,将基本上扭转尝试的更新并拒绝它。

清单 11-16。更新/插入静态密钥加密表上的触发器之前

CREATE OR REPLACE TRIGGER appsec.t_application_key_budr **BEFORE UPDATE OR DELETE**     ON appsec.t_application_key FOR EACH ROW BEGIN     **RAISE_APPLICATION_ERROR**(-20001,'Cannot UPDATE or DELETE Records in V_APPLICATION_KEY.'); END; `/

ALTER TRIGGER appsec.t_application_key_budr ENABLE;`

让我们继续将几条记录插入到v_application_key中,我们将使用其中的一条。参见清单 11-17 。我们将使用DBMS_CRYPTO包中的RANDOMBYTES函数来生成一个 128 个随机字节的字符串,作为我们的密钥起源。我说密钥起源是因为,正如你将看到的,真正的加密/解密密钥是在以后由这些密钥字节组合而成的。

清单 11-17。插入几个随机的静止加密密钥

INSERT INTO appsec.v_application_key ( key_version, key_bytes ) VALUES ( 1, SYS.DBMS_CRYPTO.RANDOMBYTES(1024/8) );

同样使用相同的INSERT命令插入数字 2 到 5 的key_version值。

加密/解密静态数据的功能

我们将构建两个 Oracle 存储函数,对要存储在数据库中的数据进行服务器端加密,并根据需要解密以供使用。它们被加密到t_application_key中密钥起源字节的特定版本,并且它们使用DBMS_CRYPT包来进行加密。我们将通过不拿走我们的数据和我们的密钥字节并直接进行加密来使其难以复制;相反,我们将首先对关键字节执行一些操作。任何阅读这些函数的人都将能够知道我们在做什么,并且能够复制它,因此我们将通过 Oracle Wrap 实用程序传递这些函数来混淆代码,从而隐藏函数代码。

我鼓励你以这些函数为起点,充分修改它们以改变加密过程,然后在安全但隐藏的地方保存一份副本。您将把它们转换成包装的函数,这将是不清晰的。

我们最初的f_mask函数,如清单 11-18 到 11-21 所示,以RAW的形式获取明文连接字符串列表。它还接受应用内部类的类名和版本。它返回一个包含连接字符串列表的加密的RAW

清单 11-18。加密存储数据的函数签名

CREATE OR REPLACE FUNCTION appsec.f_mask(     clear_raw       RAW,     m_class_name    v_app_conn_registry.class_name%TYPE,     m_class_version v_app_conn_registry.class_version%TYPE ) RETURN RAW

我们硬编码一个特定的key_version数量的 genesis 密钥字节——在这个例子中是版本 4(见清单 11-19 )。这是我们相当随意的决定。我们从v_application_key中为该版本选择key_bytes。我们将key_bytes转换成一个名为app_key的变量。

清单 11-19。硬编码密钥版本

AS     crypt_raw RAW(32767) := NULL; **    app_ver   v_application_key.key_version%TYPE := 4;**     app_key   v_application_key.key_bytes%TYPE;     iv        RAW(16); BEGIN **    SELECT key_bytes INTO app_key FROM v_application_key WHERE key_version = app_ver;**

就像那种三杯集中赌博游戏中,杯子下面有弹珠。代码比眼睛还快。我们将处理app_key的字节。我们执行的第一个过程是获取class_version并与字符串“足够长度”连接。然后,如清单 11-20 所示,我们将app_key与连接的字符串XOR在一起。也许只有app_key的前 20 个字节左右被XOR修改。

images 注意我们刚刚把这个过程变成了特定应用的特定版本所独有的(呈现内部类的那个)。

清单 11-20。将密钥与类版本进行异或运算,得到密钥的 MD5 哈希

    app_key := SYS.UTL_RAW.BIT_XOR( app_key,         SYS.UTL_RAW.CAST_TO_RAW(m_class_version||'SufficientLength') );     app_key := SYS.DBMS_CRYPTO.HASH( app_key, SYS.DBMS_CRYPTO.HASH_MD5 );     app_key := SYS.UTL_RAW.CONCAT( app_key, app_key );

我们的下一个过程将app_key设置为等于app_key的消息摘要(MD5)散列。清单 11-20 显示了这一点。MD5 是一种单向散列算法,它创建代表初始值的 16 字节(128 位)散列。对初始值的任何修改都会导致散列值改变,如果初始值不变,MD5 将总是计算相同的散列值。然后,为了得到 32 字节的密钥,我们将app_key设置为两个 MD5 散列的串联。

对于我们将要使用的加密算法,我们还需要一个 16 字节的RAW初始化向量(IV)。我们将再次通过使用应用内部类名作为 IV 的一部分,使这个函数特定于应用。参见清单 11-21 。实际上,我们将class_name与字符串“SufficientLength”连接起来,将其转换为RAW,并获得前 16 个字节作为 IV。

清单 11-21。用类名获取初始化向量,调用 DBMS_CRYPTO。加密

    iv := SYS.UTL_RAW.SUBSTR(         SYS.UTL_RAW.CAST_TO_RAW(m_class_name||'SufficientLength'), 0, 16 );     crypt_raw := SYS.DBMS_CRYPTO.ENCRYPT( clear_raw,         SYS.DBMS_CRYPTO.ENCRYPT_AES256 + SYS.DBMS_CRYPTO.CHAIN_CBC +         SYS.DBMS_CRYPTO.PAD_PKCS5, app_key, iv );     RETURN crypt_raw; END f_mask;

然后就像清单 11-21 中的一样调用DBMS_CRYPTO.ENCRYPT函数。我们传递明文连接字符串列表clear_rawapp_keyiv。我们还告诉该函数使用 256 位高级加密标准(AES256 ),采用块链接和 PKCS 填充。

哒哒!我们有一个加密的连接字符串列表,可以存储在磁盘和备份中。我们解密数据的f_unmask函数几乎与f_mask相同。我们以完全相同的方式构建app_keyiv,然后使用相同的加密算法系列将加密的连接字符串列表传递给DBMS_CRYPTO.DECRYPT函数。瞧,我们已经从冷库中取出了明文连接字符串。

包装工具

甲骨文公司几十年来一直致力于保护其知识产权。该公司开发了一个流程,通过该流程,它可以发布业务敏感的 PL/SQL 代码,就像我们的 Oracle 过程、函数和包一样,并将其分发给客户,而不会暴露代码的内部工作方式。Oracle 设计了 wrap 实用程序,它将混淆 PL/SQL 代码,使其无法被读取。我只能说,包装好的过程不经过一番努力是无法读懂的,因为据称有一些工具可以解开过程。

我们将使用 wrap 实用程序来混淆f_maskf_unmask函数。提醒一下,您应该首先修改f_maskf_unmask,使它们对您的公司是唯一的,然后包装它们。那就是避免这本书明显的信口开河把你的船弄沉。

将你的 F_MASK.sqlF_UNMASK.sql 文件的副本保存在一个安全的位置,然后将文件传递给 wrap 实用程序。包装的文件将有一个“.”。plb”扩展名,并且可以在任何文本编辑器中查看——它们不是二进制代码。最终的 Oracle 11g 包装过程将总是类似于清单 11-22 。

%ORACLE_HOME%\BIN\wrap INAME=F_MASK.sql %ORACLE_HOME%\BIN\wrap INAME=F_UNMASK.sql

清单 11-22。包裹版面膜功能

CREATE OR REPLACE FUNCTION appsec.f_mask wrapped a000000 b2 abcd abcd abcd abcd abcd abcd abcd abcd abcd abcd abcd abcd abcd abcd abcd 8 3d9 237 GehnTGWDxAhWnsVg2jYOTJ2/sF4wg/BeTCCsfI5Vgp0GvFbmFJFF9PpfKGM8NUbmI21KsMmT 9YLZz1gSTsZkw/skypO3G2z+bhL/AGJObl6IY3bf/PjNwdlhZ5argmaJytVX0RDALqjMIRvj `GLdGjZoM6cJZs4nHbLQMRgmOh9ZTnOnU0fQMG0vDHhtBL0CZSmx1R0SWpFQ20Iui96EL3CD4
...
1atpfb/f+oVZAZkY78T0YBdSmyOSgifZtm0IiEdc5rh/Lbn5pmTzHV8=

/`

注意,包装过程的第一行是一个CREATE OR REPLACE语句。我们可以将此代码复制并粘贴到任何 SQL 编辑器,如 SQL*Plus,并在数据库中创建 Oracle 结构。

我知道发生了很多事情,所以让我重申一下我们的目标。在包装这些函数时,我们的目的是让人们,无论是黑客还是探听者,不知道我们是如何加密连接字符串列表的,也不知道他们能够独立地解密和读取这些字符串。

变更为 setdecryptconns()/getcryptconns()

在作为 Java 存储过程运行的setDecryptConns()方法中间,我们获取将要存储在 Oracle 数据库中的连接字符串列表,并将它们传递给f_mask函数。清单 11-23 显示了这一点。从f_mask返回的加密字节被存储在数据库中。

清单 11-23。调用加密连接字符串进行存储

    stmt = ( OracleCallableStatement )conn.prepareCall(         "{? = call appsec**.f_mask**(?,?,?)}" );     stmt.registerOutParameter( 1, OracleTypes.RAW );     stmt.setBytes(  2, **connsHashBytes** );     stmt.setString( 3, className );     stmt.setString( 4, classVersion );     stmt.executeUpdate();     **connsHashBytes = stmt.getBytes(1);**

images 注意这段代码可以在chapter 11/orajavsec/Oracle javasecure . Java .中找到

我们还修改了getCryptConns()方法,在将连接字符串列表返回给客户端应用之前对其进行解密。这显示在清单 11-24 中。

清单 11-24。调用来解密存储中的连接字符串

`bA = stmt.getBytes(4);
    stmt = ( OracleCallableStatement )conn.prepareCall(
        "{? = call appsec.f_unmask(?,?,?)}" );
    stmt.registerOutParameter( 1, OracleTypes.RAW );
    stmt.setBytes(  2, bA );
    stmt.setString( 3, className );
    stmt.setString( 4, classVersion );
    stmt.executeUpdate();

oins = new ObjectInputStream( new ByteArrayInputStream(         stmt.getBytes(1) ) );
    Object currentConns = oins.readObject();`

管理应用的连接字符串

我们已经看到,并一直在使用该功能来添加和更新列表中的连接字符串,并将它们保存在 Oracle 数据库中。请记住,这是两个独立的步骤。第一步,在我们的列表中添加或更新连接字符串,发生在客户端,只影响客户端应用当前使用的列表。只有当我们将列表保存到 Oracle 数据库中时,我们才能使新的或替换的连接字符串对所有未来的应用用户可用。

同样,我们需要从列表中删除连接字符串。同样,我们将通过在一个单独的步骤中将列表保存到 Oracle 数据库来永久删除。

最后,我们希望能够复制连接字符串列表,供新版本的应用使用。这个复制过程必须是特定于应用的,所以一个装腔作势的应用不能获取我们的连接字符串列表供她非法使用。

预先警告任何获得应用安全访问权限的人,用户有权将连接字符串从一个应用复制到另一个应用。幸运的是,我们已经通过使f_maskf_unmask函数既针对应用又针对版本来解决这个问题。另一个应用可能持有我们的连接字符串列表,但是他们不能解密它们。然而,这意味着当我们将连接字符串列表复制到应用的新版本时,我们必须用旧版本f_unmask它们,用新版本f_mask它们。

创建应用管理用户

至此,我们已经描述了一个名为OSUSER的应用用户,我建议他可能是您(您的操作系统用户 ID)。这是一个普通的应用用户,他通过被授予CONNECT THROUGH appusr的方式被授予了查看HR模式中敏感数据的权限。

现在是时候区分普通应用用户和管理应用用户了。我们需要第二个用户,我称之为OSADMIN。这些用户之间的区别将在下一节通过单个角色授权来说明。增加该角色的原因是管理 Oracle 数据库上应用的连接字符串列表的更新。

现在,让我们创建第二个应用用户。首先,在我们的示例中,您需要有一个名为osadmin的额外操作系统用户帐户。作为 Windows 管理用户,您可以通过控制面板/用户帐户实用程序创建该帐户。一旦存在,运行清单 11-25 中的命令来创建OSADMIN用户(替换您刚刚创建的操作系统用户的操作系统用户 ID ),并授予他对HR模式中敏感数据的相同访问权限,就像我们授予OSUSER一样。我们这样做是为了测试新的管理角色所带来的差异。

清单 11-25。创建 OSADMIN Oracle 用户

`CREATE USER osadmin IDENTIFIED EXTERNALLY;
GRANT create_session_role TO osadmin;
ALTER USER osadmin GRANT CONNECT THROUGH appusr;

INSERT INTO hr.employees
    (EMPLOYEE_ID, FIRST_NAME, LAST_NAME, EMAIL, PHONE_NUMBER, HIRE_DATE,
    JOB_ID, SALARY, COMMISSION_PCT, MANAGER_ID, DEPARTMENT_ID)
VALUES
    (EMPLOYEES_SEQ.NEXTVAL, 'First', 'Last', 'OSADMIN.MAIL',     '800.555.1212', SYSDATE, 'SA_REP', 5000, 0.20, 147, 80);

COMMIT;

SELECT EMPLOYEE_ID FROM EMPLOYEES WHERE EMAIL='OSADMIN.MAIL';

INSERT INTO hr.v_emp_mobile_nos

( employee_id, user_id, com_pager_no, sms_phone_no, sms_carrier_cd )
    VALUES ( (SELECT EMPLOYEE_ID FROM EMPLOYEES WHERE EMAIL='OSADMIN.MAIL'),
      'OSADMIN', '12345', '8005551212', 'Verizon' );

COMMIT;

SELECT * FROM hr.v_emp_mobile_nos WHERE user_id = 'OSADMIN';`

该准则的重要方面如下:

  1. 我们使用与新操作系统用户 ID 相同的名称创建一个 Oracle 用户。
  2. 我们授予新用户对CONNECT THROUGH appusr的权限,这样他就可以从HR模式中选择加密的敏感数据。
  3. 我们在HR.EMPLOYEES表中为这个用户插入一条记录,并从EMPLOYEES_SEQ中获取下一个连续值作为新的EMPLOYEE_ID.
  4. 我们可以根据EMAIL地址选择新用户,这是一个唯一的字段。
  5. 使用这个选择(为了简单起见),我们可以将一个匹配的记录插入到hr.v_emp_mobile_nos中,并将user_id字段设置为新的 OS 用户 ID。
  6. 我们通过选择刚刚从hr.v_emp_mobile_nos.创建的记录来结束

我们需要这个用户在EMPLOYEES表和hr.v_emp_mobile_nos视图中的记录,这样我们就可以为这个用户完成双因素认证。我们的管理用户必须像其他用户一样完成 SSO 和双因素身份认证。

为应用验证创建管理角色

到目前为止,我们已经允许任何成功的应用用户插入和更新连接字符串列表,并将它们存储在应用的 Oracle 数据库中。让我们通过创建一个管理应用的连接字符串列表所需的 Oracle 角色来简化这个过程。我们将使用应用验证管理员appver_admin角色来执行此任务。作为SYS,执行这些命令。

CREATE ROLE appver_admin NOT IDENTIFIED;

尽管如此,任何用户都可以在客户端应用的本地实例中管理自己的连接字符串副本(我们给予用户这种自由),但是在 Oracle 数据库中插入、更新或复制连接字符串将需要预先批准。我们将通过操作系统中的用户 id 对特定人员进行预先授权,该操作系统将与 Oracle 数据库关联。对于这些用户 id,我们将授予我们新的管理角色。这是该人员的默认角色。

GRANT appver_admin TO osadmin;

为了让这个角色发挥作用,我们将把某一组appver功能和程序归类为仅供appver_admin使用。我们将这些功能和过程放在一个包appsec_admin_pkg中,并且只将包的执行权授予appver_admin。该命令可以作为SYSappsec用户运行。

GRANT EXECUTE ON appsec.appsec_admin_pkg TO appver_admin;

我们有一个现有的功能,f_set_decrypt_conns,我们将从appsec_public_pkg转移到这个新的包中。任何应用用户都不再能够插入或更新应用的连接字符串列表。只有拥有appver_admin角色的用户才会。

删除连接字符串

从列表中删除连接字符串是我们在本地客户机应用中完成的一项任务。我们简单地制定我们计划删除的连接字符串值的键,然后从connsHash中删除条目。使用removeAppConnString()方法,如清单 11-26 所示。

清单 11-26。从列表中删除连接字符串,removeAppConnString()

    private static void removeAppConnString()( String instance, String user ) {         instance = instance.trim();         user = user.trim();         String key = (instance + "/" + user).toUpperCase();         connsHash.remove( key );     }

为了使该应用的所有未来用户都可以永久使用该更改,我们需要将该列表保存到 Oracle 数据库中。这是通过调用我们在第十章的中学习过的putAppConnections(),来完成的(参见清单 10-24 )。

从以前版本的应用中复制连接字符串

当开发人员发布新版本的应用时,她会希望使用与旧版本相同的大多数连接字符串。如果她保持内部类版本号/名称不变,那么她就不需要担心复制连接字符串;她会继续使用现有的列表。但是,出于以下原因之一,她可能需要内部类的新版本,因此需要新的连接字符串列表:

  • 修改新版本的连接字符串列表。
  • 通过删除与之关联的连接字符串列表,最终禁用旧版本的应用。

如果她的应用只使用一个或两个 Oracle 连接,那么为新版本从头开始重新构建列表是没有问题的。但是,如果应用有许多可以从中提取数据的潜在连接,那么将连接字符串从应用的一个版本复制到下一个版本的能力将是一个受欢迎的特性。

应用客户端调用复制连接字符串列表

从客户端应用中,我们将调用OracleJavaSecurecopyAppConnections()中的一个新方法。它有一个参数,旧的内部类版本名。当在 Oracle 数据库上评估当前(新)内部类时,我们将能够从内部类中确定新版本。

我们在已经检查过的方法putAppConnections()上对copyAppConnections()建模。我们的新方法调用 Oracle 过程appsec_admin_pkg.p_copy_app_conns,传递应用内部类和旧版本名。在这种情况下,我们不传递连接字符串的connsHash列表——显然不需要。

当我们调用我们的过程p_copy_app_conns将应用的连接字符串列表从内部类的一个版本复制到下一个版本时,这将是一个罕见的事件。我们不会在每次更新应用时都创建内部类的新版本;只有当我们想要淘汰应用的先前版本时,当我们的更改使应用的先前版本不正常或不可接受时。

我们用新版本应用内部类的实例调用p_copy_app_conns。我们还指定了旧版本名,这样我们就知道从哪里获取连接字符串。清单 11-27 中的这个过程与我们之前检查过的各种过程非常相似。在继续之前,我们保证 SSO 和双因素身份认证。

如果这个连接是可接受的,那么我们调用函数f_copy_conns,它完成了连接字符串列表到新版本的复制。

清单 11-27。将连接字符串列表复制到新版本,p_copy_app_conns

PROCEDURE p_copy_app_conns(         m_two_factor_cd      v_two_fact_cd_cache.two_factor_cd%TYPE,         m_class_instance     v_app_conn_registry.class_instance%TYPE,         **m_prev_version**       v_app_conn_registry.class_version%TYPE,         m_application_id     v_two_fact_cd_cache.application_id%TYPE,         m_err_no         OUT NUMBER,         m_err_txt        OUT VARCHAR2 )     IS         return_user VARCHAR2(40);         m_app_user  v_application_registry.app_user%TYPE := **'APPVER'**;     BEGIN         m_err_no := 0;         return_user := **f_is_sso**( m_app_user );         IF( return_user IS NOT NULL )         THEN             IF( m_two_factor_cd IS NULL )             THEN                 m_err_txt := appsec_only_pkg**.f_send_2_factor**(return_user, m_application_id);             ELSIF( appsec_only_pkg**.f_is_cur_cached_cd**( return_user, m_application_id,           m_two_factor_cd ) = 'Y' )             THEN                 -- Reuse existing VARCHAR2, RETURN_USER                 return_user :=appsec_only_pkg**.f_copy_conns**(m_class_instance,m_prev_version);             ELSE                 -- Wrong 2-Factor code entered                 RAISE NO_DATA_FOUND;             END IF;             app_sec_pkg.p_log_error( 0, 'Success copying App Conns, ' || return_user );         ELSE             app_sec_pkg.p_log_error( 0, 'Problem copying App Conns, ' || return_user );         END IF;     -- Raise Exceptions     EXCEPTION         WHEN OTHERS THEN             m_err_no := SQLCODE;             m_err_txt := SQLERRM;             app_sec_pkg.p_log_error( m_err_no, m_err_txt,                 'p_copy_app_conns' );     END p_copy_app_conns;

复制连接字符串的 Java 存储过程

我们再次调用 Oracle 存储函数,它实际上只是用 Java 编写的完成任务的方法的包装器。这个函数,清单 11-28 中的f_copy_conns调用了copyPreviousConns()方法。

清单 11-28。 Java 存储过程复制连接字符串,f_copy_conns

    FUNCTION f_copy_conns( class_instance RAW, class_version VARCHAR2 )     RETURN VARCHAR2     AS LANGUAGE JAVA     NAME 'orajavsec.OracleJavaSecure.copyPreviousConns( oracle.sql.RAW, java.lang.String ) return java.lang.String';

数据库复制连接字符串的 Java 方法

也许是我们将要研究的最复杂的方法;然而,它只是在同时管理我们的应用的多个版本时才是复杂的,而不是在过程中。我们将执行已经在我们检查过的其他方法中完成的步骤。不过,在这里,我们将在内部类的新旧版本之间来回切换。

我们的第一步是直接从内部类classInstance RAW参数中获取当前内部类名和版本号(如清单 11-29 所示)。我们通过获取RAW的字节并将其推过ByteArrayInputStream,然后通过ObjectInputStream,从那里我们读取一个Object作为providedClass成员。从那个Object中,我们可以将名称读入到className成员中。我们还可以获取getRevLvl()方法,并将修订级别读入classVersion成员。

清单 11-29。获得新的职业版本和名字

byte[] appClassBytes = **classInstance.getBytes()**;     ByteArrayInputStream bAIS = new ByteArrayInputStream( appClassBytes );     ObjectInputStream oins =         new ObjectInputStream( bAIS );     Object **classObject = oins.readObject()**;     oins.close();     Class providedClass = classObject.getClass();     String className = **providedClass.getName()**;     Method classMethod = **providedClass.getMethod( "getRevLvl" )**;     String classVersion = ( String )classMethod.invoke( classObject );

接下来,我们想知道这将如何进行。我们真的有什么可以复制的吗?我们使用当前的内部类名(我们刚刚从Object中获得)和使用在prevVersion参数中传递的先前版本名来获得连接字符串列表。(参见清单 11-30 。)通过将这些参数(1 和 2)传递给存储过程p_get_class_conns,我们可以为之前版本的内部类(输出参数 3)获取连接字符串列表(输出参数 4)。如果我们对之前的内部类返回空值,那么就没有什么可复制的了,所以我们返回。

清单 11-30。选择以前版本的连接字符串

    stmt = ( OracleCallableStatement )conn.prepareCall(         "CALL appsec.appsec_only_pkg**.p_get_class_conns**(?,?,?,?)" );     stmt.registerOutParameter( 3, OracleTypes.RAW );     stmt.registerOutParameter( 4, OracleTypes.BLOB );     stmt.setString( 1, className );     stmt.setString( 2, prevVersion );     stmt.setNull(   3, OracleTypes.RAW );     stmt.setNull(   4, OracleTypes.BLOB );     stmt.executeUpdate();     if( null == stmt.getBytes( 3 ) ) return "Nothing to copy";

清单 11-31 展示了我们新的f_unmask Oracle 存储函数的另一个应用。既然我们在将连接字符串存储在磁盘上时对它们进行了加密,我们还需要在从存储中读取它们时对它们进行解密。

清单 11-31。解密以前版本的连接字符串

`    byte[] prevConnsBytes = stmt.getBytes(4);
    stmt = ( OracleCallableStatement )conn.prepareCall(
        "{? = call appsec.f_unmask(?,?,?)}" );
    stmt.registerOutParameter( 1, OracleTypes.RAW );
    stmt.setBytes(  2, prevConnsBytes );
    stmt.setString( 3, className );
    stmt.setString( 4, prevVersion );
    stmt.executeUpdate();

prevConnsBytes = stmt.getBytes(1);`

我们读取应用连接字符串列表的解密字节,但是我们不需要组装一个HashMap对象来表示该列表;相反,我们将为新版本原样存储字节数组。

但是,在我们将连接字符串列表复制到新版本之前,我们将确保不会覆盖现有列表。我们将调用当前内部类名和版本的p_count_class_conns过程来查看数据库中是否存在条目(参见清单 11-32 )。如果不存在,那么我们可以插入;否则,我们需要检查应用新版本的连接字符串列表。

清单 11-32。统计当前版本存储的连接字符串

    stmt = ( OracleCallableStatement )conn.prepareCall(         "CALL appsec.appsec_only_pkg**.p_count_class_conns**(?,?,?)" );     stmt.registerOutParameter( 3, OracleTypes.NUMBER );     stmt.setString( 1, className );     stmt.setString( 2, classVersion );     stmt.setInt(    3, 0 );     stmt.executeUpdate();     boolean okToOverwrite = false;     if( stmt.getInt( 3 ) == 0 ) {         // Do insert!         **okToOverwrite = true;**     } else {

如果数据库中存在一个条目,我们将确保作为参数提供给该方法的内部类实例与存储的相同。我们通过简单地从基于当前类名和版本的存储字节中获取一个对象来做到这一点(见清单 11-33 )。回想一下,Java 类装入器不能装入同名的两个完全不同的类;将会引发运行时异常。

清单 11-33。获取为当前版本存储的连接字符串和类

`    stmt = ( OracleCallableStatement )conn.prepareCall(
        "CALL appsec.appsec_only_pkg.p_get_class_conns(?,?,?,?)" );
    stmt.registerOutParameter( 3, OracleTypes.RAW );
    stmt.registerOutParameter( 4, OracleTypes.BLOB );
    stmt.setString( 1, className );
    stmt.setString( 2, classVersion );
    stmt.setNull(   3, OracleTypes.RAW );
    stmt.setNull(   4, OracleTypes.BLOB );
    stmt.executeUpdate();

byte[] cachedBytes = stmt.getBytes(3);
    oins = new ObjectInputStream( new ByteArrayInputStream(
        cachedBytes ) );
    classObject = oins.readObject();
    oins.close();`

当我们从ObjectInputStream中读取对象时,它最好与我们作为classInstance参数传递给该方法的对象相同;否则会抛出一个InvalidClassException。为了更好的测量,我们从对象中获得一个类实例,并测试它是否等于我们在前面的清单 11-34 中获得的类。如果不是,我们就已经失败了(除非内部类名不同,在这种情况下,它不应该以这个名称存储在数据库中)。

清单 11-34。测试存储的类和连接字符串

Class testClass = classObject.getClass(); **    if( testClass != providedClass )**         return "Failed to setDecryptConns";     if( null == stmt.getBytes(4) ) **okToOverwrite = true;**     else {

可能我们为这个内部类存储了一个条目,但是为相关的连接字符串列表存储了一个null。清单 11-34 对此进行了测试。如果连接字符串是null,我们可以覆盖这个条目。

更有可能的是为这个新的内部类版本存储了一个空的HashMap作为占位符。我们可以通过获取连接字符串列表并读取它来测试这一点,看看列表中是否有任何条目。但是首先,我们重新访问新的f_unmask函数来解密我们从当前类名和版本的存储器中读取的连接字符串列表,如清单 11-35 所示。

清单 11-35。解密当前版本存储的连接字符串列表

    byte[] connsBytes = stmt.getBytes(4);     stmt = ( OracleCallableStatement )conn.prepareCall(         "{? = call appsec**.f_unmask**(?,?,?)}" );     stmt.registerOutParameter( 1, OracleTypes.RAW );     stmt.setBytes(  2, connsBytes );     stmt.setString( 3, className );     stmt.setString( 4, classVersion );     stmt.executeUpdate();

从解密的连接字符串列表中创建一个对象,并将其转换为一个HashMap。接下来,测试HashMap的尺寸。如果大小为零,我们可以覆盖这个条目;但是,如果它不为空,我们返回时不会将旧版本的连接字符串复制到新版本中。参见清单 11-36 。

清单 11-36。测试存储的连接字符串列表是否为空

            oins = new ObjectInputStream( new ByteArrayInputStream(                 stmt.getBytes(1) ) );             Object currentConns = oins.readObject();             oins.close();             HashMap<String, String> currConnsHash =                 (HashMap<String, String>)currentConns;             **if( 0 == currConnsHash.size() ) okToOverwrite = true;**         }     }     if( ! okToOverwrite ) return "Current connsHash is not empty!";

如果我们到此为止,那么要么在v_app_conn_registry中没有应用内部类的当前(新)版本的条目,要么相关的连接字符串列表为 null 或空。所以我们可以自由地将旧的连接字符串复制到新版本中。但是首先,我们将像在清单 11-37 中那样,通过调用我们新的f_mask函数,为新版本加密它们。

清单 11-37。在存储之前为新版本加密旧的连接字符串列表

stmt = ( OracleCallableStatement )conn.prepareCall(         "{? = call appsec**.f_mask**(?,?,?)}" );     stmt.registerOutParameter( 1, OracleTypes.RAW );     stmt.setBytes(  2, **prevConnsBytes** );     stmt.setString( 3, **className** );     stmt.setString( 4, **classVersion** );     stmt.executeUpdate();     **prevConnsBytes = stmt.getBytes(1);**

现在我们通过调用p_set_class_conns过程为新的内部类版本存储连接字符串的加密列表。清单 11-38 显示那个调用。

清单 11-38。存储新版本的加密连接字符串列表

    stmt = ( OracleCallableStatement )conn.prepareCall(         "CALL appsec.appsec_only_pkg**.p_set_class_conns**(?,?,?,?)" );     stmt.setString( 1, className );     stmt.setString( 2, classVersion );     stmt.setBytes(  3, appClassBytes );     stmt.setBytes(  4, prevConnsBytes );     stmt.executeUpdate();

图 11-1 展示了将连接字符串从应用的先前版本复制到新的当前版本的过程。图中我想提到的唯一一项是对 Oracle 数据库的第一次调用,即p_copy_app_conns过程。该过程在appsec_admin_pkg包中,只能由被授予appver_admin角色的用户执行。

images

图 11-1。将应用连接字符串复制到新版本

添加其他认证凭证

我们并不局限于在v_app_conn_registry中只存储 Oracle 连接字符串。回想一下,HashMap只是一个字符串键和相关字符串值的列表。一旦将HashMap放回应用,您就可以根据自己喜欢的任何键请求一个特定的值。

当然,您可以存储连接字符串,或者至少存储连接到非 Oracle 数据库的密码。您还可以存储诸如安全 FTP 连接的密码之类的东西。我们当前在OracleJavaSecure中的方法是为存储 Oracle 连接字符串而定制的,但是您可以添加一个方法来存储,例如,安全 FTP 密码。清单 11-39 展示了一个你可能使用的示例方法。

清单 11-39。存储其他(FTP)凭证的示例方法

    public static void putAppFTPString( String key, String password ) {         appAuthCipherDES.init( Cipher.ENCRYPT_MODE,             appAuthSessionSecretDESKey, appAuthParamSpec );         byte[] bA = appAuthCipherDES.doFinal(password.getBytes() );         connsHash.put( “FTP” + key, new RAW( bA ) );     }

为了安全起见,您可能希望设计一种方法(如图所示)在密钥前面加上字符串“FTP”或类似的东西。我们将使用它作为过滤器,防止这个方法解密connsHash列表中的非 FTP 条目。清单 11-40 提供了一个从connsHash获取 FTP 密码的示例方法。

清单 11-40。检索其他(FTP)凭证的示例方法

    private static String getAppFTPString( String key ) {         return new String(             appAuthCipherDES.doFinal( connsHash.get( “FTP” + key ).getBytes() ) );     }

请注意,这个方法被指定为private——您可能希望在OracleJavaSecure中使用另一个方法来建立 FTP 连接并将连接返回给客户端应用,而不是将明文 FTP 密码返回给应用。我们不想把我们的密码给应用。

更新应用安全结构

在进入新主题之前,请运行我们到目前为止描述的所有命令和脚本。在 SQLPlus 提示符或其他 SQL 客户端,作为SYS用户,运行第十一章/Sys.sql* 中的命令。替换将要执行管理任务的操作系统用户的名称(您?)中的GRANT appver_admin命令。

然后,作为应用安全,appsec用户,运行 Chapter11/AppSec.sql 中的命令。那应该很容易。另外,执行 Chapter11/F_MASK.plbChapter11/F_UNMASK.plb 的代码(屏蔽版本)。

仍然作为appsec用户,删除第一行的注释,chapter 11/orajavsec/ojsc . Java(模糊版本)中的CREATE OR REPLACE AND RESOLVE JAVA,并将其作为 SQL 代码执行。最后,取消第一行chapter 11/orajavsec/Oracle javasecure . Java的注释,并编辑代码顶部的expectedDomain和 URL 字符串。从底部的main()方法中删除密码。然后将其作为 SQL 代码执行。回想一下,您可能需要SET DEFINE OFF来避免在代码中的每个&符号处被提示变量替换。

在单独的 Oracle 实例上进行身份验证

我现在要描述一些揭示我们想要追求安全的程度的东西。如果我们将应用验证任务与实际的应用数据分离开来会怎么样?那会给我们带来什么安全?我们将实现的主要优势如下:

  • 有密码的帐户越少,攻击的帐户就越少
  • 减少辅助功能(更少的可选数据库程序),从而减少漏洞
  • 能够在不妨碍数据库开发的情况下撤销一些对特别暴露的数据字典视图的访问
  • 黑客在攻击第二数据库中的敏感数据之前需要克服的第一数据库障碍

在本节中,我们将创建一个新的数据库实例,可能是在同一台服务器上。此实例将有足够的特权来完成应用验证,但仅此而已。为了确保这一点,我们不会创建我们已经讨论过的特权角色,secadm_roleapp_sec_role;相反,我们将作为SYS用户完成所有的配置步骤。

images 警告如果您不打算为应用验证创建额外的数据库实例,则不要发出本节中的任何命令。可以直接跳到测试增强安全性;但是,要了解如何创建和强化 Oracle 数据库实例以及数据库链接,请务必通读本节。

我们的应用验证呢,appver用户密码?它还容易被窥探吗?嗯,它没有被加密,任何人只要够勤奋就可以恢复我们的混淆的OJSCode类。因此,我们需要问的问题是,密码泄露会给我们带来什么样的安全风险。

我们可能会拍拍自己的背,说我们已经用调用p_appver_logon存储过程的appver模式上的登录触发器t_screen_appver_access覆盖了它。我们可能会对那些认为他们可以通过 SSO 代理要求,以及我们的双因素身份验证、加密密钥交换和应用验证过程的人嗤之以鼻。

然而,在我们的内心深处,我们意识到黑客拥有 Oracle 生产数据库帐户的密码肯定是一件坏事。我们内心的想法是正确的。即使该帐户没有其他可以访问的内容,仍然有PUBLIC数据,并且这些数据可能会透露一些信息。从标准的PUBLIC访问,黑客可以了解数据库的所有用户,设置他进行社会工程攻击。黑客还可以看到授予PUBLIC的任何程序和 Java 的所有代码,他可以看到触发器和视图中包含的逻辑。PUBLIC授权为每个用户账户提供了进入数据库的入口。

我相信甲骨文公司可以对数据库进行彻底的改变,以提高安全性。彻底的改变将是让PUBLIC成为一个常规角色。也许默认情况下会授予PUBLIC角色,并且当用户执行SET ROLE时不会丢失。然而,PUBLIC就不是现在的样子了。目前,当PUBLIC获准进入时,就好像说不需要批准。我们不能撤销用户的PUBLIC。每个 Oracle 用户始终拥有PUBLIC访问权限。

我可以看到使用PUBLIC就像让某些东西成为 Oracle 用户身份的一部分。也许我真正想要的是一个 Oracle 数据库可以默认授予的几乎是 - PUBLIC的角色,并且当用户设置角色时不会被删除。在数据库安装时,通常授予PUBLIC的一些或大部分东西(对登录和选择没有严格要求的任何东西)可以授予几乎- PUBLIC。然后,对于受限用户,我们可以撤销几乎- PUBLIC角色。

我疯了吗?让我们来看看。首先,从公共视图中选择以查看数据库中的所有用户:

SELECT * FROM ALL_USERS;

通过列出所有数据库用户,攻击者就有了多次尝试猜测密码的机会,或者找到多个人进行联系,试图进行社会工程攻击。我想关闭对那个PUBLIC视图的访问。

如果我们想知道应用安全用户使用的所有 Oracle 过程的名称,该怎么办?我们可以查询ALL_PROCEDURES PUBLIC视图:

SELECT * FROM ALL_PROCEDURES WHERE OWNER = 'APPSEC';

现在让我们通过查询ALL_SOURCE PUBLIC视图来查看其中一个过程p_check_role_access的代码:

SELECT * FROM ALL_SOURCE WHERE OWNER = 'APPSEC' AND NAME = 'P_CHECK_ROLE_ACCESS';

当然,用户只能看到授权给PUBLIC的过程的源代码,但是这个源代码真的是用户需要看到的吗?我们会看到它不是。

创建新的 Oracle 数据库实例

我们需要大约 2 GB 的硬盘空间来创建一个足够大的数据库,以保存我们进行应用验证所需的内容。“为什么这么多?”你可能会问。请记住,要进行应用验证,我们需要一个基本的 Oracle 数据库,我们需要数据字典视图,我们需要运行 PL/SQL,我们需要运行 Java。除此之外,对于双因素身份验证,我们需要发送电子邮件,并且需要配置系统权限来读取网络上的数据(URL)。所有这些功能都需要空间。

我们将调用我们的新数据库实例apver(注意,它类似于用户名appver,除了只有一个“P”)。为了构建新的数据库实例,我们需要一个初始化/参数配置文件。如果您在已经安装了实例的服务器上创建apver实例,例如 ORCL,那么您可以复制一些有用的文件。其中一个文件叫做 init.ora 。将目录更改为这些文件所在的位置,在您的服务器 Oracle 主目录之外:

D: cd \app\oracle\admin

将整个 orcl 目录复制到一个名为 apver 的新目录中。该命令将复制目录和所有内容。

xcopy orcl apver /ei

现在将目录更改为新的参数文件, pfile 目录,并将现有的 init.ora 文件模板重命名为 init.ora 。然后编辑 init.ora 文件。

cd \app\oracle\admin\apver\pfile ren init.ora.* init.ora edit init.ora

搜索并替换以下字符串:

Replace          With =======          ==== =orcl            =apver \orcl            \apver

你的最终文件应该有类似于清单 11-41 中的参数。您的db_domain和目录名可能不同。对于 apver,local_listener 将与主数据库相同。

清单 11-41。apver 实例的初始化文件

db_block_size=8192 open_cursors=300 db_domain=org.com db_name=apver control_files=("D:\app\oracle\oradata\apver\control01.ctl", "D:\app\oracle\flash_recovery_area\apver\control02.ctl") db_recovery_file_dest=D:\app\oracle\flash_recovery_area db_recovery_file_dest_size=4039114752 compatible=11.2.0.0.0 diagnostic_dest=D:\app\oracle memory_target=1288699904 local_listener=LISTENER_Orcl processes=150 audit_file_dest=D:\app\oracle\admin\apver\adump audit_trail=db remote_login_passwordfile=EXCLUSIVE dispatchers="(PROTOCOL=TCP) (SERVICE=apverXDB)" undo_tablespace=UNDOTBS1

我们希望将这个 init.ora 文件复制到它的默认位置。当我们将参数设置导入到系统参数文件中时,这将很方便。执行复制命令:

copy D:\app\oracle\admin\apver\pfile\init.ora %ORACLE_HOME%\DATABASE\INITAPVER.ORA

为辅助控制文件创建一个目录:

mkdir D:\app\oracle\flash_recovery_area\apver

此外,让我们为新的实例数据库文件创建一个目录:

mkdir D:\app\oracle\oradata\apver

创建新的 Oracle 服务

每个 Oracle 数据库实例通常在系统重启时由服务启动。你可以通过进入开始菜单并运行电脑管理应用,在 Windows 中查看这些服务。你需要使用系统管理员权限。转到服务和应用,然后转到服务,向下滚动到 Oracle 服务。它们通常都以前缀“Oracle”命名,并按字母顺序排序。我们不打算探索在 Unix 或 Linux 上创建、启动或停止进程;步骤相同,但是命令(运行命令级文件)不同。

因为我们在本书中没有使用任何 Oracle web 管理服务,所以所有的 Oracle 服务都可以设置为手动;但是,不要在生产 Oracle 数据库服务器上这样做。然后我们可以手动启动标准的OracleServiceORCL服务来启动 ORCL 实例。我们还启动标准的OracleOraDb11g_homeTNSListener服务来启动监听器。这两项 Oracle 服务正是我们所需要的。通常,客户端通过网络连接到侦听器服务,然后侦听器服务将它们连接到数据库实例。

在接下来的讨论中,我们将假设您已经将ORACLE_HOME设置为如下所示:

SET ORACLE_HOME=D:\app\oracle\product\11.2.0\dbhome_1

我们的新 Oracle 实例将被命名为apver,因此我们可以使用如下命令添加一个服务来启动该实例。您需要在 Windows 命令提示符窗口中使用管理权限,所以在开始菜单中右键单击命令提示符并选择以管理员身份运行。

%ORACLE_HOME%\BIN\oradim -NEW -SID apver -STARTMODE manual     -PFILE "D:\app\oracle\admin\apver\pfile\init.ora"

我们还希望将新服务设置为自动启动 Oracle 数据库进程。这是对自动启动服务的补充。它告诉服务向数据库发出一个STARTUP命令。我们可以稍后将服务设置为MANUAL启动,当我们手动启动服务时,它会自动启动数据库。

oradim -EDIT -SID apver -STARTMODE AUTO -SRVCSTART SYSTEM     –PFILE "D:\app\oracle\admin\apver\pfile\init.ora"

images 注意这里我们会得到一个无法启动服务的错误。没关系,因为我们还没有真正创建数据库,但是现在启动它的服务已经配置好了。

编写创建数据库命令

我们将把数据库创建命令放在一个名为ApVerDBCreate.sql的脚本文件中。CREATE DATABASE 是一个单独的命令,但是它有许多方面,我们不想依靠我们的输入技能在 SQL*Plus 提示符下正确地输入所有内容。此外,我们需要一个脚本文件,这样,如果我们对发出的命令有任何疑问,就可以参考它。

关于我们的数据库创建脚本,我想让你注意的第一件事是我们没有为SYSSYSTEM硬编码密码(我们在清单 11-42 中注释了一些行,意在保持注释和不变)。这些用户将分别使用默认密码“change_on_install”和“manager”创建。将密码放在这个命令脚本中是典型的做法,但是不太安全。更改这些默认密码是很重要的,但是问问你自己这个问题:一旦你登录并继续安装步骤(这是第一步),你是否更可能发出一个ALTER USER命令,或者你是否记得返回并编辑数据库创建脚本ApVerDBCreate.sql,从那个文件中删除真正的密码?一旦创建了数据库,我们将立即更改密码,以后我们将不必从脚本文件中删除密码。

清单 11-42。创建数据库命令

CREATE DATABASE apver --USER SYS IDENTIFIED BY password --USER SYSTEM IDENTIFIED BY password

images 该命令包含在一个名为 Chapter11/apver/ ApVerDBCreate.sql. 的文件中

清单 11-43 中数据库创建命令的下几个方面简单定义了我们将要维护的重做日志文件,以及它们的大小。如果我们需要从备份中恢复数据库,并重新应用自备份以来发生的事务,以及在数据库故障之前未应用的事务,将会使用这些日志文件。理想情况下,这些事务日志文件应该与数据库文件位于不同的硬盘上。

清单 11-43。创建数据库日志文件

LOGFILE GROUP 1 ('D:\app\oracle\oradata\apver\REDO01a.log',     'D:\app\oracle\oradata\apver\REDO01b.log') SIZE 16M, GROUP 2 ('D:\app\oracle\oradata\apver\REDO02a.log',     'D:\app\oracle\oradata\apver\REDO02b.log') SIZE 16M, GROUP 3 ('D:\app\oracle\oradata\apver\REDO03a.log',     'D:\app\oracle\oradata\apver\REDO03b.log') SIZE 16M

之后,我们的命令包括一些基本参数,这些参数可能足够了,或者可以在以后进行调整。参见清单 11-44 。

清单 11-44。创建数据库配置

MAXINSTANCES 3 MAXLOGFILES 6 MAXLOGMEMBERS 2 MAXLOGHISTORY 1 MAXDATAFILES 10 CHARACTER SET AL32UTF8 NATIONAL CHARACTER SET AL16UTF16 EXTENT MANAGEMENT LOCAL

接下来,在清单 11-45 中,我们将定义用于apver实例的数据库文件。我们定义我们的主系统数据库文件, SYSTEM01。DBF 和一个辅助系统文件 SYSAUX01。DBF ,它由一些数据库组件使用,这些组件在历史上被放置在单独的表空间中。此外,我们为USERSTEMPORARYUNDO表空间创建默认的表空间文件。确保给UNDO表空间起一个与您在 init.ora 文件中给它起的名字相同的名字,如前所述。至此,我们结束了我们的CREATE DATABASE命令(注意分号)。

清单 11-45。创建数据库文件和表空间

DATAFILE 'D:\app\oracle\oradata\apver\SYSTEM01.DBF' SIZE 512M REUSE SYSAUX DATAFILE 'D:\app\oracle\oradata\apver\SYSAUX01.DBF' SIZE 512M REUSE DEFAULT TABLESPACE users DATAFILE 'D:\app\oracle\oradata\apver\USERS01.DBF'     SIZE 256M REUSE AUTOEXTEND ON MAXSIZE UNLIMITED DEFAULT TEMPORARY TABLESPACE tempts1 TEMPFILE 'D:\app\oracle\oradata\apver\TEMP01.DBF'     SIZE 16M REUSE UNDO TABLESPACE undotbs1 DATAFILE 'D:\app\oracle\oradata\apver\UNDOTBS01.DBF'     SIZE 64M REUSE AUTOEXTEND ON MAXSIZE UNLIMITED;

创建和配置数据库

准备好配置和初始化文件,并为您的独特安装进行编辑,创建所需的目录,您就可以创建新的数据库实例了,apver。首先,让我们设置我们的环境,将我们想要处理的实例指定给apver。这使我们不会践踏默认数据库orcl的参数和身份。

在管理员命令提示符窗口中,设置ORACLE_SID环境变量。只要命令提示符窗口打开,此设置就会生效。关闭后,该设置将消失,因此,如果您需要在新的命令提示符窗口中返回这些过程,请重新执行该操作。

SET ORACLE_SID=apver

您已经为apver实例创建了一个新的 Oracle 服务(它将具有类似于OracleServiceapver的名称),并且该服务应该正在运行。您可以从“计算机管理”Windows 程序的“服务”区域查看正在运行的 Oracle 服务,向下滚动到以“Oracle”开头的服务所有其他 Oracle 服务都可以停止,事实上,如果停止所有其他服务,手动安装新的 Oracle 实例会更安全。

我们将运行 SQL*Plus,基本上不连接任何实例(不登录或NOLOG ) AS SYSDBA。这相当于早期版本的 Oracle 数据库中的CONNECT INTERNAL。为了运行这个CONNECT命令,您必须是服务器的管理员,或者是安装了 Oracle 数据库的帐户。

`%ORACLE_HOME%\BIN\sqlplus /NOLOG

CONNECT/AS SYSDBA`

images 注意这些命令可以在文件chapter 11/apver/apversys . SQL中找到

我们希望确保我们正在处理apver实例。CONNECT命令应该显示消息“Connected to an idle instance”如果您看到错误消息“ORA-12560: TNS:协议适配器错误”,那么您的apver实例服务没有运行。如前所述,使用计算机管理程序的服务区域启动它。

如果 Oracle 数据库实例正在运行,消息将只是“已连接”如果您看到该消息,请检查您使用该命令的实例:

SELECT VALUE FROM V$PARAMETER WHERE NAME = 'db_name';

如果这显示实例为apver,回顾您到目前为止的进度,以回忆您是否已经创建了数据库。如果没有,就用 shut down 命令关闭模板apver实例。(请注意,如果您停止并重新启动该服务(或重新启动计算机),模板 Oracle 实例将作为 apver 启动。)

SHUTDOWN

但是,如果那个SELECT查询显示您连接到一个不同的实例,您需要停止运行那个实例的 Oracle 服务,并确保您的ORACLE_SID环境变量被设置为apver

接下来,我们将请求将来自 SQLPlus 会话的消息假脱机到日志文件中。在退出 SQLPlus 会话之前,我们必须记住关闭假脱机文件。

SPOOL apver.log

现在,我们启动一个由 init.ora 参数文件定义的数据库实例。没有要挂载的数据库文件,事实上我们甚至还没有定义数据库文件,所以我们说NOMOUNT

STARTUP NOMOUNT PFILE=D:\app\oracle\admin\apver\pfile\init.ora

这将显示系统全局内存分配,Oracle 数据库可以使用。Java 资源将从这个内存池中分配出来(稍后讨论)。

Total System Global Area 2522038272 bytes

现在调用脚本来创建我们的数据库(用命令文件的路径修改这个命令)。随着大型数据库文件的创建,这将需要几分钟时间。(这是一个如何在 SQL*Plus 中使用 SQL 命令调用文件的示例;在文件名前加一个@符号。)

@Chapter11\apver\ApVerDBCreate.sql;

您可以通过浏览我们之前创建的目录来检查数据库文件的存在性和大小:D:\ app \ Oracle \ oradata \ apverD:\ app \ Oracle \ flash _ recovery _ area \ apver。如果您没有得到“数据库已创建”的成功消息,您可能需要删除这些目录中的文件并重新开始—这可能是初始化或命令文件中的打印错误,您需要修复。

更改系统和系统用户的密码

即使不作为 SYS 用户登录,我们也可以而且必须为 SYS 和 SYSTEM 用户设置密码。在以下每个命令中替换复杂密码:

ALTER USER SYS IDENTIFIED BY sys_password; ALTER USER SYSTEM IDENTIFIED BY system_password;

images 注意这是安全步骤一。在完成这一步之前,请不要继续。

在数据库中存储数据库参数

将我们的参数设置从 init.ora 导入到服务器参数文件中对我们最有利。这是通过数据库上的一个命令完成的。

CREATE SPFILE FROM PFILE;

这将从默认位置% ORACLE _ HOME % \ DATABASE \ INITAPVER 中的 init.ora 、PFILE 文件中提取设置。ORA 并将它们放在一个服务器格式化的(不可手工编辑的)文件中,该文件对应于这个数据库实例, SPFILEAPVER。ORA 在同一个目录下。将我们的参数设置放在服务器参数文件中的主要好处是,它们可以被数据库命令动态地修改,并且这种影响(经常)出现在 SPFILE 和正在运行的 Oracle 实例中。因为 SPFILE 会被这些命令修改,所以它们会在 Oracle 实例重新启动和服务器重新启动时保留。

重新启动数据库以使用新的参数设置。注意,您不必指定 init.ora 文件。这一次当我们调用STARTUP时,我们将使用我们新创建的服务器参数文件(SPFILE ),并将挂载数据库文件:

SPOOL OFF; SHUTDOWN; STARTUP; SPOOL apver2.log;

images 注意这些假脱机日志文件将被创建在命令提示符的当前目录中。

增加流程数量

您可能还记得在 init.ora 文件中看到过 150 个进程的标准设置。这对并发 Oracle 连接设置了限制。我们希望处理大量的并发连接来进行应用验证。想象一下,每个人在周一早上开始工作并登录一个或多个我们的安全 Oracle 应用。我们可以轻松地超过 150 个并发连接。

还记得我们为应用验证配置了一个特殊的概要文件,appver用户,appver_prof。对于该配置文件,我们将SESSIONS_PER_USER设置为无限制。但是,我们注意到实际的限制是由进程的数量控制的。让我们增加流程的数量。

首先以SYS的身份连接,并输入我们刚刚设置的新的SYS密码。您可以通过下面显示的第一种语法使用TNSlistener服务进行连接,或者像第二种语法一样直接连接到数据库,只要您的ORACLE_SID环境变量设置为apver。在这两种情况下,您可能希望从 Windows 计算机管理应用启动侦听器服务。

CONNECT SYS@apver AS SYSDBA; CONNECT SYS AS SYSDBA;

我们将把这个特殊用途实例的进程数量增加到 500 个。发出命令,将进程数设置为 500。

ALTER SYSTEM SET PROCESSES=500     COMMENT='Allow more concurrent Application Verification sessions.'     SCOPE=SPFILE;

我们给定改变的范围为SPFILE,这意味着我们只改变存储的参数。这是一个特殊的例子,我们可以发出一个ALTER SYSTEM数据库命令来修改SPFILE设置,但是我们不能在运行的数据库实例中立即更新这个参数(进程的数量)。为了实现进程数量的增加,我们需要关闭并重新启动 Oracle 数据库实例。

SHUTDOWN IMMEDIATE STARTUP

现在重新登录并检查进程数量的参数设置:

CONNECT SYS@apver AS SYSDBA; SELECT VALUE FROM V$PARAMETER WHERE NAME = 'processes';

运行 Oracle 脚本来添加基本的数据库功能

当我们运行以下脚本来添加基本数据库功能时,我们将在 SQL*Plus 命令行中输入每个脚本的路径。为了达到最佳效果,请输入完整的路径,这样就不会意外运行其他/旧/替代脚本。

我们将运行的第一个脚本将构建数据字典视图。我们需要ALL_USERS视图来进行代理认证和单点登录。除此之外,运行 Oracle 数据库的其他方面严重依赖于数据字典,因此我们需要它。:

@D:\app\oracle\product\11.2.0\dbhome_1\RDBMS\ADMIN\catalog.sql

接下来,我们将运行脚本来构建 PL/SQL 过程选项和 SQL*Plus 产品用户配置文件。为了配置和运行存储过程和函数,我们需要这些功能。这些脚本还在数据字典中创建一些视图。注意 catproc.sql 需要很长时间来运行。我建议你去喝杯咖啡——我就是这样做的。

@D:\app\oracle\product\11.2.0\dbhome_1\RDBMS\ADMIN\catproc.sql -- catproc also calls catpexec.sql calls execsec.sql calls --   secconf.sql, which configures default profile and auditing settings @D:\app\oracle\product\11.2.0\dbhome_1\sqlplus\ADMIN\pupbld.sql

我们需要运行的下一个脚本将构建可扩展标记语言(XML)数据库(XDB)。XML 是一种语法,它允许我们以结构化的文本格式呈现关系数据,XDB 特别允许 Oracle 数据以 XML 的形式呈现和交付。您可能想知道为什么我们需要这种能力——我们似乎在本书的任何地方都没有使用 XML。但是,我们执行 URL 查找(浏览网页)来完成我们的双因素身份验证,并且在我们构建 XDB 时提供了授权访问使用网络端口(DBMS_NETWORK_ACL_ADMIN)的功能。所以我们需要这个。

构建 XDB 的脚本要求我们传递许多参数。第一个参数是SYS用户密码。之后,我们列出将应用该脚本的用户和临时表空间。最后,我们指出我们将不会(NO)使用 SECUREFILE 大型对象(lob)——它们在非 ASSM(自动段空间管理)表空间中不受支持。我们不需要它,但是如果您想要包括 ASSM(自动处理pctused和空闲列表),那么在创建您的表空间时指定SEGMENT SPACE MANAGEMENT AUTO,这里不包括:

@D:\app\oracle\product\11.2.0\dbhome_1\RDBMS\ADMIN\catqm.sql sys_password users tempts1 NO

配置数据库使用 UTL 邮件包

发送邮件的能力不是 Oracle 数据库自动包含的特性。您还记得,当我们实现双因素身份验证时,我们将该特性添加到了原始的orcl实例中。我们也将把它添加到apver实例中。事实上,我们现在将把双因素身份认证作为应用验证的一项功能,而不是与我们建立的每个 Oracle 连接相关联。

@D:\app\oracle\product\11.2.0\dbhome_1\RDBMS\ADMIN\utlmail.sql @D:\app\oracle\product\11.2.0\dbhome_1\RDBMS\ADMIN\prvtmail.plb

第二个脚本文件扩展名( plb )现在应该看起来很熟悉了。这是一个包装程序。

安装 DBMS_JAVA 包

如果您之前已经在 Oracle 数据库中安装了 Java,或者您已经查看了这样做的说明,那么您可能已经看到列出了一个预备步骤。过去,您需要为 java 指定内存池大小,将java_pool_sizeshared_pool_size分别设置为 150M。从 Oracle 数据库 11g 开始,不再需要这样做。这些分配是作为 11g 自动内存管理的一部分自动处理的。

您可以通过从数据库中选择这些参数来查看它们的当前设置。

SELECT NAME,VALUE FROM V$PARAMETER WHERE NAME IN ('SHARED_POOL_SIZE','JAVA_POOL_SIZE');

您将看到在 11g 中,这些参数都被设置为 0。所需的内存将由全局MEMORY_TARGETTARGET_SGA设置自动提供。回想一下,当我们装载数据库时,我们看到了一个关于整个系统全局区域的报告。这是数据库中可供 Java 部分使用的内存。

许多脚本用于配置和启用 Oracle JVM。我们将只运行其中的两个,即我们的应用验证安全流程所需的两个。在通常列出的将 Java 构建到 Oracle 数据库的五个脚本中,我们将只执行 initjvm.sqlcatjava.sql 。同样,我们选择的理由是我们将只启用我们需要的功能,从而避免一些潜在的安全弱点。

@D:\app\oracle\product\11.2.0\dbhome_1\javavm\install\initjvm.sql; --@D:\app\oracle\product\11.2.0\dbhome_1\xdk\admin\initxml.sql; --@D:\app\oracle\product\11.2.0\dbhome_1\xdk\admin\xmlja.sql; @D:\app\oracle\product\11.2.0\dbhome_1\RDBMS\ADMIN\catjava.sql; --@D:\app\oracle\product\11.2.0\dbhome_1\RDBMS\ADMIN\catexf.sql;

运行 initjvm.sql 需要一段时间,所以休息一下。完成所有这些之后,是时候关闭我们的假脱机日志文件了,所以执行SPOOL OFF命令。然后您可以浏览假脱机文件中的错误—它位于启动 SQL*Plus 的当前目录中。

SPOOL OFF

查看安装了什么

关闭数据库,从计算机管理应用重启TNSListener和数据库服务(在开始菜单/运行命令中键入计算机管理):

`SHUTDOWN IMMEDIATE;
EXIT

Computer Management
     Restart TNSlistener service
     Start orcl instance service
     Restart apver instance service`

然后运行 SQLPlus,一次连接一个 Oracle 实例。有时,TNSListener服务在重启后需要一两分钟来注册数据库服务。如果您的 SQLPlus 连接不起作用,请稍后再试。从ALL_REGISTRY_BANNERS执行SELECT命令,查看构建了哪些服务。

`SQLPLUS SYS@ORCL AS SYSDBA
SELECT * FROM ALL_REGISTRY_BANNERS;

CONNECT SYS@apver AS SYSDBA
-- compare to what's installed in initial database
SELECT * FROM ALL_REGISTRY_BANNERS;

Oracle Database Catalog Views Release 11.2.0.1.0 - 64bit Production
Oracle Database Packages and Types Release 11.2.0.1.0 - Development
Oracle XML Database Version 11.2.0.1.0 - Development
JServer JAVA Virtual Machine Release 11.2.0.1.0 - Development
Oracle Database Java Packages Release 11.2.0.1.0 - Development`

以 SYSDBA 身份从远程连接

您可能希望能够从远程 GUI 管理应用(如 TOAD)或任何远程应用(包括 SQL*Plus)连接到作为SYSapver实例;但是,使用AS SYSDBA特权远程连接到数据库实例的能力受到限制。为了使连接成功,数据库实例必须存在远程登录密码文件。为了作为SYS AS SYSDBA远程登录到apver实例,我们需要为apver创建远程登录密码文件。

首先,检查我们是否没有用于apver实例的远程登录密码文件。这可以通过执行一个无害的命令来实现,这个命令涉及到这个文件GRANT SYSDBA。作为SYS用户,执行以下命令:

GRANT SYSDBA TO SYS;

如果远程SYSDBA登录密码文件已经存在,您将不会得到一个错误,但是在apver实例上您可能会看到这个错误:

ERROR at line 1: -ORA-01994: GRANT failed: password file missing or disabled

您可以通过执行orapwd命令(在ORACLE_HOME bin 目录中找到)来创建远程登录密码文件。您需要为apver实例、 PWDapver.ora 提供默认文件名,并为SYS用户提供密码。

orapwd file=%ORACLE_HOME%\database\PWDapver.ora password=sys_password

配置创建会话角色和应用安全用户

我们现在应该能够从本地命令提示符或远程会话连接到作为SYSapver实例。继续以SYS AS SYSDBA的身份连接,这样我们就可以创建应用验证所需的结构。

images 注意这些命令可以在文件chapter 11/apver/newsys . SQL .中找到

创建一个与我们在orcl实例上创建的角色相同的create_session_role角色,然后创建appsec用户。参见清单 11-46 。一定要给appsec一个复杂的密码。还要在默认表空间上为appsec提供足够大的QUOTA,以便保存用于应用验证的结构和数据。

清单 11-46。创建初始角色和用户

`CREATE ROLE create_session_role NOT IDENTIFIED;
GRANT CREATE SESSION TO create_session_role;

GRANT create_session_role TO appsec IDENTIFIED BY password;

ALTER USER appsec DEFAULT TABLESPACE USERS QUOTA 10M ON USERS;`

创建到 ORCL 实例的数据库链接

正如我在本节开始时提到的,我们将配置最低限度的配置,以便在这个新的apver实例中进行应用验证。出于这个原因,我们将依靠SYS用户来配置我们所有的结构,甚至是那些在appsec模式中的结构。好吧,除了一件事,我们最好现在就离开。

我们希望创建一个数据库链接,供appsec结构使用,特别是进行双因素认证。回想一下,我们已经在HR.EMPLOYEES表中存储了一个电子邮件地址,并创建了另一个表,其中包含用于在HR模式中进行双因素身份验证的地址,emp_mobile_nos。但是,这些表在不同的 Oracle 实例上,orcl。作为我们在apver实例上进行的应用验证过程的一部分,我们需要通过数据库链接读取这些表。

为我们的数据库链接更新 TNSNAMES.ora

我们从不同的实例中读取数据的方法是使用数据库链接。要使用数据库链接,想要读取数据的数据库apver需要知道如何找到另一个数据库实例。这个位置和方向信息通常保存在 Oracle 数据库的一个 TNSNAMES.ora 文件中,如清单 11-47 中的所示。确保您有一个针对orcl实例的条目。在这里,为新的apver实例添加另一个条目。

清单 11-47。编辑 TNSNAMES.ora 文件

`edit %ORACLE_HOME%\NETWORK\ADMIN\tnsnames.ora

ORCL =
  (DESCRIPTION =
    (ADDRESS = (PROTOCOL = TCP)(HOST = localhost)(PORT = 1521))
    (CONNECT_DATA =
      (SERVER = DEDICATED)
      (SERVICE_NAME = orcl)
    )
  )

APVER =
  (DESCRIPTION =
    (ADDRESS = (PROTOCOL = TCP)(HOST = localhost)(PORT = 1521))
    (CONNECT_DATA =
      (SERVER = DEDICATED)
      (SERVICE_NAME = apver)
    )
  )`

允许 appsec 创建数据库链接

As SYS用户授予appsec用户在appsec模式中创建个人数据库链接的权限。与大多数其他 create 语句不同,这是一个不能在不同模式中完成的语句;我们需要成为appsec用户,才能在appsec模式中创建个人数据库链接。作为SYS,执行清单 11-48 中的命令,为该授权创建一个有限的appsec_role

清单 11-48。授予 appsec 创建链接权限

`-- Must grant to user, not role since roles not exist without session
-- This is used in MASK/UNMASK - not needed on apver instance
GRANT EXECUTE ON sys.dbms_crypto TO appsec;

CREATE ROLE appsec_role NOT IDENTIFIED;
-- Give Application Security privilege to create Database Link
GRANT CREATE DATABASE LINK TO appsec_role;
GRANT appsec_role TO appsec;
-- Make the APPSEC_ROLE a non-default role for the APPSEC user
ALTER USER appsec DEFAULT ROLE ALL EXCEPT appsec_role;`

将个人数据库链接创建为 APPSEC

我们将appsec_role设置为非默认角色,所以现在我们需要以appsec的身份登录,并将我们的角色设置为appsec_role

SET ROLE appsec_role;

images 注意这些命令可以在文件chapter 11/apver/newappsec . SQL .中找到

执行清单 11-49 中的命令,创建我们需要附加到orcl实例的数据库链接(将orcl实例上的appsec用户的密码替换到该命令中)。

清单 11-49。作为 appsec 用户创建数据库链接

CREATE DATABASE LINK orcl_link CONNECT TO appsec IDENTIFIED BY password USING 'orcl';

通过从对orcl实例上的appsec可用的表中进行选择来测试新的数据库链接:

SELECT * FROM hr.v_emp_mobile_nos@orcl_link;

我们现在返回到SYS用户,以便在新的apver实例上完成我们的应用安全结构的大部分安装。

既然我们已经创建了数据库链接作为appsec,我们将不再需要作为我们的appsec用户登录,所以我们将想要禁用登录。最快的方法是终止appsec的密码。为此,作为SYS用户执行以下命令:

ALTER USER appsec PASSWORD EXPIRE;

你应该记得你这样做了,这样当你不能以appsec的身份连接到apver时就不用担心了。实际上,您只能以SYS的身份连接到apver实例。您可以以appver用户的身份连接,但是登录触发器和其他安全措施会阻止或限制您的操作。

授权 APPSEC 用户访问 JVM 安全沙箱

为了完成双因素认证,我们的appsec用户需要发送电子邮件并打开与 web 服务器的连接。这些能力要求appsec能够从标准安全沙箱之外的 Oracle JVM 伸出援手。我们将在清单 11-50 的中授予这样做的特权,正如我们在第九章的中对orcl实例所做的那样。

清单 11-50。授予 Oracle JVM 安全沙箱权限

`BEGIN
  DBMS_NETWORK_ACL_ADMIN.CREATE_ACL (
    acl          => 'smtp_acl_file.xml',
    description  => 'Using SMTP server',
    principal    => 'APPSEC',
    is_grant     => TRUE,
    privilege    => 'connect',
    start_date   => SYSTIMESTAMP,
    end_date     => NULL);

COMMIT;
END; /

BEGIN
  DBMS_NETWORK_ACL_ADMIN.ASSIGN_ACL (
    acl         => 'smtp_acl_file.xml',
    host        => 'smtp.org.com',
    lower_port  => 25,
    upper_port  => NULL);
  COMMIT;
END;
/

CALL DBMS_JAVA.GRANT_PERMISSION(
    'APPSEC',
    'java.net.SocketPermission',
    'www.org.com:80',
    'connect, resolve'
);`

撤销对敏感数据字典视图的公共授权

我们建立一个单独的 Oracle 实例来进行应用验证/授权的主要原因之一是,如果黑客获得了对appver密码的访问权限,我们希望对黑客能够看到的内容和做的事情进行更严格的限制。请记住,我们只是混淆了密码;我们还没有加密。

除了限制在新的apver实例上拥有密码的用户数量之外,我们还希望从 Oracle 数据库数据字典中删除在某些默认的PUBLIC视图上使用SELECT的能力。特别是,我们将从ALL_USERSALL_SOURCE, ALL_TRIGGERS and ALL_VIEWS视图中删除PUBLIC访问。这些特定的数据字典视图暴露了可以在计算机安全攻击中利用的帐户和代码。尽管对于这些视图中的大多数来说,暴露的只是已经授权给用户的代码,但是我们更喜欢允许用户在看不到代码的情况下运行代码。

为了成功执行双因素认证的过程,视图ALL_USERS仍需要能够被appsec访问。在清单 11-51 中,我们将ALL_USERS上的GRANT SELECT直接交给appsec用户,而不是交给一个角色。appsec用户密码已过期,因此黑客将无法访问ALL_USERS视图。

清单 11-51。安全公共数据字典视图

`GRANT SELECT ON sys.all_users TO appsec;

REVOKE SELECT ON sys.all_users FROM PUBLIC;
REVOKE SELECT ON sys.all_source FROM PUBLIC;
REVOKE SELECT ON sys.all_source_ae FROM PUBLIC;
REVOKE SELECT ON sys.all_triggers FROM PUBLIC;
REVOKE SELECT ON sys.all_views FROM PUBLIC;
REVOKE SELECT ON sys.all_views_ae FROM PUBLIC;`

images 注意如果这正在生产中使用,您将在撤销不适当的更宽权限之前授予适当的窄权限(如图所示),以便在更新期间保持适当的功能工作。

为应用授权创建剩余的结构

用于apver实例的 NewSys.sql 脚本的剩余部分配置了我们进行应用授权所需的所有结构。大多数结构都是在appsec模式中创建的。作为SYS用户,我们只需在我们正在创建的结构名前面加上模式名appsec.

我们作为SYS用户这样做是为了防止将管理权限授予任何其他用户,甚至是appsec。这是最安全的,但是对SYS密码的控制是强制性的。

在运行脚本之前,用您创建和包装的版本复制并粘贴包装的函数f_maskf_unmask。表 11-2 按创建顺序列出了我们将要创建的结构。

Images

Images

images 注意因为我们没有在apver实例上创建secadm用户,所以我们不能在secadm模式中创建t_screen_appver_access触发器。我们将在appsec模式中创建它。

脚本中有几个地方需要用真实的操作系统用户 ID(即 Windows 登录名)替换占位符。搜索并替换OSUSEROSADMINOSUSER是一个人(或多人)想要运行我们的安全应用。OSADMIN是一个像您一样的人,他需要连接以便在数据库中注册应用连接字符串。

apver上,几个存储过程穿过数据库链接f_is_cur_cached_cdp_get_emp_2fact_nos.在那些结构中,我们看到引用类型(类似于os_user in 清单 11-52 )被转换为标准类型声明;我们不能通过数据库链接引用。查看清单 11-52 中SELECT语句的FROM子句。我们通过数据库链接从三个视图中获取数据:v_emp_mobile_nos@orcl_linkv_employees_public@orcl_linkv_sms_carrier_host@orcl_link

清单 11-52。将数据通过数据库链接到程序中

PROCEDURE p_get_emp_2fact_nos( **        --os_user               hr.v_emp_mobile_nos.user_id%TYPE,** **        os_user               VARCHAR2,**         fmt_string            VARCHAR2,         m_employee_id     OUT NUMBER,         m_com_pager_no    OUT VARCHAR2,         m_sms_phone_no    OUT VARCHAR2,         m_sms_carrier_url OUT VARCHAR2,         m_email           OUT VARCHAR2,         m_ip_address      OUT v_two_fact_cd_cache.ip_address%TYPE,         m_cache_ts        OUT VARCHAR2,         m_cache_addr      OUT v_two_fact_cd_cache.ip_address%TYPE,         m_application_id      v_two_fact_cd_cache.application_id%TYPE,         m_err_no          OUT NUMBER,         m_err_txt         OUT VARCHAR2 )     IS BEGIN         m_err_no := 0;         SELECT e.employee_id, m.com_pager_no, m.sms_phone_no, s.sms_carrier_url,             e.email, SYS_CONTEXT( 'USERENV', 'IP_ADDRESS' ),             TO_CHAR( c.cache_ts, fmt_string ), c.ip_address         INTO m_employee_id, m_com_pager_no, m_sms_phone_no, m_sms_carrier_url,             m_email, m_ip_address, m_cache_ts, m_cache_addr         --FROM hr.v_emp_mobile_nos m, hr.v_employees_public e,         --    hr.v_sms_carrier_host s, v_two_fact_cd_cache c **        FROM hr.v_emp_mobile_nos@orcl_link m, hr.v_employees_public@orcl_link e,** **            hr.v_sms_carrier_host@orcl_link s,** v_two_fact_cd_cache c

创建 Java 结构

从 SQL 客户端打开这些文件并在apver实例上执行它们。对于每一个。java 文件,用 SQL CREATE OR REPLACE AND RESOLVE JAVA SOURCE语句取消对最上面一行的注释,对于OracleJavaSecure.java,像以前一样编辑域、SMTP 主机和基本 URL 成员。执行其中的每一个来创建apver实例中的 Java 结构。在 Oracle 中创建 Java 结构之前,请确保将适用于您公司环境的域名和主机名的值替换到OracleJavaSecure.java中。

chapter 11/orajavsec/Oracle javasecure . Java
chapter 11/orajavsec/revlvlclassintfc . Java
chapter 11/orajavsec/ojsc . Java
chapter 11/testo js/testoraclejavasecure . SQL

从 ORCL 实例中删除应用验证

此时,我们不再需要在ORCL实例中进行应用验证。我们将通过应用验证,禁用appver用户到ORCL的连接。使用以下命令,以appsec或 SYS 用户的身份从 SQL 客户端连接到ORCL实例。

images 注意这将只在ORCL实例上进行,并且只有在您安装第二个数据库实例apver来进行应用验证时才进行。

ALTER USER appver PASSWORD EXPIRE;

测试增强的安全性

我们现在可以测试我们已经建立的一切,包括在单独的 Oracle 实例apver上隔离应用身份验证。我们将分两部分进行测试:首先使用OracleJavaSecure中的main()方法,然后使用单独的应用testojs.TestOracleJavaSecure

再次确认您已经在OracleJavaSecure.java的顶部设置了适合您组织的域名和其他地址。还要确保文件顶部的 SQL 命令已经被注释。(SQL 命令在其他的顶部。本章中的 java 文件也应该保留注释以便编译。)

images 注意以下内容假设您正在一个单独的数据库实例apver上运行应用验证。如果您不是,那么唯一的区别是您将能够作为appsec用户连接到orcl实例——您不必作为SYS连接。

为应用实例编码应用用户密码

第一次运行OracleJavaSecure时,我们只有一个目标:在apver实例上为appver用户创建一个新的编码 Oracle 连接字符串。向下滚动到OracleJavaSecure.java中的main()方法,编辑encodeThis String组件,如清单 11-53 所示,指向apver实例而不是orcl

清单 11-53。将 appver 连接字符串从 ORCL 切换到 apver

    encodeThis = "jdbc:oracle:thin:appver/" + encodeThis +         //"@localhost:1521:orcl";         **"@localhost:1521:apver";**

images 注意如果您没有创建专用于应用验证的额外 Oracle 数据库实例,则不要更改代码。您不需要更新编码的连接字符串prime

然后编译该类并运行它。从第十一章目录中,执行:

javac orajavsec/OracleJavaSecure.java java orajavsec.OracleJavaSecure appverPassword

结果您会看到如下所示的内容:

`Main encodes a new APPVER password if given.
After encoding, paste encoded string
in setAppVerConnection() method.

030a42105f1b3311133a0048370707005f020419190b524204041819015c390f5300121b3314303a 0a112203060116174e585a5c115704041e0a16

jdbc:oracle:thin:appver/appverPassword@localhost:1521:apver`

我们需要将编码后的字符串(见粗体数据)放入 OracleJavaSecure.java 的setAppVerConnection()方法中。完成后,该方法将类似于清单 11-54(prime String是一行,尽管这里显示的是换行到第二行)。

清单 11-54。将新编码的 appver 连接字符串嵌入到 OracleJavaSecure 代码中

`    private static void setAppVerConnection() {
        try {
            // Set this String from encoded String at command prompt (main)
**            String prime =**
"030a42105f1b3311133a0048370707005f020419190b524204041819015c390f5300121b3314303a0a112203060116174e585a5c115704041e0a16";

setConnection( OJSC.y( prime ) );
            appVerConn = conn;
        } catch( Exception x ) {
            x.printStackTrace();
        }
    }`

images 注意正是这个值prime指导我们的应用使用备用数据库实例进行应用验证。

编辑要使用的应用密码

我们将为HRVIEW应用向 Oracle 数据库上传一个连接字符串列表。更新OracleJavaSecuremain()方法,使HRappusr用户拥有正确的密码(替换“密码”字符串),如清单 11-55 所示。还要更正 Oracle 应用连接字符串的其他方面。

images 注意这些连接字符串旨在连接到orcl实例,而不是新的apver实例。

清单 11-55。应用的连接字符串,OracleJavaSecure.main()

    putAppConnString()( "Orcl", "hr",         "**password**", "localhost", String.valueOf( 1521 ) );     putAppConnString()( "Orcl", "appusr",         "**password**", "localhost", String.valueOf( 1521 ) );

然后重新编译这个类。从第十一章目录中,执行:

javac orajavsec/OracleJavaSecure.java

运行主测试

现在我们将至少再运行OracleJavaSecure五次。您必须以 OS (Windows)用户的身份运行这个程序,其匹配的 Oracle 用户被授予了appver_admin角色:您的OSADMIN的等价物。第一次,我们将生成一个双因素身份验证代码。结果将如下所示:

Chapter11>java orajavsec.OracleJavaSecure Main encodes a new APPVER password if given. After encoding, paste encoded string in setAppVerConnection method. You may enter APPVER password on command line. Domain: ORGDOMAIN, Name: OSADMIN Please rerun with 2-Factor Auth Code!

我们应该通过双因素认证码分发来接收该代码,或者通过查询apver实例上的appsec.v_two_fact_cd_cache视图来找到它。您将无法以appsec用户的身份连接查看该视图,因为我们的appsec密码已经过期;但是您可以从视图中选择作为SYS用户。

然后,我们在命令行上使用双因素身份验证代码作为参数执行相同的命令:

Chapter11>java orajavsec.OracleJavaSecure 1234-5678-9012 Main encodes a new APPVER password if given. After encoding, paste encoded string in setAppVerConnection method. Domain: ORGDOMAIN, Name: OSADMIN connsHash.size = 0 connsHash.size = 0 Domain: ORGDOMAIN, Name: OSADMIN 2011-06-05 21:00:06

您可以看到,当我们从apver实例获得该应用的 Oracle 连接列表时,该列表是空的,connsHash.size = 0。我们能够将连接字符串插入列表并使用它们。使用其中一个连接字符串,我们查询数据库以获得SYSDATE

当我们再次运行这个命令时,我们看到一会儿是我们的connsHash.size = 2,一会儿是= 1。第一个值是我们从 Oracle 获得的这个应用的列表中连接字符串的数量。第二个值是我们呼叫removeAppConnString()后的号码。我们立即调用putAppConnString()两次来添加和覆盖连接字符串,然后我们调用putAppConnections()来为这个应用在 Oracle 中存储两个连接字符串的列表。我们再次使用其中一个从 Oracle 获取SYSDATE

Chapter11>java orajavsec.OracleJavaSecure 1234-5678-9012 Main encodes a new APPVER password if given. After encoding, paste encoded string in setAppVerConnection method. Domain: ORGDOMAIN, Name: OSADMIN connsHash.size = 2 connsHash.size = 1 Domain: ORGDOMAIN, Name: OSADMIN 2011-06-05 21:00:23

运行 Main 将连接字符串复制到新版本

我们将再次编辑OracleJavaSecure.java,以测试我们从旧版本的应用中复制连接字符串列表到新版本的能力。打开文件并编辑内部类InnerRevLvlClass(靠近顶部),增加版本号,例如从20110101a20110101b。见清单 11-56 。

清单 11-56。改变应用内部类的版本/修订版

    private static class InnerRevLvlClass         implements Serializable, RevLvlClassIntfc     {         private static final long **serialVersionUID** = 2011010100L;         private String innerClassRevLvl = "**20110101b**";         public String getRevLvl() {             return innerClassRevLvl;         }     }

images 注意我们不必在 Oracle 数据库上创建这个 java 代码的新版本,因为 serialVersionUID 没有改变。

main()方法中,找到对copyAppConnections()的注释调用,并取消该调用的注释。确保该调用中的旧版本号String,清单 11-57 ,与您之前更改的innerClassRevLvl相匹配。

清单 11-57。从先前版本复制连接字符串列表

copyAppConnections( "**20110101a**" );

编译OracleJavaSecure并运行它。如果不到十分钟,请使用相同的双因素授权码,否则请请求并使用新的双因素授权码。将连接字符串列表从旧版本复制到新版本后,此操作将退出。

Chapter11>javac orajavsec/OracleJavaSecure.java Chapter11>java orajavsec.OracleJavaSecure Chapter11>java orajavsec.OracleJavaSecure 1234-5678-9012 Main encodes a new APPVER password if given. After encoding, paste encoded string in setAppVerConnection method. Domain: ORGDOMAIN, Name: OSADMIN

作为apver上的SYS用户,通过查询appsec.v_app_conn_registry视图,您可以看到有一个新版本的应用类文件和相关的连接字符串列表。

"CLASS_NAME"                                  "CLASS_VERSION" "CLASS_INSTANCE" "UPDATE_DT" "orajavsec.OracleJavaSecure$InnerRevLvlClass" "20110101b"     "ACED00057372... "11-JUN-11" "orajavsec.OracleJavaSecure$InnerRevLvlClass" "20110101a"     "ACED00057372... "11-JUN-11"

我们再次编辑OracleJavaSecure.java并注释main()方法的两个区域。首先注释将连接字符串列表从旧版本复制到新版本的那一行。

    //copyAppConnections( "20110101a" );

然后注释会覆盖的行,并在列表中插入新的连接字符串。

    //putAppConnString( "Orcl", "hr",     //    "password", "localhost", String.valueOf( 1521 ) );     //putAppConnString( "Orcl", "appusr",     //    "password", "localhost", String.valueOf( 1521 ) );

最后一次,编译OracleJavaSecure并运行它。它第一次运行时,您会看到列表中已经有两个连接字符串(connsHash.size = 2)是从应用的前一版本复制过来的。在运行过程中,我们调用removeAppConnString(),然后运行putAppConnections(),用这个应用仅有的一个连接字符串列表更新 Oracle。第二次运行时,你会看到,因为我们没有更新和添加连接字符串,只有一个开始(connsHash.size = 1)。

Chapter11>javac orajavsec/OracleJavaSecure.java Chapter11>java orajavsec.OracleJavaSecure Chapter11>java orajavsec.OracleJavaSecure 1234-5678-9012 Main encodes a new APPVER password if given. After encoding, paste encoded string in setAppVerConnection method. Domain: ORGDOMAIN, Name: OSADMIN connsHash.size = 2 connsHash.size = 1 Domain: ORGDOMAIN, Name: OSADMIN 2011-06-11 18:04:15 Chapter11>java orajavsec.OracleJavaSecure 1234-5678-9012 Main encodes a new APPVER password if given. After encoding, paste encoded string in setAppVerConnection method. Domain: ORGDOMAIN, Name: OSADMIN connsHash.size = 1 connsHash.size = 1 Domain: ORGDOMAIN, Name: OSADMIN 2011-06-11 18:04:26

我们得出结论,我们的copyAppConnections()流程将现有的连接字符串列表从应用的一个版本复制到下一个版本(在内部类中使用更新的innerClassRevLvl)。)当我们升级我们的应用并且不需要改变连接字符串时,这将节省时间。

从不同的应用 TestOracleJavaSecure 进行测试

TestOracleJavaSecure独立于运行应用验证的任何实例。它将作为一个客户端应用,调用OracleJavaSecure类中的方法来使用那些安全特性。使用OracleJavaSecure,测试应用将做以下事情:

  • 完成单点登录和双因素认证。
  • 注册自己(它的名字和应用内部类)。
  • 存储应用的连接字符串列表。
  • 从 Oracle 检索连接字符串列表。
  • 以加密形式传输和存储连接字符串。
  • 解密并使用连接字符串来查询应用数据。
  • 以加密形式查询敏感数据并解密以供使用

编辑文件chapter 11/testo js/testoraclejavasecure . Java。更正清单 11-58 中显示的appusr的密码。我们将把这个用户添加到TestOracleJavaSecure应用的连接字符串列表中。我们仍然使用HRVIEW作为我们的应用 ID,这意味着我们将作为appusr连接,并使用角色hrview_role(在appsec.v_application_registry中注册)。

清单 11-58。编辑 TestOracleJavaSecure 进行测试

`    String applicationID = "HRVIEW";
    Object appClass = new AnyNameWeWant();
    OracleJavaSecure.setAppContext( applicationID, appClass, twoFactorAuth );

OracleJavaSecure.getAppConnections();
    if( twoFactorAuth.equals( "" ) ) {
        return;
    }

// Demonstrate copy connsHash from previous version
    // Only do this once, make sure it worked (see appsec.V_APP_CONN_REGISTRY),
    // then comment this line
    //OracleJavaSecure.copyAppConnections( "20110131a" );
    // Get copied list of connection strings for new version number
    OracleJavaSecure.getAppConnections();

OracleJavaSecure.putAppConnString( "Orcl", "appusr",
        "password", "localhost", String.valueOf( 1521 ) );
    //Only do this line once -- must be admin account
    OracleJavaSecure.putAppConnections();`

清单 11-58 中的最后一行是对putAppConnections()的调用。该方法调用 Oracle 函数f_set_decrypt_conns,我们将它移到了appsec_admin_pkg包中。只有具有appver_admin角色的用户才能执行appsec_admin_pkg中的结构。在我们的例子中,我们将appver_admin授予了osadmin用户。因此,为了测试这方面的安全性,我们将同时以osadmin(管理帐户)和osuser(非管理帐户)的身份运行TestOracleJavaSecure。您可以通过修改内部类的版本号和取消注释copyAppConnections()来测试copyAppConnections()TestOracleJavaSecure,传递之前的版本号。

以管理用户 OSADMIN 的身份编译运行

TestOracleJavaSecure.main()的代码中,我们在尝试获取加密数据之前,有三次连接 Oracle 数据库的尝试。我们试图做这些事情

  • 调用getAppConnections()进行密钥交换,并获得该应用的连接字符串列表。
  • 调用putAppConnections()来更新 Oracle 中的连接字符串列表。
  • 调用getAAConnRole()解码并使用一个连接字符串得到一个Connection.

我们将以管理用户的身份编译并运行TestOracleJavaSecure(在我们的例子中,是一个具有appver_admin角色的用户,OSADMIN——可能是您的操作系统用户 ID)。)我们开始运行这个应用时没有双因素身份验证代码。以下是命令和结果:

Chapter11>javac testojs/TestOracleJavaSecure.java Chapter11>java testojs/TestOracleJavaSecure Domain: ORGDOMAIN, Name: OSADMIN Please rerun with 2-Factor Auth Code! java.lang**.NullPointerException** Please rerun with 2-Factor Auth Code!

putAppConnections()的调用报告了一个NullPointerException——我们不会像捕获常规客户端应用调用的方法中的异常那样捕获管理命令中的异常。其他连接到 Oracle 的尝试指出需要返回双因素身份验证代码。

让我们用一个假的双因素身份验证代码再次运行代码:

Chapter11>java testojs.TestOracleJavaSecure **123** Domain: ORGDOMAIN, Name: OSADMIN Oracle error 21) 100, ORA-01403: **no data found** java.lang.NullPointerException Oracle error 21) 100, ORA-01403: **no data found** Wrong or old 2-Factor code parameter

同样的NullPointerExceptionputAppConnections()报告,另外两次连接 Oracle 的尝试报告了“未找到数据”错误,这是我们从不良用户或双因素身份验证代码得到的结果。当我们试图使用来自getAAConnRole()Connection来获取一个 Oracle Statement时,我们还在清单 11-59 中捕获了一个NullPointerException。如果我们走到这一步,我们的理解是,我们提供了一个可疑的双因素身份验证代码,所以我们报告问题并退出。

清单 11-59。捕捉到不正确的双因素认证码

    try {         mStmt = **conn.createStatement();**     } catch( NullPointerException n ) {         System.out.println( "Wrong or old 2-Factor code parameter" );         return;     }

对于这个新用户,appsec.v_two_fact_cd_cache视图中将会有一个新条目。在这个例子中,这个新条目将被指定给新的employee_id,304。这里有一个选自appsec.v_two_fact_cd_cache的例子。

`SQL> select * from appsec.v_two_fact_cd_cache;

EMPLOYEE_ID APPLICATION_ID TWO_FACTOR_CD  IP_ADDRESS DISTRIB_CD CACHE_TS


304 HRVIEW         2747-4367-3056 127.0.0.1           1 12-JUN-11
        300 HRVIEW         3471-8557-5210 127.0.0.1           3 12-JUN-11`

当我们最终使用正确的双因素身份验证代码返回执行TestOracleJavaSecure时,我们能够打印出关于我们到 Oracle 的数据查询代理连接的许多方面,通过appusr用户连接,使用hrview_role。我们还从我们熟悉的p_select_employees_sensitive过程中选择和解密数据。

`Chapter11>java testojs.TestOracleJavaSecure 1234-5678-9012
Domain: ORGDOMAIN, Name: OSADMIN
Domain: ORGDOMAIN, Name: OSADMIN
osadmin

APPUSR

127.0.0.1
OSADMIN
OSADMIN
HRVIEW_ROLE
Oracle success 2)
100, Steven, King, SKING, 515.123.4567, 2003-06-17 00:00:00, AD_PRES, 24000, null, null, 90`

以非管理用户 OSUSER 的身份运行

我们将测试我们对应用连接字符串更新的限制,仅限于被授予了appver_admin角色的用户。为此,您需要作为一个操作系统用户登录,该用户对应于一个没有appver_admin角色的 Oracle 用户,在我们的示例中,这个角色相当于OSUSER。再次运行TestOracleJavaSecure。您将会看到,当应用调用方法putAppConnections()时,抛出了一个关于appsec_admin_pkg包的异常。该用户没有执行从该方法调用的 Oracle 函数f_set_decrypt_conns的权限。

Chapter11>java testojs/TestOracleJavaSecure Domain: ORGDOMAIN, Name: OSUSER Please rerun with 2-Factor Auth Code! java.sql.SQLException: ORA-06550: line 1, column 13: PLS-00201: **identifier 'APPSEC.APPSEC_ADMIN_PKG' must be declared** ORA-06550: line 1, column 7: PL/SQL: Statement ignored

我们希望使用非管理用户演示其他非管理功能。为此,编辑TestOracleJavaSecure.java并注释调用putAppConnections()方法的代码行。然后重新编译并重新运行应用。您将看到以下成功结果:

Chapter11>javac testojs/TestOracleJavaSecure.java Chapter11>java testojs.TestOracleJavaSecure Chapter11>java testojs.TestOracleJavaSecure 1234-5678-9012 Domain: ORGDOMAIN, Name: OSUSER Domain: ORGDOMAIN, Name: OSUSER osuser APPUSR 127.0.0.1 OSUSER OSUSER HRVIEW_ROLE Oracle success 2) 100, Steven, King, SKING, 515.123.4567, 2003-06-17 00:00:00, AD_PRES, 24000, null, null, 90

章节回顾

我们在这一章的目标是增强我们到目前为止构建的所有东西的安全性。我们在以下方面实现了这一目标:

  • 我们用 Java 对用户密码(连接字符串)进行编码。
  • 我们混淆了编码/解码的 Java 程序。
  • 我们为存储在数据库中的数据——特别是我们的连接字符串列表——实现了安全的数据加密。
  • 我们建立了一个管理角色,限制谁可以更新应用的连接字符串。
  • 我们将应用验证流程转移到一个新的、经过强化的 Oracle 数据库实例中,apver.

除了致力于增强安全性,我们还深入研究了以下主题:

  • 通过各种方式保护 Oracle 用户口令
  • 使用 Oracle 客户机 wallet
  • 使用 Oracle 客户端跟踪日志记录
  • 使用 Oracle 瘦客户端(JDBC)跟踪日志记录
  • 使用 Oracle wrap 实用程序混淆 Oracle 函数
  • 将应用连接字符串从早期版本复制到当前应用版本
  • 向我们的应用连接字符串列表添加其他身份验证凭证,例如 FTP 密码
  • 审查公众访问数据字典视图的弱点
  • 使用数据库链接从另一个数据库实例读取数据**

十二、安全管理

既然我们已经构建了 Oracle 结构以及 Oracle 和 Java 代码,我们将需要维护保持其运行的数据。这些数据主要由用户、代理授权、应用注册和应用连接字符串组成。执行 SQL 脚本为一两个用户和一两个应用插入记录并不太困难——尤其是在我们研究问题和需求的时候。但是在一年左右的时间里,完成这些任务的步骤将会被遗忘很久,还有做这些事情的原因。

但是,如果我们能够将业务规则、逻辑和过程步骤封装在一个用户友好的应用中,那么我们添加新用户和应用将会容易得多。事实上,一旦我们做到了这一点,我们将开发出一个非常方便的接口,其中一部分我们将作为模板提供给我们组织中的应用开发人员,以便他们可以实现我们的安全代码。

安全管理界面

当我谈到用户友好的应用时,我指的是设计良好、简单的图形用户界面。

我们将在本章中探讨的安全管理界面由一个登录屏幕、一个菜单屏幕和七个功能屏幕组成。在登录屏幕上,我们希望用户输入我们发送给他的双因素身份验证代码。正是这个屏幕,我们将作为 GUI 模板提供给其他应用开发人员,以便他们可以实现我们的安全结构。

images 安全管理接口应用的文件可以在 第十二章 /orajavsec 目录下找到。

功能屏幕将引导管理员完成诸如编辑员工和用户数据(尤其是我们用于单点登录和双因素身份验证的那些元素)等任务。还有授予管理和应用代理权限的功能屏幕。我们需要能够注册一个新的应用,这个过程的大部分是通过 GUI 完成的。然后,我们希望编辑应用的现有连接字符串,偶尔从应用的先前版本复制连接字符串。所有这些功能都显示在我们 GUI 的屏幕上。

应用登录屏幕

我们所说的应用登录是什么意思?因为我们使用的是单点登录,所以我们实际上并不要求用户输入用户名和密码,但是我们要求他们输入双因素身份验证代码。我们的计划是一直停留在登录屏幕上,直到他们输入正确的双因素代码或退出程序。我们需要处理几个案例:

  • 当他们的帐户不能进行 SSO 时,我们需要告诉他们
  • 当他们输入错误或旧的双因素身份验证代码时,我们需要告诉他们
  • 当他们输入正确的密码时,我们需要继续

在幕后,发生了很多事情。在用户有机会输入双因素身份验证代码之前,登录屏幕将从操作系统获取用户身份,并尝试进行 SSO,然后使用该操作系统用户 ID 作为appver用户代理到apver实例。还记得我们为了混淆appver密码和解码密码的 Java 代码所做的努力吗——所有这些都是这个过程的一部分。

建立代理会话后,Oracle 数据库会确定用户是否输入了双因素身份验证代码;如果没有,它会生成一个并发送给用户的移动设备。为了找出将身份验证代码发送到哪里,Oracle 数据库查看从apver实例到orcl实例,再到HR模式EMPLOYEE,emp_mobile_nos表的数据库链接,以找出这个特定用户可以使用哪些设备。接下来的步骤是向每台设备发送身份验证代码并返回登录屏幕。

images 注意我们将为两个 Oracle 数据库实例orclapver构建这个管理界面。如果您还没有创建apver实例,您将需要使用修改后的代码在单个实例上运行,在 * Chapter12 /single* 目录中。

然后,当用户在他们的设备上收到双因素身份验证代码,并在登录屏幕上输入代码时,将采取一系列附加步骤。登录屏幕采取的第一步是将某些数据和对象传递给 Oracle 数据库。它传递双因子代码、特定于该应用的内部类实例和应用标识字符串。此外,在客户机上生成 RSA 公钥/私钥对,模数和指数工件也被传递给数据库。

Oracle 数据库首先确保用户输入的双因素身份验证代码与在过去十分钟内从该特定客户端网络地址发送给该用户的特定应用的代码相匹配。然后,如果是正确的代码,数据库会确保内部类实例与为相关应用存储的内容相匹配,然后检索与之关联的连接字符串列表。回想一下,连接字符串列表以加密的形式存储在数据库中,加密和解密的代码已经使用 Oracle database wrap 实用程序进行了“包装”。

Oracle 数据库使用客户机提供的工件构建一个 RSA 公钥,并生成一个 DES 密码密钥。DES 密钥的工件使用 RSA 公钥加密,然后返回给客户端。

这个特定应用的连接字符串列表用秘密密码密钥加密,加密后的列表也返回给客户端。此时,登录屏幕即将完成。然而,在它关闭之前,我们继续使用应用,我们使用 DES secret 密码密钥在客户端建立一个匹配的密钥。只要我们在这个特定的应用中,我们就会使用这个密钥来解码应用的连接字符串;因此,我们克隆或复制所有相关的关键组件以供继续使用。此后,基本 RSA 和 DES 加密密钥成员可供与我们将为应用事务建立的每个 Oracle 连接关联的新密钥重用。

我刚刚描述的所有内容相当于一个函数列表,但这一切都发生在图 12-1 所示的相对简单的界面上。双因素身份验证代码的单个输入字段和继续按钮是我们完成工作所需的全部内容。

images

图 12-1。登录界面

应用内部类

我们在我们的Login类中提供了一个应用内部类结构,供任何应用使用。内部类的代码如清单 12-1 所示。通过更改Login类的包,我们可以将这个内部类用于多个应用,减轻开发人员在代码中包含内部类的负担。在我们的安全管理界面应用中,包是orajavsec。完整的内部类名将orajavsec.Login$InnerRevLvlClass。如果有另一个应用在名为mynewpkg的包中定义了Login类,那么内部类将被命名为mynewpkg.Login$InnerRevLvlClass。这种差异足以区分不同的和独立的应用及其关联的应用连接字符串。即使内部类的定义除了包名之外是相同的,也是如此。

清单 12-1。登录类,内部类

`package orajavsec;

public class Login extends JDialog {

public static class InnerRevLvlClass
                implements Serializable, RevLvlClassIntfc
        {
        private static final long serialVersionUID = 2011010100L;
        private String innerClassRevLvl = "20110101a";

public String getRevLvl() {
            return innerClassRevLvl;         }
    }
...`

用于登录的代码是我们可以提供给其他应用程序员的,以包含在他们的应用中。它是一个模板和一段可重用的代码——他们只需要将它包含在他们的包中。然后,他们不需要担心任何设置细节。登录完成后,他们可以通过调用getAAConString()方法从OracleJavaSecure获取连接字符串。

为了在 Oracle 数据库中使用Login$InnerRevLvlClass,我们需要在数据库中有一个代表性的类来实例化。我们需要执行清单 12-2 所示的脚本,在数据库中创建 Java 结构。我们将为每个应用在数据库中创建一个类似的 Java 结构。不同应用的类之间的唯一区别是包名。

您可以作为apver实例上的sys用户来执行这个脚本,但是您不需要这样做。当我们读到本章末尾时,我们将使用这个安全管理接口通过一个引导过程来注册它自己。该过程将包括在 Oracle 数据库中创建代表性的内部类,并生成该应用将使用的连接字符串。

清单 12-2。构建登录内部类的脚本

`CREATE OR REPLACE AND RESOLVE JAVA SOURCE NAMED APPSEC."orajavsec/Login" AS

package orajavsec;

import java.io.Serializable;

import orajavsec.RevLvlClassIntfc;

// Drop the "extends JDialog" from class definition
// It is unneeded and will be invalid on Oracle server
public class Login {
    public static class InnerRevLvlClass implements Serializable,
                                                     RevLvlClassIntfc {
        private static final long serialVersionUID = 2011010100L;
        private String innerClassRevLvl = "20110101a";

public String getRevLvl() {
            return innerClassRevLvl;
        }
    }
}
/`

images 注意这个脚本可以在文件chapter 12/log in . SQL中找到。

居中法

我们需要将登录屏幕放在用户显示器的中心,否则它可能会出现在显示器的左上角,但不会被注意到。将 GUI 界面居中是标准做法,我们将在所有用户界面屏幕中使用。因为我们想让所有的 GUI 屏幕居中,我们将把方法放在Login中,如清单 12-3 所示,并使该方法既是公共的又是静态的——任何人都可以调用它而不用引用Login的实例。它是模板的一部分,是我们不需要为每个屏幕复制的代码。我们将任何想要居中的项目传递给这个方法,这个方法调整项目的位置。

清单 12-3。 GUI 中心()方法

    public static void center(Component item) {         Dimension screenSize = Toolkit.getDefaultToolkit().getScreenSize();         Dimension frameSize = item.getSize();         if (frameSize.height > screenSize.height) {             frameSize.height = screenSize.height;         }         if (frameSize.width > screenSize.width) {             frameSize.width = screenSize.width;         }         **item.setLocation**((screenSize.width - frameSize.width)/2,                          (screenSize.height - frameSize.height)/2);     }

center()方法的代码通常由 GUI 应用的 IDE 自动生成(这个版本来自 JDeveloper)。我刚刚将代码从一个标准的 GUI main()方法移到这个静态方法中。清单 12-3 显示了代码。

JavaBeans

我已经提到了 JavaBeans,但是这是我们第一次介绍它们。JavaBeans 是 Java 类,有一组必需的方法和接口。特别是,JavaBean 是一个 GUI 对象,它为用户界面的每个属性都提供了 getter 和 setter 方法。通过这种方式,可以将 JavaBean 提供给 IDE,IDE 自动知道如何在屏幕上显示它,以及如何提供对其属性的访问。GUI 应用中的每一段文本、输入框和按钮都是一个 JavaBean。

每个 IDE 处理 JavaBeans 的方式稍有不同,将属性设置代码放在稍有不同的位置。IDE 通常会用注释来标记这些代码,比如“这些代码是自动生成的,不要修改它们。”相反,当您在 IDE 的 GUI 应用屏幕上添加或绘制组件时,就会生成代码,并且您可以编辑代码。但是,如果您以意外的方式修改了代码,IDE 可能无法再在屏幕上显示它。对于标志注释,更好的措辞可能是,“这段代码让 IDE 显示您的组件,请不要修改或添加 IDE 无法理解的代码。”

在 JDeveloper(Oracle 的免费 Java IDEs 之一,另一个是 NetBeans)中,当您开发 GUI 应用时,管理用户界面的大部分代码都放在一个名为jbInit()的方法中。因为我们最初在那里找到了所有的参数设置,所以很容易将我们的修改添加到那个方法的 UI 中。然而,我发现生成另一个方法(我称之为ojsInit())并在那个方法中编写我们对 UI 的修改会更好。我们改变标准构造函数,在调用jbInit()之后立即调用我们的方法。无论您使用什么 IDE,您都希望有一个类似的代码分离。

登录屏幕构造器

在清单 12-4 中,你可以看到我们是如何在调用构造函数中的jbInit()之后立即调用ojsInit()的。此外,我们还提供了第三个接受Frame类的构造函数。我们将调用那个构造函数,而不是默认的构造函数,并提供对我们的主应用类parent的引用。然后,当登录过程完成时,我们将使用这个引用返回到我们的应用。

清单 12-4。登录界面构造器

`public class Login extends JDialog {

public Login() {
        this(null, "", false);
    }

public Login(Frame parent, String title, boolean modal) {
        super(parent, title, modal);
        try {
            jbInit();
            ojsInit();
        } catch (Exception e) {
            e.printStackTrace();
        }
    }

public Login(Frame parent) {
        // For Oracle Java Secure, call this constructor
        this(parent, "two-factor Authentication Login", true);
    }

public Login(Frame parent, String applicationID, String appClassName) {
        // This replacement constructor is used when managing a selected application
        super(parent, "two-factor Authentication Login", true);
        try {
            this.applicationID = applicationID;
            Class applicationClass = Class.forName(appClassName);
            appClass = applicationClass.newInstance();
            jbInit();
            ojsInit();
        } catch (Exception e) {
            System.out.println("Specified application class is not available");
            System.exit(0);
        }
    }
...`

还有第四个构造函数绕过其他构造函数(重复代码)。第四个构造函数接受一个替代的应用名和内部类名。当被调用时,这个构造函数使用反射来实例化一个名为的类型的类,并将使用那个类来代替内部类Login。这很少使用,但是在我们的安全管理界面中是需要的。为了修改相关的连接字符串,我们必须变成不同的应用。我们将在本章后面详细讨论这一点。

Login正在扩展的JDialog类有一个带三个参数的构造函数。您可以看到我们对JDialog构造函数super()的调用。这三个参数中的最后一个是一个boolean,它指定JDialog是否是模态的。我们将登录屏幕的模态boolean设置为true。我们希望它出现在当前应用可见的任何其他屏幕的顶部。

images 注意在调用其他构造函数,甚至是超类构造函数的时候,那些调用需要在调用构造函数的第一行。这就是为什么我们需要一个独立的第四构造者。在调用初始化方法之前,我们需要实例化一个新的替代内部类。如果我们想调用我们的原始构造函数来进行初始化,这是不可能发生的。必须首先调用原始构造函数。

“处理中等待”模式对话框

在 GUI 应用中,如果用户不耐烦,而您的应用代码正在进行一些复杂的计算或数据检索,就会发生不好的事情。用户可能会重复按下一个按钮,这可能会导致应用的代码多次执行一个功能;或者用户可能会沮丧地退出应用。当你的应用要做一些复杂的事情或者可能要花很长时间的时候,最好在应用前面放一个小通知,让用户知道你正忙着为他们工作,并请他们耐心等待。描述了登录屏幕中发生的复杂任务列表后,您就可以理解为什么它可能需要一些时间来处理,在此期间我们希望用户耐心等待。

我定义了一个非常简单的JDialog类,sayWaitDialog,我把它放在了Login类中。sayWaitDialogLoginstatic成员,由static初始化程序块配置。该对话框屏幕被定义为模态对话框——参见清单 12-5 中的粗体代码。模态对话框出现在用户显示器上其他窗口的顶部,它们不能轻易隐藏在其他窗口后面,至少是那些与当前应用相关的窗口,即使用户点击了其他窗口。我相信你以前在其他对话中已经看到并注意到了那种行为。

清单 12-5。要求用户耐心等待的模态对话框

`public static JDialog sayWaitDialog = new JDialog();

**    static {**

sayWaitDialog.setDefaultCloseOperation(WindowConstants.DO_NOTHING_ON_CLOSE);
        sayWaitDialog.setModal(true);
        sayWaitDialog.setTitle("Please Wait");
        JPanel jPanel1 = new JPanel();
        sayWaitDialog.setSize(new Dimension(255, 93));
        sayWaitDialog.getContentPane().setLayout(null);
        jPanel1.setBounds(new Rectangle(5, 5, 230, 45));         jPanel1.setLayout(null);
        jPanel1.setBackground(new Color(255, 222, 214));
        JLabel jLabel1 = new JLabel();
        jLabel1.setText("Working. Please wait!");
        jLabel1.setBounds(new Rectangle(5, 5, 220, 35));
        jLabel1.setHorizontalAlignment(SwingConstants.CENTER);
        jLabel1.setFont(new Font("Tahoma", 0, 16));
        jPanel1.add(jLabel1, null);
        sayWaitDialog.getContentPane().add(jPanel1, null);
        Login.center(sayWaitDialog);
    }`

注意,sayWaitDialog定义中的最后一行是对我们的center()方法的调用,将对话框放在监控器的中央。关于这个对话框,最后要提到的是在第一行。我们将对话框设置为关闭时不做任何事情。我们禁止用户点击窗口右上角的 X 来关闭对话框。我们实际上从不打开或关闭对话框;我们只是让它在我们需要的时候可见,在我们不需要的时候不可见。

后台处理线程

现在让我们看一下ojsInit()方法,我们用它来设置登录屏幕 GUI 的附加属性,并跳转到登录过程。清单 12-6 显示了代码。在方法代码的下半部分,我们看到了将 static Login.sayWaitDialog设置为可见的调用,以及将登录屏幕居中然后使其可见的调用。我们还稍微修改了一下Login Screen,将reEnterLabel设置为不可见。如果用户输入了不正确的双因素身份验证代码,我们会将该帮助消息设置为可见。

清单 12-6。自管理 GUI 初始化和后台处理线程

`private static String applicationID = "OJSADMIN";
    private static Object appClass = new InnerRevLvlClass();

private void ojsInit() {

**        SwingUtilities.invokeLater(new Runnable() {**

public void run() {
                    OracleJavaSecure.setAppContext(applicationID, appClass, "");
                    // Under some circumstances, this will throw an exception
                    // if OS User not allowed, also test isAppverOK
                    OracleJavaSecure.getAppConnections();
                    // on success, original error message will be blanked out
                    if (OracleJavaSecure.isAppverOK())
                        twoFactCodeTextField.setText("");
                    Login.sayWaitDialog.setVisible(false);
                }
            });
        Login.sayWaitDialog.setVisible(true);
        Login.center(this);
        reEnterLabel.setVisible(false);
        appIDLabel.setText(applicationID);
        this.setVisible(true);         return;
    }`

那么在我们的ojsInit()方法顶部的那段复杂的代码是什么呢?当我们的模态sayWaitDialog被显示时,有一段代码我们想在后台运行。那听起来比它容易。你看,像这样的 Java GUI 应用通常是单线程——也就是说,一次只处理一个轨道的代码。由于我们使用了模态对话框,这种单一焦点变得更加顽固。模态对话框完全支配单线程处理,直到对话框被删除(在我们的例子中,它是不可见的)。

在旧版本的 Java 中,我不得不编写单独的Thread池来处理带外处理,但是在当前的 Java 版本中,SwingUtilities类提供了解决这个问题的标准方法。我们将一个新的Runnable实例(Thread)传递给SwingUtilities.invokeLater()方法(你可以在清单 12-6 中看到这一点),它会延迟运行Thread,直到你的当前代码得到进一步处理。这种延迟似乎对我们的目的很有效。

您可以看到在new Runnable()语法中创建了新的Thread。这是一个匿名类定义。在类定义中,我们定义了一个名为run()的方法来完成我们的工作。您可以看到,run()方法完成了Login类的重要初始工作——等待用户输入双因素验证码。大部分工作是在对OracleJavaSecuresetAppContext()getAppConnections()中的方法的两次调用中完成的。

在我们需要解决的一个特定情况下,对getAppConnections()的调用很容易失败——每当操作系统用户未被授权通过 Oracle 应用用户进行代理连接时。在抛出异常的某些情况下会发生这种情况,在 Oracle connection as appver user 为空的其他情况下也会发生这种情况。我们在Oracle Java Secure中增加了一个方法来测试appver连接是否为空:isAppverOK()。在这两种情况下,我们都需要通知用户。然而,在延迟的Thread中,很难捕捉异常并对其做任何有帮助的事情。这里的情况是我们只需要通知用户这个事实。他们需要注销,然后以不同用户的身份重新登录,才能使用该应用。我们将这样告诉他们:我们在jbInit()中为twoFactCodeTextField设置的初始值是“您的操作系统用户帐户无法登录。”如果我们成功地建立了代理连接,那么就不会抛出Exception,我们将继续执行延迟的Thread。这将设置twoFactCodeTextField等于一个空字符串。但是,如果出现异常,我们不会更改该值,文本字段将保留为原始错误消息。我不把这种方法称为“错误报告”,而是称之为“缺乏成功报告”。在这种情况下,记住“缺乏成功报告”是一种可行的方法。

在我们推迟了Threadrun()的方法之后,我们使sayWaitDialog隐形了。ojsInit()中的代码顺序与执行的时间顺序相反。所以我们最终在让sayWaitDialog不可见之前让它可见。

继续按钮

我们要处理的最后一组活动是在用户输入双因素身份验证代码并按下 enter 键或单击 Continue 按钮后发生的活动。这两个事件都调用了continueButton_actionPerformed()方法。这个方法如清单 12-7 中的所示。注意,我们调用了之前调用的OracleJavaSecure的两个相同的方法;然而,这次我们提供了来自twoFactCodeTextField的双因素验证码,用户可能收到并输入了这个验证码。

清单 12-7。继续提供双因素认证

private void continueButton_actionPerformed(ActionEvent e) {         if (twoFactCodeTextField.getText().equals("Bad two-factor code"))             twoFactCodeTextField.setText("");         if (twoFactCodeTextField.getText().equals("") ||             twoFactCodeTextField.getText().equals("Your OS User account cannot log in"))             return;         OracleJavaSecure.setAppContext(applicationID, appClass,             **twoFactCodeTextField.getText());**         OracleJavaSecure.getAppConnections();         if (!**OracleJavaSecure.test2Factor()**) {             twoFactCodeTextField.setText("Bad two-factor code");             reEnterLabel.setVisible(true);             return;         }         **this.setVisible(false);**         return;     }

这一次,在我们从getAppConnections()方法返回之后,我们可以通过调用OracleJavaSecure.test2Factor()来测试输入的双因子代码是否成功。这是本章中添加的一个新方法,其代码如清单 12-8 所示。我们只是测试连接字符串列表的大小,该列表是向getAppConnections()方法提供双因素身份验证代码的结果。大小为 0 是可以的,任何其他值也是可以的——这意味着双因子代码是成功的,并且test2Factor()返回一个true。然而,如果对consHash.size()的调用导致抛出Exception,那么连接字符串列表为空,我们确定双因子代码不可接受,所以我们返回一个false

清单 12-8。来自 OracleJavaSecure 的双因素代码测试成功

    public static boolean test2Factor() {         try {             connsHash.size();         } catch( Exception x ) {             return false;         }         return true;     }

回到我们的Login类,在清单 12-7 中,如果双因素代码不好,我们将twoFactCodeTextField中的消息设置为“坏的双因素代码”,并使reEnterLabel文本可见。我们在用于用户输入的相同文本字段中呈现用户反馈。回头看看上面那个方法,continueButton_actionPerformed()。您可以看到我们如何在希望用户输入双因素身份验证代码的字段中处理消息。如果他们在该字段中有空白或我们的错误消息字符串之一,我们不会尝试将其作为双因素身份验证代码提交,而只是返回。

一个成功的登录事件,其中双因素代码是可接受的,并且返回了一个连接字符串列表,导致最后几行continueButton_actionPerformed()被运行。在这种情况下,我们基本上将登录屏幕设置为不可见。

登录屏幕关闭

您可能感兴趣的最后一个技巧是:如果用户没有完成预期的活动就退出登录屏幕,那么我们将退出运行应用的整个 JVM。这是通过调用System.exit()this_WindowClosing()方法来处理的,如清单 12-9 所示。每当用户点击窗口右上角的 X 来关闭窗口时,this_WindowClosing()方法就会被自动调用。在进入一个应用的正常过程中,我们不会关闭Login屏幕,只是简单的使其不可见。

现在您可能会问,登录屏幕最初是如何变得可见的,当它不再可见时会发生什么。登录屏幕是从一个应用调用的,在登录屏幕过程之后,控制权返回给应用,但是我有点超前了,我们将在下一节中看到这一点。

清单 12-9。关闭登录并退出应用

    private void this_windowClosing(WindowEvent e) {         System.exit(0);     }

安全管理菜单

我们将构建一个应用,使用我们在本书中描述的所有安全特性,我们将使用该应用来管理安全特性——可以说,我们将“吃自己的狗粮”。我们可以称这个应用为OracleJavaSecure (OJS)管理,它将由一个菜单和几个功能屏幕组成,如图 12-2 所示。

images

图 12-2。安全管理菜单

菜单将驻留在一个名为OJSAdmin的类中,该类有一个main()方法,它只是实例化一个新的OJSAdmin类。参见清单 12-10 。在实例化时,OJSAdmin类运行它的 JavaBeans 初始化方法,jbInit()和我们在ojsInit()方法中的附加初始化。

清单 12-10。安全管理菜单 main()方法和构造器

`    public static void main(String[] args) {
        // Put main() method right in this JFrame
        new OJSAdmin(args);
    }

public OJSAdmin(String[] args) {
        try {
            jbInit();
            // Add extra init in separate method
            ojsInit(args);
        } catch (Exception e) {
            e.printStackTrace();
        }
    }`

清单 12-11 中的显示了OJSAdmin.ojsInit()方法。它的主要功能是实例化一个新的Login类,我们之前已经讨论过了,然后将其自身居中并使其可见。使用这本书的安全特性只需要实例化Login类。注意在清单 12-11 中,如果ojsInit()有两个(或更多)参数,我们调用其中一个Login构造函数,如果没有,则调用另一个Login构造函数。如果我们调用菜单来代表一个不同的应用(稍后讨论),将会有争论。

清单 12-11。附加 OJSAdmin 菜单初始化

`    private void ojsInit(String[] args) throws Exception {
        // Method for initializing OJS functionality
        JPanel disablePanel = bottomMenuPanel;
        // Login does SSO, two-factor Auth and Application Authorization

**        if (args.length < 2)**

new Login(this);
        else {
            // Call Login with alternate Application ID and Class name
            new Login(this, args[0], args[1]);
            disablePanel = topMenuPanel;
        }
        // By default, we only use the top menu, so disable bottom components
        // When managing alternate application, we only use bottom menu
        Component[] comps = disablePanel.getComponents();
        for (int i = 0; i < comps.length; i++) {
            comps[i].setEnabled(false);
        }
        // This static utility method centers any component on the screen
        Login.center(this);
        // Finally, to see this frame, it must be made visible
        this.setVisible(true);
    }`

在菜单中,我们将区分将要显示的两组菜单按钮。这些按钮位于两个 Java Swing 组件JPaneltopMenuPanelbottomMenuPanel上。以这种方式收集按钮允许我们通过在for循环中遍历JPanel上的所有组件并为每个组件调用setEnabled()方法来禁用其中一组按钮。

我们将按钮分成两组,因为对于由topMenuPanel表示的功能,我们将使用安全管理界面(该应用)的权限,并使用与该应用相关联的 Oracle 连接。然而,对于由bottomMenuPanel表示的函数,我们将作为一个不同的、指定的应用进行连接,并将管理与该应用相关联的连接字符串。

当双因素认证成功后,登录屏幕关闭时,OJSAdmin屏幕变得可见,因为Login实例化的线程处理已经完成,并返回到OJSAdmin.ojsInit()方法,清单 12-11 。此时,应用用户可以从菜单选项中进行选择。例如,如果用户选择了添加/修改用户按钮,事件将调用addUserButton_actionPerformed()方法(清单 12-12 ),该方法为活动new AddUser()实例化一个 JavaBean。从OJSAdmin菜单中可用的每个活动屏幕都将隐藏OJSAdmin菜单屏幕,并使其再次可见。他们可以这样做,因为我们通过引用活动屏幕的构造函数来传递菜单——注意调用中对this的引用,以实例化清单 12-12 中的new AddUser类。

清单 12-12。添加/修改用户按钮的动作方法

    private void addUserButton_actionPerformed(ActionEvent e) {         new AddUser(**this**);     }

添加/修改用户功能屏幕

在我们深入研究添加/修改用户功能屏幕的代码和操作之前,让我们先回顾一下我们拥有这个屏幕的原因。回想一下,我们在HR模式中添加了一个名为t_emp_mobile_nos的表。我们使用该表来存储手机和寻呼机号码,并且(为了方便起见)还存储员工的操作系统用户 ID。我们需要维护这些数据来完成单点登录和双因素身份验证。此外,我们使用HR.EMPLOYEES表中的电子邮件地址进行双因素认证,所以我们希望能够编辑它。

为了维护HR数据,我们想要一个类似于我们的添加/修改用户界面的功能界面,如图图 12-3 所示。这个安全管理界面不适合维护员工数据,但是为了展示我们在第七章中开发的安全查询和更新程序,我们允许在这里使用它。我们将允许查看和更新敏感的SALARYCOMMISSION_PCT列。

images

图 12-3。添加/修改用户功能屏幕

实例化 AddUser 屏幕

添加/修改用户代码可以在文件chapter 12/orajavsec/adduser . Java中找到。我们将通过检查构造函数来开始探索这个功能屏幕。清单 12-13 中显示了这两个构造函数。第一个构造函数是我们从OJSAdmin菜单中调用的。我们传递一个对OJSAdmin的引用,这样我们就可以通过调用parent.setVisible()方法来隐藏它。几乎与此同时,我们将添加/修改用户屏幕设置为可见。请注意,任何构造函数中的第一行都可能是对超类构造函数或同一个类中的另一个构造函数的调用。这里我们调用this(),这是对默认构造函数的调用(不带参数);也就是清单 12-13 中显示的第二个构造函数。

关于清单 12-13 中显示的第一个构造函数,我想指出的最后一点是,我们通过测试数据库连接是否为空来结束它。至少有几个可能的原因导致连接为空,但最重要的原因是操作系统用户帐户没有访问数据库的权限。具体来说,操作系统用户可能无权通过需要用于此功能的 Oracle 应用用户帐户进行代理。当这种情况发生时,我们不会继续下去;相反,我们会弹出一个Dialog屏幕来通知用户。在这个 GUI 单线程世界中,我们将等到用户点击按钮确认我们的消息,然后隐藏添加/修改用户屏幕并使OJSAdmin菜单再次可见。

清单 12-13。添加/修改用户屏幕构造器

`    public AddUser(JFrame parent) {
        this();
        this.parent = parent;
        // Post jbInit visual setup
        userMessageLabel.setVisible(false);
        userMessageLabel1.setVisible(false);
        ButtonGroup empGroup = new ButtonGroup();
        empGroup.add(existingEmpRadioButton);
        empGroup.add(newEmpRadioButton);
        existingEmpRadioButton.setSelected(true);
        Login.center(this);
        parent.setVisible(false);
        this.setVisible(true);
        if (null == conn) {
            JOptionPane.showMessageDialog(thisComponent,
                "Your account is not permitted to use this functional screen!");
            parent.setVisible(true);
            this.setVisible(false);
        }
    }

public AddUser() {
        try {
            jbInit();
            conn = OracleJavaSecure.getAAConnRole("orcl", "appusr");
            // Possibly reentering - need new keys for new Oracle session
            OracleJavaSecure.resetKeys();
            locModulus = OracleJavaSecure.getLocRSAPubMod();
            locExponent = OracleJavaSecure.getLocRSAPubExp();
            sessionSecretDESPassPhrase = null;
            dataInit();
        } catch (Exception e) {
            e.printStackTrace();
        }
    }`

在清单 12-13 所示的第二个构造函数中,我们做了以下四件事:

  • 调用jbInit()来初始化 GUI 组件。
  • 调用getAAConnRole()来获得一个连接,专门用于这个功能。因为我们正在执行与HR模式中的雇员数据相关的任务,所以我们将需要通过orcl实例上的appusr用户获得的hrview_role
  • 为此新连接重置加密密钥。
  • 调用dataInit()用现有值填充下拉组合框。

可能这里需要知道的最重要的事情是,每当我们获得一个新的连接时,我们都需要建立新的加密密钥,即使我们从OJSAdmin菜单多次重新进入同一个屏幕。为了确保我们自己清理干净,每当我们关闭这个窗口时,我们调用closeConnection() 。这发生在清单 12-14 中的方法中。我们还在清单 12-14 中看到了我们所描述的AddUser类使自己不可见,并在完成后使OJSAdmin菜单屏幕再次可见。请使用这种方法作为打开和关闭连接以及重置加密密钥的模板。

清单 12-14。关窗方法

    private void this_windowClosing(WindowEvent e) {         OracleJavaSecure**.closeConnection();**         parent.setVisible(true);         this.setVisible(false);     }

除了在默认构造函数中调用AddUserresetKeys()之外,我们还获得了一个新的 RSA 密钥对,将指数和模数存储在静态成员中。我们在AddUser中也有静态成员,用于 DES 秘密密码密钥的工件。我们将清单 12-13 中的工件sessionSecretDESPassPhrase设置为空。我们稍后测试这个静态成员,看看我们是否需要重新获取秘密的密码密钥。如果我们在选择之前进行更新或插入,那么我们可能需要调用一个额外的存储过程来交换键,p_get_shared_passphrase

我们应用中的每个功能屏幕都要求我们连接到一个特定的 Oracle 实例(orclapver),并且我们作为一个特定的用户进行连接。屏幕、实例、用户和角色在表 12-1 中列出。需要不同的用户,因为每个屏幕需要不同的安全权限,我们将通过基于特定应用和代理用户设置的不同 Oracle 角色获得这些权限。角色是通过我们的getAAConnRole()方法和p_check_role_access过程来设定的。该过程通过查询appsec.v_application_registry视图找到应用/代理用户和所需角色之间的关系。我们需要在orcl实例上插入清单 12-15 所示的关系数据作为appsec用户。

images

清单 12-15。将应用用户与角色相关联的脚本

`INSERT INTO appsec.v_application_registry
(application_id, app_user, app_role) VALUES
('OJSADMIN','APPUSR','HRVIEW_ROLE');

INSERT INTO appsec.v_application_registry
(application_id, app_user, app_role) VALUES
('OJSADMIN','OJSAADM','OJS_ADM_ADMIN');

INSERT INTO appsec.v_application_registry
(application_id, app_user, app_role) VALUES
('OJSADMIN','AVADMIN','APPVER_ADMIN');`

images 注意这个脚本可以在文件 chapter 12/orclappsec . SQL 中找到

在调用与清单 12-6 中的OJSAdmin应用相关联的Login类的过程中,我们将OJSADMIN指定为应用 ID。对于这个应用 ID 和特定的应用内部类ojsadmin.Login$InnerRevLvlClass,在v_app_conn_registry视图中存储了一个连接字符串列表。我们从Login的处理中得到的就是这个列表。使用列表中的连接字符串,我们可以作为各种安全管理功能所需的不同用户进行连接。当前功能“添加/修改用户”要求我们通过hrview_role连接到orcl作为appusr

AddUser中有许多代码,根据用户的选择和输入,使 GUI 像它应该的那样运行。我将把这些细节中的大部分留给您在代码中探索和发现,但是我将提到几个重要的方面。例如,如果用户正在创建一名新员工,则在保存该员工并从下拉组合框列表中选择她之前,他们不能输入 OS 用户身份的数据。在清单 12-13 的第一个构造函数中,我们添加了一个ButtonGroup,并添加了我们的两个单选按钮existingEmpRadioButtonnewEmpRadioButton作为成员。按钮组确保一次只能选择一个单选按钮。我们还确保在创建新员工和/或用户时,之前显示在屏幕上的个人数据会被删除。当用户在 GUI 界面上点击时,跟踪什么是可用的,什么是无效的,这使得程序员变成了一个“会计”。

初始化数据选择组件

在默认构造函数中,如清单 12-13 中的第二个所示,在获得一个新的 Oracle 连接并重置我们的加密密钥后,我们调用一个方法来填充下拉组合框列表dataInit()。清单 12-16 中的dataInit()方法有几个针对HR模式中非敏感视图的静态 Oracle 查询。虽然数据是非敏感的,但是我们没有授予PUBLIC访问数据的权限;相反,我们授予对hrview_role的访问权,我们在这个添加/修改用户功能屏幕中获取并使用它。applicationIDOJSADMIN,应用用户是appusr,所以根据表 12-1 和清单 12-15 ,我们从v_application_registry获取的对应角色是hrview_role

使用这个 Oracle 角色,我们可以从公共视图中选择雇员列表,hr.v_employees_public。我们在第二章的 Chapter2/HR.sql 脚本文件中创建了这个视图。我们按照雇员的姓氏对列表进行排序,并将姓氏、逗号、名字和括号中的employee_id连接起来。我们在dataInit()中做类似的查询来填充工作 ID、部门 ID 和经理下拉组合列表。注意清单 12-16 中的注意,在我们通过while(rs.next())循环处理结果之前,我们首先删除列表中所有现有的项目,并添加一个空白字符串作为第一个项目。在每次更新后,我们可能会重复调用dataInit(),因为值列表可能会因为我们的更新而发生变化。因此,我们在初始化期间清除旧的清单并添加新的清单。我们添加空白字符串作为每个列表中的第一项,作为默认选择。

清单 12-16。 AddUser 数据初始化方法

    private void dataInit() throws Exception {         Statement stmt = null;         ResultSet rs = null;         try {             stmt = conn.createStatement();             rs = stmt.executeQuery("SELECT last_name || ', ' || first_name || " +                 "' (' || employee_id || ')' " +                 "**FROM hr.v_employees_public** ORDER BY last_name");             // This throws event to run existingEmpComboBox_actionPerformed() method             // Calls blankAll()             existingEmpComboBox**.removeAllItems();**             existingEmpComboBox**.addItem("");**             **while (rs.next())** {                 existingEmpComboBox**.addItem(rs.getString(1));**             }             if (rs != null)                 rs.close(); ...

existingEmpComboBox中的列表成为我们确定应用用户想要查看或编辑哪个员工的来源。当应用用户选择一个雇员时,我们需要在HR模式中找到相应的数据,我们希望通过employee_id找到该数据。计划是将雇员 ID 从existingEmpComboBox中所选项的括号中取出,因此我们将创建一个名为pullIDFromParens()、清单 12-17 的静态实用程序方法,我们将把它放在一个名为 utility 的类中。我们将在这个应用的几个地方使用它,所以我们希望它位于中央,而不是作为几个功能屏幕的成员方法重复。

清单 12-17。从数据值中获取数据索引

    static String pullIDFromParens(String inValue) {         String rtrnValue = "";         try {             int openPlace = inValue.indexOf("(");             int closePlace = inValue.indexOf(")", openPlace);             if (openPlace > -1 && closePlace > -1)                 rtrnValue = inValue.substring(openPlace + 1, closePlace);         } catch (Exception x) {         }         return rtrnValue;     }

在我们的dataInit()方法中,我们对使用静态 Oracle 查询而不是存储过程来获取雇员列表和其他列表很有信心(参见清单 12-16 ),因为没有应用用户提供的参数,所以没有 SQL 注入的机会,并且数据是非敏感的,尽管仍然受到角色授权的保护(不是PUBLIC)。这并不违背我们所说的使用存储过程进行查询;这是对该方法的补充。同样,只要查询是静态的,没有应用用户提供的参数,直接从视图查询数据是安全的。

为了让hrview_roleHR.v_employees_public视图中查询数据,我们需要向hrview_role授予SELECT特权。我们以HR用户的身份授予该权限,同时授予从清单 12-18 中的HR.v_sms_carrier_host中进行选择的权限。在dataInit()中,我们还填充了工作 ID、部门 ID、经理和 SMS 运营商下拉框的内容,因此我们需要从这两个视图中进行选择。

清单 12-18。授权进一步选择人力资源观点

GRANT SELECT ON hr.v_employees_public TO hrview_role; GRANT SELECT ON hr.v_sms_carrier_host TO hrview_role;

images 注意这些授权的脚本位于名为 的文件 Chapter12 /OrclHR.sql 中。

选择现有员工

AddUser屏幕初始化后,应用用户可以选择输入数据并添加一名新员工,或者从组合框列表中选择一名现有员工。当选择一个现有的雇员时,调用existingEmpComboBox_actionPerformed()方法。我们再次要求应用用户有耐心,所以我们让sayWaitDialog可见——见清单 12-19 底部的。我们在清单 12-19 中展示了existingEmpComboBox_actionPerformed()方法的结构,但是我们延迟的Thread的大部分run()方法将在后面的清单中展示。

清单 12-19。选择现有员工的方法

`private void existingEmpComboBox_actionPerformed(ActionEvent e) {
        // When action from dataInit() at removeAllItems(), getItemCount() = 0
        if (0 == existingEmpComboBox.getItemCount() ||
            0 == existingEmpComboBox.getSelectedIndex()) {
            osUserIDTextField.setEnabled(false);
            blankAll();
            return;
        }

**        employeeID = Integer.parseInt(Utility.pullIDFromParens(**
**            (String)existingEmpComboBox.getSelectedItem()));**

blankAll();
        SwingUtilities.invokeLater(new Runnable() {
                public void run() {

// The bulk of the run() method has been removed from this Listing

Login.sayWaitDialog.setVisible(false);                 }
            });
        // It may take a while to get the user data, esp while we set up encryption
        // So ask the user to be patient while working
        Login.sayWaitDialog.setVisible(true);
    }`

这个方法,existingEmpComboBox_actionPerformed()通过使用Utility.pullIdFromParens()方法从所选择的项目中获取雇员 ID。在我们显示主导当前 GUI Thread进程的模态sayWaitDialog之前,我们创建一个新的Runnable Thread并将其传递给SwingUtilities.invokeLater()方法。这个延迟的Thread将用所选员工的数据填充AddUser屏幕。

在我们的延迟线程run()方法中,我们调用两个 Oracle 存储过程,以缩写形式显示在清单 12-20 和清单 12-22 中。第一个程序是来自第七章、p_select_employee_by_id_sens的一个标准加密数据查询。该过程完成了密钥交换(如果还没有完成的话),并为所选的员工 id 从HR.EMPLOYEES表中选择数据。salarycommission_pct值以加密形式返回。我们调用OracleJavaSecure.getDecryptData()来解密这些值,然后用我们检索到的值填充AddUsers上的每个字段。

如果我们对p_select_employee_by_id_sens的调用有错误,我们会显示一个错误消息对话框,这是一个模态对话框。如果没有错误,那么我们将从 Oracle 数据库中获得共享密码(DES)密钥的组成部分。我们对OracleJavaSecure.getDecryptData()的第一次调用将会产生副作用,即构建一个等效的 DES 密钥供客户端使用。

清单 12-20。选择现有员工时的缩写 run()方法,第一部分

`public void run() {
        int errNo;
        String errMsg;
        OracleCallableStatement stmt = null;
        OracleResultSet rs = null;
        try {
            stmt =
                (OracleCallableStatement)conn.prepareCall(
                "CALL hr.hr_sec_pkg.p_select_employee_by_id_sens(?,?,?,?,?,?,?,?,?,?)");
...
            stmt.setInt(10, employeeID);
            stmt.executeUpdate();

errNo = stmt.getInt(8);
            if (errNo != 0) {
                errMsg = stmt.getString(9);
                JOptionPane.showMessageDialog(thisComponent,
                   "Oracle error p_select_employee_by_id_sens) " + errNo + ", " + errMsg);
            } else {
                sessionSecretDESSalt = stmt.getRAW(3);
...
                rs = (OracleResultSet)stmt.getCursor(7);
                // Should be only one record for this Employee ID
                if (rs.next()) {
                    firstNameTextField.setText(rs.getString(2));
...
                    // Our stored procedure passes Hire Date back as sql.Date                     // So process here to format
                    java.sql.Date sDate = rs.getDate(6);
                    Date hDate = new Date(sDate.getTime());
                    hireDateTextField.setText(hDateFormat.format(hDate));
                    jobIDComboBox.setSelectedItem(rs.getString(7));
                    deptIDComboBox.setSelectedItem(rs.getString(11));
                    // Find this user's manager id in parentheses of combo box
                    for (int i = 0; i < managerIDComboBox.getItemCount(); i++) {
                        if (rs.getString(10).equals(Utility.pullIDFromParens(
                            (String)managerIDComboBox.getItemAt(i))))
                        {
                            managerIDComboBox.setSelectedIndex(i);
                            break;
                        }
                    }
                    // Decrypt salary and commission pct using shared password key
                    salaryTextField.setText(OracleJavaSecure.getDecryptData(rs.getRAW(8),
                        sessionSecretDESPassPhrase, sessionSecretDESAlgorithm,
                        sessionSecretDESSalt, sessionSecretDESIterationCount));

**                    commissionPctTextField.setText(**

**                        OracleJavaSecure.getDecryptData(rs.getRAW**(9),

sessionSecretDESPassPhrase, sessionSecretDESAlgorithm,
                        sessionSecretDESSalt, sessionSecretDESIterationCount));
                }
                if (rs != null) rs.close();
                if (stmt != null) stmt.close();
            }`

你可以在清单 12-20 中看到我们如何用特定的数据元素填充AddUser表单。我们只需在表单上设置文本字段的文本。对于日期字段,我们尽力将java.sql.Date转换成java.util.Date,并且我们使用静态hDateFormat成员来格式化日期,其定义如下:

    static SimpleDateFormat hDateFormat = new SimpleDateFormat("MM/dd/yy");

对于我们的下拉组合框,例如jobIDComboBox,我们在现有列表中选择与我们的查询值相对应的项目。在managerIDComboBox的特殊情况下,我们遍历现有列表中的所有项目,并调用pullIDFromParens()方法来获取经理的员工 ID。如果它与我们从查询中获得的经理 ID 相匹配,我们将该经理设置为我们选择的项目。此外,您可以看到,我们将salaryTextFieldcommissionPctTextField的值设置为我们通过解密从查询中收到的数据得到的值。

我们还用来自用户手机号码视图hr.v_emp_mobile_nos的数据填充了AddUser屏幕下半部分的用户数据。为此,我们在一个包中调用一个新的过程,这个包是我们在HR模式中定义的,hr_pub_pkg.p_select_emp_mobile_nos_by_id。创建这个包的脚本如清单 12-21 所示。此时,您应该对这个脚本非常熟悉。注意这里定义了两个过程,一个是我们已经提到过的 select 过程,另一个是我们将用来更新v_emp_mobile_nos中数据的过程,名为p_update_emp_mobile_nos。我们授权hrview_role执行这个包。

清单 12-21。创建人力资源公共包的脚本

`CREATE OR REPLACE PACKAGE BODY hr.hr_pub_pkg IS

PROCEDURE p_select_emp_mobile_nos_by_id(
        m_employee_id             emp_mobile_nos.employee_id%TYPE,
        resultset_out         OUT RESULTSET_TYPE,
        m_err_no              OUT NUMBER,
        m_err_txt             OUT VARCHAR2)
    IS BEGIN
        m_err_no := 0;
        OPEN resultset_out FOR SELECT
            user_id, com_pager_no, sms_phone_no, sms_carrier_cd
        FROM v_emp_mobile_nos
        WHERE employee_id = m_employee_id;
    EXCEPTION
        WHEN OTHERS THEN
            m_err_no := SQLCODE;
            m_err_txt := SQLERRM;
            appsec.app_sec_pkg.p_log_error( m_err_no, m_err_txt,
                'HR p_select_emp_mobile_nos_by_id' );
    END p_select_emp_mobile_nos_by_id;

PROCEDURE p_update_emp_mobile_nos(
        m_employee_id        emp_mobile_nos.employee_id%TYPE,
        m_user_id            emp_mobile_nos.user_id%TYPE,
        m_com_pager_no       emp_mobile_nos.com_pager_no%TYPE,
        m_sms_phone_no       emp_mobile_nos.sms_phone_no%TYPE,
        m_sms_carrier_cd     emp_mobile_nos.sms_carrier_cd%TYPE,
        m_err_no         OUT NUMBER,
        m_err_txt        OUT VARCHAR2 )
    IS
        test_emp_ct      NUMBER(6);
    BEGIN
        -- Note: Use of this procedure assumes you have already done a select
        -- and that you are using the same Session Secret PassPhrase
        m_err_no := 0;
        SELECT COUNT(*) INTO test_emp_ct FROM v_emp_mobile_nos WHERE
            employee_id = m_employee_id;
        IF test_emp_ct = 0
        THEN
            INSERT INTO v_emp_mobile_nos
                (employee_id, user_id, com_pager_no, sms_phone_no, sms_carrier_cd)
            VALUES
                (m_employee_id, m_user_id, m_com_pager_no, m_sms_phone_no,
                m_sms_carrier_cd);
        ELSE
            UPDATE v_emp_mobile_nos
            SET user_id = m_user_id, com_pager_no = m_com_pager_no,
                sms_phone_no = m_sms_phone_no,
                sms_carrier_cd = m_sms_carrier_cd             WHERE employee_id = m_employee_id;
        END IF;
    EXCEPTION
        WHEN OTHERS THEN
            m_err_no := SQLCODE;
            m_err_txt := SQLERRM;
            appsec.app_sec_pkg.p_log_error( m_err_no, m_err_txt,
                'HR p_update_emp_mobile_nos' );
    END p_update_emp_mobile_nos;

END hr_pub_pkg;
/

GRANT EXECUTE ON hr.hr_pub_pkg TO hrview_role;`

从新的hr_pub_pkg.p_select_emp_mobile_nos_by_id程序中选择数据的过程与我们之前回顾的从v_employees_public视图中选择数据的过程非常相似。然而,对于来自v_emp_mobile_nos的数据,没有敏感数据,所以没有一列以加密形式返回。这个程序也从AddUserexistingEmpComboBox_actionPerformed()方法的延迟Thread run()方法中调用。该调用如清单 12-22 中的所示。这里唯一独特的是,当我们从这个查询填充它时,我们禁用了osUserIDTextField。我们这样做是因为一旦为员工设置了用户 ID,我们就不允许更改。禁用此字段允许显示值,尽管以灰色形式显示,但不允许编辑数据值。如果这个查询没有在v_emp_mobile_nos中找到雇员 ID 的值,那么我们启用osUserIDTextField

清单 12-22。选择现有员工时的缩写 run()方法,第二部分

    // Select from mobile_nos where emp id     stmt = (OracleCallableStatement)conn.prepareCall(         "CALL **hr.hr_pub_pkg.p_select_emp_mobile_nos_by_id**(?,?,?,?)"); ...     stmt.setInt(1, employeeID); ...         if (rs.next()) {             // Will not let you change a user ID for an employee             **osUserIDTextField.setEnabled(false);**             osUserIDTextField.setText(rs.getString(1));             pagerNumberTextField.setText(rs.getString(2));             smsPhoneNumberTextField.setText(rs.getString(3));             smsCarrierCodeComboBox.setSelectedItem(rs.getString(4));         } else {             **osUserIDTextField.setEnabled(true);**         }

这两个 Oracle 存储过程(如清单 12-20 和清单 12-22 所示)表示动态查询,其中查询包含了对employee_id = user_selected_employee_id的需求。像这样的动态查询容易受到 SQL 注入的影响,除非它们被封装在像这样的 Oracle 存储过程中。这是一个安全考虑的设计,应该在每个安全代码审查中加以考虑。

创建新员工

我们可以从添加/修改用户功能屏幕创建新员工。在这种情况下,将分配一个新的员工 ID。在创建员工之前,我们不允许应用用户输入用户 ID。这样,这是一个两步过程。创建员工后,我们可以从existingEmpComboBox中选择他,并为他输入用户和手机号码数据。

保存员工数据

一旦应用用户输入了她希望为所选员工或新员工保存的数据,她将选择 save 按钮。该动作调用saveButton_actionPerformed()方法。我们在清单 12-23 中展示了该方法的结构。在这个清单中,我们忽略了延迟线程run()方法的大部分,稍后将会介绍。我们测试 AddUser 表单上输入的值,以确保所有必填字段都输入了值。在我们测试它们之后,我们再次让模态sayWaitDialog可见。同时,我们定义另一个延迟线程来完成我们的工作。在延迟线程run()方法的末尾,我们设置了sayWaitDialog不可见。由于我已经多次描述了延迟的ThreadsayWaitDialog的使用,我将不再提及它们;但是,我们将在整个应用中继续使用这些功能。

清单 12-23。保存员工和用户数据

    private void saveButton_actionPerformed(ActionEvent e) {         if (lastNameTextField.getText().equals("") ||             jobIDComboBox.getSelectedIndex() == 0 ||             eMailNameTextField.getText().equals("") ||             managerIDComboBox.getSelectedIndex() == 0 ||             deptIDComboBox.getSelectedIndex() == 0) {             JOptionPane.showMessageDialog(thisComponent,                 **"Must have values** for Last Name, Job ID, E-Mail, Dept ID and Mgr ID!");             return;         }         if (existingEmpComboBox.getSelectedIndex() > 0 &&             osUserIDTextField.getText().equals("") &&             (!(pagerNumberTextField.getText().equals("") &&                smsPhoneNumberTextField.getText().equals("")))) {             JOptionPane.showMessageDialog(thisComponent,                 **"Must have value** for User ID, else blank mobile nos!");             return;         }         SwingUtilities.invokeLater(new Runnable() {             public void run() { ...                 **Login.sayWaitDialog.setVisible(false);**             }         });         // Ask the user to be patient while working         **Login.sayWaitDialog.setVisible(true);**     }

saveButton_actionPerformed()方法中定义的延迟线程执行我们的 Oracle 更新处理。在这种情况下,真正的处理发生在两个 update Oracle 存储过程中。第一个是我们的加密敏感数据更新程序p_update_employees_sensitive,来自第七章。第二个是hr_public_pkg包中一个新的更新过程,用于hr.v_emp_mobile_nos视图中的数据,如清单 12-21 所示的p_update_emp_mobile_nos

在清单 12-24 中,我们看到了用于调用p_update_employees_sensitive过程的代码。我们传递当前雇员 ID 来指示我们正在更新哪个雇员。注意,我们将参数 8 和 9 设置为 salary 和 commission percent 字段的加密值。我们还获取为雇佣日期输入的格式化日期,并使用hDateFormat.parse()方法将其解析为java.util.Date,然后将其转换为java.sql.Date以传递给 Oracle 存储过程进行更新。如果雇用日期字段中不存在条目,我们将当前日期提交给该过程。我们在AddUser实施的用户界面保护措施之一是,应用用户不能编辑现有员工的雇佣日期。

images 注意这个过程从雇佣日期中去除了时间的重要性。我们这样做只是为了简单起见。

清单 12-24。更新员工数据

    stmt = (OracleCallableStatement)conn.prepareCall(     "CALL hr.hr_sec_pkg**.p_update_employees_sensitive**(?,?,?,?,?,?,?,?,?,?,?,?,?)");     stmt.registerOutParameter(12, OracleTypes.NUMBER);     stmt.registerOutParameter(13, OracleTypes.VARCHAR);     **stmt.setInt(1, employeeID);** ...     if (hireDateTextField.getText().equals("")) {         stmt.setDate(6, new java.sql.Date((new Date()).getTime()));     } else {         Date hDate = hDateFormat.parse(hireDateTextField.getText());         stmt.setDate(6, new java.sql.Date(hDate.getTime()));     }     stmt.setString(7, (String)jobIDComboBox.getSelectedItem());     stmt.setRAW(8, **OracleJavaSecure.getCryptData(salaryTextField.**getText()));     stmt.setRAW(9, **OracleJavaSecure.getCryptData(commissionPctTextField.**getText())); ...     stmt.executeUpdate();

如果我们已经选择了一个现有的雇员并且如果osUserIDTextField不为空,我们只调用清单 12-25 中的p_update_emp_mobile_nos过程。我们在这里不做任何进一步的数据验证,但是您可能希望这样做。

images 注意hr.v_emp_mobile_nos视图中添加一个条目并不等同于使用我们在视图中输入的用户 ID 创建一个 Oracle 用户。但是,我们需要创建 Oracle 用户,以便该用户能够访问我们的应用。作为一种必然的情况,我们可以删除一个 Oracle 用户,但是hr.v_amp_mobile_nos中的条目不会被自动删除。

清单 12-25。更新用户和手机号码

   if (existingEmpComboBox.getSelectedIndex() > 0 &&        (!osUserIDTextField.getText().equals(""))) {        stmt = (OracleCallableStatement)conn.prepareCall(        "CALL hr.hr_pub_pkg.p_update_emp_mobile_nos(?,?,?,?,?,?,?)"); ...

如果我们要创建一个新员工,而之前没有选择现有员工,那么我们还没有交换加密密钥。如果是这种情况,那么sessionSecretDESPassPhrase成员(以及其他成员)将为空。我们测试sessionSecretDESPassPhrase是否为空,如果为空,我们从第七章中调用一个额外的 Oracle 存储过程p_get_shared_passphrase,在调用数据更新过程之前交换密钥。

Oracle 用户是什么时候创建的?

到目前为止,我们还没有创建与我们在添加/修改用户功能屏幕上输入的用户 ID 相对应的 Oracle 用户。对于HR.EMPLOYEES表中的每个雇员,我们可以在hr.v_emp_mobile_nos中创建一个包含用户 ID 的记录。用户 ID 与 Windows(操作系统)帐户的名称相同。我们将创建一个具有相同名称的 Oracle 用户,并且我们将利用具有相同名称的事实来进行单点登录。Oracle 用户将被授予访问权限,因为该用户已经以相同的操作系统用户身份登录。

您现在可能想知道为什么我们从来没有从我们的添加/修改用户功能屏幕运行一个过程来创建 Oracle 用户。这不是疏忽,而仅仅是拖延到最后一分钟。如果 Oracle 用户无权访问任何应用,我们就没有理由创建他,因此我们将只在第一次授予他访问应用的权限时创建该用户。这将发生在管理用户或分配应用功能屏幕中,如下所述。

用户管理屏幕

在安全管理界面应用中,我们的每个功能屏幕都有许多相似之处。在查看添加/修改用户功能屏幕时,我们已经讨论了其中的许多功能,我们不再重复这些细节。然而,每个画面都引入了许多新的想法,给我们提供了思考的食粮。管理用户功能屏幕是我们当前讨论的主题。

在图 12-4 所示的管理用户界面中,我们有一个下拉组合框,从中可以找到所有用户的列表userComboBox。这不是所有雇员的列表,而是在hr.v_emp_mobile_nos视图中有记录的所有雇员的列表。对于每个用户,我们从hr.v_emp_mobile_nos获得用户 ID,从hr.v_employees_public获得名字和姓氏。我们在AdminUsers类的dataInit()方法中实现了这一点,该过程与我们在AddUserdataInit()方法中所做的相同,如前所述。

images

图 12-4。管理员用户功能屏幕

同样在 Admin Users 屏幕上,您会看到三个复选框:每个管理代理用户一个:appusrojsaadmavadmin。当从下拉列表中选择一个用户时,这些复选框将填充与该用户的当前代理授权相对应的复选标记或空白。通过选中或取消选中这些复选框并单击“保存更新”按钮,应用用户可以授予和撤销所选用户的这些权限。

管理员用户屏幕上还有一个按钮,撤销用户访问按钮。此按钮的功能不包括删除 Oracle 用户帐户;然而,它撤销了用户拥有的所有应用授权,包括通过appver授予代理的授权——因此用户实际上不能再使用我们的应用。

创建 OJSAAdm 用户

在管理用户功能屏幕中,我们作为ojsaadm用户连接到orcl实例。这是我们在表 12-1 中列出的代理用户之一。我们需要创建那个 Oracle 用户,我们需要授予他访问填充userComboBox所需的HR视图的权限。我们将作为安全管理员,secadm用户,在orcl实例上这样做。我们将要使用的命令如清单 12-26 所示。当您执行这些命令时,用您想让其管理管理员列表的人的操作系统用户名替换占位符osadmin,并给新的ojsaadm用户一个强密码。

清单 12-26。创建 OJSAAdm 用户

`GRANT create_session_role TO ojsaadm IDENTIFIED BY password;
CREATE ROLE ojs_adm_admin IDENTIFIED USING appsec.p_check_role_access;
ALTER USER osadmin GRANT CONNECT THROUGH ojsaadm;

GRANT SELECT ON hr.v_employees_public TO ojs_adm_admin;
GRANT SELECT ON hr.v_emp_mobile_nos TO ojs_adm_admin;

GRANT CREATE DATABASE LINK TO ojsaadm;
GRANT CREATE VIEW TO ojsaadm;
GRANT CREATE PROCEDURE TO ojsaadm;`

images 注意这些命令列在文件chapter 12/orclsecadm . SQL中。

除了授予我们的新用户从HR视图中选择来填充用户列表的权限之外,您可以在清单 12-26 中看到,我们正在给这个用户一些额外的权限。我们正在授予ojsaadm创建数据库链接、视图和过程的系统特权。我们将让这个 Oracle 用户帐户发挥作用。事实上,我们正在招募这个 Oracle 用户通过从orclapver实例的数据库链接来完成双方的用户管理。也就是说,我们需要在apver实例上创建一个相同的用户。清单 12-27 显示了我们将用来完成这项工作的脚本。像往常一样,我们作为SYS用户进行连接,以完成我们在apver实例上的大部分工作。

清单 12-27。在 apver 实例上创建 OJSAAdm 用户

GRANT create_session_role TO ojsaadm IDENTIFIED BY password; GRANT SELECT ON sys.proxy_users TO ojsaadm;

images 清单 12-27 中部分显示的脚本包含在 Chapter12/ApverSys.sql文件中。

从我们对 Admin Users 屏幕功能的描述中,您可以想象这个用户需要从 Oracle 数据库中读取一些不寻常的数据来完成他的任务。ojsaadm需要做的一件事是从数据字典视图sys.proxy_users中读取数据。从这个角度来看,我们将观察从 Oracle 用户到应用角色的访问途径,SSO 通过 Oracle 用户匹配我们的 OS 用户,应用角色与充当代理的 Oracle 用户保持一致。通过添加或删除通过我们的应用用户之一进行代理的能力,个人用户能够或不能够访问应用。

并非任何 Oracle 用户都可以修改sys.proxy_users视图中的数据。在清单 12-27 中,您可以看到我们将视图上的SELECT授予了apver实例上的ojsaadm用户。我们将在orcl实例上对ojsaadm做出同样的授权。仅仅拥有那个视图的特权是不足以完成我们的计划的。要授予和撤销代理特权,我们需要ALTER USER特权。此外,正如我在添加/修改用户界面的讨论结束时所描述的,我们需要能够在授予他们第一个应用代理时CREATE USER。为此,我们将在SYS模式中创建一个名为usr_role_adm_pkg的包,其中包含创建用户、授予和撤销代理特权的过程。在orcl实例上,创建这个包的脚本如清单 12-28 所示。将在apver实例上创建一个相同的包。

在清单 12-28 中的usr_role_adm_pkg包定义中,你可以看到定义了五个程序。创建一个用户需要两个步骤,p_create_user_oncep_create_user_many。我们将该过程分为两个步骤,因为第一个步骤,即我们实际创建 Oracle 用户的步骤,如果用户已经存在,将会遇到一个异常。第二个步骤p_create_user_many也是创建 Oracle 用户帐户以访问应用的过程的一部分。在这个过程中,我们将create_session_role授予用户,并授予他通过appver用户进行代理的权限。该代理是进行 SSO 和双因素身份验证所必需的。即使 Oracle 用户已经存在并使用密码创建,他也不能使用我们的应用,除非他能够通过appver进行代理。请注意,当我们严格为应用访问创建新用户时,我们不会给他们一个密码;相反,我们指示它们是IDENTIFIED EXTERNALLY。通常,意味着我们使用操作系统或命名服务来认证用户,但是在我们的例子中,我们认证我们的一个大应用用户(appver),并允许这个IDENTIFIED EXTERNALLY用户通过代理。不会对 Oracle 用户个人进行身份验证。

还有一个撤销用户对我们的应用的访问的过程。这个名字有点夸张,因为我们实际上并没有删除 Oracle 用户帐户。除了访问我们的应用之外,该帐户可能出于某种原因而存在。它甚至可能有一个相关的密码,并允许用户在自己的模式中使用 Oracle 数据库。我们真正想要实现的是阻止用户访问我们的应用,我们可以通过简单地撤销通过appver用户进行代理的特权来实现。这就是p_drop_user程序所做的一切。

清单 12-28。创建 sys.usr_role_adm_pkg 包

`CREATE OR REPLACE PACKAGE BODY sys.usr_role_adm_pkg IS

PROCEDURE p_create_user_once( username sys.proxy_users.client%TYPE )
    AS
        PRAGMA AUTONOMOUS_TRANSACTION;
    BEGIN
        EXECUTE IMMEDIATE 'CREATE USER ' || username || ' IDENTIFIED EXTERNALLY';
        COMMIT;
    EXCEPTION
        WHEN OTHERS
        THEN
            appsec.app_sec_pkg.p_log_error( SQLCODE, SQLERRM,
                'sys.usr_role_adm_pkg.p_create_user_once for ' || username );
    END p_create_user_once;

PROCEDURE p_create_user_many( username sys.proxy_users.client%TYPE )
    AS
        PRAGMA AUTONOMOUS_TRANSACTION;
    BEGIN
        EXECUTE IMMEDIATE 'GRANT create_session_role TO ' || username;
        EXECUTE IMMEDIATE 'ALTER USER ' || username || ' GRANT CONNECT THROUGH appver';
        COMMIT;
    EXCEPTION
        WHEN OTHERS
        THEN
            appsec.app_sec_pkg.p_log_error( SQLCODE, SQLERRM,
                'sys.usr_role_adm_pkg.p_create_user_many for ' || username );
    END p_create_user_many;

PROCEDURE p_drop_user( username sys.proxy_users.client%TYPE )
    AS
        PRAGMA AUTONOMOUS_TRANSACTION;
    BEGIN
        EXECUTE IMMEDIATE 'ALTER USER ' || username || ' REVOKE CONNECT THROUGH appver';
        COMMIT;
    EXCEPTION
        WHEN OTHERS
        THEN
            appsec.app_sec_pkg.p_log_error( SQLCODE, SQLERRM,                 'sys.usr_role_adm_pkg.p_drop_user for ' || username );
    END p_drop_user;

PROCEDURE p_set_proxy_through(
        username sys.proxy_users.client%TYPE,
        proxyname sys.proxy_users.proxy%TYPE )
    AS
        PRAGMA AUTONOMOUS_TRANSACTION;
    BEGIN
        EXECUTE IMMEDIATE 'ALTER USER ' || username ||
            ' GRANT CONNECT THROUGH ' || proxyname;
        COMMIT;
    EXCEPTION
        WHEN OTHERS
        THEN
            appsec.app_sec_pkg.p_log_error( SQLCODE, SQLERRM,
                'sys.usr_role_adm_pkg.p_set_proxy_through for ' ||
                username || '/' || proxyname );
    END p_set_proxy_through;

PROCEDURE p_drop_proxy_through(
        username sys.proxy_users.client%TYPE,
        proxyname sys.proxy_users.proxy%TYPE )
    AS
        PRAGMA AUTONOMOUS_TRANSACTION;
    BEGIN
        EXECUTE IMMEDIATE 'ALTER USER ' || username ||
            ' REVOKE CONNECT THROUGH ' || proxyname;
        COMMIT;
    EXCEPTION
        WHEN OTHERS
        THEN
            appsec.app_sec_pkg.p_log_error( SQLCODE, SQLERRM,
                'sys.usr_role_adm_pkg.p_drop_proxy_through for ' ||
                username || '/' || proxyname );
    END p_drop_proxy_through;

END usr_role_adm_pkg;
/

GRANT EXECUTE ON sys.usr_role_adm_pkg TO ojs_adm_admin;`

为了授予和撤销访问特定应用的权限,我们需要向用于该应用的特定应用代理用户ALTER USERGRANTREVOKE CONNECT THROUGH执行命令。这些步骤由p_set_proxy_throughp_drop_proxy_through程序完成。

在每个过程中,我们连接一个 SQL 命令来表示我们想要完成的任务,并调用EXECUTE IMMEDIATE,这对于执行动态数据定义语言(DDL)语句来说非常方便,比如CREATE USERALTER USER。所有这些程序都用修饰符PRAGMA AUTONOMOUS_TRANSACTION定义。这允许每个过程提交自己的事务。

问题来了,“为什么我们不把系统特权授予CREATE USERALTER USERojsaadm?”首先,我们仍然希望将这些命令包装在过程中,以控制参数并避免 SQL 注入攻击。第二,我们想限制ojsaadm实际能做的事情。注意,在这些过程中,ojsaadm只能创建IDENTIFIED EXTERNALLY用户。除此之外,他只能授予和撤销通过其他用户进行代理的权限——这比授予ALTER USER提供的权限要有限得多。

我提到了在apver实例上创建一个相同的包。该脚本在文件chapter 12/apversys . SQL中。在这方面,两个实例有一个不同之处。在orcl实例上,我们将usr_role_adm_pkg上的执行授权给ojs_adm_admin角色;而在apver实例上,我们将它直接授予ojsaadm用户。现在,我们面临的挑战是如何同时在两个数据库实例上执行命令。我们将通过数据库链接来实现这一点。

启用跨数据库链接的 OJSAAdm 用户

作为ojsaadm用户连接到orcl实例。在那里,您可以测试对sys.proxy_users数据字典视图的访问。该命令首先显示在清单 12-29 的中。清单 12-29 的第二个命令是创建数据库链接的命令。将密码占位符更改为您在apver实例上创建ojsaadm时使用的密码。

images 注意花点时间确保apver实例上的ojsaadm的密码非常复杂。我们通过在apver实例上创建另一个用户,在我们的边界上开辟了另一条攻击途径。有了非常强的密码(例如 15 个随机字符,数字和特殊字符的混合大小写),我们可以加强我们的防御。这是一个将在数据库链接中使用的密码,但很少输入。我们更愿意禁用ojsaadm的密码,或者创建一个拒绝他访问的登录触发器,但是为了使用我们的数据库链接,ojsaadm需要能够登录到apver实例。

清单 12-29。作为 OJSAAdm 用户创建链接

`SELECT * FROM sys.proxy_users;

-- Private database link to apver as ojsaadm
CREATE DATABASE LINK apver_link
CONNECT TO ojsaadm
IDENTIFIED BY password
USING 'apver';

CREATE OR REPLACE VIEW ojsaadm.instance_proxy_users AS
SELECT 'APVER' INSTANCE, proxy, client FROM sys.proxy_users@apver_link
UNION SELECT 'ORCL' INSTANCE, proxy, client FROM sys.proxy_users;

-- Test the link and view
SELECT * FROM ojsaadm.instance_proxy_users;

GRANT SELECT ON ojsaadm.instance_proxy_users TO ojs_adm_admin; -- To let appsec read view across database link
GRANT SELECT ON ojsaadm.instance_proxy_users TO appsec;`

清单 12-29 中的第三个命令创建了一个视图,它将把来自orclapver实例的sys.proxy_users上的选择结果集合在一起。这个视图是两个查询结果的UNION,每个查询来自一个实例,并且包括一个前缀列,指示在其上观察代理的实例。通过从中选择来测试新视图——这也将是对apver上授予ojsaadmsys.proxy_users中选择的测试。我们将向通过ojsaadm代理并获得ojs_adm_admin角色的所有用户授予对该视图的 select 权限。

清单 12-29 中的最后一个命令提供了另一种访问途径。您看,有时我们需要能够从运行在apver实例上的过程中看到orcl实例上的所有代理用户。在apver上,appsec用户已经有了一个到orcl实例的链接。该链接最初是为了应用安全而设置的,appsec用户可以通过选择orcl上的hr.v_emp_mobile_nos进行双因素身份验证。这里,我们将私有数据库链接的功能从apver扩展到orcl,允许它从新视图中选择所有代理用户。这也使我们不必将sys.proxy_users上的SELECT授予apver实例上的appsec;她将通过选择orcl上的视图从apver上的proxy_users中进行选择,该视图通过链接从proxy_users选择到apver。如果这听起来很循环,那是因为它确实如此。

正如我们所观察到的,从sys.proxy_users中选择不符合我们管理代理授权计划的目标。在 Admin Users 屏幕上,我们希望能够在orclapver实例上通过应用用户授予和撤销代理权限。如果可能的话,我们还希望通过单个 Oracle 连接来实现这一点。有可能通过数据库链接做到这一点吗?的确如此。在清单 12-30 中,我们看到了用于这个目的的包的定义ojsaadm.apver_usr_adm_pkg。我们将在ojsaadm模式中创建包,包将通过私有数据库链接apver_link访问,以在apver实例上执行过程。

请注意清单 12-30 中的,我们用一个参数化的EXECUTE IMMEDIATE语句跨链接调用过程。我们的 PL/SQL 风格调用是在m_stmt字符串中定义的。对于变量(例如用户名),我们不将它与语句字符串连接起来;相反,我们有一个占位符,:1。我们从一个EXECUTE IMMEDIATE命令调用该语句字符串,并通过指定USING username传递用户名来填充占位符。这是另一种形式的参数化语句,它也像 Java 中的存储过程和准备语句一样不受 SQL 注入的影响,我们在第七章的“避免 SQL 注入”一节中讨论过。

清单 12-30。跨链路执行程序的包定义

`CREATE OR REPLACE PACKAGE BODY ojsaadm.apver_usr_adm_pkg IS

PROCEDURE p_create_apver_user( username VARCHAR2 )
    AS
        m_stmt VARCHAR2(100);
    BEGIN
        m_stmt := 'BEGIN sys.usr_role_adm_pkg.p_create_user_once@apver_link( :1 ); END;';
        EXECUTE IMMEDIATE m_stmt USING username;
        m_stmt := 'BEGIN sys.usr_role_adm_pkg.p_create_user_many@apver_link( :1 ); END;';
        EXECUTE IMMEDIATE m_stmt USING username;
    END p_create_apver_user;

PROCEDURE p_drop_apver_user( username VARCHAR2 )
    AS
        m_stmt VARCHAR2(100);
    BEGIN         m_stmt := 'BEGIN sys.usr_role_adm_pkg.p_drop_user@apver_link( :1 ); END;';
        EXECUTE IMMEDIATE m_stmt USING username;
    END p_drop_apver_user;

PROCEDURE p_set_apver_proxy_through( username VARCHAR2, proxyname VARCHAR2 )
    AS
        m_stmt VARCHAR2(100);
    BEGIN
        m_stmt :=
        'BEGIN sys.usr_role_adm_pkg.p_set_proxy_through@apver_link( :1, :2 ); END;';
        EXECUTE IMMEDIATE m_stmt USING username, proxyname;
    END p_set_apver_proxy_through;

PROCEDURE p_drop_apver_proxy_through( username VARCHAR2, proxyname VARCHAR2 )
    AS
        m_stmt VARCHAR2(100);
    BEGIN
        m_stmt :=
        'BEGIN sys.usr_role_adm_pkg.p_drop_proxy_through@apver_link( :1, :2 ); END;';
        EXECUTE IMMEDIATE m_stmt USING username, proxyname;
    END p_drop_apver_proxy_through;

END apver_usr_adm_pkg;
/

-- Grant to role
GRANT EXECUTE ON ojsaadm.apver_usr_adm_pkg TO ojs_adm_admin;`

除了一个例外,ojsaadm.apver_usr_adm_pkg中的程序与sys.usr_role_adm_pkg中的程序一致。这个包中的过程p_create_apver_user调用了sys.usr_role_adm_pkg中的p_create_user_oncep_create_user_many过程。sys.usr_role_adm_pkg过程没有抛出异常,所以即使用户已经存在并且p_create_user_once失败,我们也能成功调用p_create_user_many。最后,我们将这个包中的EXECUTE授予ojs_adm_admin角色。

选择一个现有用户

我们现在从检查完成我们的计划所需的 Oracle 结构和授权返回到管理用户功能屏幕。我们将首先看一下当应用用户从下拉菜单userComboBox中选择一个现有用户时发生的动作。调用userComboBox_actionPerformed()方法。我们首先通过调用如下的pullIDFromParens()方法从所选项的括号中获取用户 ID:

    userID = Utility.pullIDFromParens((String)userComboBox.getSelectedItem());

接下来,我们创建一个延迟线程,它的run()方法包含清单 12-31 中所示的查询。注意,我们选择了instance_proxy_users视图的所有行——这是一个静态查询,不容易受到 SQL 注入的影响。我们遍历ResultSet,测试每一行中与我们选择的用户相匹配的client值。然后,我们检查proxy的值是否与我们的任何管理用户匹配,并在管理用户屏幕上选择相应的复选框。在我们到达这一点之前,在userComboBox_actionPerformed()方法中,我们调用了blankAll()方法,它取消了所有复选框的选择。因此,在此过程结束时,选中的复选框仅代表授予所选用户的代理权限。

清单 12-31。查询获取所选用户的管理代理权限

    rs = stmt.executeQuery("SELECT INSTANCE, proxy, client " +         "FROM ojsaadm.instance_proxy_users ");     while (rs.next()) {         if (rs.getString(3).equalsIgnoreCase(userID)) {             if (rs.getString(2).equalsIgnoreCase("OJSAADM"))                 ojsaadmCheckBox.setSelected(true);             if (rs.getString(2).equalsIgnoreCase("APPUSR"))                 appusrCheckBox.setSelected(true);             if (rs.getString(2).equalsIgnoreCase("AVADMIN"))                 avadminCheckBox.setSelected(true);         }     }

保存对管理权限的更新

当应用用户选择了一个用户并对选中的管理权限复选框进行了更改时,他需要保存更改。他通过选择 Save Updates 按钮来做到这一点。当该按钮被选中时,调用saveButton_actionPerformed()方法。再次从userComboBox中的选中项目中拉出选中的userID。用清单 12-32 中的代码在其run()方法中创建了一个延迟线程。

这个方法非常广泛,因为我们独立地调用过程来授予或撤销 Admin Users 屏幕上的复选框所代表的每个权限。我们可以将几个这样的调用合并成一个动态循环,但是我们不会节省太多的代码,清晰度也会降低。我们采取的第一步是尝试在orclapver实例上创建用户。用户可能已经存在,但这是一个足够简单的步骤,可以确保用户既存在又拥有运行我们的应用所需的特权。我们调用三个 Oracle 过程来创建用户——两个在orcl实例(sys.usr_role_adm_pkg.p_create_user_oncep_create_user_many)上操作,一个通过数据库链接操作到apver实例(ojsaadm.apver_usr_adm_pkg.p_create_apver_user)。每个过程调用都有相同的语法,所以我只展示完整的语法一次。一些过程调用接受一个参数,即用户 ID;有些有两个参数,用户 ID 和代理用户。

清单 12-32。保存管理权限更新,缩写

`**    stmt = (OracleCallableStatement)conn.prepareCall(**
**        "CALL sys.usr_role_adm_pkg.p_create_user_once(?)");**
**    stmt.setString(1, userID);**
**    stmt.executeUpdate();**
**    if (stmt != null) stmt.close();**

stmt = (OracleCallableStatement)conn.prepareCall(
        "CALL sys.usr_role_adm_pkg.p_create_user_many(?)");
...
    stmt = (OracleCallableStatement)conn.prepareCall(
        "CALL ojsaadm.apver_usr_adm_pkg.p_create_apver_user(?)"); ...
    // Next, grant or revoke each proxy
    if (ojsaadmCheckBox.isSelected()) {
        stmt = (OracleCallableStatement)conn.prepareCall(
            "CALL sys.usr_role_adm_pkg.p_set_proxy_through(?,?)");
        stmt.setString(1, userID);
        stmt.setString(2, "OJSAADM");
...
    } else {
        stmt = (OracleCallableStatement)conn.prepareCall(
            "CALL sys.usr_role_adm_pkg.p_drop_proxy_through(?,?)");
...
    }
    if (appusrCheckBox.isSelected()) {
        stmt = (OracleCallableStatement)conn.prepareCall(
            "CALL sys.usr_role_adm_pkg.p_set_proxy_through(?,?)");
        stmt.setString(1, userID);
        stmt.setString(2, "APPUSR");
...
    } else {
        stmt = (OracleCallableStatement)conn.prepareCall(
            "CALL sys.usr_role_adm_pkg.p_drop_proxy_through(?,?)");
...
    }
    if (avadminCheckBox.isSelected()) {
        stmt = (OracleCallableStatement)conn.prepareCall(
            "CALL sys.usr_role_adm_pkg.p_set_proxy_through(?,?)");
        stmt.setString(1, userID);
        stmt.setString(2, "AVADMIN");
...
        stmt = (OracleCallableStatement)conn.prepareCall(
            "CALL ojsaadm.apver_usr_adm_pkg.p_set_apver_proxy_through(?,?)");
...
        stmt = (OracleCallableStatement)conn.prepareCall(
            "CALL ojsaadm.apver_usr_adm_pkg.p_grant_apver_appver_conns(?)");
        stmt.setString(1, userID);
...
    } else {
        stmt = (OracleCallableStatement)conn.prepareCall(
            "CALL sys.usr_role_adm_pkg.p_drop_proxy_through(?,?)");
...
        stmt = (OracleCallableStatement)conn.prepareCall(
            "CALL ojsaadm.apver_usr_adm_pkg.p_drop_apver_proxy_through(?,?)");
...
        stmt = (OracleCallableStatement)conn.prepareCall(
            "CALL ojsaadm.apver_usr_adm_pkg.p_revoke_apver_appver_conns(?)");
...
    }
    blankAll();`

我们为每个管理代理用户调用p_set_proxy_throughp_drop_proxy_through,这取决于该管理代理的复选框是否被选中。另外,对于代理用户 avadmin,我们也调用通过链接授予或撤销代理的过程,ojsaadm.apver_usr_adm_pkg.p_set_apver_proxy_throughp_set_apver_proxy_through,因为这个代理用户在两个实例中都被使用。

如果选择了avadminCheckBox,我们运行另外一个程序p_grant_apver_appver_conns,如果没有选择,我们运行p_revoke_apver_appver_conns。我们将在进入注册新应用屏幕时详细讨论这些步骤。该过程授予或撤销我们届时将需要的另一个角色。

撤销用户运行应用的权限

当管理用户在 Admin Users 屏幕上选择一个用户,然后选择 Revoke User Access 按钮时,就会调用revokeUserButton_actionPerformed()方法。在该方法中运行的代码删除了两个实例上所选用户的所有代理。该代码如清单 12-33 所示。

我们实例化了一个普通的Statementstmt2,它将支持一个返回ResultSet的查询。这是对我们实例化来调用存储过程的OracleCallableStatement的补充。类似于我们从ojsaadm.instance_proxy_users中选择的方式,当从我们的userComboBox中选择一个现有用户时,设置管理代理授权复选框;在这里,我们再次选择所有的代理用户,并且当我们遍历ResultSet时,只关注我们自己,关注那些被授予给所选用户的代理授权。然后,如果我们看到代理授权的实例等于“apver”,我们调用存储过程来撤销跨数据库链接的代理授权。否则,我们调用存储过程在本地撤销代理授权。

清单 12-33。放弃所有管理和应用代理权限

    stmt2 = conn.createStatement();     rs = stmt2.executeQuery("SELECT INSTANCE, proxy, client " +         "FROM **ojsaadm.instance_proxy_users** ");     while (rs.next()) {         if (rs.getString(3)**.equalsIgnoreCase(userID)**) {             if (rs.getString(1).equalsIgnoreCase(**"apver"**)) {                 stmt = (OracleCallableStatement)conn.prepareCall(                     "CALL ojsaadm.apver_usr_adm_pkg**.p_drop_apver_proxy_through**(?,?)");             } else {                 stmt = (OracleCallableStatement)conn.prepareCall(                     "CALL sys.usr_role_adm_pkg**.p_drop_proxy_through**(?,?)");             }             stmt.setString(1, userID);             stmt.setString(2, rs.getString(1));             **stmt.executeUpdate();**             if (stmt != null) stmt.close();         }     }     if (rs != null) rs.close();     blankAll();

我考虑以稍微不同的方式编写这个方法,只删除那些我们知道与我们的应用相关的代理,如果这是您的目标,您可能想要重写这个方法(调用p_drop_userp_drop_apver_user过程来撤销对appver的代理授权)。该方法将删除所有应用代理,包括通过appver用户进行代理的权限,这将阻止 Oracle 用户完成 SSO 或双因素身份验证。但是,我们不会删除 Oracle 用户帐户。

应用分配屏幕

Assign Application functional 屏幕与我们在 Admin Users 屏幕中使用的许多概念和代码相同。但是,在这里,我们处理的不是管理代理授权,而是代理其他应用的授权。图 12-5 显示分配应用屏幕。

images 注意能够通过ojsadmin代理的用户能够将任何应用分配给任何用户。也许另一个层次的过滤是合适的,但这并没有在本书中增加。从积极的一面来看,这些ojsadmin管理员可以互相代替,分配用户对彼此应用的访问权。我们在这里进行的唯一特定于应用的管理过滤是限制哪些管理员可以更改哪些应用的连接字符串(密码)。

images

图 12-5。分配应用功能屏幕

当有许多应用代理时,在一个可选择的表中表示它们比每个代理都有一个复选框更容易。从这样的列表中进行选择的一个缺点是更有可能做出错误的选择并被忽略。出于这个原因,我们没有在这个列表中显示管理代理用户。

“分配应用”屏幕顶部的下拉组合框是我们选择用户的地方。我们将管理该用户对应用的访问。最右边的表格显示了已经授予所选用户的代理授权列表,中间的表格列出了所有可用的代理授权。当选择了一个可用的代理时,最左边的表显示了使用该代理的应用列表。例如,appusr代理被分配给HRVIEW应用和OJSADMIN应用的用户。

这些表格是嵌套 GUI 对象中的两层。我们的表位于一个滚动窗格中,这样当表增长到超出我们提供的小框所能看到的范围时,我们将有垂直和/或水平滚动条来访问剩余的数据。

要为所选用户添加代理,将按下“添加所选”按钮,同样,要从所选用户撤销代理,将按下“删除所选”按钮。要保存这些新设置,应用用户将按下保存更新按钮。

初始化数据选择组件

当我们实例化一个新的AssignApp类时,我们将调用dataInit()方法来填充三个数据选择组件。我们将为我们的userComboBox建立用户列表,就像我们在其他功能屏幕中看到的一样。我们还将构建一个可用应用代理帐户的列表。我们将构建一个使用代理用户的应用列表的查找表。然后,当从“可用代理”列表中选择代理用户时,我们将在“分配应用”屏幕的最左侧列中显示相关的应用列表。

在我们的应用可用代理用户列表中,我们不会提供真实的可用代理列表,因为每个 Oracle 用户实际上都是潜在的代理用户。我们不想显示所有 Oracle 用户,否则肯定会出现错误选择。相反,我们将在可用代理列表中显示的代理用户将只是那些来自appsec.v_application_registry视图的app_user条目,它们也不是安全管理代理。回想一下,我们通过 Admin Users 屏幕授予和撤销对管理代理用户的访问权限。可用的代理将是所有注册的非管理应用的app_user条目列表。

对于我们的可用代理列表,我们将填充一个名为JTable的新 GUI 小部件。填充JTable不像填充组合框那样简单。我们不能只打电话给addItem()。在JTable中,有一个我们填充的底层表模型。我们使用了被声明为AssignApps静态成员的DefaultTableModel的实例。这在清单 12-34 中显示。

每个表模型都有数据和列标签。数据和列标签可以以多种形式提供给表格模型,我们选择以Vectors的形式提供数据和列标签。我们的表中只有一个单独的列,它要么是“代理”列表,要么是“应用”列表我们的列标识符向量被定义为AssignApp的静态成员,如清单 12-34 所示。如果我们的表中有多个列,我们将为每个列添加一个标签到我们的列标识符Vectors。我们不需要告诉JTable我们将有多少列数据——它会从我们的列标识符Vector中的条目数量来推测。

清单 12-34。分配应用的静态成员

static DefaultTableModel availableProxiesTM = new DefaultTableModel();     static DefaultTableModel userProxiesTM = new DefaultTableModel();     static DefaultTableModel appsTM = new DefaultTableModel();     static Vector columnIdentifiers = new Vector();     static {         columnIdentifiers.add("Proxies");     }     static Vector appColumnIdentifiers = new Vector();     static {         appColumnIdentifiers.add("Applications");     }     static Hashtable<String, Vector> appsHashtable =         new Hashtable<String, Vector>();

我们在清单 12-34 的中声明的最后一个静态成员是一个HashtableappsHashTable将有一个键,即代理用户名,和一个值,即使用代理用户的应用的Vector

A DefaultTableModel假设每一行可能有多列,所以它不把数据作为一个简单的列表来处理,即使只有一列。相反,在使用Vectors的情况下,它期望每一行都是数据元素的Vector,每一列一个。所以最终,我们提供给表模型以构建我们的表的数据将是一个VectorsVector,它保存了一个潜在的多列行列表。

现在您已经知道了我们在为表构建数据Vectors时的方向,让我们看看来自dataInit()的构建可用代理列表的代码。该代码如清单 12-35 所示。仔细看看我们处理查询中的ResultSetwhile循环。我们实例化一个新的itemVector,并用一个元素填充它,即我们的ResultSet的第一列。itemVector代表我们表中的一行。然后我们将itemVector添加到dataVector中。dataVector代表表格的行列表。清单 12-35 中的最后一行是对可用代理表模型setDataVector()方法的调用,该方法将填充该表。

清单 12-35。为可用代理建立数据表

    rs = stmt.executeQuery(                 "SELECT DISTINCT a.app_user FROM appsec.v_application_registry a " +         "WHERE a.app_user NOT IN ('APPVER','AVADMIN','APPSEC','OJSAADM') " +         "ORDER BY a.app_user");     // dataVector must be Vector of Vectors     Vector **dataVector** = new Vector();     Vector **itemVector;**     while (rs.next()) {         **itemVector = new Vector();**         **itemVector.add(rs.getString(1));**         **dataVector.add(itemVector);**     }     if (rs != null)         rs.close();     **availableProxiesTM.setDataVector(dataVector, columnIdentifiers);**

dataInit()方法中,我们还为代理表构建应用的数据。该表的内容将发生变化,以匹配当前选择的任何代理用户。对于每个代理用户,我们将有一个VectorsVector来填充这个表。那些VectorsVectors将被保存为appsHashTable中的值,我们可以用代理用户名的键来选择。

清单 12-36 显示了用于构建appsHashTable. In building the data, we again skip administrative proxies that are handled by the Admin Users screen, and we skip proxies that are used on the apver instance. We are prepared to call dataInit() multiple times, whenever the data may have changed, i.e., after the Save Updates button is selected. Because we may be rebuilding the appsHashTable, we start out by removing any existing content by calling the clear() method. We also set the table model, appsTMnull Vector的代码——在选择可用的代理之前,我们不想显示任何应用。

当我们遍历ResultSet时,我们检查是否已经为代理用户rs.getString(2)启动了dataVector。我们检查appsHashTable是否包含一个同名的键。如果是这样的话,我们从appsHashTable中得到这个值,它是代理应用的Vector,否则我们实例化一个新的 Vector。同样,每个应用都是添加到这个代理的dataVector中的新Vector中的一个项目。然后将dataVector放入appsHashTable中,将代理用户名作为关键字。

清单 12-36。为代理表的应用建立数据

    rs = stmt.executeQuery("SELECT DISTINCT a.application_id, p.proxy " +         "FROM ojsaadm.instance_proxy_users p, appsec.v_application_registry a " +         "WHERE p.instance <> 'APVER' " +         "AND p.proxy NOT IN ('APPVER','AVADMIN','APPSEC','OJSAADM') " +         "AND a.app_user = p.proxy ORDER BY a.application_id");     // appsHashtable must be Hashtable of Vectors of Vectors     // empty static Hashtable each time you enter this screen     **appsHashtable.clear();**     appsTM.setDataVector(**null,** appColumnIdentifiers);     while (rs.next()) {         if (appsHashtable.containsKey(rs.getString(2))) {             **dataVector = appsHashtable.get(rs.getString(2));**             itemVector = new Vector();             itemVector.add(rs.getString(1));             dataVector.add(itemVector);         } else {             **dataVector = new Vector();**             itemVector = new Vector();             itemVector.add(rs.getString(1));             dataVector.add(itemVector);         }         **appsHashtable.put(rs.getString(2), dataVector);**     }

为了构建可用代理表和代理表应用的表数据,我们从v.application_registry视图中进行选择,在这个功能屏幕中,我们作为ojsaadm用户与ojs_adm_admin角色进行连接。为了从appsec模式视图中进行选择,代理用户需要SELECT权限。作为安全管理员,secadmorcl实例上,我们执行这个授权:

GRANT SELECT ON appsec.v_application_registry TO ojs_adm_admin;

在表格中选择可用的代理

当应用用户从表中选择一个可用的代理时,就会调用availableProxiesTable_mouseClicked()方法。该方法的重要代码如清单 12-37 所示。要查找表中所选数据的值,我们需要获取指定行、指定列中的值。在单列表中,所有数据都在列索引零中。因此,要获得表中所选的可用代理的值,我们需要获得所选行和列零处的值。我们使用选择的代理用户名作为从appsHashtable获取相关的dataVector的密钥。然后我们设置应用表模型来使用那个dataVector

清单 12-37。从表格中选择可用的代理

    String key = (String)**availableProxiesTM.getValueAt**(         availableProxiesTable.**getSelectedRow(), 0**);     Vector dataVector = **appsHashtable.get**(key);     appsTM**.setDataVector(dataVector**, appColumnIdentifiers);

拥有这些为每个代理预先构建的应用列表,使我们不必在应用用户单击不同的可用代理用户时调用 Oracle 来获取列表。与打开 Oracle 连接、查询数据库和处理ResultSet相比,从本地Hashtable获取数据非常快。然而,当我们知道要存储的项目数量很小时,我们只想在内存中构建这样的数据结构。

从列表中选择用户

每当从下拉菜单userComboBox中选择一个用户时,我们将查询 Oracle 来查找该用户已经被授予了哪些代理权限。这是通过userComboBox_actionPerformed()方法完成的。该方法显示了sayWaitDialog消息,并在一个延迟的Thread中执行一个查询,就像我们之前看到的那样。清单 12-38 中的部分显示了该线程的run()方法。

清单 12-38 看起来应该很熟悉。我们构建一个包含一个条目itemVectorsdataVector,它代表我们授予所选用户的代理列表。在处理ResultSet时,我们跳过任何不属于所选用户的返回行。在我们收集了代理列表之后,我们将它设置为用户代理表模型userProxiesTM的数据Vector

清单 12-38。从列表中选择用户

    stmt = conn.createStatement();     rs = stmt.executeQuery(         "SELECT DISTINCT p.proxy, p.client FROM ojsaadm.instance_proxy_users p " +         "WHERE p.instance <> 'APVER' " +         "AND p.proxy NOT IN ('APPVER','AVADMIN','APPSEC','OJSAADM') " +         "ORDER BY p.proxy");     Vector dataVector = new Vector();     Vector itemVector;     while (rs.next()) {         **if (rs.getString(2).equals(userID))** {             itemVector = new Vector();             itemVector.add(rs.getString(1));             dataVector.add(itemVector);         }     }     if (rs != null)         rs.close();     **userProxiesTM.setDataVector(dataVector, columnIdentifiers);**

向用户列表添加代理

当按下“添加选定项”按钮时,在“可用代理”列表中选择的任何代理都将被添加到用户代理列表中。这是由清单 12-39 中的代码完成的,它来自于addButton_actionPerformed()方法。

我们从可用代理表中获取选定的值,并将其作为一个项目添加到新的VectoritemVector中。然后我们测试用户列表中是否已经存在相同的itemVector(代理)。如果没有,我们将新的itemVector添加到列表中。

这里有几件事需要注意。我们从userProxiesTM表模型中获得对当前dataVector的引用。我们在任何时候都不会用新的Vector来代替它。然后在最后我们将userProxiesTM的数据Vector设置回我们得到的dataVector

有必要吗?我们不能在没有获得本地引用的情况下,将新的itemVector添加到现有的dataVector中吗?难道我们不能避免用本地参考来称呼userProxiesTM.setDataVector()吗?

第一个问题的答案是否定的,没有必要。第二个问题的答案是肯定的,我们可以在表模型中引用现有的dataVector而不创建本地引用,但这毕竟会导致更长的引用和更多的代码。第三个问题的答案是可能的,但是我们指望的是setDataVector()方法的副作用——它也用新数据更新 GUI 显示。还有其他方法可以更新显示的表格数据,但是没有一种方法如此简洁。

清单 12-39。向用户列表添加代理

    Vector **dataVector = userProxiesTM.getDataVector**();     String value = (String)**availableProxiesTM.getValueAt**(         availableProxiesTable.getSelectedRow(), 0);     Vector itemVector = new Vector();     **itemVector.add(value);**     **if (!dataVector.contains(itemVector))** {         dataVector.add(itemVector);         **userProxiesTM.setDataVector(dataVector, columnIdentifiers);**     }

从用户列表中删除代理

当按下 Remove Selected 按钮时,从用户的代理表中删除项目的操作相对简单。代码如清单 12-40 所示。在dataVector上调用remove()方法即可。我们再次调用表格模型setDataVector()方法来确保 GUI 显示被更新。

清单 12-40。从用户列表中删除代理

    Vector **dataVector = userProxiesTM.getDataVector();**     String value = (String)userProxiesTM.getValueAt(         userProxiesTable.getSelectedRow(), 0);     Vector itemVector = new Vector();     itemVector.add(value);     **dataVector.remove(itemVector);**     **userProxiesTM.setDataVector(dataVector, columnIdentifiers);**

保存对用户代理的更新

当按下保存更新按钮时,将运行saveButton_actionPerformed()方法。这个方法的重要代码如清单 12-41 所示。这来自那里定义的延迟线程run()方法。

在那个线程中,我们做的第一件事是尝试在orclapver实例上创建用户。在我们决定授予用户访问应用的权限之前,我们并不需要 Oracle 用户。

然后我们分配应用。我们从用户的代理表模型中获得dataVector。这是我们为用户计划的代理结果列表。其中一些可能是添加的代理。一些可能是用户先前拥有的代理。也可能存在用户拥有的现在已经从该列表中移除的代理授权。所以我们必须处理以下三种情况:

  • 保留列表中的现有代理。
  • 添加列表中的新代理。
  • 删除列表中不再存在的现有代理。

在清单 12-41 中,我们处理这三种情况。我们从 Oracle 查询中选择现有的代理列表,并遍历ResultSet。我们只关心所选用户的代理。对于ResultSet中的每个现有代理,我们创建一个新的itemVector,并将其作为一个条目。我们可以通过调用contains()方法来发现dataVector是否有类似的itemVector。如果是的话,那么这就是我们想要保留的代理。我们已经处理了这个代理,所以我们将它从dataVector中移除。稍后,我们将看到在dataVector中还剩下哪些代理——这些代理将不得不被新授予用户。

如果我们从查询中获得的现有代理不在dataVector中,那么它将从列表中删除,我们需要撤销代理授权。我们通过调用p_drop_proxy_through过程来做到这一点。

最后,我们只剩下那些以前不存在的代理。对于其中的每一个,我们调用p_set_proxy_through过程为所选用户添加一个新的代理授权。

清单 12-41。保存对用户代理的更新

OracleCallableStatement stmt2 = null; Statement stmt = null; ResultSet rs = null; try {     // First, try to create user on both instances     stmt2 = (OracleCallableStatement)conn.prepareCall( "CALL sys.usr_role_adm_pkg**.p_create_user_once**(?)");     stmt2.setString(1, userID);     stmt2.executeUpdate();     if (stmt2 != null)         stmt2.close();     stmt2 = (OracleCallableStatement)conn.prepareCall( "CALL sys.usr_role_adm_pkg**.p_create_user_many**(?)");     stmt2.setString(1, userID);     stmt2.executeUpdate();     if (stmt2 != null)         stmt2.close();     stmt2 = (OracleCallableStatement)conn.prepareCall( "CALL ojsaadm.apver_usr_adm_pkg**.p_create_apver_user**(?)");     stmt2.setString(1, userID);     stmt2.executeUpdate();     if (stmt2 != null)         stmt2.close();     stmt = conn.createStatement();     rs = stmt.executeQuery(         "SELECT DISTINCT p.proxy, p.client FROM ojsaadm.instance_proxy_users p " +         "WHERE p.instance <> 'APVER' " +         "AND p.proxy NOT IN ('APPVER','AVADMIN','APPSEC','OJSAADM') " +         "ORDER BY p.proxy");     Vector **dataVector = userProxiesTM.getDataVector()**;     Vector itemVector;     String proxyID;     while (rs.next()) {         **if (rs.getString(2).equals(userID))** {             proxyID = rs.getString(1);             itemVector = new Vector();             **itemVector.add(proxyID);**             **if (dataVector.contains(itemVector))** {                 //System.out.println("retaining proxy to: " + proxyID);                 **dataVector.remove(itemVector);**             } else {                 //System.out.println("removing proxy to: " + proxyID);                 stmt2 = (OracleCallableStatement)conn.prepareCall(                     "CALL sys.usr_role_adm_pkg**.p_drop_proxy_through**(?,?)");                 stmt2.setString(1, userID);                 stmt2.setString(2, proxyID);                 stmt2.executeUpdate();                 if (stmt2 != null) stmt2.close();             }         }     }     if (rs != null)         rs.close();     **for (Object element : dataVector)** {         itemVector = (Vector)element;         proxyID = (String)itemVector.get(0);         //System.out.println("adding proxy to: " + proxyID);         stmt2 = (OracleCallableStatement)conn.prepareCall(             "CALL sys.usr_role_adm_pkg**.p_set_proxy_through**(?,?)");         stmt2.setString(1, userID);         stmt2.setString(2, proxyID);         stmt2.executeUpdate();         if (stmt2 != null) stmt2.close();     } ...

申请注册屏幕

我们安全管理议程的下一步,也是OJSAdmin菜单上“下一步”按钮的目标,是注册新应用功能屏幕。到目前为止,我们已经注册了几个应用:HRVIEW和这个应用,OJSADMIN,如果你已经运行了本章中的一些 SQL。您可以在图 12-6 的上看到注册新应用所需的字段列表。

images

图 12-6。注册新应用功能屏幕

应用 ID 实际上只是应用的名称——在数据库中引用它的一种方式。应用 ID 的主要用途是作为选择适当的安全应用角色的密钥之一。另一个键是应用用户(代理用户)的名称。这三个数据元素就是我们插入到appsec.v_application_registry视图中的内容。

在屏幕的底部,有三个组成应用内部类定义的字段。第一个字段是包,然后是类名,最后是内部类名。我们将在本地创建该类的一个实例,并提交给 Oracle,以便在appsec.v_app_conn_registry视图中为这个新应用的连接字符串列表创建一个占位符。为了在appsec.v_app_conn_registry中创建该条目,需要在 Oracle 数据库中存在一个代表性的类。

注册新应用屏幕上有一个可选按钮,如果用户在课程字段中键入“登录”,该按钮就会出现。在这种情况下,将出现“从登录模板在 Oracle 上创建应用类”按钮。确保为将使用此屏幕注册应用的其他应用安全管理员记录此功能。我可以建议给他们买一本这本书吗?

如果应用使用我们在本章中开发的Login类,那么在 Oracle 上创建一个代表性的类就很容易了。这个内部类从一个应用到另一个应用的唯一变化是Login类所在的包。我们利用了这个按钮中的事实。选择此按钮后,将在 Oracle 数据库上创建一个新的 Java 类。

当用户选择注册按钮时,我们要做几件事。首先,我们将表单上前三个字段的数据插入到appsec.v_application_registry视图中。此外,我们将当前 Oracle 用户添加为该特定应用的管理员。这些数据被放入一个名为appsec.t_application_admins的新表中。第三,我们在一个新表中输入一个值,该表将appsec.v_application_registry视图中的条目与appsec.v_app_conn_registry视图中的应用内部类条目相关联。这张新桌子被命名为appsec.t_app_class_id。我们将在本章后面讨论这些新表。

注册新应用时,只有一个问题。我们实际上将上下文从OJSAdmin应用更改为新应用的上下文,以便作为该应用与 Oracle 数据库通信,并在appsec.v_app_conn_registry视图中输入内部类。出于这个原因,在注册按钮被按下后,关闭按钮变成退出按钮,而不是返回到OJSAdmin,退出按钮退出 JVM。

应用验证管理员角色

在第十一章中,我们创建了一个名为appver_admin的角色。我们将一些管理任务委派给了这个角色,其中主要的任务是执行appsec.appsec_admin_pkg包中的过程和功能。这些程序和功能包括f_set_decrypt_connsp_copy_app_conns,以及我们将在本章后面介绍的程序p_create_template_class。这些是为各种应用处理存储的 Oracle 连接字符串列表的过程,它们需要额外的安全措施。

在第十一章中,我们将appver_admin角色分配给一个特定的用户,我们在示例代码中称之为osadmin(可能是你。)在本章中,我们将更改该角色,使其更像我们的标准应用角色之一,为被允许通过 Oracle 应用帐户进行代理的人员设置。您可能已经从表 12-1 和清单 12-5 中得到这方面的提示,其中我们将一个新用户avadminappver_admin角色相关联。在讨论管理用户功能屏幕时,我们还看到了如何将代理用户分配给人员。

之前,我们已经在orclapver实例上定义了appver_admin角色,我们将其指定为NOT IDENTIFIED,并将角色授予了osadmin。在这里,我们将撤销该授权,并将角色更改为由我们的安全应用角色过程来标识,appsec.p_check_role_access。我们作为orclapver实例上的SYS用户完成这些任务,如清单 12-42 所示。同时,我们将创建新的代理用户avadmin,他将设置这个角色。并且我们授予通过avadmin(代理)连接到osadmin(你)的权利。确保给用户avadmin一个非常强的密码。同样如清单 12-42 所示,在apver实例上,我们将重复我们已经在orcl实例上完成的对appsec.v_application_registry的插入——建立avadmin用户和appver_admin角色的关联。

清单 12-42。应用验证管理员

REVOKE appver_admin FROM osadmin; ALTER ROLE appver_admin IDENTIFIED USING appsec.p_check_role_access; `GRANT create_session_role TO avadmin IDENTIFIED BY password;
ALTER USER osadmin GRANT CONNECT THROUGH avadmin;

INSERT INTO appsec.v_application_registry
(application_id, app_user, app_role) VALUES
('OJSADMIN','AVADMIN','APPVER_ADMIN');`

创建应用类按钮

当应用开发人员在他的应用中包含了Login类,修改了应用 ID 和包,他就使得在 Oracle 数据库中注册他的应用变得容易多了。如果他没有走这条路,那么我们必须从他那里得到他的应用内部类定义,并且必须执行命令CREATE OR REPLACE AND RESOLVE JAVA SOURCE NAMED。然而,由于他遵循首选的 GUI 开发路径,使用 Login,我们仅从他的包就知道了足够多的信息,可以在 Oracle 数据库上创建一个类来表示他的应用。

因此,当我们进入 Register New Application 屏幕时,我们看到在 Class 字段中键入了单词“Login ”,我们提供了一个按钮,允许在数据库上轻松创建一个代表性的应用内部类。清单 12-43 中的代码用于显示或隐藏该按钮。我们使用一个keyReleased事件来告知用户何时在classTextField中输入了一个字符。如果在输入字符后,字段的文本等于“Login”,那么我们使按钮可见。否则,我们把它藏起来。

清单 12-43。显示或隐藏创建应用类按钮

    private void classTextField_keyReleased(KeyEvent e) {         if (classTextField.getText().equals("Login"))             createTemplateButton.setVisible(true);         else             createTemplateButton.setVisible(false);     }

当按钮可见时,可以选择它,然后它执行createTemplateButton_actionPerformed()方法中的代码。在那个方法中,我们找到了清单 12-44 中的代码。为了在数据库中创建模板类,我们调用了p_create_template_class过程。这是我们在appsec_admin_pkg一揽子计划中定义的新程序。我们传入完全限定的类名;然而,我们并不需要阅读所有用户提供的字段来得到答案。相反,我们读取用户提供的包名并添加字符串。以包名登录$InnerRevLvlClass"。

清单 12-44。在 Oracle 中创建模板类

    stmt = (OracleCallableStatement)conn.prepareCall(                 "call **appsec.appsec_admin_pkg.p_create_template_class**( ?,?,? )");     stmt.registerOutParameter(2, OracleTypes.NUMBER);     stmt.registerOutParameter(3, OracleTypes.VARCHAR);     **stmt.setString(1, packageTextField.getText() + ".Login$InnerRevLvlClass");**     stmt.setInt(2, 0);     stmt.setNull(3, OracleTypes.VARCHAR);     stmt.executeUpdate();

清单 12-45 显示了新p_create_template_class程序的代码。该过程需要驻留在apver数据库实例上,因此定义包含在脚本文件chapter 12/apversys . SQL中。这个过程的大部分是给 Java 内部类的定义。注意,我们在内部类定义中的两个地方插入了包名:一个是在 Java 结构的名称中,另一个是在 package 语句中。在我们决定创建类之前,我们做了几个测试。第一个测试是看这个类是否已经在数据库中创建了。我们查询所有的 Java 类(sys.all_java_classes),看看是否已经存在同名的类。如果是这样的话,我们就不创建类。因为我们正在执行一个CREATE命令,而不是一个CREATE OR REPLACE命令,如果类已经存在,我们将得到一个异常。我们进行的第二个测试将确保单词“Login”出现在类名中,第三个测试将确保当我们在单词“Login”之前截断类名时,会留下一些东西,作为一个包。

在我们将要EXECUTE IMMEDIATE的 DDL 命令中,我们连接了 ASCII 字符代码 10、13 和 34。这些用来给我们的 Java 类提供一些格式。一对 13 和 10 是移至下一行开头的回车/换行符。字符 34 是一个双引号,我们希望在类名的两边都加上这个双引号,以便与模式名APPSEC区分开来。

清单 12-45。创建模板类的步骤

`PROCEDURE p_create_template_class(
        m_class_name     v_app_conn_registry.class_name%TYPE,
        m_err_no     OUT NUMBER,
        m_err_txt    OUT VARCHAR2 )
    IS
        v_count   INTEGER;
        v_package v_app_conn_registry.class_name%TYPE;
    BEGIN
        m_err_no := 0;
        SELECT COUNT(*) INTO v_count
        FROM sys.all_java_classes
        WHERE owner='APPSEC' AND name = m_class_name;
        IF v_count < 1 THEN
            v_count := INSTR( m_class_name, 'Login' );
            IF v_count > 0 THEN
                v_package := SUBSTR( m_class_name, 0, v_count - 2 );
                IF LENGTH( v_package ) > 0 THEN

**                    EXECUTE IMMEDIATE**

'CREATE AND RESOLVE JAVA SOURCE NAMED APPSEC.' || CHR(34) ||
v_package || '/Login' || CHR(34) || ' AS ' || CHR(13) || CHR(10) ||
'package ' || v_package || '; ' || CHR(13) || CHR(10) ||
'import java.io.Serializable; ' || CHR(13) || CHR(10) ||
'import orajavsec.RevLvlClassIntfc; ' || CHR(13) || CHR(10) ||
'public class Login { ' || CHR(13) || CHR(10) ||
'    public static class InnerRevLvlClass ' || CHR(13) || CHR(10) ||
'       implements Serializable, RevLvlClassIntfc{ ' || CHR(13) || CHR(10) ||
'        private static final long serialVersionUID = 2011010100L; ' || CHR(13) ||CHR(10)||
'        private String innerClassRevLvl = "20110101a"; ' || CHR(13) || CHR(10) ||
'        public String getRevLvl() { ' || CHR(13) || CHR(10) ||
'            return innerClassRevLvl; ' || CHR(13) || CHR(10) ||
'}   }   }';
                END IF;
            END IF;
        END IF;
    EXCEPTION
        WHEN OTHERS THEN             m_err_no := SQLCODE;
            m_err_txt := SQLERRM;
            app_sec_pkg.p_log_error( m_err_no, m_err_txt,
                ' p_create_template_class' );
    END p_create_template_class;`

要让这个按钮起作用,是有要求的。appsec_admin_pkg包的所有者,即appsec用户,必须能够创建 Java 类。为此,我们必须将CREATE PROCEDURE系统特权授予appsec。此外,她还必须能够从数据字典视图中选择sys.all_java_classes,以查看已经存在的类。我们以SYS用户的身份在apver实例上这样做:

GRANT CREATE PROCEDURE TO appsec; GRANT SELECT ON sys.all_java_classes TO APPSEC;

特定应用管理员和应用到类注册表的表格

在某些情况下,我们会希望将某些应用的管理委托给一个人,而将其他应用的管理委托给另一个人,我们不希望他们能够管理彼此的应用。我们将在本章后面完成的最终目标就是我们要构建appsec.t_application_admins表的原因。t_application_admins的定义在清单 12-46 中给出。它只是允许管理特定应用内部类名的操作系统用户名列表。我们想要保护的是那些应用内部类所代表的 Oracle 连接字符串列表。

images 注意这些表只在 apver 实例上需要。

清单 12-46。应用管理员表

`CREATE TABLE appsec.t_application_admins
(
    -- match appsec.t_app_conn_registry.class_name
    class_name VARCHAR2(2000) NOT NULL,
    -- match hr.emp_mobile_nos.user_id
    user_id    VARCHAR2(20) NOT NULL
);
/

CREATE UNIQUE INDEX application_admins_pk ON appsec.t_application_admins
    ( class_name, user_id );

ALTER TABLE appsec.t_application_admins ADD (
    CONSTRAINT application_admins_pk
    PRIMARY KEY
    ( class_name, user_id )
    USING INDEX application_admins_pk );
/

CREATE OR REPLACE VIEW appsec.v_application_admins
    AS SELECT * FROM appsec.t_application_admins;

INSERT INTO appsec.v_application_admins
    ( class_name, user_id )
    ( SELECT DISTINCT class_name, 'OSADMIN' FROM appsec.t_app_conn_registry );

COMMIT;`

我们将开始列出各种应用的各个管理员,首先将所有应用的管理分配给我们称为osadmin的用户。我们用清单 12-46 中的一个插入命令来完成,并提交插入。

我们将在这里创建第二个表,即appsec.t_app_class_id表。当每个应用被单独管理时,就像我们已经完成的第十一章一样,我们自动知道我们在做什么应用。但是,在这个安全管理界面应用中,我们管理所有的应用。现在我们需要象征性地将应用与其应用内部类和连接字符串列表相关联。为了避免在我们想要指定应用时必须键入应用 ID 和内部类名,这种关联是必要的。

t_app_class_id表中的一个示例条目就是这个应用:类名是orajavsec.Login$InnerRevLvlClass,应用 id 是OJSADMIN。清单 12-47 显示了我们将用来构建和填充表格的脚本。

清单 12-47。ID 表的应用类

`CREATE TABLE appsec.t_app_class_id
(
    class_name    VARCHAR2(2000 BYTE) NOT NULL ENABLE,
    application_id VARCHAR2(24 BYTE) NOT NULL ENABLE
);
/

CREATE UNIQUE INDEX app_class_id_pk ON appsec.t_app_class_id
    ( class_name, application_id );

ALTER TABLE appsec.t_app_class_id ADD (
    CONSTRAINT app_class_id_pk
    PRIMARY KEY
    ( class_name, application_id )
    USING INDEX app_class_id_pk
);
/

CREATE OR REPLACE VIEW appsec.v_app_class_id AS SELECT * FROM appsec.t_app_class_id;

INSERT INTO appsec.v_app_class_id
(CLASS_NAME, APPLICATION_ID) VALUES
('testojs.TestOracleJavaSecureAnyNameWeWant,HRVIEW);INSERTINTOappsec.vappclassid(CLASSNAME,APPLICATIONID)VALUES(orajavsec.LoginInnerRevLvlClass','OJSADMIN');

COMMIT;`

我在本节的介绍中提到,当我们注册新的应用时,我们会将数据插入到这些表中。我们输入当前的 Oracle 用户作为新应用的管理员,在appsec.v_application_admins视图中插入一条记录。我们还通过插入到appsec.v_app_class_id视图中来输入应用类与应用 ID 的关系。我们现有应用的插入语句如清单 12-47 所示。

安全表访问分析

我们将看看如何在appsec模式中获取数据表,以及如何使用这些数据。这将帮助我们定义谁应该获得访问权限,以及我们需要向每个 Oracle 用户授予什么访问权限。

首先,我们需要理解对appsec模式中数据的大部分访问是由appsec模式中的过程、函数和包完成的,因此不需要对数据进行额外的授权。我们有几个软件包将程序和功能分成以下几组:

  • APP_SEC_PKG:应用需要访问的过程和功能。我们将这个包的 execute
    授予应用模式,比如HR。那些应用模式具有调用app_sec_pkg的敏感数据访问过程。
  • 处理 Oracle 和 Java 安全活动内部工作的过程和函数。此套餐没有授权。
  • APPSEC_PUBLIC_PKG:需要所有 Oracle 用户都能访问的过程。此程序包被授予公共执行权限。通常,这提供了一个起点,是应用验证过程的一部分,用户通过appver进行代理,但是除了create_session_role之外没有其他角色。这个包中的一个特殊过程是p_get_app_conns过程,通过这个过程,用户可以在 SSO、双因素认证和密钥交换之后获得他们正在使用的应用的连接字符串列表。我们的计划是在这个特定的过程中确保选择,这样用户只能选择他们已经被授权的应用。
  • APPSEC_ADMIN_PKG:只有应用安全管理员可以使用的程序和功能,这些管理员可以通过avadmin用户代理并获得appver_admin角色。该包的执行权限被授予appver_admin

我们还有一组不在包中的过程和函数。所有这些结构都有两个方面使它们无法在一个包中工作:它们要么以当前用户权限AUTHID CURRENT_USER执行,要么被 Oracle wrap 实用程序混淆。

我们还支持从appsec模式的结构之外直接访问应用安全数据。特别是,在这个安全管理界面应用中,我们有从三个视图中选择数据的查询:v_app_class_idv_application_registry和、??。这些查询在 Oracle 应用用户通过avadmin代理的功能屏幕中运行。因此,我们将那些视图上的SELECT授予appver_admin角色。此外,在我们正在检查的 Register New Application 屏幕中,我们将数据插入到三个视图中:v_app_class_idapplication_registryapplication_admin。我们目前已经设置了appver_admin角色,所以我们将那些视图上的INSERT授予appver_admin

将来我们需要在v_application_admin视图中选择、更新和删除记录。除了最初注册应用的人之外,我们还需要确定其他人来管理每个应用的连接字符串。我们将设置应用管理员工作委托给特定的用户。我们将v_application_admin上的SELECTUPDATEDELETE授予osadmin用户(可能是你。)

在分配应用功能屏幕上,我们需要从v_application_registry视图中选择记录。用户通过ojsaadm代理使用ojs_adm_admin角色操作该屏幕。回想一下ojsaadm拥有自己的一整套授权和程序包——查看 Admin Users 屏幕上对该讨论的描述。我们将v_application_registry上的SELECT授予ojs_adm_admin角色。

清单 12-48 显示了用于在apver实例上进行剩余授权的脚本。这些授权满足了我前面提到的所有数据需求。

清单 12-48。对 Apver 上 Appsec 数据的授权

`GRANT SELECT ON appsec.v_app_class_id TO appver_admin ;
GRANT SELECT ON appsec.v_application_registry TO appver_admin;
GRANT SELECT ON appsec.v_app_conn_registry TO appver_admin;

GRANT INSERT ON appsec.v_app_class_id TO appver_admin ;
GRANT INSERT ON appsec.v_application_registry TO appver_admin ;
GRANT INSERT ON appsec.v_application_admins TO appver_admin;

GRANT UPDATE, SELECT, DELETE ON appsec.v_application_admins TO osadmin;`

我们还有一个剩余的安全数据访问障碍需要处理。通常(直到第十一章),我们只是在应用中嵌入了密码,并使用第一次通过应用来设置连接字符串。我们不想这么做!我们所讨论的是有原因的。我们需要通过作为应用的appver用户进行代理,以便为应用设置连接字符串。一旦我们调用getAAConnRole()方法从列表中获取一个应用连接,我们就不能再更新与该应用相关联的连接字符串。我确信OracleJavaSecure可以被重写以不同的方式运作。

当我们将一些过程移动到appsec_admin_pkg包中并需要一个安全角色来访问时,问题就出现了。实际上,我们已经使更新连接字符串的过程无法调用。当我们通过appver用户代理连接时,我们没有通过执行p_check_role_access过程或类似的事情来设置安全应用角色。通过appver代理的单个 Oracle 用户不能执行appsec_admin_pkg,即使他们被授权通过avadmin代理来获得appver_admin角色。

我们的解决方案是在apver实例上创建另一个角色,它将是所有需要更新应用连接字符串的用户的默认角色。我们的应用管理员既可以通过avadmin获得代理权限,也可以获得这个新角色的权限,我们称之为appver_conns。创建appver_conns角色的代码如清单 12-49 所示。

清单 12-49。应用验证连接角色

CREATE ROLE appver_conns NOT IDENTIFIED; GRANT EXECUTE ON appsec.appsec_admin_pkg TO appver_conns ; GRANT appver_conns TO osadmin;

注意,我们将appver_conns授予了osadmin用户。我们将对所有通过这个安全管理界面维护应用连接字符串的应用管理员授予相同的权限。这使他们可以访问appsec_admin_pkg,他们需要用它来更新连接字符串。

当我们添加管理员时,我们需要授予通过avadmin连接的权限,以及授予appver_conns角色的权限。出于这个原因,我们在名为sys.appver_conns_role_pkgapver实例的包中添加了额外的过程。包装代码在清单 12-50 中给出。appver_conns的授予和撤销都有p_grant_appver_conns_rolep_revoke_appver_conns_role两个程序。

清单 12-50。授予 Appver 实例上的 Appver Conns 角色

`CREATE OR REPLACE PACKAGE BODY sys.appver_conns_role_pkg IS

PROCEDURE p_grant_appver_conns_role (
        username sys.proxy_users.client%TYPE )
    AS
        PRAGMA AUTONOMOUS_TRANSACTION;
    BEGIN
        EXECUTE IMMEDIATE 'GRANT appver_conns TO ' || username;
        COMMIT;
    EXCEPTION
        WHEN OTHERS
        THEN
            appsec.app_sec_pkg.p_log_error( SQLCODE, SQLERRM,
                'sys.p_grant_appver_conns_role for ' ||
                username );
    END p_grant_appver_conns_role;

PROCEDURE p_revoke_appver_conns_role (
        username sys.proxy_users.client%TYPE )
    AS
        PRAGMA AUTONOMOUS_TRANSACTION;
    BEGIN
        EXECUTE IMMEDIATE 'REVOKE appver_conns FROM ' || username;
        COMMIT;
    EXCEPTION
        WHEN OTHERS
        THEN
            appsec.app_sec_pkg.p_log_error( SQLCODE, SQLERRM,
                'sys.p_revoke_appver_conns_role for ' ||
                username );
    END p_revoke_appver_conns_role; END appver_conns_role_pkg;
/

-- From New Application Registration
GRANT EXECUTE ON sys.appver_conns_role_pkg TO appver_admin ;

-- From Admin Users, grant to user, not to role
GRANT EXECUTE ON sys.appver_conns_role_pkg TO ojsaadm;`

当我们注册一个新的应用时,我们将为当前会话用户调用p_grant_appver_conns_role,正如我们将在本章的下一小节中看到的。在这一点上,我们通过avadmin用户代理,角色为appver_admin;所以你可以在清单 12-50 中看到,我们将sys.appver_conns_role_pkg的执行授权给了appver_admin

当我们通过管理用户屏幕添加管理员时,我们希望调用p_grant_appver_conns_rolep_revoke_appver_conns_role,这取决于是否选择了应用注册复选框。然而,在这一点上,我们是通过具有ojs_adm_admin角色的ojsaadm用户代理的,并且我们在错误的实例上(orcl而不是apver)。)我们已经有一个包和一个数据库链接来解决这个问题,所以我们将在ojsaadm.apver_usr_adm_pkg包中创建两个额外的过程,并使用它们通过数据库链接调用sys.appver_conns_role_pkg包。其代码如清单 12-51 所示。回头看看清单 12-50 的末尾,你会看到我们将sys.appver_conns_role_pkg上的执行授权给了ojsaadm用户。

清单 12-51。授予 Appver Conns 跨链路角色

`    PROCEDURE p_grant_apver_appver_conns( username VARCHAR2, proxyname VARCHAR2 )
    AS
        m_stmt VARCHAR2(100);
    BEGIN
        m_stmt :=
'BEGIN sys.appver_conns_role_pkg.p_grant_appver_conns_role@apver_link( :1 ); END;';
        EXECUTE IMMEDIATE m_stmt USING username, proxyname;
    END p_grant_apver_appver_conns;

PROCEDURE p_revoke_apver_appver_conns( username VARCHAR2, proxyname VARCHAR2 )
    AS
        m_stmt VARCHAR2(100);
    BEGIN
        m_stmt :=
'BEGIN sys.appver_conns_role_pkg.p_revoke_appver_conns_role@apver_link( :1 ); END;';
        EXECUTE IMMEDIATE m_stmt USING username, proxyname;
    END p_revoke_apver_appver_conns;`

注册申请按钮

在 Register New Application 屏幕上输入所有数据,并在 Oracle 数据库中创建一个代表性的应用内部类后,我们就可以注册应用了。这个代表性的内部类可能已经使用我之前描述的 Create App Class 按钮在数据库中创建了,或者您将需要执行一个CREATE OR REPLACE AND RESOLVE JAVA命令来创建它。

当选择注册按钮时,该事件调用registerButton_actionPerformed()方法。我们再次使用了一个延迟线程进程,并且在处理的时候显示了sayWaitDialog。简而言之,这个方法做了四件事。它创建了一个由用户条目描述的类的本地实例;它执行三个 insert 语句来注册应用的各个方面;它授予当前会话用户一个管理角色;它为新应用设置上下文,并调用方法在v_app_conn_registry视图中为未来的连接字符串列表创建一个占位符。在这个过程中没有双因素身份验证,也没有新应用的密钥交换。

清单 12-52 显示了用于创建所描述的应用内部类的本地实例的代码。我们丢弃任何尾随的点(。)上的包名。

清单 12-52。创建已定义类的本地实例

`    String innerClassName = packageTextField.getText();

**    if (innerClassName.endsWith("."))**

innerClassName.substring( 0, innerClassName.length() - 1 );
    innerClassName = innerClassName + "." + classTextField.getText() +
        "$" + innerClassTextField.getText();

Class classToRegister = Class.forName(innerClassName);

Object appClass = classToRegister.newInstance();`

images 注意为了注册应用,该应用内部类必须以编译形式存在于运行注册新应用屏幕的同一客户端上。应用内部类也必须位于当前的类路径中。

我们执行的第一个插入操作会将当前用户添加为该应用的应用管理员。插入命令如清单 12-53 中的所示。注意,我们使用客户机标识符的系统上下文值,我们已经将它设置为等同于 SSO 用户。他是应用的指定初始管理员,该应用与我们设置为第一个参数的完全限定内部类名相关联。

清单 12-53。插入应用管理员

    String updateString =         "insert into appsec**.v_application_admins "** +         "( class_name, user_id ) values ( ?, " +         "**SYS_CONTEXT( 'USERENV', 'CLIENT_IDENTIFIER' ))**";     PreparedStatement pstmt =         conn.prepareStatement(updateString);     pstmt.setString(1, **innerClassName);**     pstmt.executeUpdate();

这是一个动态更新语句,但是它不容易受到 SQL 注入的影响。这是一个参数化的预准备语句。我们在第八章中讨论并测试了这类语句的安全性。

我们做的第二个插入是到appsec.v_app_class_id视图中。我们将把完全限定的内部类名与特定的应用 ID 相关联。该插入命令的代码在清单 12-54 中提供。这也是使用预准备语句完成的。

清单 12-54。插入应用类别到 ID 关系

    String updateString =         "insert into appsec**.v_app_class_id** " +         "( class_name, application_id ) values ( ?, ? )";     PreparedStatement pstmt = conn.prepareStatement(updateString);     pstmt.setString(1, **innerClassName);**     pstmt.setString(2, **applicationIDTextField.**getText().toUpperCase());     pstmt.executeUpdate();

三个 insert 语句中的最后一个是我们在appsec.v_application_registry视图中的应用注册条目。该数据条目将该应用的代理用户与安全应用角色相关联。清单 12-55 中给出了插入命令的代码。

清单 12-55。申请登记入口

    String updateString =         "insert into appsec**.v_application_registry** " +         "( application_id, app_user, app_role ) values ( ?, ?, ? )";     PreparedStatement pstmt = conn.prepareStatement(updateString);     pstmt.setString(1, **applicationIDTextField.**getText().toUpperCase());     pstmt.setString(2, **applicationUserTextField.**getText().toUpperCase());     pstmt.setString(3, **applicationRoleTextField.**getText().toUpperCase());     pstmt.executeUpdate();

当我们在apver数据库实例的v_application_registry视图中创建一个条目时,我们需要在orcl实例的v_application_registry视图中有一个相同的条目。这很容易用数据库链接上的插入触发器来安排。(对于消息队列或流,也有更健壮的方法来做到这一点。)因为我们已经有了一个从apver实例上的appsecorcl实例上的apsec的私有数据库链接,所以我们需要的只是触发器。(之前我们已经使用了orcl_link从运行在apver实例上的过程中访问orcl实例上的HR模式中的数据。)清单 12-56 显示了我们将用来在orcl实例的v_application_registry视图中插入一条与我们在apver实例中插入的记录相同的记录的触发器。

清单 12-56。在应用注册表上插入触发器

`CREATE OR REPLACE TRIGGER appsec.t_application_registry_iar
    AFTER INSERT ON appsec.t_application_registry FOR EACH ROW
BEGIN
    INSERT INTO appsec.v_application_registry@orcl_link
    ( application_id, app_user, app_role ) VALUES
    ( :new.application_id, :new.app_user, :new.app_role );
    -- OJDBC will auto-commit this insert on orcl instance
END;
/

ALTER TRIGGER appsec.t_application_registry_iar ENABLE;`

我们希望给当前用户访问权限,以编辑他们刚刚注册的应用的连接字符串。为了完成这项任务,用户需要在apver实例上被授予appver_conns角色。除此之外,还可以通过avadmin用户进行代理。我们创建一个准备好的语句来调用存储过程sys.appver_conns_role_pkg.p_grant_appver_conns_role。这显示在清单 12-57 中。

清单 12-57。授予当前用户 Appver_Conns 角色

    String updateString =         "BEGIN sys.appver_conns_role_pkg.p_grant_appver_conns_role( " +         "SYS_CONTEXT( 'USERENV', 'CLIENT_IDENTIFIER' ) ); END;";     PreparedStatement pstmt =         conn.prepareStatement(updateString);     pstmt.executeUpdate();

最后,我们进行registerButton_actionPerformed()方法的最后一步。我们在v_app_conn_registry视图中为这个新应用使用的连接字符串创建一个占位符。为此,我们通过调用setAppContext()将上下文更改为新应用的上下文。然后我们执行getAppConnections()方法,接着调用putAppConnections()。如清单 12-58 中的所示,这一系列命令将在v_app_conn_registry中创建条目。

清单 12-58。将条目插入应用连接注册表

    OracleJavaSecure.setAppContext(         applicationIDTextField.getText().toUpperCase(), appClass, "");     OracleJavaSecure.getAppConnections();     OracleJavaSecure.putAppConnections();

当我们在清单 12-58 中设置新的应用上下文时,我们提供了用户输入的应用 ID,以及我们根据屏幕上的描述实例化的类和一个空字符串来代替双因素认证码。

images 注意我们还没有完成这个新应用的双因素认证和密钥交换。当我们用一个空的连接列表调用putAppConnections()时,Oracle 数据库并不回避解密和加密,而是为新应用插入一个空的连接字符串列表。

当这些步骤完成时,新的应用已经注册。我们仍然需要在这个应用的列表中输入一些应用连接字符串。我们可以在安全管理界面应用的另一个功能屏幕上这样做。

回想一下,在注册按钮被按下之后,关闭按钮变成退出按钮,退出 JVM。因为我们已经更改了应用上下文,所以我们不能返回到OJSAdmin菜单并继续使用OJSAdmin应用的连接字符串列表。

应用选择屏幕

在我们到目前为止讨论的功能屏幕中,我们已经完成了作为OJSAdmin应用所能完成的所有应用安全管理。我们甚至将我们的上下文设置为另一个应用的上下文,以便代表该应用并注册它。但是为了管理与不同应用相关的连接字符串,我们必须从该应用的身份开始。

在图 12- 7 所示的选择要管理的应用功能屏幕中,我们将从我们想要管理的已注册应用列表中选择一个特定的应用。列表显示在顶部的下拉框中,Manage Selected Application 按钮将在新的 JVM 中启动一个新的应用。因为实际上我们所有的安全结构,尤其是那些在OracleJavaSecure类中的,都是静态的,我们需要在一个单独的 JVM 中启动每个不同的应用,以便对我们的静态成员进行特定于应用的设置。

images

图 12-7。选择一个应用来管理功能屏幕

初始化应用列表

我们将运行 Pick 应用来管理屏幕,PickAppManage类,并通过一个由avadmin用户代理的连接。我们将拥有appver_admin角色,以便读取已注册的应用列表。在我们的dataInit()方法中,我们使用一个查询来获取应用列表。这个查询如清单 12-59 所示。请注意,在查询中,我们正在获取应用 ID 和相关的应用内部类名来表示应用。我们在屏幕顶部的下拉组合框中显示这个列表。

清单 12-59。查询获取注册应用列表

    stmt.executeQuery(         "SELECT DISTINCT **application_id || '/' || class_name** o FROM appsec.v_app_class_id "         + "where class_name in " +         "( select distinct class_name from appsec. v_app_conn_registry_filtered ) " +         "ORDER BY o");

我们尚未对其进行配置,但我们的目的是将此处显示的应用列表限制为当前用户被分配管理的那些应用。这将通过限制当前用户可以访问v_app_conn_registry_filtered视图中的哪些条目来实现。一旦我们限制了这个视图,那么清单 12-59 中的PickAppManage,查询也将受到where子句的限制。

选择管理选定的应用按钮

当您选择了要管理的应用并按下管理选定的应用按钮时,事件将运行manageAppButton_actionPerformed()方法。我们首先确保从列表中选择了一个应用,然后从所选项中解析出应用 ID 和应用内部类名。代码如清单 12-60 所示。

清单 12-60。解析选中的应用

    if (0 == appClassComboBox.getSelectedIndex())         return;     String appId = (String)appClassComboBox.getSelectedItem();     int place = appId.indexOf("/");     String appClass = appId.substring(place + 1);     appId = appId.substring(0, place);

有了这两个数据元素,应用 ID 和应用内部类名,我们就可以启动一个新的 JVM 作为这个特定的应用。我们计划使用我们在这里选择的应用的身份,而不是默认的身份OJSAdmin,来启动安全管理界面应用OJSAdmin的新实例。我们用来启动新的 JVM 和新的OJSAdmin实例的代码显示在清单 12-61 中。

注意在清单 12-61 的最后一行中,我们将运行OJSAdminmain()方法,我们将应用 ID 和应用内部类名作为参数提供。在我们讨论清单 12-4 中的Login构造函数和清单 12-11 中的OJSAdmin.ojsInit()方法时,我提到了我们如何获得不同应用的身份。在这里,我们看到了使用反射来创建应用内部类的实例的代码,该实例在参数二中被命名,我们将Login的应用 ID 设置为参数一的值。

清单 12-61。启动一个新的 JVM 和 OJSAdmin 的新实例

    Runtime rt = Runtime.getRuntime();     Process proc =         **rt.exec**("C:/Java/jdk1.6.0_24/bin/**javaw** -classpath " +                 "C:/dev/mywork/OraJavSec/Project1/classes;C:/dev/ojdbc6.jar " +                 **"orajavsec.OJSAdmin " + appId + " " + appClass)**;     BufferedReader stdError =         new BufferedReader(new InputStreamReader(**proc.getErrorStream**()));     String inLine;     while ((inLine = stdError.readLine()) != null) {         System.out.println(inLine);     }

我们得到一个新的Runtime实例,并调用它的exec()方法。exec()方法返回一个Process的实例,我们设置一个局部成员变量。通常,您可能会得到来自Process的输出和错误流,并且您可能会打印或处理它们。在我们的例子中,我们有意请求不要让我们的Process生成输出流。我们通过运行启动无窗口javaw.exe的 Java 运行时版本来做到这一点。注意可执行文件名称末尾的“w”。这样,我们没有得到一个输出流,也不必处理它。然而,我们选择将错误流打印到当前系统中调用新的Runtime.exec()的当前 JVM 之外。我们希望看到抛出的任何异常消息,如果我们不处理错误流,我们的进程可能会在等待我们处理时被锁定。

如果可以的话,我建议在您的公司环境中,在CLASSPATH设置中给出您的 javaw 可执行文件以及当前应用、OJSAdmin和 Oracle drivers jar 文件的完整路径。如果这对您环境中所有运行该代码的计算机都不起作用,您将需要为Runtime设置环境变量。您还可以考虑使用ProcessBuilder类来获得对环境设置的更细粒度的控制。您还可以考虑将这些路径放在代码附带的属性文件中。您可以从属性文件中读取这些特定值;但是,对于每个不同的客户端计算机环境,您将需要不同的属性文件。

您可能希望将这些设置放在属性文件中并从那里读取它们的另一个原因是为了更容易处理 JVM 的升级。当 java.exe 的路径从 jdk1.6.0_24 目录更改为 jdk1.6.0_28 目录时,您只需编辑属性文件。这假定在所有客户机上编辑一个文本文件比重新编译代码并向所有客户机分发一个新的 jar 文件更容易。这可能是个人偏好。

反对重新编译的一个理由是在编译过程中引入错误的可能性,但我从未见过仅仅在编译过程中发生这种情况,也许我们希望重新编译我们的类,以便从更新的编译器提供的任何效率中获益。也许这是基于经验或标准做法的个人偏好。

images 注意当新的运行时进程启动时,登录对话框将使用新的应用身份再次实例化。这将导致输入双因素身份验证代码的新提示,一个特定于所选应用的代码。

连接字符串编辑器

您可以在图 12- 8 中看到,一个 Oracle 连接字符串的所有元素都由编辑应用连接屏幕上的文本字段表示。我们在这里提供了两个功能按钮:一个用于将新描述的连接添加到列表中,另一个用于将列表保存到 Oracle 数据库中。

当我们选择按钮将连接添加到我们的列表时,更新连接字符串按钮,它只更新本地列表;它不在数据库中存储新的/更新的连接字符串。编辑或添加连接到本地列表后,选择保存列表按钮更新数据库。

images

图 12-8。编辑应用连接功能屏幕

初始化连接字符串列表

在编辑应用连接字符串屏幕的dataInit()方法中,我们调用了OracleJavaSecure类中的一个新方法listConnNames()。名单本身,connsHash是私有的,所以我们必须要求OracleJavaSecure提供Hashtable的名字,keys。清单 12-62 中给出了listConnNames()方法的代码。

清单 12-62。获取连接名称列表

    static Vector listConnNames() {         Vector<String> rtrnV = new Vector<String>();         try {             for (String key : connsHash.keySet())                 rtrnV.add(key);         } catch (Exception x) {}         return rtrnV;     }

这个方法有默认的访问修饰符,因为我们没有指定privatepublic。默认的修饰符是“包”可访问性。只有属于同一个包orajavsec的类才能执行这个方法。对我们来说幸运的是,OJSAdminorajavsec的包装中。

EditAppConns.dataInit()方法对这个新方法的调用如下所示。我们使用返回的向量来填充连接字符串名称的下拉列表。

Vector<String> connList = OracleJavaSecure.listConnNames();

选择一个已有的连接字符串

显示应用的现有连接字符串列表没有什么价值。我们不能编辑字符串的单个参数,比如密码,而让其他参数保持原样。这是因为我们不打算在这里解密和公开密码,尽管它会提供方便。通过显示连接字符串列表,我们可以看到输入多个字符串时的进度,并且可以确保至少更新条目的键名拼写正确。

当用户从列表中选择一个连接字符串名称时,我们用所选项中的数据库实例名称和用户名填充前两个文本字段。其他字段保持空白。

更新列表中的连接字符串

编辑应用连接字符串屏幕上的所有文本字段都被适当填充后,用户可以选择更新连接字符串按钮。这个动作将调用updateButton_actionPerformed()方法,其主要职责是调用OracleJavaSecure.putAppConnString()方法。这个调用如清单 12-63 中的代码所示。

清单 12-63。更新列表中的连接字符串

    connSuccessLabel.setText(**OracleJavaSecure.putAppConnString**(         instanceTextField.getText().toUpperCase(),         userTextField.getText().toUpperCase(),         **new String(passwordField.getPassword())**,         serverTextField.getText(),         portTextField.getText(),         **true**));

我们将文本字段值传递给putAppConnString()。对于密码字段,它是一个JPasswordField类型,为文本字段中输入的每个字符显示一个星号。我们无法将密码字段的文本作为String获取;相反,我们调用getPassword()方法来获取一个字符数组,从中我们实例化一个String来传递给putAppConnString()

当我们调用putAppConnString()时,我们将一个boolean集合作为最后一个参数传递给true。这告诉putAppConnString()测试生成的连接字符串,看看我们是否能够用指定的字符串连接到数据库。

我们修改了putAppConnString()方法,该方法使用这个 final boolean返回一个String,而不是一个void。以前,我们只向System.out流发送了一条成功或失败的消息。现在我们额外返回成功或失败消息。我们将该屏幕上的connSuccessLabel值设置为返回的消息值。在我们更新了列表中的连接字符串后,我们想在existingConnComboBox中刷新列表,所以我们像在dataInit()中一样调用OracleJavaSecure.listConnNames()来重新填充下拉组合框。

将连接字符串列表保存到数据库

在编辑应用连接字符串屏幕上要采取的最后一个动作是调用saveButton_actionPerformed()方法,这发生在选择保存列表按钮时。在这个方法中,我们简单地称之为OracleJavaSecure.putAppConnections()。那很容易!

连接字符串复制屏幕

如果您已经修改了应用内部类的innerClassRevLvl成员,以便用一组新的/修改过的 Oracle 连接字符串指定应用的新版本,那么您可以使用图 12-9 中的所示的复制到新版本屏幕,将以前的应用版本连接字符串复制到当前版本。

images 注意只有当前版本的连接字符串列表(即数据库中的列表)为空时,才能将连接字符串从先前版本复制到当前版本。

images

图 12-9。复制到新版本功能屏幕

复制到新版本屏幕非常简单。它有一个文本字段,用于输入将从中复制连接字符串的先前版本号,还有一个标记为复制现有连接字符串的按钮。当选择该按钮时,结果是调用OracleJavaSecure.copyAppConnections()方法,传递用户输入的先前版本号。

将某些管理员限制于某些应用

在我结束本章之前,让我详细说明一下我已经多次提到的事情:限制管理员可以管理的应用。您应该还记得,在我们讨论 Register New Application 屏幕时,我们创建了一个新表来指定新应用的管理员appsec.t_application_admins。除了维护表中数据之外,我们还没有对表做任何事情。

您可能还记得清单 12-48 中的,我们将管理员表上的某些特权限制为只有一个用户,osadmin。管理员的管理员工作可以扩展到更多的个人,但是我们没有创建一个角色来处理这个问题。这可能是一个很好的未来改进。我们不限制“插入”权限,因为我们将当前会话用户作为“注册新应用”屏幕中应用的初始管理员插入。

虚拟私有数据库

我们将如何使用这个应用管理员表?一种方法是使用 Oracle 数据库的一个特性,它有许多名称。这个特性是在一个名为DBMS_RLS,的数据库包中实现的,它代表“行级安全性”我想这可能是最好的名字,但是这个特性通常被称为“虚拟专用数据库”(VPD),就像在 Oracle 数据库安全指南文档中一样。您可能还会看到它被称为“动态 Where 子句”或“细粒度访问控制”,这取决于它的使用方式。

它相当于一个应用于表上的查询和执行命令的动态 where 子句,并且由数据库安全策略强制执行。这个特性的主要好处是,它透明地应用于所有指定的用户与数据表的交互,对于用户来说,没有办法绕过这个策略。

举个例子,假设我定义了一个策略,规定每次用户处理HR.EMPLOYEES表时,我们将应用动态 where 子句,“where employee_id = this_employee_id”,假设我们已经计算了this_employee_id。因为这个策略应用于表,所以即使 ID 为300的员工通过视图或过程(如AUTHID CURRENT_USER))选择或更新数据,他们仍然只能处理自己的记录,因为他们的employee_id, 300

images 注意使用模式特权而不是AUTHID CURRENT_USER操作的过程将被限制为模式用户可用的数据,而不是当前用户。但是,如果 VPD 策略通过用户身份的不同方面(如会话上下文参数)来限制访问,则用户通过这些过程的访问也会受到 VPD 策略的限制。

images 注意如果您以拥有该表的 SYS 用户或 schema 用户的身份连接,VPD 策略不适用于您对该表的访问。你可以看到一切,除非 VPD 函数依赖于会话上下文参数。

对于我们所有的应用连接,我们将把SYS_CONTEXT参数CLIENT_IDENTIFIER设置为 SSO 进程的用户 ID。我们希望将对appsec.t_app_conn_registry表的访问限制为那些 SSO 用户 id 在appsec.v_application_admins中作为特定应用的管理员列出的用户。这是容易的部分—我们的动态 where 子句将是:

WHERE class_name IN ( SELECT class_name FROM appsec.t_application_admins     WHERE user_id = SYS_CONTEXT( 'USERENV', 'CLIENT_IDENTIFIER' ) )

但是我们还希望允许管理员的任何管理员查看所有行。我们可以通过找出谁被授予了额外的特权来找出这些人是谁,比如在appsec.v_application_admins视图上的UPDATE。我们将把它添加到动态 where 子句中:

OR SYS_CONTEXT( 'USERENV', 'CLIENT_IDENTIFIER' ) =     ( SELECT GRANTEE FROM SYS.DBA_TAB_PRIVS         WHERE TABLE_NAME='V_APPLICATION_ADMINS'         AND OWNER='APPSEC'         AND PRIVILEGE='UPDATE'         AND GRANTEE=SYS_CONTEXT( 'USERENV', 'CLIENT_IDENTIFIER' ) )

该添加还将允许当前用户从appsec.v_app_conn_registry中选择行,其中当前用户确实在SYS.DBA_TAB_PRIVS数据字典视图中找到,并且具有对appsec.v_application_admins视图进行UPDATE的权限。如果这是真的,我们的动态 where 子句将返回所有行。这要求我们将数据字典视图上的 select,SYS.DBA_TAB_PRIVS授予将执行 VPD 代码的appsec用户:

GRANT SELECT ON SYS.DBA_TAB_PRIVS TO APPSEC WITH GRANT OPTION;

在这里的几个部分中,我们将在视图中使用一个类似的动态 where 子句,v_app_conn_registry_filtered。然后,我们将让其他用户从该视图中进行选择。这揭示了我们之前发放的资助的潜在局限性。在这种情况下,我们指定了WITH GRANT OPTION,好像appsec有兴趣将sys.dba_tab_privs视图上的 select 权限授予其他人。Appsec不会这样做,但她希望允许其他用户查看从数据字典视图中选择的数据。如果我们没有指定WITH GRANT OPTION,那么appsec将无法让其他人看到她在dba_tab_privs视图中看到的内容。没有指定WITH GRANT OPTION,授权不能转让给他人。

使用 VPD 的第一步是使用特定的模板表单创建一个函数,该函数将返回动态 where 子句作为一个VARCHAR2。清单 12-64 显示了封装了我刚才描述的动态 where 子句的函数appsec.apps_for_admin

清单 12-64。 VPD 管理员功能

CREATE OR REPLACE FUNCTION appsec.apps_for_admin(     m_schema_nm VARCHAR2,     m_table_nm  VARCHAR2 ) RETURN VARCHAR2 IS     rtrn_clause VARCHAR2(400); BEGIN     rtrn_clause :=     'class_name IN ( SELECT class_name FROM appsec.t_application_admins '     || 'WHERE user_id = SYS_CONTEXT( ''USERENV'', ''CLIENT_IDENTIFIER'' ) ) '     || 'OR SYS_CONTEXT( ''USERENV'', ''CLIENT_IDENTIFIER'' ) = '     || '( SELECT GRANTEE FROM SYS.DBA_TAB_PRIVS '     || '  WHERE TABLE_NAME=''V_APPLICATION_ADMINS'' '     || '  AND OWNER=''APPSEC'' '     || '  AND PRIVILEGE=''UPDATE'' '     || '  AND GRANTEE=SYS_CONTEXT( ''USERENV'', ''CLIENT_IDENTIFIER'' ) )';     RETURN rtrn_clause; END apps_for_admin; /

我们在apver实例上创建这个过程,并将其应用于t_app_class_registry表。清单 12-65 显示了创建使用该功能的策略的命令。请注意,我们仅将此策略应用于以下语句类型:INSERTUPDATEDELETE。我们认为这是对管理员的限制。还要注意,该策略被声明为STATIC。这给了它很高的速度,因为它将驻留在系统全局区 SGA 内存中并从那里执行。有关 VPD 的更多信息,请参见 Oracle 数据库安全指南文档。

清单 12-65。 VPD 管理员政策

BEGIN DBMS_RLS.ADD_POLICY (     object_schema => 'appsec',     object_name => 't_app_conn_registry',     policy_name => 'apps_for_admin_policy',     function_schema => 'appsec',     policy_function => 'apps_for_admin',     **statement_types => 'INSERT,UPDATE,DELETE',**     **policy_type => DBMS_RLS.STATIC );** END; /

如果我们能做到这一点,那么我们当然可以限制我们的应用用户可以使用哪些应用连接字符串列表。我们想像以前一样应用同样的where条款,但有额外的让步。如果用户被授予了通过与应用相关联的应用用户进行代理的权限,那么用户应该能够选择连接字符串列表。对where条款的补充如下:

WHERE class_name IN ( SELECT class_name FROM appsec.t_app_class_id '     WHERE application_id IN ( '         SELECT application_id FROM appsec.t_application_registry '         WHERE app_user IN ( '             SELECT proxy FROM ojsaadm.instance_proxy_users@orcl_link '             WHERE client = SYS_CONTEXT( ''USERENV'', ''CLIENT_IDENTIFIER'' ))) '

从里到外,where 子句查看我们在清单 12-29 中配置的代理用户的跨实例视图,获取所有已经授权给当前用户的代理用户,然后从t_application_registry表中选择与该代理用户相关联的所有应用,然后选择这些应用的所有应用内部类名。当前用户将能够从与那些应用内部类相关联的v_app_conn_registry中选择应用连接字符串列表。清单 12-66 显示了返回动态 where 子句的函数和将它应用于appsec.t_app_conn_registry表上的SELECT语句的策略。

清单 12-66。 VPD 为用户

`CREATE OR REPLACE FUNCTION appsec.apps_for_user(
    m_schema_nm VARCHAR2,
    m_table_nm  VARCHAR2 )
RETURN VARCHAR2
IS
    rtrn_clause VARCHAR2(400);
BEGIN
--    appsec.app_sec_pkg.p_log_error( 122, 'dave',
--    'appsec.apps_for_user: ' || SYS_CONTEXT( 'USERENV', 'CLIENT_IDENTIFIER' ) );

rtrn_clause :=
    'class_name IN ( SELECT class_name FROM appsec.t_app_class_id '
    || 'WHERE application_id IN ( '
    || 'SELECT application_id FROM appsec.t_application_registry '
    || 'WHERE app_user IN ( '
    || 'SELECT proxy FROM ojsaadm.instance_proxy_users@orcl_link '
    || 'WHERE client = SYS_CONTEXT( ''USERENV'', ''CLIENT_IDENTIFIER'' ))) '
    || 'UNION SELECT class_name FROM appsec.t_application_admins '
    || 'WHERE user_id = SYS_CONTEXT( ''USERENV'', ''CLIENT_IDENTIFIER'' ) '     || 'OR SYS_CONTEXT( ''USERENV'', ''CLIENT_IDENTIFIER'' ) = ( '
    || 'SELECT GRANTEE FROM SYS.DBA_TAB_PRIVS '
    || 'WHERE TABLE_NAME=''V_APPLICATION_ADMINS'' '
    || 'AND OWNER=''APPSEC'' '
    || 'AND PRIVILEGE=''UPDATE'' '
    || 'AND GRANTEE=SYS_CONTEXT( ''USERENV'', ''CLIENT_IDENTIFIER'' )))';
    RETURN rtrn_clause;
END apps_for_user;
/

BEGIN
DBMS_RLS.ADD_POLICY (
    object_schema => 'appsec',
    object_name => 't_app_conn_registry',
    policy_name => 'apps_for_user_policy',
    function_schema => 'appsec',
    policy_function => 'apps_for_user',
    statement_types => 'SELECT',
    policy_type => DBMS_RLS.STATIC );
END;
/`

VPD 最大的好处之一也是它最大的问题之一。问题是这可能很难处理。这是一个隐藏的限制,没有在授权或申请代码中列出。动态 where 子句在幕后应用,对用户是透明的。这个困难是一个特性,一个在一些安全计算环境中值得花钱购买的特性。如果你需要 VPD 类固醇,你可以收购甲骨文标签安全。

要禁用我们配置的 VPD 策略,您应该执行清单 12-67 中的语句。我打算这样做,因为我们可以采取不同的、更容易控制的方法来达到同样的效果。也禁用 VPD,因为在appsec包中作为“定义者的权利”运行的某些程序不能从v_app_conn_registry中选择,而appsec不能。

清单 12-67。禁用 VPD 政策

BEGIN DBMS_RLS.DROP_POLICY (     object_schema => 'appsec',     object_name => 't_app_conn_registry',     policy_name => 'apps_for_admin_policy' ); END; / BEGIN DBMS_RLS.DROP_POLICY (     object_schema => 'appsec',     object_name => 't_app_conn_registry',     policy_name => 'apps_for_user_policy' ); END; /

向过程添加动态 Where 子句

只有两个过程以我们关心的方式访问t_app_conn_registry表。这两个程序都在appsec.appsec_only_pkg包里:p_get_class_connsp_set_class_conns。在这些过程之外,只有SYSappsec模式用户帐户和通过avadmin(具有appver_admin角色)被授予代理的用户可以访问表或它的视图。

我建议不要使用 VPD 来限制数据访问,而是使用我们为 VPD 提出的相同动态 where 子句来修改相关的特定过程。结果程序如清单 12-68 所示。

清单 12-68。受动态 Where 子句保护的程序

`PROCEDURE p_get_class_conns(
        m_class_name         v_app_conn_registry.class_name%TYPE,
        m_class_version      v_app_conn_registry.class_version%TYPE,
        m_class_instance OUT v_app_conn_registry.class_instance%TYPE,
        m_connections    OUT v_app_conn_registry.connections%TYPE )
    IS BEGIN
        SELECT class_instance, connections
        INTO m_class_instance, m_connections
        FROM appsec.v_app_conn_registry
        WHERE class_name = m_class_name
        AND class_version = m_class_version

**        AND class_name IN ( SELECT class_name FROM appsec.v_app_class_id**
**        WHERE application_id IN (**
**        SELECT application_id FROM appsec.v_application_registry**
**        WHERE app_user IN (**
**        SELECT proxy FROM ojsaadm.instance_proxy_users@orcl_link**
**        WHERE client = SYS_CONTEXT( 'USERENV', 'CLIENT_IDENTIFIER' )))**
**        UNION SELECT class_name FROM appsec.v_application_admins**
**        WHERE user_id = SYS_CONTEXT( 'USERENV', 'CLIENT_IDENTIFIER' )**
**        OR SYS_CONTEXT( 'USERENV', 'CLIENT_IDENTIFIER' ) = (**
**        SELECT GRANTEE FROM SYS.DBA_TAB_PRIVS**
**        WHERE TABLE_NAME='V_APPLICATION_ADMINS'**
**        AND OWNER='APPSEC'**
**        AND PRIVILEGE='UPDATE'**
**        AND GRANTEE=SYS_CONTEXT( 'USERENV', 'CLIENT_IDENTIFIER' )));**

END p_get_class_conns;

PROCEDURE p_set_class_conns(
        m_class_name     v_app_conn_registry.class_name%TYPE,
        m_class_version  v_app_conn_registry.class_version%TYPE,
        m_class_instance v_app_conn_registry.class_instance%TYPE,
        m_connections    v_app_conn_registry.connections%TYPE )
    IS
        v_count INTEGER;
        v_count_able INTEGER;
    BEGIN         SELECT COUNT(*) INTO v_count
            FROM appsec.v_app_conn_registry
            WHERE class_name = m_class_name
            AND class_version = m_class_version;

**        SELECT COUNT(*) INTO v_count_able**
**            FROM appsec.v_app_conn_registry**
**            WHERE class_name = m_class_name**
**            AND class_version = m_class_version**
**            AND class_name IN (**
**            SELECT class_name FROM appsec.v_application_admins**
**            WHERE user_id = SYS_CONTEXT( 'USERENV', 'CLIENT_IDENTIFIER' )**
**            OR SYS_CONTEXT( 'USERENV', 'CLIENT_IDENTIFIER' ) = (**
**            SELECT GRANTEE FROM SYS.DBA_TAB_PRIVS**
**            WHERE TABLE_NAME='V_APPLICATION_ADMINS'**
**            AND OWNER='APPSEC'**
**            AND PRIVILEGE='UPDATE'**
**            AND GRANTEE=SYS_CONTEXT( 'USERENV', 'CLIENT_IDENTIFIER' )));**

IF v_count = 0 THEN
            INSERT INTO v_app_conn_registry ( class_name, class_version,
                class_instance, connections ) VALUES
                ( m_class_name, m_class_version, m_class_instance, m_connections );
        ELSE
**            IF v_count_able > 0 THEN**
                UPDATE v_app_conn_registry
                    SET class_instance = m_class_instance,
                    connections = m_connections, update_dt = SYSDATE
                WHERE class_name = m_class_name
                AND class_version = m_class_version;
            END IF;
        END IF;
    END p_set_class_conns;`

p_set_class_conns中,我们自由地允许插入,这将在新应用注册期间发生,但是如果这个应用的条目已经存在,我们将应用一个使用我们的管理动态 where 子句的测试。如果在使用动态 where 子句时找到记录(v_count_able > 0),那么我们允许更新记录。

向视图添加动态 Where 子句

为了完成对appsec.t_app_conn_registry表的 VPD 的模拟,我们需要限制对该表的视图appsec.v_app_conn_registry_filtered的选择所返回的行数。只有SYSappsec模式用户和通过avadmin获得代理权限的用户能够从视图中进行选择,但是我们也可以限制这些人看到的内容。我们的计划实际上只是限制通过avadmin代理的管理用户将看到的内容,因为SYS和模式所有者可以在不使用视图的情况下看到表中的所有数据。

该视图中的动态 where 子句从授予appsecsys.dba_tab_privs中选择。Appsec将该视图授予appver_admin角色。幸运的是,appsecdba_tab_privs被授予了WITH GRANT OPTION权限的 select,所以她可以允许其他人看到她在那个数据字典视图中看到的内容;否则,其他用户无法从v_app_conn_registry_filtered中选择,即使他们拥有角色appver_admin

appsec.v_app_conn_registry_filtered的定义包括一个动态 where 子句,它限制访问,就像我们在清单 12-68 中对p_set_class_conns的管理限制一样。我们不会增加允许应用用户选择他们通过代理授权可以访问的应用的权限。v_app_conn_registry_filtered的代码如清单 12-69 所示。您可能还记得,我们已经在一个功能屏幕中使用过该视图,即选择要管理的应用屏幕。回头看看清单 12-59 ,看看我们如何限制一个appver_admin用户可以从中选择的应用,以便管理连接字符串。这个视图限制了用户能看到什么,但是用户能做什么的限制是由我们添加到p_get_class_conns特别是p_set_class_conns中的动态 where 子句强制执行的。

清单 12-69。受动态 Where 子句保护的视图

`CREATE OR REPLACE VIEW appsec.v_app_conn_registry_filtered
AS
    SELECT * FROM appsec.t_app_conn_registry
    WHERE class_name IN (
        SELECT class_name FROM appsec.v_application_admins
        WHERE user_id = SYS_CONTEXT( 'USERENV', 'CLIENT_IDENTIFIER' )
    OR SYS_CONTEXT( 'USERENV', 'CLIENT_IDENTIFIER' ) = (
        SELECT GRANTEE FROM SYS.DBA_TAB_PRIVS
        WHERE TABLE_NAME='V_APPLICATION_ADMINS'
        AND OWNER='APPSEC'
        AND PRIVILEGE='UPDATE'
        AND GRANTEE=SYS_CONTEXT( 'USERENV', 'CLIENT_IDENTIFIER' )));

GRANT SELECT ON appsec.v_app_conn_registry_filtered TO appver_admin;`

脚本执行和代码编译

如果你已经运行了我们讨论过的第十二章的文件中的代码,那么你基本上已经完成了这一部分。您可以跳到我们讨论编辑 Java 代码的地方。否则,您需要执行以下脚本(注意它们之间有一些依赖关系):

作为orcl实例(您的主数据库实例)上的secadm用户,执行chapter 12/orclsecadm . SQL。在执行脚本之前,更改ojsaadmavadmin用户的密码。

orcl实例上执行 第十二章 /OrclSys.sql 作为SYS。将占位符操作系统用户 IDosadmin更改为您的操作系统用户 ID 或主要应用安全管理员。

作为orcl实例上的HR模式用户,执行第十二章 /OrclHR.sql

作为orcl实例上的appsec用户,执行第十二章 /OrclAppSec.sql

apver实例上执行 第十二章 /ApverSys.sql 作为SYS。在执行脚本之前,更改ojsaadmavadmin用户的密码。

作为orcl实例上的ojsaadm用户,执行chapter 12/orclojsaadm . SQL。在执行脚本之前,在数据库链接中更改apver实例上的ojsaadm用户的密码。

第十二章 目录中,编辑如下代码:

  • orajavsec/Oracle javasecure . Java的顶部,编辑expectedDomaincomDomainsmtpHostbaseURL的值。同样,在setAppVerConnection()方法中插入prime(在apver实例上appver用户的编码连接字符串)的正确值。
  • orajavsec/pickappmanage . Java中,编辑manageAppButton_actionPerformed()方法中的Runtime.exec()命令所使用的javaw.exe、你的类和 ojdbc.jar 文件的路径。
  • orajavsec/RegNewApp.java 中,编辑默认构造函数中avadmin用户的密码。另外,暂时取消对putAppConnString()putAppConnections()的调用。

images 注意avadmin密码,在RegNewApp.javaputAppConString()putAppConnections()的调用只是暂时的。在引导完OJSAdmin之后,请确保移除密码并注释这些方法调用。然后重新编译OJSAdmin

执行以下命令来编译OJSAdmin和所有相关的类:

javac orajavsec/OJ sadmin . Java

最后,在OracleJavaSecure.java的顶部取消对CREATE OR REPLACE AND RESOLVE JAVA SOURCE的命令注释,并在orcl实例(如appsecSYS)和apver实例(如SYS)上作为脚本执行该代码。

OracleJavaSecure 的最终更新

在前面的章节中,我们使用了几种不同的遗留方法:getAppAuthConn()和三种setConnection()方法。最初我们声明这些方法具有public可见性,但是现在我们使用getAAConnRole()公共方法来处理这些调用。所以我们将把那些遗留方法的访问修饰符改为private

还有一个方法我们还没有用到:removeAppConnString()。我们的目标是有一天从OJSAdmin应用中实现它。OJSAdmin驻留在orajavsec包中,就像OracleJavaSecure级一样。所以为了让removeAppConnString()OJSAdmin可见,我们将把访问修饰符改为 default (package)。

单个 Oracle 实例代码

如果你没有创建第二个 Oracle 实例来单独运行appver进程,那么你需要运行 第十二章单个中的代码。执行上一节列出的所有脚本和命令,除了没有 ApverSys.sql 脚本。您将使用的脚本名称的一个例子是chapter 12single/orclsecadm . SQL

您也将以类似的方式编辑和编译 Java 代码,除了您将编译和运行第十二章单个目录中的代码。

自举 OJSAdmin

我们需要一种方法来启动安全管理界面。我们将能够通过使用OJSAdmin启动所有未来的应用,但是对于OJSAdmin本身,我们将采取一些步骤来引导应用。你有没有意识到我们一直在引导我们的应用,从第七章到第十一章?没错,当我们通过对到putAppConnString()的几次调用和对putConnections()的另一次调用来运行每个应用时,我们正在启动泵,并使用这些密码字符串启动应用引擎。对putConnections()的调用在appsec.v_app_conn_registry表中输入了一条记录,我们可以从那时起使用这条记录。

在我们讨论了注册新应用的RegNewApp功能屏幕后,您可以得出结论,在本章中注册新应用变得更加复杂。这种复杂性部分是为了增强安全性-只允许某些管理员修改某些应用的连接字符串。一部分是为了允许这个安全管理接口管理多个应用;甚至更多,成为那些应用,以便编辑相关联的连接字符串。

不再是仅仅在v_app_conn_registry中插入一条记录就足以注册一个应用的情况;然而,这是最重要的一步。现在我们必须为v_application_admin中的应用指定一个管理员,并且我们必须将应用 ID 与v_app_class_id中的应用内部类名相关联。我们一直需要在v_application_registry表中建立一个条目,将安全应用角色与特定的应用 ID 和应用用户关联起来。

对于安全管理接口的OJSAdmin类,我们已经从脚本中输入了许多数据元素。对于orclapver实例,我们在v_application_registry中都有一个条目。我们在orclapver独立地将那些记录插入到v_application_registry中,但是将来我们将在apver上进行插入,并且它们将通过插入触发器自动插入到orcl中。我们还在v_application_adminsv_app_class_id中为OJSAdmin应用创建了条目。现在我们所需要的是v_app_conn_registry中的一个条目,它将包含我们的应用内部类和一个连接字符串列表(从空开始)。

我们将调用注册新应用功能屏幕的工具,以进入v_app_conn_registry。这就是我们经历先有鸡还是先有蛋的窘境的地方(顺便说一下,先有鸡)。在这种情况下,我们需要从v_app_conn_registry获取一个连接字符串,然后才能使用 Register New Application。同时,我们想使用 Register New Application 将连接字符串的初始(空白)列表插入到v_app_conn_registry中。

正如我们在前面章节中所做的,我们将引导应用连接列表。在第十二章,我们需要分两步来完成。和前面一样,为了引导应用,我们通过调用putAppConnString()将一个连接字符串放在客户端应用的内存中。我们在RegNewAppojsInit()方法中这样做,如清单 12-70 中的所示。我们也调用putAppConnections(),但是当数据库中没有现有的连接字符串列表时,这不起作用——此时,该调用不会将列表保存在数据库中。

清单 12-70。 Bootstrap 为 OJSAdmin 注册新应用

`    public RegNewApp() {
        try {
            jbInit();
            // First times through way to build conn string list, or use this tool

**            OracleJavaSecure.putAppConnString("apver", "avadmin", "password",**
**                "localhost", String.valueOf(1521));**
**            OracleJavaSecure.putAppConnections();**

conn = OracleJavaSecure.getAAConnRole("apver", "avadmin");
        } catch (Exception e) {
            e.printStackTrace();
        }
    }`

我们将在图 12-6 所示的注册新应用屏幕上输入一些代表性数据。不需要应用 ID、应用用户和应用角色的前三个条目——我们已经在脚本中输入了带有必要数据值的记录,并且我们还没有获得执行相关插入所需的appver_admin角色。但是,在前三个文本字段中输入ojsadminavadminappver_admin。输入正确的包orajavsec,并输入类名Login。此时会出现创建应用类按钮。点击创建 App 类按钮,创建 Java 结构。如果您输入了错误的包名,并且创建了不正确的类,您可以使用如下命令删除该类(不要这样做,仅供参考):

DROP JAVA SOURCE appsec."badPackageName/Login";

请注意,当您单击 Create App Class 按钮时,屏幕上的最后一个文本字段会自动填充上默认的内部类名LoginInnerRevLvlClass。填充所有字段后,您现在可以点击注册按钮。当你这样做时,注册新应用屏幕将实例化由你的包名和类名Login$InnerRevLvlClass描述的类。将会报告一些错误,因为该事件通常会将数据插入到v_application_registryv_application_adminsv_app_conn_id视图中。这些插入会失败,但是将应用内部类和一个空的连接字符串列表插入到v_app_conn_registry会成功。这就是引导过程!

关闭按钮变为退出,因为我们已经更改了应用上下文。此时可以选择退出按钮。您的应用已注册,但您仍需要将连接字符串插入列表,以便使用OJSAdmin的功能屏幕。再次启动OJSAdmin,并提供发送给您的双因素验证码。这似乎是一个奇怪的步骤,但是您需要再次进入注册新应用屏幕。这一次,当您进入时,putAppConnString()方法被调用,以再次将avadmin连接字符串添加到内存中的列表,但是这一次当我们调用putAppConnections()时,该连接字符串将被保存在驻留在数据库中的列表中。因为我们的连接字符串列表包含了avadmin连接字符串,所以我们使用appver_admin角色进入这个屏幕,并且我们可以UPDATE查看v_app_conn_registry视图。

images 注意你必须相当于osadmin,或者已经通过avadmin被授予代理权,并在v_application_admins中为该申请列出,OJSADMIN,才能完成这最后一步。

此时,您可以关闭屏幕并退出应用,或者您可以关闭注册新应用屏幕并继续我们的下一步。无论哪种情况,当你最终退出应用时,一定要从RegNewApp.java中的代码中删除引导密码。

完成安全管理界面应用的各种任务需要多个 Oracle 连接。到目前为止,我们在列表中只有一个连接字符串,所以我们需要添加其他的。运行OJSAdmin并点击选择要管理的应用按钮。在下拉选择框中,选择OJSAdmin应用OJSADMIN/orajavsec.Login$InnerRevLvlClass。您将收到另一个双因素身份验证提示;然而,这是同一个应用。如果自第一个双因素代码发送给您后不到 10 分钟,请输入该代码;否则等待新的代码。

您将看到OJSAdmin菜单屏幕底部的菜单项可用。选择“更新连接字符串”菜单项。输入两个额外的连接字符串,如表 12-2 中所列。

每次输入后,单击更新连接字符串按钮,并确保显示连接成功消息。输入两个条目后,单击 Save List 按钮将连接字符串的完整列表存储在数据库中。这就完成了OJSAdmin应用的配置,它的所有特性现在都可以使用了。在两个 JVM 中退出OJSAdmin,然后重新输入以重新加载连接字符串。

章节回顾

这一章也许写起来和读起来更有趣,因为它提供了我们一直在学习的概念的可视化表示。各种菜单和屏幕使管理应用安全基础设施变得更加容易和容易。此外,通过将规则编码到 GUI 应用中,我们不必记住所有的部分。

除了讨论功能屏幕的操作,我们还讨论了一般的 GUI 开发,特别关注单线程操作和延迟线程的使用以及SwingUtilities.invokeLater()方法。我们还探索了modal对话框,并根据需要使对话框和功能屏幕可见和不可见。

为了让这个安全管理界面应用工作,我们添加了两个表、几个包、一个数据库链接、两个用户和角色,以及各种其他 Oracle 结构。我们还为OracleJavaSecure添加了几个实用方法,并修改了几个现有的方法。开发人员的工作永远不会结束,那些实现本书中介绍的安全基础设施的人会找到修改和扩展它的理由。

当您阅读完这本书后,您会希望继续阅读源代码下载中包含的补充资料。在附录中,我们提出了几种方案,您可以将它们用作实现计算机安全的模板和指南。这些是我们将涉及的场景:

  • 测试新的应用、用户和管理员
  • 应对紧急情况–保护新的应用和数据
  • 更改所有密码
  • 审核和日志记录
  • 执行以资产为中心的风险评估
  • 定制安装和实施

我鼓励您在这些问题上努力,并希望您在使用 Oracle 和 Java 的应用安全性方面取得圆满成功!读完这本书后,您现在是 Oracle 和 Java 安全方面的专家了。

十三、附录 A:OracleJavaSecure类的方法列表

Image

Image

Image

Image

十四、附录 B:用于 Oracle 和 Java 安全性的 Oracle 过程、函数和触发器

Image

Image

Image

Image

Image

posted @   绝不原创的飞龙  阅读(27)  评论(0编辑  收藏  举报
相关博文:
阅读排行:
· 一个费力不讨好的项目,让我损失了近一半的绩效!
· 清华大学推出第四讲使用 DeepSeek + DeepResearch 让科研像聊天一样简单!
· 实操Deepseek接入个人知识库
· CSnakes vs Python.NET:高效嵌入与灵活互通的跨语言方案对比
· Plotly.NET 一个为 .NET 打造的强大开源交互式图表库
点击右上角即可分享
微信分享提示