精通 Oracle 的 .NET 应用程序开发
了解如何在您的 .NET 应用程序中充分利用 Oracle 内建的安全特性。
随着对安全性重视程度的不断提高,各公司对安全性的认识已经超越了仅限于拒绝非法访问的层面而变得越发成熟。国际、国内、州和省的法律法规对安全性的要求也越来越细化。仅保护数据库免受未授权的访问已不能满足要求。还要求保留审计线索,以显示哪些用户在数据库中执行了哪些操作。 幸运的是,选择 Oracle 作为其数据库平台的 .NET 开发人员可以通过 Oracle Data Provider for .NET (ODP.NET) 利用 Oracle 的安全特性构建全面安全策略的基础。 在“精通 Oracle 的 .NET 应用程序开发”的这一部分中,我将介绍如何在考虑安全性的前提下设计应用程序,这包括
您将有机会应用您在五个实验中学到的内容。这个五个实验从比较简单到较复杂难度不等。 本文假设您熟悉 Oracle Data Provider for .NET,并熟悉在 Visual Studio.NET 中创建工程(请参阅我先前的一篇文章在 Oracle 数据库上构建 .NET 应用程序 。 代理认证 以下是摘自前一篇文章的代码,用于定义一个简单的 Oracle 连接字符串:
Dim oradb As String = "Data Source=OraDb;User Id=scott;Password=tiger;" ' VB.NET string oradb = "Data Source=OraDb;User Id=scott;Password=tiger;"; // C# 应用程序经常使用类似这样的连接字符串提示用户输入用户 ID 和口令。Web 应用程序通常会这样做。它允许通过连接池提高性能,这是因为所有应用程序用户使用相同的 Oracle 证书进行连接,这也是对连接池的要求。从审计的角度来看,通过通用用户 ID 进行连接不是最理想的方式,这是因为所有的数据库操作都被记录成是由连接字符串中定义的用户而非实际用户所做出的。享受连接池的性能优势意味着您不得不忍受匿名现象。在数据库中,您将无法识别实际用户的身份。 代理认证将一个 Proxy User Id 和 Proxy Password 添加到连接字符串中。换句话说,它传递了两个用户 ID,一个用于实际用户,一个用于聚集的用户(代理用户)。此特性使您能够维护一个中间层连接池,同时保持通过用户的 User Id 审计其操作的能力。留意 User Id 和 Password 使用方式的细微差别,这取决于是否使用代理认证。当使用代理认证时,聚集的用户证书是通过 Proxy User Id 和 Proxy Password 而不是 User Id 和 Password 传递的。例如:
Dim oradb As String = "Data Source=OraDb;User Id=ActualUser;Password=secret; Proxy User Id=scott;Proxy Password=tiger;" ' VB.NET string oradb = "Data Source=OraDb;User Id=ActualUser;Password=secret; Proxy User Id=scott;Proxy Password=tiger; "; // C# 在实际的应用程序中,您决不会把实际的用户 ID 和口令硬编码。相反,您会提供文本框,用户可在其中输入用户 ID 和口令,并将其传递给连接字符串。当在 Windows 窗体或 Web 页面(在 Visual Studio 中称为 Web 表单)上为用户提供一个输入用户 ID 名和口令的对话框时,Microsoft 称之为表单认证。 在数据库中,代理认证创建第二个会话,称为轻型会话,它用于使数据库获知实际的用户。为此,数据库管理员必须为代理用户进行显式授权才能创建轻型连接。下面您将使用 SCOTT 作为我们的代理(聚集的)用户。
alter user ActualUser grant connect through scott; 您可以选择使连接字符串包含或排除实际用户的口令。如果在不提供口令的情况下使用实际用户,则连接将会成功。对于要认证的实际用户,您必须使用 Password 连接字符串属性。如果为用户提供无效的口令,则认证将会失败。 代理连接用于那些代表用户调用的数据库操作。当把代理连接返回连接池时,将终止轻型会话。 使用客户端标识符与在没有 Password 连接字符串属性的情况下使用代理认证相似。一个关键的区别是使用客户端标识符不会在数据库中创建第二个会话。另一个区别是任何字符串都可以用作客户端标识符。它不必对应于数据库用户的名称。要设置客户端标识符,可以在打开连接后将一个字符串值指定给连接对象的 ClientId 属性:
conn.ClientId = "SomeUser" ' VB.NET conn.ClientId = "SomeUser"; // C# 该指定操作设置了用户会话的 CLIENT_IDENTIFIER 值。如果应用程序使用表单认证,则用户所输入的用户 ID 可用于设置 ClientId 的值。由于 ClientId 是 USERENV 的一部分,因此可以将其与 Oracle 虚拟专用数据库结合使用来限制访问,即使该用户不是在 ALL_USERS 中定义的实际用户亦可如此。ClientId 还可用于提高可伸缩性。例如,当一个用户工作完成并将 ClientId 重置为下一个用户时,Web 应用程序可以使数据库连接保持打开的状态。 当您登录到 Windows 时,操作系统会记住您的 Windows 用户,并可以使用 Windows.Security.Principal 类在 .NET 应用程序中获取该用户。完成此项工作并为 Windows 用户设置 ClientId 的代码如下:
Dim user As New WindowsPrincipal(WindowsIdentity.GetCurrent()) conn.ClientId = user.Identity.Name ' VB.NET WindowsPrincipal user = new WindowsPrincipal(WindowsIdentity.GetCurrent()); conn.ClientId = user.Identity.Name; // C# 通过这项技术,Oracle 数据库可以获知 Windows 用户。在没有 Password 连接字符串属性的情况下使用代理认证时,还可以将 Windows 用户身份通过 User Id 连接字符串属性传递给数据库。在这种情况下,如前文所述, User Id 连接字符串属性仅用于识别而非认证实际用户。 Windows 认证 本文讨论了使用 Oracle 用户 ID 将用户认证到数据库的过程。也可以使用 Windows 操作系统将用户认证到 Oracle,这就是所谓的一次性登录。我在 Windows IT Pro 杂志上的在 Oracle 上执行 Windows 认证 一文中详细介绍了这部分内容。使用 Windows 认证需要修改连接字符串:
"Data Source=ORCL10g;User Id=/;" 斜线 (/) 向 Oracle 表明将使用 Windows 认证。因为只在建立 Oracle 数据库连接时才使用 Password 连接字符串属性,所以在这里把它删除了。如果在使用 Windows 认证时连接字符串中有 Password ,则将其忽略。 使用 Windows 认证时,Windows 用户必须属于一个在 Oracle 服务器上有权限的 Windows 组(如 ORA_DBA),或者必须启用了外部认证。由于外部认证不如通过组成员访问安全,因此建议不要使用外部认证。 了解 SQL 注入攻击 到目前为止,本文已经介绍了如何跟踪谁在访问数据库。虽然这很有用,但却不如防止人们损害数据库的内容重要。对数据库应用程序的最大威胁之一是 SQL 注入。SQL 注入是指恶意用户用不安全的代码将 SQL 命令注入应用程序。SQL 注入漏洞是通过用户输入构建 SQL 语句的结果,并不是由于使用任何特定厂商的产品造成的。如果没有遵循编程安全措施,则所有厂商的所有 SQL 数据库都会有漏洞。 来看一个计算从事某种工作的员工数量的简单应用程序:
仔细查看用于构建数据库命令字符串的代码:
cmd.CommandText = "select count(ename) from emp where " _ + "job = '" + TextBox1.Text + "'" ' VB.NET cmd.CommandText = "select count(ename) from emp where " + "job = '" + TextBox1.Text + "'"; // C# 如果用户按照上面所示输入 CLERK,则数据库接收的命令文本如下:
select count(ename) from emp where job = 'CLERK' 只要用户不更改该逻辑,则一切运行正常。而允许在运行时基于用户输入构建 SQL 字符串则使用户能够更改 SQL 逻辑。假设用户决定不输入 CLERK 而是输入以下内容:
' or 1=1 -- 这样就从根本上将查询的逻辑更改为:
select count(ename) from emp where job = '' or 1=1 --' 应用程序代码总是附加一个尾随的单引号。行内注释使 SQL 分析器忽略尾随的单引号。如果没有这种使用行内注释的技巧,则修改后的 SQL 会具有以下的无效语法:
select count(ename) from emp where job = '' or 1=1 ' 用户修改后的 SQL 语法导致将表中的每一行进行计数。然而 where job = '' 本身会导致计数为零,而 or 1=1 导致对每行进行计数。 在前面的示例中,SQL 注入只是导致用户获得了与应用程序设计者初衷不符的结果。现在看一下用于认证用户的应用程序窗口:
下面是用于处理登录信息的代码:
cmd.CommandText = "select user_role from app_login where " _ + "user_id = '" + TextBox1.Text + "' and " _ + "password = '" + TextBox2.Text + "'" ' VB.NET cmd.CommandText = "select user_role from app_login where " + "user_id = '" + TextBox1.Text + "' and " + "password = '" + TextBox2.Text + "'"; // C# 假设有个恶意用户输入了以下内容:
admin' or 1=1 -- 则得到的 SQL 为:
select user_role from app_login where user_id = 'admin' or 1=1 --' and password='' 行内注释再次在破坏预期查询逻辑方面发挥了重要作用。-- 行内注释导致将查询字符串的其余部分当作注释处理,这样就使口令变得可有可无了。得到的查询字符串具有一个始终为真的 where 子句,这就使用户即使不提供口令也能以管理员身份登录。甚至有可能不用提供用户 ID 或口令就可以登录某些应用程序,这取决于基本查询字符串的结构。 防止 SQL 注入攻击 无论您怎么绞尽脑汁地验证用户输入,恶意用户还是有可能攻击成功,毕竟他们相当聪明。您也无法通过输入验证发现所有 SQL 注入企图。问题的根源并不是用户没有按您的预期输入 SQL 语法,而是把用户的输入当作 SQL 语法而不仅仅是字符串来处理了。 将输入视为字符串参数意味着不再将用户的输入作为 SQL 语句的一部分来处理,而只是将其视为一个字符串值传递给 SQL 查询。使用 OracleParameter 对象使恶意输入变得无害,因为将它解释为如下的形式:
select user_role from app_login where user_id = 'admin' or 1=1 --' and password = '' 没有将双短划线视为行内注释,而只是将其视为文本字符串。用户输入没有成为所执行 SQL 查询语法的一部分。 要创建参数化的查询,如下修改查询字符串:
cmd.CommandText = "select user_role from app_login where " _ + "user_id = :user_id and password = :password" ' VB.NET cmd.CommandText = "select user_role from app_login where " + "user_id = :user_id and password = :password"; // C# 然后将 OracleParameter 对象实例化,并将其添加到 OracleParameters 集合。
Dim p1 As New OracleParameter("dname", OracleDbType.Varchar2) ' VB.NET p1.Value = TextBox1.Text cmd.Parameters.Add(p1) Dim p2 As New OracleParameter("loc", OracleDbType.Varchar2) p2.Direction = ParameterDirection.Input ' optional property p2.Size = 13 ' optional property p2.Value = TextBox2.Text cmd.Parameters.Add(p2) OracleParameter p1 = new OracleParameter("dname", OracleDbType.Varchar2); p1.Value = textBox1.Text; // C# cmd.Parameters.Add(p1); OracleParameter p2 = new OracleParameter("loc", OracleDbType.Varchar2); p2.Direction = ParameterDirection.Input; ' optional p2.Size = 13; ' optional p2.Value = textBox2.Text; cmd.Parameters.Add(p2); 其中有几个用于 OracleParameter 的构造符和多个 Parameters.Add 上的重载。此外,还有各种可以或必须设置的多个属性,这完全取决于您的查询。将参数传递给存储过程同样遵循以上介绍的编码样式。
John Paul Cook ( johnpaulcook@email.com ) 是居住在休斯顿的一位数据库和 .NET 顾问。他撰写了许多关于 .NET、Oracle 和其他主题的文章,并从 1986 年以来一直开发关系数据库应用程序。他目前的兴趣包括安全性和 IT 管理、Visual Studio 2005 和 Oracle 10g。他是 Oracle 认证 DBA 和 Microsoft MCSD for .NET。 |