目标
本章的目标是:

• 创建一个使用窗体身份验证针对 SQL Server 验证用户身份的 Web 应用程序。
 
• 使用密码哈希值安全地存储和验证用户凭据。
 

返回页首
适用范围
本章适用于以下产品和技术:

• Microsoft Windows® XP 或 Windows 2000 Server (Service Pack 3) 以及更高版本的操作系统
 
• Microsoft.NET Framework 版本 1.0 (Service Pack 2) 以及更高版本
 
• Microsoft Visual Studio® 1.0 .NET 以及更高版本
 
• Microsoft Visual C#® .NET
 
• SQL Server 2000 (Service Pack 2) 以及更高版本
 

返回页首
如何使用本章内容
若要学好本章内容:

• 您必须具有使用 Visual C# .NET 和 Visual Studio .NET 的经验。
 
• 您必须具有使用 ASP.NET 开发 Web 应用程序的经验。
 
• 您必须具有使用 SQL Server 和创建 SQL Server 表的经验。
 
• 您必须具有使用 ADO.NET 访问 SQL Server 的经验。
 
• 您必须访问一个用于测试您的应用程序的 SQL Server 实例。该实例不能是生产系统。
 
• 阅读本指南中的第 12 章数据访问安全性。这一章提供了有关安全数据库访问、创建和存储密码哈希值、以及防止 SQL 注入式攻击的详细信息。
 
• 阅读如何在 ASP.NET 中使用 DPAPI(计算机存储)。这一章提供了有关如何使用 DPAPI 安全地存储 SQL Server 连接字符串的详细信息。
 
• 阅读如何利用窗体身份验证创建 GenericPrincipal 对象。这一章提供了有关构造包含用户角色细节的窗体身份验证票证的详细信息。
 

返回页首
摘要
借助 ASP.NET 窗体身份验证,用户可以通过在 Web 窗体中输入凭据(用户名和密码)来表明自己的身份。在收到这些凭据后,Web 应用程序针对数据源检查凭据,以便验证用户的身份。

本章介绍了如何在 Microsoft® SQL Server® 中安全地存储用户凭据以及如何针对 SQL Server 中包含的帐户数据库验证用户的身份。

返回页首
您必须了解的背景知识
以下是两个有关安全地存储用户凭据的重要概念:

• 存储密码摘要。出于安全方面的原因,不应以明文的方式将密码存储在数据库中。本章介绍如何创建和存储用户密码的单向哈希值,而不是存储密码本身。为了避免出现与加密技术有关的密钥管理问题,应首选使用此方法,而不是存储加密的用户密码版本。
为了提高安全性并降低与字典攻击有关的威胁,本章中介绍的方法在创建密码哈希值之前将 Salt(使用加密技术生成的随机数)与密码结合起来。

重要说明:不将密码存储在数据库中的一个缺点是,如果用户忘记了密码,则它不可以恢复。因此,应用程序应该使用密码提示,并将它们与密码摘要一起存储在数据库中。
 
• 验证用户输入。将用户输入传递给 SQL 命令时(例如,作为比较或模式匹配语句中的字符串),应该仔细验证输入以确保生成的命令不包含语法错误,并且还要确保黑客不能使您的应用程序运行任意 SQL 命令。在登录过程中验证提供的用户名是非常重要的,因为您的应用程序安全模型完全取决于能否正确安全地验证用户的身份。 

有关验证 SQL 命令和验证函数的用户输入的详细信息,请参见第 12 章数据访问安全性中的 SQL 注入式攻击。
 

返回页首
创建一个具有登录页的 Web 应用程序
此过程创建一个包含登录页的简单 Visual C# Web 应用程序,用户可以在该页面上输入用户名和密码。

• 创建一个具有登录页的 Web 应用程序

1.
 启动 Visual Studio .NET,然后创建一个名为 FormsAuthSQL 的新的 Visual C# ASP.NET Web 应用程序。
 
2.
 使用解决方案资源管理器将 WebForm1.aspx 重命名为 Logon.aspx。
 
3.
 向 Logon.aspx 添加表 1 中列出的控件,创建一个简单的登录窗体。

表 1:Logon.aspx 控件

控件类型 文本 ID 
标签
 用户名:
 -
 
标签
 密码
 -
 
