十、应用授权
每个 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 中。
注意你可以在名为 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_id
和app_user
列成为唯一的索引和主键。在我们到达第十二章之前,我们不会依赖那个键。现在,可以说每个应用可以使用多个安全应用角色。我们将通过代理各种应用用户来获得这些角色。所以一对application_id
和app_user
是获得一把app_role
的唯一钥匙。
现在,让我们插入一个带有已知标签的数据记录:用户APPUSR
和角色HRVIEW_ROLE
。我们将这些设置赋予HRVIEW
的application_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_id
从v_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) := ''; ...
注意作为
AUTHID CURRENT_USER
执行的过程和函数从不放在包中,它们通常被授予EXECUTE
到PUBLIC
。
添加仅供应用安全使用的 Oracle 包
我们将增加严格由我们的应用安全用户appsec
使用的函数和过程的数量,几乎增加一个数量级,因此我们将使用一个包来分组和组织它们。我们将这个包称为appsec_only_pkg
包。该包还允许我们在一个位置保护代码——在这种情况下,我们不会授权任何人在appsec_only_pkg
上执行。我们将删除函数f_is_cur_cached_cd
和f_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_id
和app_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
需要所有应用都可以执行。我们将授权PUBLIC
在p_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;
注意您可以在名为 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_nos
和p_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;`
注意你可以在名为*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 数据库进行对话,我们需要使用用户名和密码进行连接。我们将花大量时间解决这个问题。
简而言之,我们将如何进行应用授权,我们也称之为应用验证:
- 我们将首先通过新用户
appver.
代理连接到 Oracle - 与过去一样,我们需要通过 SSO 和双因素身份认证要求。
- 一旦我们获得了双因素认证,我们就交换加密密钥。
- 我们还检索了一个加密的连接字符串列表,可以在当前的应用中使用。
- 当我们使用其中一个应用连接字符串时,我们再次保证我们的 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;
注意你可以在名为 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 v
s
e
s
s
i
o
n
.
S
I
D
p
s
e
r
i
a
l
v
session.SID%TYPE; p_serial v
session.SID pserialvsession.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;`
注意你可以在名为 Chapter10/Sys.sql 的文件中找到清单 10-15 的函数。
该功能的核心在EXECUTE IMMEDIATE
命令中。按照这些思路,另一个可行的方法是,我们将被杀死的SID
和SERIAL#
插入到一个表中。然后使用一个独立的预定任务,读取表并终止会话,然后从表中删除记录。
反正用最直接的方式粗暴对待是行不通的。这样也好,因为我们对p_appver_logon
到RAISE_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;
注返回文件 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.EMPLOYEES
和HR.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
)数据类型,其中包含一个相关连接字符串的列表。
正如您可能想象的那样,很难对RAW
或BLOB
类型进行索引和选择。所以我们将类名和类版本作为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() );
注意你可以在名为 Chapter10/AppSec.sql 的文件中找到这个脚本。
我们在class_name
和class_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();
}`
注意你可以在名为*chapter 10/orajavsec/revlvlclassintfc . Java .*的文件中找到这段代码
接口中有一个方法,getRevLvl()
。这将返回一个修订级别,innerClassRevLvl
,我们可以用它来支持一个应用的多个版本。例如,如果您将应用数据移动到新的 Oracle 表或新的 Oracle 数据库,您可以更新内部类中的innerClassRevLvl
,并使用我们的进程注册更新的应用,并生成一组与之相关的新连接字符串。旧的应用/版本和新的应用/版本将能够同时获得它们各自的连接。此外,为了强制每个人迁移到应用的新版本并禁用旧版本,我们可以从应用注册表中删除旧版本的内部类,或者只删除相关的连接字符串列表。
我们将要求该接口的实现者(开发人员)也提供一个名为serialVersionUID
的static long
,用于对象序列化(打包以存储在数据库中并通过网络传输)。)我们稍后将对此进行更多讨论。
在 OracleJavaSecure 中实现内部类
出于本章测试的目的,我们将在实现RevLvlClassIntfc
的OracleJavaSecure
类中定义一个内部类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; } }
注意在本章余下的大部分时间里,我们将反复引用名为*chapter 10/orajavsec/Oracle javasecure . Java、*的文件中的 Java 代码,以及名为 Chapter10/AppSec.sql 的文件中的 SQL 和 PL/SQL 代码。
注意,我们的内部类被指定为public static
。static
不是必需的,但是它向我们保证当父类OracleJavaSecure
被实例化时,这个类是存在的。另一方面,从安全角度来看,public
标志意义重大。我们更愿意将这个内部类私有,我们可以为OracleJavaSecure.InnerRevLvlClass
这样做。但是对于其他独立的应用,内部类需要是公共的,以便 Oracle 数据库上的OracleJavaSecure
可以实例化它们。
我们的内部类也必须实现Serializable
接口(我们提供给开发人员的模板的另一个必需元素。)通过实现Serializable
(不需要任何方法),我们确保可以获取内部类,获取对象的字节,并将它们传递给 Oracle 数据库进行存储。如果对象实现了Serializable
接口,我们只能用那种方式处理对象。
应用交给我们的类的serialVersionUID
必须与 Oracle 数据库中的类定义的serialVersionUID
相同(这个讨论将在下一节继续)。我们为serialVersionUID
设置了一个静态成员变量,但是如果我们没有这个成员,JVM 会计算一个值。如果该值不相同,则类无法实现,对象无法实例化,并将引发异常。大多数情况下,除非我们对类进行结构性修改,否则计算出的serialVersionUID
将是相同的,但是“大多数情况下”的统计数据会带来我们不愿意承担的风险。
注意,我们为innerClassRevLvl
和serialVersionUID
提供的值几乎是我们想要的任何值;然而,我认为对于任何一个日期具有多值灵活性的日期形式将会是一个好的值。如果我在 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 数据库中反序列化对象,在一个应用不运行的地方。要做到这一点,我们必须有类定义;因此,对于每个应用,我们将存储内部类定义(我们将在后面看到如何做)。对于内部类OracleJavaSecure
的InnerRevLvlClass
,类定义在 Oracle 数据库中,因为我们在服务器上创建了OracleJavaSecure
。对于其他应用,我们必须在 Oracle 数据库上创建内部类。
在对象序列化和反序列化之间的过渡阶段,可能会对类定义进行一些非结构化的更改,Java 编译器和运行时版本可能会继续发展;但是我们应该能够,甚至十年后,反序列化我们的对象。这就是serialVersionUID
成员变量发挥作用的地方。在开始使用我们的序列化对象时,根据 suid (当serialVersionUID
成员不存在时,该值的另一个名称)的运行时计算,我们可能没有问题。)然而,随着时间的推移和事物的变化,我们可能会开始经历我们序列化的内容和我们试图反序列化的内容之间的不匹配。
我们可以通过简单地编码一个serialVersionUID
成员,而不是依赖于运行时计算,来避免串行版本不匹配的问题(由技术进步而不是我们的类的变化引起的)。
如果我们对我们的类定义做了结构性的修改,我们将需要做下面两件事:
- 更改我们的类定义中的
serialVersionUID
值。 - 在 Oracle 中存储新版本的类定义。
注意,这种情况不同于我们只在类定义中编码一个新的innerClassRevLvl
的情况。我们将这样做来处理应用版本的更改(并提供 Oracle 连接字符串的更新列表。)
设置应用上下文
您可能还记得,在上一节的讨论中,我们将所有特定于应用的元素都集中在一个地方。我们将把applicationID
、appClass
和twoFactorAuth
代码作为静态类成员存储在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 ) {} } }
最后,如果我们决定喜欢这个连接字符串(testSuccess
是true
,我们就把它添加到connsHash
。并且,就像我们(可能)从 Oracle 收到的连接字符串一样,我们基于从 Oracle 数据库获得的共享密码密钥,以加密的形式将其存储在这里。
HashMap
、connsHash
的关键字是字符串“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
。然而,它一开始看起来确实有点令人生畏。深入这段代码,我们将我们的对象写入一个ObjectOutputStream
、oout
。ObjectOutputStream
直接连接到ByteArrayOutputStream
、baos
。在我们写完我们的对象后,我们刷新oout
并关闭它,确保我们的整个对象都被提交到baos
。此时,我们调用baos
的toByteArray()
方法,以字节数组的形式获取对象。
***清单 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 ) {}
}
}`
我们在应用内部类appClass
和connsHash
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
来传递连接HashMap
。f_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。
显然,我们不希望任何人都向此函数提交对象,因此我们需要通过以下一种或多种方式来保护它:
- 从
PUBLIC
中撤销f_set_decrypt_conns
上的GRANT EXECUTE
,仅向需要访问的用户(操作系统用户名)添加授权。 - 保护用于完成此功能的代码—将其分离到一个管理应用中。
- 在这段代码中实现一些额外的测试,可能是在一个 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()
的一部分是从我们称之为classInstance
的RAW
中获取一个byte
数组。我们将那个byte
数组输入到一个ByteArrayInputStream
、bAIS
。然后我们实例化一个ObjectInputStream
、oins
,耦合到bAIS
。我们调用oins
的readObject()
方法来重建一个名为classObject
的Object
。关闭流之后,我们得到了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 所示。然后我们调用classMethod
的invoke()
方法,传递我们在清单 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 (
)
−
o
u
r
p
l
a
n
n
e
d
r
e
q
u
i
r
e
m
e
n
t
s
i
f
(
−
1
=
=
c
l
a
s
s
N
a
m
e
.
i
n
d
e
x
O
f
(
"
.
"
)
∣
∣
c
l
a
s
s
N
a
m
e
.
i
n
d
e
x
O
f
(
"
) - our planned requirements if( -1 == className.indexOf( "." ) || className.indexOf( "
)−ourplannedrequirements if(−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;
,但是如果不相等,我们将处理一个冒名顶替者。
一些开发人员的困惑和解决方案
不幸的是,我们的应用开发人员可能成为他们自己行为的受害者。如果开发人员改变了内部类的代码而没有改变版本号,这将导致我们的相等性测试失败。在这种情况下,开发人员应该更改他的内部类中的serialVersionUID
和innerClassRevLvl
,通过我们的进程注册它,并创建一个新的连接字符串列表或从以前的版本中复制连接字符串。
可以说,应用开发人员可能搬起石头砸自己的脚的另一种方式是在代码中移动他的内部类。例如,如果他把他的内部类移到公共类定义之外,它就变成了外部类,或者如果他把它从类的主体移到一个方法中(从技术上来说,这是一个完全可以接受的移动),那么包和类名就会改变以反映这种移动。在这些情况下,内部类将被视为一个新的实体并被注册,但在开发人员为新版本重建列表之前,它不会有任何关联的连接字符串。在这种情况下,他不能从以前的版本中复制他的连接字符串列表,因为这被认为是一个新的类,在一个新的路径中被发现(它甚至可能与以前的类具有相同的版本号)。
获取应用 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
是一个两步过程。我们分别得到了RAW
和BLOB
的字节。然后我们在流中移动字节,就像我们之前看到的那样,来重组我们的对象。清单 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%TYPE
将m_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_conns
从v_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 安全编程实践。
为了让开发人员更容易使用该应用,我们给了开发人员一些模板,让他们可以遵循这些模板来完成所有工作。我们让他做以下四件事:
- 在他的
CLASSPATH
中包含 OracleJavaSecure.class 和 RevLvlClassIntfc.class 文件(我们提供一个 jar 文件)。 - 编写实现
RevLvlClassIntfc
和Serializable
的应用 ID 内部类。 - 调用
OracleJavaSecure.setAppContext()
方法,传递他的应用名、ID 类和双因素身份验证代码(是的,他必须处理从第一个请求到使用双因素代码进行第二次调用的循环)。 - 通过调用
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
无效。出于这个原因,我们将修改几个方法来测试null
的sessionSecretDESPassPhraseChars
,而不是测试null
的sessionSecretDESKey
:getCryptData()
,getDecryptData()
和方法集getCryptSessionSecretDESPassPhrase()
/ Algorithm
/ Salt
/ IterationCount
。我知道有些人希望我不要详细描述陷阱、测试和其他场景,但是这里的目标不仅仅是开发一个应用,还包括开发一个理解。如果我们不在这里探讨这些问题,您将不得不自己去发现它们,这并不坏,但会很费时间。
获取连接字符串列表
从作为应用connsHash
对象返回的RAW
字节中,我们通过将字节传递给ByteArrayInputStream
和ObjectInputStream
来生成对象。如果结果对象不为空,我们将该对象转换为一个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 )来保留应用验证连接。
获取应用连接字符串列表:服务器端
在这里,我们看到了带有十几个IN
和OUT
参数、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 阶段
和上一章一样,这里我们再次需要编辑代码,为双因素身份验证提供我们公司特定的地址。
注意编辑在名为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.sql 和 SecAdm.sql 文件中的所有命令。按照这个顺序执行命令,因为 SecAdm.sql 中存在依赖关系。
注意你可以在名为 Chapter10/AppSec.sql 和 SecAdm.sql 的文件中找到这些脚本。
检查测试步骤
为了测试我们的应用身份验证,我们将采取以下步骤:
- 设置我们的应用上下文:应用 ID、内部类和双因素代码。
- 调用
getAppConnections()
来获取这个应用的连接字符串列表——第一次将注册我们的应用。 - 调用
putAppConnections()
将我们的连接字符串列表上传到 Oracle。 - 调用
getAppAuthConn()
以获得在该应用中使用的特定连接。 - 使用连接从 Oracle 获取数据。
对于测试的第一阶段,我们将在OracleJavaSecure
的main()
方法中完成所有这些步骤。
我们将至少运行这个测试代码两次。第一次,我们将没有一个有效的双因素认证码,所以我们将在某一点退出程序。简而言之,我们将在得知一个双因素代码被发送给我们之后,并且在我们尝试使用与该应用相关联的连接字符串之前退出。
设置应用上下文
如果我们有一个双因素身份验证代码,我们应该在命令行上将它作为应用的参数传递给应用。或者,如果您从 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 的存根进行比较。
注比较名为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()
方法中定义它)。注意,在任何情况下,都应该保留public
和private
修改器。我们希望内部类被声明为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
创建存根内部类。
注意在 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()
的调用。
注意编辑在名为*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 连接。
***图 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
返回给客户端应用使用。
***图 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)的这些序列被选择来混淆。它们都以一个字母字符开始,以符合编译器。这一步模糊处理的最大损失是这些方法和成员的有意义的名称的损失。
经过这些翻译,我们的代码几乎无法辨认。只是代码结构看起来像 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 连接字符串
我们将向OracleJavaSecure
的main()
方法添加代码,如清单 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
注意在 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.ora 和 tnsnames.ora 文件进行一些补充。TNS 之于 SQLnet (Oracle 的数据库网络通信协议)如同域名服务(DNS)之于 TCP/IP。TNS 允许我们为单个 Oracle 数据库实例使用多个名称(别名),并在编写应用时引用别名,这些别名可以在不同的时间指向不同的 Oracle 实例。这种灵活性是命名服务的主要原因;另一个主要原因是进行远程查找(不在本地存储所有的姓名和地址)及其必然的原因:集中管理姓名/地址关联。当然,我们需要为 TNSNames 服务使用 LDAP 或类似的东西来实现第二个目标。
TNSNames 服务有许多我们在本书中没有涉及的特性。有关更多信息,请参考 Oracle 数据库网络服务参考手册。
sqlnet.ora 和 tnsnames.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
注意将
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_appver
是 tnsnames.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)))`
注意至少主机名在你的情况下会有所不同,所以修改那些设置。
使用 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
警告对于 Oracle client wallet 文件,攻击者可以使用您在任何实例上配置的指定用户进行连接,只要该用户存在并具有相同的密码。
也许您的用户只存在于一个实例上,或者在每个实例上只有完全相同的特权;然而,更有可能的是,用户在较低优先级(沙盒、开发或验收)区域上比在生产区域上具有扩展特权。任何实例上的这些扩展权限都可能提供额外的攻击媒介。
跟踪 Oracle 客户端代码
在开发过程中,您经常会有这样的经历:事情不顺利,您的应用无法告诉您问题出在哪里。错误发生在底层协议的某个地方,并且是隐藏的或模糊的。您甚至可能会看到误导性的错误消息。
在这种情况下,您可能需要调用后备资源,要求您的网络管理员在您的子网上放置一个网络嗅探器,并捕获数据包进行分析;希望你能发现问题。
但是,在此之前,在处理 Oracle 数据库时,您自己也有一些选择。您可以在客户端打开跟踪,查看客户端和 Oracle 数据库之间底层协议对话。只需在客户端 sqlnet.ora 文件中设置跟踪级别。值 16 是最大细节追踪;您也可以在级别 8、4、2 和 1 中选择较少的细节。
TRACE_LEVEL_CLIENT=4
警告当您完成当前问题的故障排除后,请确保禁用跟踪日志记录。它会生成大量文件,这些文件可能包含大量数据,这些数据不仅会占用大量磁盘空间,还会带来安全隐患。在更高级别的跟踪日志记录中,从查询返回的数据也会显示在跟踪文件中。
跟踪文件(有几个)的默认位置之一是一个如下命名的文件夹:
%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
注意你会在*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
级别为中等细节。其他级别设置在FINE
和TRACE_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
。
注意你会在一个名为 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
修改。
注意我们刚刚把这个过程变成了特定应用的特定版本所独有的(呈现内部类的那个)。
***清单 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_raw
、app_key
和iv
。我们还告诉该函数使用 256 位高级加密标准(AES256 ),采用块链接和 PKCS 填充。
哒哒!我们有一个加密的连接字符串列表,可以存储在磁盘和备份中。我们解密数据的f_unmask
函数几乎与f_mask
相同。我们以完全相同的方式构建app_key
和iv
,然后使用相同的加密算法系列将加密的连接字符串列表传递给DBMS_CRYPTO.DECRYPT
函数。瞧,我们已经从冷库中取出了明文连接字符串。
包装工具
甲骨文公司几十年来一直致力于保护其知识产权。该公司开发了一个流程,通过该流程,它可以发布业务敏感的 PL/SQL 代码,就像我们的 Oracle 过程、函数和包一样,并将其分发给客户,而不会暴露代码的内部工作方式。Oracle 设计了 wrap 实用程序,它将混淆 PL/SQL 代码,使其无法被读取。我只能说,包装好的过程不经过一番努力是无法读懂的,因为据称有一些工具可以解开过程。
我们将使用 wrap 实用程序来混淆f_mask
和f_unmask
函数。提醒一下,您应该首先修改f_mask
和f_unmask
,使它们对您的公司是唯一的,然后包装它们。那就是避免这本书明显的信口开河把你的船弄沉。
将你的 F_MASK.sql 和 F_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);**
注意这段代码可以在*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_mask
和f_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’;`
该准则的重要方面如下:
- 我们使用与新操作系统用户 ID 相同的名称创建一个 Oracle 用户。
- 我们授予新用户对
CONNECT THROUGH appusr
的权限,这样他就可以从HR
模式中选择加密的敏感数据。 - 我们在
HR.EMPLOYEES
表中为这个用户插入一条记录,并从EMPLOYEES_SEQ
中获取下一个连续值作为新的EMPLOYEE_ID.
- 我们可以根据
EMAIL
地址选择新用户,这是一个唯一的字段。 - 使用这个选择(为了简单起见),我们可以将一个匹配的记录插入到
hr.v_emp_mobile_nos
中,并将user_id
字段设置为新的 OS 用户 ID。 - 我们通过选择刚刚从
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
。该命令可以作为SYS
或appsec
用户运行。
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 连接,那么为新版本从头开始重新构建列表是没有问题的。但是,如果应用有许多可以从中提取数据的潜在连接,那么将连接字符串从应用的一个版本复制到下一个版本的能力将是一个受欢迎的特性。
应用客户端调用复制连接字符串列表
从客户端应用中,我们将调用OracleJavaSecure
、copyAppConnections()
中的一个新方法。它有一个参数,旧的内部类版本名。当在 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
角色的用户执行。
***图 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.plb 和 Chapter11/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_role
和app_sec_role
;相反,我们将作为SYS
用户完成所有的配置步骤。
警告如果您不打算为应用验证创建额外的数据库实例,则不要发出本节中的任何命令。可以直接跳到测试增强安全性;但是,要了解如何创建和强化 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"
注意这里我们会得到一个无法启动服务的错误。没关系,因为我们还没有真正创建数据库,但是现在启动它的服务已经配置好了。
编写创建数据库命令
我们将把数据库创建命令放在一个名为ApVerDBCreate.sql
的脚本文件中。CREATE DATABASE 是一个单独的命令,但是它有许多方面,我们不想依靠我们的输入技能在 SQL*Plus 提示符下正确地输入所有内容。此外,我们需要一个脚本文件,这样,如果我们对发出的命令有任何疑问,就可以参考它。
关于我们的数据库创建脚本,我想让你注意的第一件事是我们没有为SYS
和SYSTEM
硬编码密码(我们在清单 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
注该命令包含在一个名为 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 ,它由一些数据库组件使用,这些组件在历史上被放置在单独的表空间中。此外,我们为USERS
、TEMPORARY
和UNDO
表空间创建默认的表空间文件。确保给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`
注意这些命令可以在文件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 \ apver和D:\ app \ Oracle \ flash _ recovery _ area \ apver。如果您没有得到“数据库已创建”的成功消息,您可能需要删除这些目录中的文件并重新开始—这可能是初始化或命令文件中的打印错误,您需要修复。
更改系统和系统用户的密码
即使不作为 SYS 用户登录,我们也可以而且必须为 SYS 和 SYSTEM 用户设置密码。在以下每个命令中替换复杂密码:
ALTER USER SYS IDENTIFIED BY sys_password; ALTER USER SYSTEM IDENTIFIED BY system_password;
注意这是安全步骤一。在完成这一步之前,请不要继续。
在数据库中存储数据库参数
将我们的参数设置从 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;
注意这些假脱机日志文件将被创建在命令提示符的当前目录中。
增加流程数量
您可能还记得在 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_size
和shared_pool_size
分别设置为 150M。从 Oracle 数据库 11g 开始,不再需要这样做。这些分配是作为 11g 自动内存管理的一部分自动处理的。
您可以通过从数据库中选择这些参数来查看它们的当前设置。
SELECT NAME,VALUE FROM V$PARAMETER WHERE NAME IN ('SHARED_POOL_SIZE','JAVA_POOL_SIZE');
您将看到在 11g 中,这些参数都被设置为 0。所需的内存将由全局MEMORY_TARGET
和TARGET_SGA
设置自动提供。回想一下,当我们装载数据库时,我们看到了一个关于整个系统全局区域的报告。这是数据库中可供 Java 部分使用的内存。
许多脚本用于配置和启用 Oracle JVM。我们将只运行其中的两个,即我们的应用验证安全流程所需的两个。在通常列出的将 Java 构建到 Oracle 数据库的五个脚本中,我们将只执行 initjvm.sql 和 catjava.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)连接到作为SYS
的apver
实例;但是,使用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
配置创建会话角色和应用安全用户
我们现在应该能够从本地命令提示符或远程会话连接到作为SYS
的apver
实例。继续以SYS AS SYSDBA
的身份连接,这样我们就可以创建应用验证所需的结构。
注意这些命令可以在文件*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;
注意这些命令可以在文件*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_USERS
、ALL_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;`
注意如果这正在生产中使用,您将在撤销不适当的更宽权限之前授予适当的窄权限(如图所示),以便在更新期间保持适当的功能工作。
为应用授权创建剩余的结构
用于apver
实例的 NewSys.sql 脚本的剩余部分配置了我们进行应用授权所需的所有结构。大多数结构都是在appsec
模式中创建的。作为SYS
用户,我们只需在我们正在创建的结构名前面加上模式名appsec.
我们作为SYS
用户这样做是为了防止将管理权限授予任何其他用户,甚至是appsec
。这是最安全的,但是对SYS
密码的控制是强制性的。
在运行脚本之前,用您创建和包装的版本复制并粘贴包装的函数f_mask
和f_unmask
。表 11-2 按创建顺序列出了我们将要创建的结构。
注意因为我们没有在
apver
实例上创建secadm
用户,所以我们不能在secadm
模式中创建t_screen_appver_access
触发器。我们将在appsec
模式中创建它。
脚本中有几个地方需要用真实的操作系统用户 ID(即 Windows 登录名)替换占位符。搜索并替换OSUSER
和OSADMIN
。OSUSER
是一个人(或多人)想要运行我们的安全应用。OSADMIN
是一个像您一样的人,他需要连接以便在数据库中注册应用连接字符串。
在apver
上,几个存储过程穿过数据库链接、f_is_cur_cached_cd
和p_get_emp_2fact_nos.
在那些结构中,我们看到引用类型(类似于os_user in
清单 11-52 )被转换为标准类型声明;我们不能通过数据库链接引用。查看清单 11-52 中SELECT
语句的FROM
子句。我们通过数据库链接从三个视图中获取数据:v_emp_mobile_nos@orcl_link
、v_employees_public@orcl_link
和v_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
实例。
注意这将只在
ORCL
实例上进行,并且只有在您安装第二个数据库实例apver
来进行应用验证时才进行。
ALTER USER appver PASSWORD EXPIRE;
测试增强的安全性
我们现在可以测试我们已经建立的一切,包括在单独的 Oracle 实例apver
上隔离应用身份验证。我们将分两部分进行测试:首先使用OracleJavaSecure
中的main()
方法,然后使用单独的应用testojs.TestOracleJavaSecure
。
再次确认您已经在OracleJavaSecure.java的顶部设置了适合您组织的域名和其他地址。还要确保文件顶部的 SQL 命令已经被注释。(SQL 命令在其他的顶部。本章中的 java 文件也应该保留注释以便编译。)
注意以下内容假设您正在一个单独的数据库实例
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";**
注意如果您没有创建专用于应用验证的额外 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();
}
}`
注意正是这个值
prime
指导我们的应用使用备用数据库实例进行应用验证。
编辑要使用的应用密码
我们将为HRVIEW
应用向 Oracle 数据库上传一个连接字符串列表。更新OracleJavaSecure
的main()
方法,使HR
和appusr
用户拥有正确的密码(替换“密码”字符串),如清单 11-55 所示。还要更正 Oracle 应用连接字符串的其他方面。
注意这些连接字符串旨在连接到
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
(靠近顶部),增加版本号,例如从20110101a
到20110101b
。见清单 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; } }
注意我们不必在 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
同样的NullPointerException
由putAppConnections()
报告,另外两次连接 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 密码
- 审查公众访问数据字典视图的弱点
- 使用数据库链接从另一个数据库实例读取数据**