八、单点登录
单点登录(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();`
这些类在两个客户端上都不可用,您只能在存在该类的系统上编译上面显示的代码。所以你应该在你的应用代码中只包含对NTSystem
或UnixSystem
的一个或另一个调用。您可以包含其中的每一个并注释其中一个来编译另一个,以便在适当的时候分发给那些客户端。
使用 Java 从 Windows Active Directory 读取当前用户身份还有其他方法,例如作为轻量级目录访问协议(LDAP)服务;但是,安全性和配置将因您的公司环境而异。我不会在这里讨论这种方法。
使用反射进行跨平台编码
这种“选择您的客户端平台”的方法并不令人满意,也不符合 Java“在任何地方运行”的目标。但是我们必须处理这样一个事实,即NTSystem
和UnixSystem
类不仅是特定于平台的,而且是跨平台不可用的。有一种方法可以解决这个问题:反射。有了反思,我们可以忘记那些讨厌的非此即彼import
语句和对平台不合适代码的评论。
有了反射,我们可以编写代码,用可能性而不是细节来编译。我们可能会在 Windows 平台上运行,在这种情况下,我们希望使用NTSystem
。但是也有可能我们运行在 UNIX 平台上,在这种情况下我们希望运行UnixSystem
。
有了反射,我们将加载一个平台合适的类,看不见(就像去相亲),然后我们将使用该类的资源。反射使用运行时类型自省来查找和使用特定类的属性。
清单 8-3 显示了使用反射从 Windows 操作系统获取用户身份的代码。UNIX 的代码类似,但是使用了UnixSystem
类而不是NTSystem
类。
注意你会在*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
语句编译代码。相反,我们引入了反射类Method
。Method
可以表示一个类中的任何特定方法。
接下来,注意我们没有像之前那样实例化一个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
必须返回我们期望的域名,否则我们不接受用户身份的主张。
注在单机系统上,域名可能只等于系统名。
使用 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 连接设置的身份特征。它可以用于很多事情,但是出于我们的目的,我们将把它设置为等于我们从NTSystem
或UnixSystem
获得的用户身份。
使用一个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;
注意你可以在名为 Chapter8/AppSec.sql 的文件中找到这个脚本。
if
语句中关于IP_ADDRESS
和SYSDATE
时间限制的前两个测试与我们在第二章中实现的相同。第三个测试确保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_USER
、just_os_user
与我们在客户端标识符中设置的值进行比较。
测试所有这些用户身份特征是相同的,并不能真正为我们带来更多的安全性,但却为潜在的黑客设置了另一个障碍。您可能会惊讶地发现,要防止非法闯入,往往只需要一个额外的障碍,而一个简单的额外障碍却被当作商业安全解决方案出售。
设置了客户标识符的审计活动
以下查询将显示设置了客户端标识符的连接的审计线索条目。你可能还没有,但是我们会在测试后看到这些联系。
SELECT * FROM sys.dba_audit_trail WHERE client_id IS NOT NULL ORDER BY TIMESTAMP DESC;
注你可以在名为 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
”
(
典型值
)
的
‘
O
S
A
U
T
H
E
N
T
P
R
E
F
I
X
‘
,并创建一个名为
‘
O
P
S
”(典型值)的`OS_AUTHENT_PREFIX`,并创建一个名为`OPS
”(典型值)的‘OSAUTHENTPREFIX‘,并创建一个名为‘OPSOSUSER的 Oracle 用户。
OSUSER用户在连接到 Oracle 数据库时不需要提供密码,但是通过操作系统的认证
OSUSER`就可以获得访问权限。设置它需要一些步骤,包括从 Oracle 数据库获得对目录或域服务器的访问权。这是单点登录的一种形式。
然而,我们正在做的是有意不同的。我们正在 Oracle 数据库中创建没有身份验证的个人用户。它们没有口令,标识的外部修饰符只是告诉 Oracle 数据库用户没有通过数据库的身份验证。
另一种方法是创建由随机密码标识的用户,管理员也不会保留该密码。如果没有人知道密码,就没有人可以用它进行身份验证。这样做的问题是,任何存在的密码都需要管理,并且至少要定期更改(更改为另一个随机密码)。
建立代理会话
要建立代理会话,我们要做两件事。首先,我们创建一个Properties
类(基本上是一个带有键和值的散列表,例如,key=PROXY_USER_NAME,value=OSUSER)。然后我们将这个Properties
类传递给OracleConnection
类的openProxySession()
方法,如清单 8-9 所示。这段代码来自于OraSSOTests
类的doTest2()
方法。另见main()
方法。
注意在名为*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 );`
注意,我们从NTSystem
或UnixSystem
获取操作系统用户身份到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 代理会话中,USER
、CURRENT_USER
和SESSION_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_USER
是appusr
(仅设置客户端标识符时)或者PROXY_USER
是appusr
(用于代理会话)。
接下来,我们对 IP 地址和SYSDATE
时间限制进行标准测试。然后我们还有两个测试,基本上保证这三个身份特征是相同的:
SESSION_USER = CLIENT_IDENTIFIER = OS_USER
代理此会话的用户与我们从NTSystem
或UnixSystem
获得的操作系统用户相同,并且与 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;
使用连接池
如果您非常关注桌面客户端应用,那么您可以跳过这一部分。连接池通常只在多线程、多用户服务器应用中需要。连接池是可供客户端或客户端线程使用的连接的集合。根据需要,客户机将从池中获取一个连接,使用它来查询或更新 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(连接字符串)、用户和密码(appusrConnURL
、appusrConnUser
、appusrConnPassword
),池中的所有连接都将拥有它们,如清单 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
,这是我们从NTSystem
或UnixSystem
获得的操作系统用户身份。当我们从cpool.getProxyConnection()
方法清单 8-12 中请求一个代理连接时,我们在请求中传递带有PROXY_USER_NAME
的Properties
。
***清单 8-12。*从 OCI 连接池中获取代理连接
prop.setProperty(OracleOCIConnectionPool.PROXY_USER_NAME, userName ); conn = (OracleConnection)cpool.getProxyConnection( OracleOCIConnectionPool.PROXYTYPE_USER_NAME, prop);
对于这个来自 OCI 池的连接,我们设置客户端标识符的方式与我们之前设置标准连接的方式相同。
查看代理会话
此时,如果查询OracleConnection.isProxySession()
方法,会发现这不是一个代理会话。不要让那打扰你。如果您查询 Oracle 数据库,您会发现PROXY_USER
是appusr
,USER
,CURRENT_USER
和SESSION_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 所示。获得池连接后,我们请求一个代理会话。将来自NTSystem
或UnixSystem
的用户身份作为属性值传递给OracleConnection
的openProxySession()
方法。同样,为了方便起见,我们只是在现有的 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.exe和javac.exe)。注释的用途多种多样,您可以通过定义自己的注释类型以及编写 Java 代码供 Java 工具在响应注释时使用来扩展它们。
Oracle JVM 不接受许多这样的注释,所以在 Oracle 数据库中创建 Java 结构之前,我们将在代码中对它们进行注释。
瘦客户端连接池摘要
瘦客户端连接池快速而方便。它支持我们通过代理和客户端标识符进行单点登录的方法。
看到用于连接池的关键方法被标记为“已弃用”有点令人不安但这不是路障。
通用连接池
UCP 是连接池领域的最新成员。因为它太新了,所以你应该关注并实现 UCP 的任何更新。
注意在名为*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 );
注意你可以在名为 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 );
结果是一样的——原来的OracleConnection
,conn
现在有了代理特征。
如果我们只传递 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
的方法。第一个将调用第二个,以便两者都配置带有客户端标识符集的代理连接。来自NTSystem
或UnixSystem
的操作系统用户身份用于代理和客户端标识符:
***清单 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 );** }
注意如果你要在
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 类NTSystem
和UnixSystem
来识别 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()
,它使用NTSystem
或UnixSystem
从操作系统获取用户 ID(用户名),视情况而定。使用这个操作系统用户 ID,我们通过一个同名的 Oracle 用户为我们的Connection
打开一个代理会话。
使用我们的代理会话,我们调用p_check_hrview_access
过程,确保我们的 SSO 凭证是正确的,然后设置安全应用角色hrview_role
。此时,我们已经完成了对 Oracle 数据库的 SSO 请注意,我们没有为特定的 Oracle 用户输入密码。然后,我们可以使用授予hrview_role
的特权从EMPLOYEES
表中选择敏感数据。
***图 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.sql 和 prvtmail.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`
注意你可以在名为 的文件中找到这些命令第九章 /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;
/`
注意你可以在名为*chapter 9/sec ADM . SQL .*的文件中找到清单 9-2 中命令的脚本
你会注意到这种语法与我们在调用过程时所习惯的完全不同。毕竟,这些只是对默认安装在 Oracle 数据库中的DBMS_NETWORK_ACL_ADMIN
包中的CREATE_ACL
和ASSIGN_ACL
过程的调用。在清单 9-2 中,我们使用命名符号指定这些程序的参数,与我们通常的位置符号相反。(我在这里给出了命名符号,因为我看到的每个例子都使用了那个符号;我们可能都只是复制 Oracle 文档中给出的例子)。
在命名符号中,分配给每个参数的值在定义的参数名称和赋值运算符=>
后给出,例如acl => 'smtp_acl_file.xml'
。当我们使用位置符号调用一个过程时,我们按照参数在过程中定义的顺序列出参数(因此得名位置);但是在命名符号中,顺序并不重要。
位置表示法和命名表示法之间的另一个区别是如何处理可选参数。您可能还记得,我们的错误记录表t_appsec_errors
,有两个可选参数msg_txt
和update_ts
。它们是可选的,因为我们用默认值配置了它们。update_ts
的默认值为SYSDATE
。在我们的p_log_error
过程中,我们从不为update_ts
提供值。我们需要默认的系统日期。
回到当前的主题:在位置表示法中,只有在所有必需参数之后定义的可选参数可以被忽略。使用命名符号,我们可以跳过提供可选参数,不管它们出现在参数定义中的什么位置,只要我们通过名称为所有必需的参数提供值。
在清单 9-2 中调用的两个过程、CREATE_ACL
和ASSIGN_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" ...
测试发送电子邮件
当我们在这里时,我们应该执行一个测试,以确保我们可以发送电子邮件。我们将作为我们的应用安全用户来执行测试。我很抱歉在这些账户之间跳来跳去,但我们正在委派任务,并在每一步检查我们的工作。
注意你可以在名为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 安全沙箱拒绝的活动。
首先,我们将让SYS
或DBA
将管理特定 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.java
p
o
l
i
c
y
policy
policy 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
发送短信。有短信聚合器将短信发送到由许多运营商处理的手机上。如果你已经付费使用了一个聚合器,那么你可以用它作为所有手机的运营商。
双因素分销渠道的安全考虑
双因素身份验证消息可以通过各种设备交付给我们的应用用户。我们将考虑通过手机、寻呼机和电子邮件传递信息。对于每一种设备,我们都必须考虑其安全隐患。我们倾向于将双因素代码发送到手机或寻呼机。只有当这些失败时,我们才会将代码发送到一个电子邮件地址。
电子邮件双因素传递的安全问题
电子邮件本身是一个相当安全的应用。它有密码保护,通常管理良好。但是,数据在传输过程中通常不受加密保护,而且冒充电子邮件发件人并以他们的身份发送邮件非常容易。只要你愿意过滤掉垃圾邮件,并根据需要通过其他方式验证发件人,使用电子邮件交换非敏感数据就不会有什么固有的问题。
注意我喜欢电子邮件,因为它让我们以一种非并发的、非同处一地的、人对人的或广播的方式进行交流。它还可以作为对话的战术记录和通信档案。
然而,双因素身份认证的整体理念是,我们要求用户拥有两种不同且独立的身份特征。是的,电子邮件和我们的 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
)的表格。在人力资源模式中创建此表。
注意你可以在名为 * 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
EMPLOYEES
和emp_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_id
和user_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 显示了潜在值。
我不会在这里展示它,但是在这个表上,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 。
注编辑在名为 的文件中找到的代码第九章/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.Date
或java.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 中的所示的格式Strings
和SimpleDateFormat
类是我们在 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_CHAR
和TO_DATE
函数;在 Java 中,我们使用SimpleDateFormat.parse()
和SimpleDateFormat.format()
方法。在 Oracle 数据库和 Java 之间交换日期的标准方法如表 9-2 所示。
用户有哪些路线?
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_cd
与v_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
并将nulls
与String.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.sql 、 SecAdm.sql 、 AppSec.sql 和 HR.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
用户设置合适的密码和连接字符串。
注意你可以在文件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.java和TestOracleJavaSecure.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 中。
***图 9-1。*双因素认证码流程