文本框
 -
 txtUserName
 
文本框
 -
 txtPassword
 
按钮
 注册
 btnRegister
 
按钮
 登录
 btnLogon
 
标签
 -
 lblMessage
 

您的 Web 页应类似于图 1 中所示的页面。

 
 

按此在新窗口打开图片
图 1
登录页 Web 窗体
 
4.
 将 txtPassword 的 TextMode 属性设置为 Password。
 
 

返回页首
配置 Web 应用程序的窗体身份验证
此过程编辑应用程序的 Web.config 文件,以便配置应用程序的窗体身份验证。 

• 配置 Web 应用程序的窗体身份验证

1.
 使用解决方案资源管理器打开 Web.config。
 
2.
 查找 <authentication> 元素,并将 mode 属性更改为 Forms。
 
3.
 将 <forms> 元素添加为 <authentication> 元素的子元素,并按以下所示设置 loginUrl、name、timeout 和 path 属性:

<authentication mode="Forms">
  <forms loginUrl="logon.aspx" name="sqlAuthCookie" timeout="60" path="/">
  </forms>
</authentication>

 
4.
 在 <authentication> 元素下面添加以下 <authorization> 元素。这样将只允许通过身份验证的用户访问该应用程序。先前为 <authentication> 元素建立的 loginUrl 属性会将未通过身份验证的请求重定向到 logon.aspx 页。

<authorization> 
  <deny users="?" />
  <allow users="*" />
</authorization>

 
 

返回页首
开发函数,以生成哈希值和 Salt 值
此过程将两个实用程序方法添加到 Web 应用程序中:一个用于生成随机 Salt 值,另一个基于提供的密码和 Salt 值创建一个哈希值。

• 开发函数,以生成哈希值和 Salt 值

1.
 打开 Logon.aspx.cs,并将下面的 using 语句添加到文件顶部的现有 using 语句的下面:

using System.Security.Cryptography;
using System.Web.Security;

 
2.
 将以下静态方法添加到 WebForm1 类中以生成随机 Salt 值,并将它作为 Base 64 加密字符串返回:

private static string CreateSalt(int size)
{
  // 使用加密服务提供程序生成一个加密的
  // 随机数
  RNGCryptoServiceProvider rng = new RNGCryptoServiceProvider();
  byte[] buff = new byte[size];
  rng.GetBytes(buff);
  // 返回 Base64 字符串表示形式的随机数
  return Convert.ToBase64String(buff);
}

 
3.
 添加以下静态方法,以基于提供的密码和 Salt 值生成一个哈希值:

private static string CreatePasswordHash(string pwd, string salt)
{
  string saltAndPwd = String.Concat(pwd, salt);
  string hashedPwd = 
        FormsAuthentication.HashPasswordForStoringInConfigFile(
                                             saltAndPwd, "SHA1");
  return hashedPwd;
}

 
 

返回页首
创建用户帐户数据库
此过程在 SQL Server 中创建一个新的用户帐户数据库,它包含用于查询用户数据库的单个 users 表和存储过程。

• 创建用户帐户数据库

1.
 在 Microsoft SQL Server“程序”菜单上,单击“查询分析器”,然后连接到您的本地 SQL Server。
 
2.
 输入以下 SQL 脚本。注意,必须在脚本结尾将“LocalMachine”替换为您自己的计算机名称。 

USE master
GO
-- 为安全信息创建一个数据库
IF EXISTS (SELECT * FROM   master..sysdatabases WHERE  
name = 'UserAccounts')
  DROP DATABASE UserAccounts
