安全存储密码的入门指南






4.91/5 (81投票s)
本文将解释如何安全地在数据库中存储用户密码。
目录
引言
本文将解释如何安全地在数据库中存储密码,以及如何创建一个简单的登录功能。
这不是一篇关于架构或通用安全性的文章,而是关于如何安全地在数据库中传输密码以及如何在其中存储密码。
背景
撰写本文的原因是,我在 CodeProject 和其他论坛上阅读了大量问题。许多现有的代码容易受到 SQL 注入攻击,并且密码经常以明文形式存储。我将尝试用一种简单易懂的方式来解释如何处理这个问题,即使是新手也能保护他们的数据。
使用代码
首先,您需要一个地方来存储用户名和密码。在本文中,我将使用 SQL Server,但对于所有类型的数据库,技术都是相同的。
我不会创建一个包含各种用户数据的庞大表,而只会创建三个属性:UserId、Username 和 Password。
为了开始,我将创建一个表来存储用户。我将称这个表为“Users”,它将包含 3 列。(稍后我将添加另一列)
UserId int (not null) Identity
Username varchar(50)
Password varchar(50)
-- Create script for Users table
CREATE TABLE [Users](
[UserId] [int] IDENTITY(1,1) NOT NULL,
[Username] [varchar](50) NOT NULL,
[Password] [varchar](50) NOT NULL
)
为了将用户添加到我们的数据库,我们可以创建一个函数来实现。该函数接受用户名和密码作为参数。我将创建一个名为“UserDatabase”的类,将所有与 Users 表的交互放在一个地方。
public class UserDatabase
{
private static string connectionstring = "SERVER=127.0.0.1; DATABASE=MyDatabase; UID=***; PWD=***";
public static bool AddUser(string username, string password)
{
// This function will add a user to our database
SqlConnection con = new SqlConnection(connectionstring);
using (SqlCommand cmd = new SqlCommand("INSERT INTO [Users] VALUES (@username, @password)", con))
{
// Add the input as parameters to avoid sql-injections
// I'll explain later in this article.
cmd.Parameters.AddWithValue("@username", username);
cmd.Parameters.AddWithValue("@password", password);
con.Open();
cmd.ExecuteNonQuery();
con.Close();
}
return true;
}
}
现在我们有了一个简单的添加用户函数。它不检查用户名是否存在或执行其他任何操作,只是简单地插入一个新用户。
所以现在我们可以创建一个 WebForm 让用户注册
<form id="form1" runat="server">
<asp:TextBox ID="txtUsername" runat="server"/>
<asp:TextBox ID="txtPassword" runat="server" TextMode="Password"/>
<asp:Button ID="btnAddUser" runat="server" Text="Submit" OnClick="btnAddUser_Click" />
</form>
以及后台代码
protected void btnAddUser_Click(object sender, EventArgs e)
{
string username = txtUsername.Text;
string password = txtPassword.Text;
bool result = UserDatabase.AddUser(username, password);
}
现在用户可以填写表单,点击提交,用户的用户名和密码将被存储在我们的数据库中。唯一的问题是密码将以明文形式存储,这是不行的!
如果我填写表单并点击提交,这将存储在我们的表中
UserId | Username | Password ------------------------------------------- 1 | AlluvialDeposit | mySecretPassword
而这正是本文将帮助您通过哈希密码来避免的问题。
什么是哈希?
有大量关于哈希及其工作原理的文章。但我将解释最基础的部分。
如果您对字符串(或其他数据)进行哈希处理,您将得到一个无法还原回其原始形式的字符串。但关键是,如果您使用相同的哈希算法,您将始终获得相同的结果。
如果我(使用 SHA1)哈希我的密码“mySecretPassword”,我将得到字符串“F032680299B077AFB95093DE4082F625502B8251”。这个字符串无法还原回原始值!
哈希字符串很容易。这个函数将完成这项工作
public class Security
{
public static string HashSHA1(string value)
{
var sha1 = System.Security.Cryptography.SHA1.Create();
var inputBytes = Encoding.ASCII.GetBytes(value);
var hash = sha1.ComputeHash(inputBytes);
var sb = new StringBuilder();
for (var i = 0; i < hash.Length; i++)
{
sb.Append(hash[i].ToString("X2"));
}
return sb.ToString();
}
}
我使用的是 SHA-1,它将返回一个长度为 40 的字符串,无论输入值是什么。如果您哈希一兆字节的明文,哈希值仍将是 40 个字符长。所以,是的,理论上存在通过两个不同的输入值产生两个相同的哈希值的方法。但这种情况很少见!非常罕见。
添加哈希功能
所以,如果我们回顾 UserDatabase 类中的 AddUser 函数
// From the example above
cmd.Parameters.AddWithValue("@password", password);
// Now we switch this to:
cmd.Parameters.AddWithValue("@password", Security.HashSHA1(password));
所以,如果您回到浏览器并尝试注册一个新用户,表中将添加另一行 userId 为 2。但这次用户的密码是经过哈希处理的!
UserId | Username | Password ------------------------------------------- 1 | AlluvialDeposit | mySecretPassword 2 | AlluvialDeposit | F032680299B077AFB95093DE4082F625502B8251
如果有人入侵了我们的数据库,他们将无法看到我们用户的密码!或者?
嗯,那不完全是。哈希值是无法反向解析的,但黑客经常使用大型哈希字符串表。如果一个黑客拥有一个包含 1000 万个随机密码(以及最常用的密码)的生成数据库,他可以很容易地在他自己的数据库中查找相同的字符串“F032680299B077AFB95093DE4082F625502B8251”,看看是否能找到匹配项。不幸的是,他通常能找到匹配项!
这种方法的另一个问题是,两个具有相同密码的用户将拥有相同的哈希密码。这也可能是一个安全风险。
幸运的是,这很容易避免。关键在于您需要为每个用户使用某种唯一的字符串,在哈希密码时会用到它。这称为“密码盐”。我使用 System.Guid 来完成这项工作。
此 Guid 必须存储在用户表中,以便我们在用户想要认证时使用它来验证密码。
向“Users”表添加“UserGuid”列,数据类型为 uniqueidentifier
ALTER TABLE [Users]
ADD UserGuid uniqueidentifier NULL
我们还需要对 AddUser() 方法进行一些更改
public static bool AddUser(string username, string password)
{
// This function will add a user to our database
// First create a new Guid for the user. This will be unique for each user
Guid userGuid = System.Guid.NewGuid();
// Hash the password together with our unique userGuid
string hashedPassword = Security.HashSHA1(password + userGuid.ToString());
SqlConnection con = new SqlConnection(connectionstring);
using (SqlCommand cmd = new SqlCommand("INSERT INTO [Users] VALUES (@username, @password, @userguid)", con))
{
cmd.Parameters.AddWithValue("@username", username);
cmd.Parameters.AddWithValue("@password", hashedPassword); // store the hashed value
cmd.Parameters.AddWithValue("@userguid", userGuid); // store the Guid
con.Open();
cmd.ExecuteNonQuery();
con.Close();
}
return true;
}
如果我们再次运行 AddUser.aspx 并存储另一个具有与之前相同的密码的用户,我们将得到以下结果
UserId | Username | Password | UserGuid -------------------------------------------------------------------------------------- 1 | AlluvialDeposit | mySecretPassword | null 2 | AlluvialDeposit | F032680299B077AFB95093DE4082F625502B8251 | null 3 | AlluvialDeposit | 42444F86F185B7F229DD155E8BCF2A9E6D06453C | E089D2FC-97DB-4DA5-A13C-9FCBBD8B7E95
如果我们再次添加一个具有相同条件的另一个用户
UserId | Username | Password | UserGuid -------------------------------------------------------------------------------------- 1 | AlluvialDeposit | mySecretPassword | null 2 | AlluvialDeposit | F032680299B077AFB95093DE4082F625502B8251 | null 3 | AlluvialDeposit | 42444F86F185B7F229DD155E8BCF2A9E6D06453C | E089D2FC-97DB-4DA5-A13C-9FCBBD8B7E95 4 | AlluvialDeposit | D329E99795960F93857CA49D5FB0A8F141C34671 | C6B73B2D-F704-4250-81C4-EFAEC5DB5F54
用户 ID 3 和 4 具有相同的密码!但在数据库中看不出来!黑客也无法检查我的 passwordHash 是否能在他预生成的数据库中找到匹配项。
黑客有可能找出密码是什么,但这需要花费大量时间!他将不得不为数据库中的每个用户执行耗时的操作。
所以现在我们的数据是(足够)安全的。
别忘了在用户注册新用户时检查用户名是否已存在!
用户认证
将此函数添加到您的“UserDatabase”类中。此函数将根据用户名和密码为您提供 userId。
public static int GetUserIdByUsernameAndPassword(string username, string password)
{
// this is the value we will return
int userId = 0;
SqlConnection con = new SqlConnection(connectionstring);
using (SqlCommand cmd = new SqlCommand("SELECT UserId, Password, UserGuid FROM [Users] WHERE username=@username", con))
{
cmd.Parameters.AddWithValue("@username", username);
con.Open();
SqlDataReader dr = cmd.ExecuteReader();
while(dr.Read())
{
// dr.Read() = we found user(s) with matching username!
int dbUserId = Convert.ToInt32(dr["UserId"]);
string dbPassword = Convert.ToString(dr["Password"]);
string dbUserGuid = Convert.ToString(dr["UserGuid"]);
// Now we hash the UserGuid from the database with the password we wan't to check
// In the same way as when we saved it to the database in the first place. (see AddUser() function)
string hashedPassword = Security.HashSHA1(password + dbUserGuid);
// if its correct password the result of the hash is the same as in the database
if(dbPassword == hashedPassword)
{
// The password is correct
userId = dbUserId;
}
}
con.Close();
}
// Return the user id which is 0 if we did not found a user.
return userId;
}
我们在这里所做的是根据用户名从数据库获取用户。如果我们找到匹配的用户,我们就必须检查密码是否正确。密码已使用密码盐进行哈希处理,所以我们必须对用户输入的密码执行相同的过程(哈希处理),就像用户注册时所做的一样。如果密码相同,哈希函数将返回与数据库中的哈希密码相同的哈希结果。我们比较这两个哈希密码以查看它们是否匹配。
现在我们可以使用这个简单的代码来认证用户
int userId = UserDatabase.GetUserIdByUsernameAndPassword("AlluvialDeposit", "mySecretPassword");
if(userId > 0)
{
// Username and password are correct and we know which user!
}
创建一个简单的登录表单,并在后台代码中验证用户名/密码
<form id="form1" runat="server">
<asp:TextBox ID="txtUsername" runat="server"/>
<asp:TextBox ID="txtPassword" runat="server" TextMode="Password"/>
<asp:Button ID="btnLogin" runat="server" Text="Submit" OnClick="btnLogin_Click" />
<hr />
<asp:Label runat="server" id="lblLoginResult"/>
</form>
protected void btnLogin_Click(object sender, EventArgs e)
{
var username = txtUsername.Text;
var password = txtPassword.Text;
int userId = UserDatabase.GetUserIdByUsernameAndPassword(username, password);
if(userId > 0)
{
// Now you can put users id in a session-variable or what you prefer
// and redirect the user to the protected area of your website.
lblLoginResult.Text = string.Format("You are userId : {0}", userId);
}
else
{
lblLoginResult.Text = "Wrong username or password";
}
}
密码恢复怎么办?
有时用户会忘记密码。我们必须处理这个问题,但我们无法读取用户的密码,因此我们无法发送电子邮件告知用户其密码( anyway,这也不是个好主意!)。
因此,如果用户丢失了密码,我们将不得不发送一封包含链接到某个页面的电子邮件,用户可以在该页面设置新密码,并以与用户注册时相同的方式存储。
非常基础的 SQL 注入解释
SQL 注入是攻击网站最常用的方法之一。它很容易做到,而且有很多网站容易受到这种攻击。
SQL 注入就是篡改您的 SQL 查询。如果我能够将我的字符串插入到您的 SQL 查询中,那么您就有麻烦了!
假设您有一个带有此代码的登录表单
protected void btnLogin_Click(object sender, EventArgs e)
{
SqlConnection con = new SqlConnection(connectionstring);
string sql = "SELECT UserId FROM [Users] WHERE username='" + TextBox1.Text + "'";
using (SqlCommand cmd = new SqlCommand(sql, con))
{
SqlDataReader dr = cmd.ExecuteReader();
// Read som data and return userid..
}
}
变量 sql 将包含要针对我们的数据库执行的查询。所以,如果用户在 TextBox1 中输入“hello”,sql 将是
SELECT UserId FROM [Users] WHERE username='hello'
这不会有问题。但如果用户输入 '; DELETE FROM [Users],我们的 sql 将是
SELECT UserId FROM [Users] WHERE username=''; DELETE FROM [Users]
现在您所有的用户都已被删除!
这只是一个简单的例子来说明正在发生的事情。一个聪明的用户可以在那个文本框中输入一些内容来登录您的数据库中的任何用户。
如果您始终使用 SqlParameters(如本文所示),您将避免最明显的 SQL 注入方式。
摘要
这是一个示例,涵盖了最基础的部分。但如果您遵循这些指南,您将朝着正确的方向迈出巨大一步。这是关于您用户安全的问题!
祝您拥有安全的数据库用户!