构建安全的数据访问组件
面的代码显示了 CheckProductStockLevel 方法(用来在产品数据库中查询库存量)的示例实现,该代码阐释了本模块前面介绍的数据访问代码的许多重要安全功能。
using System; using System.Data; using System.Data.SqlClient; using System.Text.RegularExpressions; using System.Collections.Specialized; using Microsoft.Win32; using DataProtection; public static int CheckProductStockLevel(string productCode) { int quantity = 0; // (1) 由 try/catch 块保护的代码 try { // (2) 使用正则表达式验证的输入内容 // 应该从资源程序集中检索错误消息,以帮助实现 // 本地化。为简短起见,省略了 Localization(本地化)代码。 if (Regex.IsMatch(productCode, "^[A-Za-z0-9]{12}$") == false) throw new ArgumentException("Invalid product code" ); //(3) using 语句确保连接被断开 using (SqlConnection conn = new SqlConnection(GetConnectionString())) { // (4) 使用参数化存储过程可以应对 // SQL 注入攻击 SqlCommand cmd = new SqlCommand("spCheckProduct", conn); cmd.CommandType = CommandType.StoredProcedure; // 对参数的类型进行检查 SqlParameter parm = cmd.Parameters.Add("@ProductCode", SqlDbType.VarChar,12); parm.Value = productCode; // 定义输出参数 SqlParameter retparm = cmd.Parameters.Add("@quantity", SqlDbType.Int); retparm.Direction = ParameterDirection.Output; conn.Open(); cmd.ExecuteNonQuery(); quantity = (int)retparm.Value; } } catch (SqlException sqlex) { // (5) 记录完整的异常详细信息。一般(安全的)错误消息 // 基于 SQL 错误代码返回调用方 // 为清楚起见,省略了日志和错误标识代码 throw new Exception("Error Processing Request"); } catch (Exception ex) { // 记录完整的异常详细信息 throw new Exception("Error Processing Request"); } return quantity; } // (6) 将加密的数据库连接字符串存保留在注册表中 private static string GetConnectionString() { // 从注册表中检索密码文本;进程帐户必须 // 由注册表项的 ACL 授予“读取”访问权限 string encryptedString = (string)Registry.LocalMachine.OpenSubKey( @"Software\OrderProcessing\") .GetValue("ConnectionString"); // 使用托管的 DPAPI helper 库对该字符串进行解密 DataProtector dp = new DataProtector(DataProtector.Store.USE_MACHINE_STORE); byte[] dataToDecrypt = Convert.FromBase64String(encryptedString); return Encoding.ASCII.GetString(dp.Decrypt(dataToDecrypt,null)); }
上面的代码显示出下列安全特征(由注释行中的数字进行标识)。
1.数据访问代码放在 try/catch 块中。这是防止在出现异常时将系统级信息返回到调用方所必需的。调用 ASP.NET Web 应用程序或 Web 服务会处理异常,并向客户端返回适当的一般错误消息,但是数据访问代码不依赖这些消息。
2.使用正则表达式验证输入。检查所提供的产品 ID,以便验证它只包含 A–Z 和 0–9 范围内的字符,而且不超过 12 个字符。这是旨在防止 SQL 注入攻击的一组对策中的第一个。
3.SqlConnection 对象是在 Microsoft Visual C#® using 语句的内部创建的。这可确保无论是否发生了异常,都断开方法内部的连接。这会降低拒绝服务攻击的威胁,该威胁尝试使用到数据库的所有可用连接。通过使用 finally 块可以实现类似的功能。
4.参数化存储过程用于数据访问。这是防止 SQL 注入的另一个对策。
5.不向客户端返回详细的错误信息。记录异常详细信息,以便帮助诊断问题。
6.加密的数据库连接字符串存储在注册表中。存储数据库连接字符串最安全的方法之一是,使用 DPAPI 加密该字符串,并将加密的密码文本存储在具有受限 ACL 的受保护的注册表项中。(例如,使用“管理员:完全控制”和“ASP.NET 或企业服务进程帐户:读取”,具体情况取决于由哪个进程托管该组件。)
注意 该代码显示了如何从注册表检索连接字符串,然后使用托管的 DPAPI helper 库对其进行解密。此库在“Microsoft patterns & practices Volume I, Building Secure ASP.NET Web Applications: Authentication, Authorization, and Secure Communication”的“How To”部分中的 How To: Create a DPAPI Library (英文)中提供。