GO
CREATE DATABASE UserAccounts
GO
USE UserAccounts
GO
CREATE TABLE [Users] (
  [UserName] [varchar] (255) NOT NULL ,
  [PasswordHash] [varchar] (40) NOT NULL ,
  [salt] [varchar] (10) NOT NULL,
  CONSTRAINT [PK_Users] PRIMARY KEY  CLUSTERED 
  (
    [UserName]
  )  ON [PRIMARY] 
) ON [PRIMARY]
GO
-- 创建一个存储过程来注册用户详细信息
CREATE PROCEDURE RegisterUser
@userName varchar(255),
@passwordHash varchar(40),
@salt varchar(10)
AS
INSERT INTO Users VALUES(@userName, @passwordHash, @salt)
GO
-- 创建一个存储过程来检索用户详细信息
CREATE PROCEDURE LookupUser
@userName varchar(255)
AS
SELECT PasswordHash, salt 
FROM Users
WHERE UserName = @userName
GO
-- 添加一个本地 ASPNET 帐户登录
-- 在下面的语句中,用您的本地计算机名 
-- 替代 LocalMachine
exec sp_grantlogin [LocalMachine\ASPNET]
-- 为 UserAccounts 数据库添加一个
ASPNET 帐户的数据库登录
exec sp_grantdbaccess [LocalMachine\ASPNET]
-- 向 LookupUser 和 RegisterUser 存储过程授予执行权限
procs
grant execute on LookupUser to [LocalMachine\ASPNET]
grant execute on RegisterUser to [LocalMachine\ASPNET]

 
3.
 运行查询以创建 UserAccounts 数据库。
 
4.
 退出查询管理器。
 
 

返回页首
使用 ADO.NET 在数据库中存储帐户详细信息
此过程修改 Web 应用程序代码以便将提供的用户名、生成的密码哈希值和 Salt 值存储在数据库中。 

• 使用 ADO.NET 在数据库中存储帐户详细信息

1.
 返回到 Visual Studio .NET,然后双击 Web 窗体上的“注册”按钮以创建一个按钮单击事件处理程序。
 
2.
 将以下代码添加到方法中:

string salt = CreateSalt(5);
string passwordHash = CreatePasswordHash(txtPassword.Text,salt);
try
{
  StoreAccountDetails( txtUserName.Text, passwordHash, salt);
}
catch(Exception ex)
{
  lblMessage.Text = ex.Message;
}

 
3.
 在文件顶部的现有 using 语句下面,添加以下 using 语句:

using System.Data.SqlClient;

 
4.
 使用以下代码添加 StoreAccountDetails 实用程序方法。此代码使用 ADO.NET 连接到 UserAccounts 数据库,并将提供的用户名、密码哈希值和 Salt 值存储在 Users 表中。

private void StoreAccountDetails( string userName, 
                                  string passwordHash, 
                                  string salt )
{
  // 请参见“如何在 ASP.NET 中使用 DPAPI(计算机存储)”,以了解 
  // 有关安全地存储连接字符串的信息。
  SqlConnection conn = new SqlConnection( "Server=(local);" +
                                          "Integrated Security=SSPI;" +
                                          "database=UserAccounts");

  SqlCommand cmd = new SqlCommand("RegisterUser", conn );
  cmd.CommandType = CommandType.StoredProcedure;
  SqlParameter sqlParam = null;

  sqlParam = cmd.Parameters.Add("@userName", SqlDbType.VarChar, 255);
  sqlParam.Value = userName;

  sqlParam = cmd.Parameters.Add("@passwordHash ", SqlDbType.VarChar, 40);
  sqlParam.Value = passwordHash;

  sqlParam = cmd.Parameters.Add("@salt", SqlDbType.VarChar, 10);
  sqlParam.Value = salt;

  try
  {
    conn.Open();
    cmd.ExecuteNonQuery();
  }
  catch( Exception ex )
  {
    // 查看是否存在主键冲突(重复的帐户名)
    // 或为清楚起见而省略的其他数据库错误的代码
    引发一个新异常(“添加帐户时出现异常。”+ ex.Message);
  }
  finally
  {
    conn.Close();
  } 
}
 
 

返回页首
针对数据库验证用户凭据
此过程开发 ADO.NET 代码以便在数据库中查找提供的用户名,并通过匹配密码哈希值来验证提供的密码。

注意:在很多窗体身份验证方案(使用基于 .NET 角色的授权)中,此时也可以从数据库中检索用户所属的角色。可随后使用这些角色生成 GenericPrincipal 对象(可与已通过身份验证的 Web 请求关联起来)以进行 .NET 授权。

有关构造包含用户角色细节的窗体身份验证票证的详细信息,请参见本指南的“参考”部分中的如何利用窗体身份验证创建 GenericPrincipal 对象。

• 针对数据库验证用户凭据

1.
 返回到 Logon.aspx.cs,并按以下代码中所示添加 VerifyPassword 私有帮助器方法:

