本模块用于:
• | 创建使用 Forms 身份验证通过 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 开发工具 |
• | Microsoft 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 的实例 — 此实例不应是一个生产系统。 |
• | 阅读模块“Data Access Security”。本模块提供了有关保护数据库访问、创建和存储密码哈希值以及保护以防止 SQL 注入式攻击的详细信息。 |
• | 阅读“How To Use DPAPI (Machine Store) from ASP.NET”。本模块提供了有关如何使用 DPAPI 安全地存储 SQL Server 连接字符串的详细信息。 |
• | 阅读“How To Create GenericPrincipal Objects with Forms Authentication”。本模块提供了有关构造包含用户角色详细信息的 Forms 身份验证票的详细说明。 |
本页内容
摘要 | |
预备知识 | |
创建一个有登录页的 Web 应用程序 | |
配置 Web 应用程序进行 Forms 身份验证 | |
开发生成哈希和 Salt 值的函数 | |
创建用户帐户数据库 | |
使用 ADO.NET 将帐户详细信息存储在数据库中 | |
根据数据库对用户凭据进行身份验证 | |
测试应用程序 | |
其他资源 |
摘要
ASP.NET Forms 身份验证允许用户将凭据(用户名和密码)输入到 Web Form 来标识其身份。在收到这些凭据时,Web 应用程序可以根据数据源来检查这些凭据,从而对用户进行身份验证。
本模块描述如何使用密码哈希安全地将用户凭据存储在 SQL Server 中,以及如何根据包含在 SQL Server 中的帐户数据库对用户进行身份验证。
预备知识
安全地存储用户凭据包含两个关键概念:
• | 存储密码摘要。出于安全性考虑,请不要将密码明文存储在数据库中。本模块描述如何创建和存储用户密码的单向哈希而非密码本身。如果要存储加密的用户密码,建议选择这种方法,因为它避免了与加密技术相关的密钥管理问题。 为增加安全性并减轻与字典攻击相关的威胁,本模块中描述的方法在创建密码哈希前,将 salt(以加密方式生成的随机数)与密码结合起来。
| ||
• | 验证用户输入。当将用户输入传递给 SQL 命令时,例如比较语句或模式匹配语句中的字符串,应非常小心地验证此输入,以确保最终的命令不包含语法错误,并且还要确保黑客不会使您的应用程序运行任意 SQL 命令。在登录过程中验证提供的用户名特别重要,因为应用程序的安全模型完全取决于是否能够正确而安全地对用户进行身份验证。 有关验证 SQL 命令和验证功能的用户输入的详细信息,请参阅模块“Data Access Security”中的“SQL Injection Attacks”。 |
创建一个有登录页的 Web 应用程序
此过程创建一个简单的 Visual C# Web 应用程序,它包含一个用户可以输入用户名和密码的登录页。
要创建一个有登录页的 Web 应用程序,请执行下列步骤:
• | 启动 Visual Studio .NET 并创建一个新的名为 FormsAuthSQL 的 Visual C# ASP.NET Web 应用程序。 | |||||||||||||||||||||||||||
• | 使用解决方案资源管理器将 WebForm1.aspx 重命名为 Logon.aspx | |||||||||||||||||||||||||||
• | 将表 1 中列出的控件添加到 Logon.aspx 中来创建简单的登录窗体。
|
您的 Web 页应与图 1 中所示的页类似。
图 1. 登录页 Web 窗体
• | 将 txtPassword 的 TextMode 属性设置为 Password。 |
配置 Web 应用程序进行 Forms 身份验证
此过程编辑应用程序的 Web.config 文件来配置应用程序以进行 Forms 身份验证。
要配置 Web 应用程序以进行 Forms 身份验证,请执行下列步骤:
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. | 将下列 <authorization> 元素添加到 <authentication> 元素下这一步的目的是只允许经过身份验证的用户访问应用程序。以前建立的 <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) { // Generate a cryptographic random number using the cryptographic // service provider RNGCryptoServiceProvider rng = new RNGCryptoServiceProvider(); byte[] buff = new byte[size]; rng.GetBytes(buff); // Return a Base64 string representation of the random number 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 中创建一个新的用户帐户数据库,此数据库包含一个用户表和一个用于查询用户数据库的存储过程。
要创建用户帐户数据库,请执行下列操作:
1. | 在 Microsoft SQL Server programs 菜单上,单击 Query Analyzer,然后连接到本地 SQL Server。 |
2. | 输入下列 SQL 脚本。注意,必须用自己的计算机名称替换此脚本末尾的“LocalMachine”。 USE master GO -- create a database for the security information 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 stored procedure to register user details CREATE PROCEDURE RegisterUser @userName varchar(255), @passwordHash varchar(40), @salt varchar(10) AS INSERT INTO Users VALUES(@userName, @passwordHash, @salt) GO -- create stored procedure to retrieve user details CREATE PROCEDURE LookupUser @userName varchar(255) AS SELECT PasswordHash, salt FROM Users WHERE UserName = @userName GO -- Add a login for the local ASPNET account -- In the following statements, replace LocalMachine with your -- local machine name exec sp_grantlogin [LocalMachine/ASPNET] -- Add a database login for the UserAccounts database for the ASPNET account exec sp_grantdbaccess [LocalMachine/ASPNET] -- Grant execute permissions to the LookupUser and RegisterUser stored procs grant execute on LookupUser to [LocalMachine/ASPNET] grant execute on RegisterUser to [LocalMachine/ASPNET] |
3. | 运行查询来创建 UserAccounts 数据库。 |
4. | 退出 Query Manager。 |
使用 ADO.NET 将帐户详细信息存储在数据库中
此过程修改 Web 应用程序代码,以将提供的用户名、生成的密码哈希和 salt 值存储在数据库中。
要使用 ADO.NET 将帐户详细信息存储在数据库中,请执行下列操作:
• | 返回到 Visual Studio .NET 并双击 Web 窗体上的 Register 按钮来创建按钮单击事件处理程序。 |
• | 将下列代码添加到方法中。 string salt = CreateSalt(5); string passwordHash = CreatePasswordHash(txtPassword.Text,salt); try { StoreAccountDetails( txtUserName.Text, passwordHash, salt); } catch(Exception ex) { lblMessage.Text = ex.Message; } |
• | 将下列 using 语句添加到位于文件顶部的现有 using 语句下。 using System.Data.SqlClient; |
• | 使用下列代码添加 StoreAccountDetails 实用工具方法。此代码使用 ADO.NET 连接到 UserAccounts 数据库,并将提供的用户名、密码哈希和 salt 值存储在 Users 表中。 private void StoreAccountDetails( string userName, string passwordHash, string salt ) { // See "How To Use DPAPI (Machine Store) from ASP.NET" for information // about securely storing connection strings. 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 ) { // Code to check for primary key violation (duplicate account name) // or other database errors omitted for clarity throw new Exception("Exception adding account. " + ex.Message); } finally { conn.Close(); } } |
根据数据库对用户凭据进行身份验证
此过程开发 ADO.NET 代码,用于在数据库中查找提供的用户名并按匹配的密码哈希来验证所提供的密码。
注 在使用基于 .NET 角色授权的许多 Forms 身份验证情况中,可能还要从此处的数据库中检索用户所属的角色。这些角色随后可用于生成可与经过身份验证的 Web 请求关联的 GenericPrincipal 对象,以便进行 .NET 授权。
有关构造包含用户角色详细信息的 Forms 身份验证票的详细信息,请参阅“How To Create GenericPrincipal Objects with Forms Authentication”。
要根据数据库验证用户凭据,请执行下列操作:
• | 返回到 Logon.aspx.cs 并添加 VerifyPassword 私有 Helper 方法,如下列代码所示。 private bool VerifyPassword(string suppliedUserName, string suppliedPassword ) { bool passwordMatch = false; // Get the salt and pwd from the database based on the user name. // See "How To: Use DPAPI (Machine Store) from ASP.NET," "How To: Use DPAPI // (User Store) from Enterprise Services," and "How To: Create a DPAPI // Library" for more information about how to use DPAPI to securely store // connection strings. 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(); // Advance to the one and only row // Return output parameters from returned data stream string dbPasswordHash = reader.GetString(0); string salt = reader.GetString(1); reader.Close(); // Now take the salt and the password entered by the user // and concatenate them together. string passwordAndSalt = String.Concat(suppliedPassword, salt); // Now hash them string hashedPasswordAndSalt = FormsAuthentication.HashPasswordForStoringInConfigFile( passwordAndSalt, "SHA1"); // Now verify them. passwordMatch = hashedPasswordAndSalt.Equals(dbPasswordHash); } catch (Exception ex) { throw new Exception("Execption verifying password. " + ex.Message); } finally { conn.Close(); } return passwordMatch; } |
测试应用程序
此过程将测试应用程序。您将注册一个用户,这会导致用户名、密码哈希和 salt 值被添加到 UserAccounts 数据库的 Users 表中。然后以同一个用户的身份登录,以确保密码验证例程执行正确的操作。
要测试应用程序,请执行下列操作:
1. | 返回到 Logon 窗体并双击 Logon 按钮来创建按钮单击事件处理程序。 |
2. | 将下列代码添加到 Logon 按钮单击事件处理程序来调用 VerifyPassword 方法,并根据提供的用户名和密码是否有效来显示消息。 bool passwordVerified = false; try { passwordVerified = VerifyPassword(txtUserName.Text,txtPassword.Text); } catch(Exception ex) { lblMessage.Text = ex.Message; return; } if (passwordVerified == true ) { // The user is authenticated // At this point, an authentication ticket is normally created // This can subsequently be used to generate a GenericPrincipal // object for .NET authorization purposes // For details, see "How To: Use Forms authentication with GenericPrincipal // objects lblMessage.Text = "Logon successful: User is authenticated"; } else { lblMessage.Text = "Invalid username or password"; } |
3. | 在 Build 菜单上,单击 BuildSolution。 |
4. | 在解决方案资源管理器中,右键单击 logon.aspx,然后单击 View in Browser。 |
5. | 输入用户名和密码,然后单击 Register。 |
6. | 使用 SQL Server Enterprise Manager 查看 Users 表的内容。您会看到一个新行,新用户名与生成的密码哈希一起位于这一行中。 |
7. | 返回到 Logon Web 页,重新输入密码,然后单击 Logon。您会看到“Logon successful:User is authenticated”消息。 |
8. | 现在请输入一个无效的密码(用户名保持不变)。您会看到“Invalid username or password”消息。 |
9. | 关闭 Internet Explorer。 |