private bool VerifyPassword(string suppliedUserName, 
                            string suppliedPassword )

  bool passwordMatch = false;
  // 基于用户名从数据库获取 salt 和 pwd。
  // 请参见“如何在 ASP.NET 中使用 DPAPI(计算机存储)”、“如何在企业服务中使用 DPAPI 
  // (用户存储)”和“如何创建 DPAPI 
  // 库”,以了解如何使用 DPAPI 来安全地存储 
  // 连接字符串的详细信息。
  SqlConnection conn = new SqlConnection( "Server=(local);" + 
                                          "Integrated Security=SSPI;" +
                                          "database=UserAccounts");
  SqlCommand cmd = new SqlCommand( "LookupUser", conn );
  cmd.CommandType = CommandType.StoredProcedure;

  SqlParameter sqlParam = cmd.Parameters.Add("@userName", 
                                             SqlDbType.VarChar, 255);
  sqlParam.Value = suppliedUserName;
  try
  {
    conn.Open();
    SqlDataReader reader = cmd.ExecuteReader();
    reader.Read(); // 前进到唯一的行
    // 从返回的数据流中返回输出参数
    string dbPasswordHash = reader.GetString(0);
    string salt = reader.GetString(1);
    reader.Close();
    // 现在使用用户输入的 salt 和密码
    // 并将它们连接在一起。
    string passwordAndSalt = String.Concat(suppliedPassword, salt);
    // 现在对它们进行哈希计算
    string hashedPasswordAndSalt =       
               FormsAuthentication.HashPasswordForStoringInConfigFile(
                                               passwordAndSalt, "SHA1");
    // 现在验证它们。
    passwordMatch = hashedPasswordAndSalt.Equals(dbPasswordHash);
  }
  catch (Exception ex)
  {
    throw new Exception("验证密码时出现异常。 " + ex.Message);
  }
  finally
  {
    conn.Close();
  }
  return passwordMatch;
}

 
 

返回页首
测试应用程序
此过程测试应用程序。您将注册一个用户,导致将用户名、密码哈希值和 Salt 值添加到 UserAccounts 数据库的 Users 表中。然后以相同的用户登录,以确保密码验证例程的操作正确无误。

• 测试应用程序

1.
 返回到登录窗体,然后双击“登录”按钮创建一个按钮单击事件处理程序。
 
2.
 将以下代码添加到“登录”按钮单击事件处理程序中以调用 VerifyPassword 方法,并根据提供的用户名和密码是否有效显示一条消息:

bool passwordVerified = false;
try
{
   passwordVerified = VerifyPassword(txtUserName.Text,txtPassword.Text);
}
catch(Exception ex)
{
  lblMessage.Text = ex.Message;
  return;
}
if (passwordVerified == true )
{
  // 用户已通过身份验证
  // 此时,通常创建一个身份验证票证
  // 随后可以使用该票证生成一个 GenericPrincipal
  // 对象,用于 .NET 授权
  // 有关详细信息,请参见“如何将窗体身份验证用于 
  GenericPrincipal 
  // 对象
  lblMessage.Text = "登录成功:用户已通过身份验证";
}
else
{
  lblMessage.Text = "无效的用户名或密码";
}

 
3.
 在“生成”菜单上,单击“生成解决方案”。
 
4.
 在解决方案资源管理器中,右键单击“logon.aspx”,然后单击“在浏览器中查看”。
 
5.
 输入用户名和密码,然后单击“注册”。
 
6.
 使用 SQL Server 企业管理器查看 Users 表的内容。您应该看到一个新的行,它包含新的用户名以及生成的密码哈希值。
 
7.
 返回到“登录”Web 页,重新输入密码,然后单击“登录”。您应该看到消息“登录成功:用户已通过身份验证”。
 
8.
 现在输入一个无效的密码(用户名保持不变)。您应该看到消息“无效的用户名或密码”。
 
9.
 关闭 Internet Explorer。
 
 

返回页首
其他资源
有关更多信息,请访问以下资源:

• 如何在 ASP.NET 中使用 DPAPI(计算机存储)
 
• 如何利用窗体身份验证创建 GenericPrincipal 对象
 
• 第 12 章数据访问安全性中的 SQL 注入式攻击
 


原文地址:http://www.microsoft.com/china/tech ... uidance/secmod17.mspx