安全地允许一次性访问 ASP.NET MVC 5 应用程序的一部分
Securely allowing one-time access to a section of an ASP.NET MVC 5 app
我正在构建的应用程序的一部分要求管理员用户可以让员工访问应用程序的一个页面以执行任务。员工完成该任务后,他们没有理由 return 使用该应用程序。
此应用是在线托管的,因此员工访问需要通过登录来保护。
我的问题是,为只使用系统一次的用户提供登录帐户的最佳方法是什么?
在我看来,我有两个选择:
为管理员用户提供一个员工永久登录帐户,每个员工都可以重复使用(我需要为每个员工提供一个额外的密码,以便系统可以查看它看看他们到底是谁)
在每个员工需要访问时为他们创建一个登录帐户,然后在使用后删除登录帐户。对于这个用户名,我会将一个常用词(例如公司名称)与一个唯一 ID(可能是他们任务的 ID)连接起来
选项 2 似乎在安全方面最有意义。这种方法是否有任何缺陷,或者是否有任何替代解决方案?
就我个人而言,我会考虑第三种选择:为此页面创建并行访问控制 table。换句话说,你会有这样的东西:
public class PageAccess
{
public string Email { get; set; }
public string Token { get; set; }
public DateTime Expiration { get; set; }
}
当管理员想要授予对页面的访问权限时,他们会提供应该具有访问权限的用户的电子邮件 (Email
)。然后将生成一个随机令牌(保存散列为 Token
)。然后,用户会收到一封电子邮件,地址为 URL,页面包含一个由电子邮件地址和令牌组成的参数,然后进行 base 64 编码。
单击 link 后,用户将被带到该页面,首先,将验证参数:base 64 解码、拆分电子邮件和令牌、通过电子邮件查找访问记录、哈希令牌和与存储的令牌进行比较,并且(可选)将到期日期与现在进行比较(这样您就可以防止人们尝试从几个月或几年前发送的电子邮件中访问 URL)。
如果一切都符合标准,则会向用户显示该页面。当他们完成他们需要做的任何操作时,您删除访问记录。
这基本上与密码重置所采用的过程相同,只是在这里,您只是使用它来授予一次性访问权限,而不是允许他们更改密码。
更新
以下是我使用的实用程序class。我不是安全专家,但我做了一些广泛的阅读,并大量借鉴了我在某个时候、某个地方发现的 StackExchange 代码,这些代码要么不再公开存在,要么逃避了我的搜索技巧。
using System;
using System.Security.Cryptography;
using System.Text;
public static class CryptoUtil
{
// The following constants may be changed without breaking existing hashes.
public const int SaltBytes = 32;
public const int HashBytes = 32;
public const int Pbkdf2Iterations = /* Some int here. Larger is better, but also slower. Something in the range of 1000-2000 works well. Don't expose this value. */;
public const int IterationIndex = 0;
public const int SaltIndex = 1;
public const int Pbkdf2Index = 2;
/// <summary>
/// Creates a salted PBKDF2 hash of the password.
/// </summary>
/// <param name="password">The password to hash.</param>
/// <returns>The hash of the password.</returns>
public static string CreateHash(string password)
{
// TODO: Raise exception is password is null
// Generate a random salt
RNGCryptoServiceProvider csprng = new RNGCryptoServiceProvider();
byte[] salt = new byte[SaltBytes];
csprng.GetBytes(salt);
// Hash the password and encode the parameters
byte[] hash = PBKDF2(password, salt, Pbkdf2Iterations, HashBytes);
return Pbkdf2Iterations.ToString("X") + ":" +
Convert.ToBase64String(salt) + ":" +
Convert.ToBase64String(hash);
}
/// <summary>
/// Validates a password given a hash of the correct one.
/// </summary>
/// <param name="password">The password to check.</param>
/// <param name="goodHash">A hash of the correct password.</param>
/// <returns>True if the password is correct. False otherwise.</returns>
public static bool ValidateHash(string password, string goodHash)
{
// Extract the parameters from the hash
char[] delimiter = { ':' };
string[] split = goodHash.Split(delimiter);
int iterations = Int32.Parse(split[IterationIndex], System.Globalization.NumberStyles.HexNumber);
byte[] salt = Convert.FromBase64String(split[SaltIndex]);
byte[] hash = Convert.FromBase64String(split[Pbkdf2Index]);
byte[] testHash = PBKDF2(password, salt, iterations, hash.Length);
return SlowEquals(hash, testHash);
}
/// <summary>
/// Compares two byte arrays in length-constant time. This comparison
/// method is used so that password hashes cannot be extracted from
/// on-line systems using a timing attack and then attacked off-line.
/// </summary>
/// <param name="a">The first byte array.</param>
/// <param name="b">The second byte array.</param>
/// <returns>True if both byte arrays are equal. False otherwise.</returns>
private static bool SlowEquals(byte[] a, byte[] b)
{
uint diff = (uint)a.Length ^ (uint)b.Length;
for (int i = 0; i < a.Length && i < b.Length; i++)
diff |= (uint)(a[i] ^ b[i]);
return diff == 0;
}
/// <summary>
/// Computes the PBKDF2-SHA1 hash of a password.
/// </summary>
/// <param name="password">The password to hash.</param>
/// <param name="salt">The salt.</param>
/// <param name="iterations">The PBKDF2 iteration count.</param>
/// <param name="outputBytes">The length of the hash to generate, in bytes.</param>
/// <returns>A hash of the password.</returns>
private static byte[] PBKDF2(string password, byte[] salt, int iterations, int outputBytes)
{
Rfc2898DeriveBytes pbkdf2 = new Rfc2898DeriveBytes(password, salt);
pbkdf2.IterationCount = iterations;
return pbkdf2.GetBytes(outputBytes);
}
public static string GetUniqueKey(int length)
{
char[] chars = "abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ1234567890".ToCharArray();
byte[] bytes = new byte[length];
using (var rng = new RNGCryptoServiceProvider())
{
rng.GetNonZeroBytes(bytes);
}
var result = new StringBuilder(length);
foreach (byte b in bytes)
{
result.Append(chars[b % (chars.Length - 1)]);
}
return result.ToString();
}
public static string Base64Encode(string str)
{
return Convert.ToBase64String(System.Text.Encoding.UTF8.GetBytes(str));
}
public static string Base64Decode(string str)
{
return System.Text.Encoding.UTF8.GetString(Convert.FromBase64String(str));
}
public static string Base64EncodeGuid(Guid guid)
{
return Convert.ToBase64String(guid.ToByteArray());
}
public static Guid Base64DecodeGuid(string str)
{
return new Guid(Convert.FromBase64String(str));
}
}
然后,我执行类似以下操作来生成密码重置:
var token = CryptoUtil.GetUniqueKey(16);
var hashedToken = CryptoUtil.CreateHash(token);
var emailToken = CryptoUtil.Base64Encode(string.Format("{0}:{1}", email, token));
hashedToken
变量存储在您的数据库中,而 emailToken
是发送给您的用户的 URL 中的内容。关于处理 URL:
的操作
var parts = CryptoUtil.Base64Decode(emailToken).Split(':');
var email = parts[0];
var token = parts[1];
使用email
查找记录。然后比较使用:
CryptoUtil.ValidateHash(token, hashedTokenFromDatabase)
我正在构建的应用程序的一部分要求管理员用户可以让员工访问应用程序的一个页面以执行任务。员工完成该任务后,他们没有理由 return 使用该应用程序。
此应用是在线托管的,因此员工访问需要通过登录来保护。
我的问题是,为只使用系统一次的用户提供登录帐户的最佳方法是什么?
在我看来,我有两个选择:
为管理员用户提供一个员工永久登录帐户,每个员工都可以重复使用(我需要为每个员工提供一个额外的密码,以便系统可以查看它看看他们到底是谁)
在每个员工需要访问时为他们创建一个登录帐户,然后在使用后删除登录帐户。对于这个用户名,我会将一个常用词(例如公司名称)与一个唯一 ID(可能是他们任务的 ID)连接起来
选项 2 似乎在安全方面最有意义。这种方法是否有任何缺陷,或者是否有任何替代解决方案?
就我个人而言,我会考虑第三种选择:为此页面创建并行访问控制 table。换句话说,你会有这样的东西:
public class PageAccess
{
public string Email { get; set; }
public string Token { get; set; }
public DateTime Expiration { get; set; }
}
当管理员想要授予对页面的访问权限时,他们会提供应该具有访问权限的用户的电子邮件 (Email
)。然后将生成一个随机令牌(保存散列为 Token
)。然后,用户会收到一封电子邮件,地址为 URL,页面包含一个由电子邮件地址和令牌组成的参数,然后进行 base 64 编码。
单击 link 后,用户将被带到该页面,首先,将验证参数:base 64 解码、拆分电子邮件和令牌、通过电子邮件查找访问记录、哈希令牌和与存储的令牌进行比较,并且(可选)将到期日期与现在进行比较(这样您就可以防止人们尝试从几个月或几年前发送的电子邮件中访问 URL)。
如果一切都符合标准,则会向用户显示该页面。当他们完成他们需要做的任何操作时,您删除访问记录。
这基本上与密码重置所采用的过程相同,只是在这里,您只是使用它来授予一次性访问权限,而不是允许他们更改密码。
更新
以下是我使用的实用程序class。我不是安全专家,但我做了一些广泛的阅读,并大量借鉴了我在某个时候、某个地方发现的 StackExchange 代码,这些代码要么不再公开存在,要么逃避了我的搜索技巧。
using System;
using System.Security.Cryptography;
using System.Text;
public static class CryptoUtil
{
// The following constants may be changed without breaking existing hashes.
public const int SaltBytes = 32;
public const int HashBytes = 32;
public const int Pbkdf2Iterations = /* Some int here. Larger is better, but also slower. Something in the range of 1000-2000 works well. Don't expose this value. */;
public const int IterationIndex = 0;
public const int SaltIndex = 1;
public const int Pbkdf2Index = 2;
/// <summary>
/// Creates a salted PBKDF2 hash of the password.
/// </summary>
/// <param name="password">The password to hash.</param>
/// <returns>The hash of the password.</returns>
public static string CreateHash(string password)
{
// TODO: Raise exception is password is null
// Generate a random salt
RNGCryptoServiceProvider csprng = new RNGCryptoServiceProvider();
byte[] salt = new byte[SaltBytes];
csprng.GetBytes(salt);
// Hash the password and encode the parameters
byte[] hash = PBKDF2(password, salt, Pbkdf2Iterations, HashBytes);
return Pbkdf2Iterations.ToString("X") + ":" +
Convert.ToBase64String(salt) + ":" +
Convert.ToBase64String(hash);
}
/// <summary>
/// Validates a password given a hash of the correct one.
/// </summary>
/// <param name="password">The password to check.</param>
/// <param name="goodHash">A hash of the correct password.</param>
/// <returns>True if the password is correct. False otherwise.</returns>
public static bool ValidateHash(string password, string goodHash)
{
// Extract the parameters from the hash
char[] delimiter = { ':' };
string[] split = goodHash.Split(delimiter);
int iterations = Int32.Parse(split[IterationIndex], System.Globalization.NumberStyles.HexNumber);
byte[] salt = Convert.FromBase64String(split[SaltIndex]);
byte[] hash = Convert.FromBase64String(split[Pbkdf2Index]);
byte[] testHash = PBKDF2(password, salt, iterations, hash.Length);
return SlowEquals(hash, testHash);
}
/// <summary>
/// Compares two byte arrays in length-constant time. This comparison
/// method is used so that password hashes cannot be extracted from
/// on-line systems using a timing attack and then attacked off-line.
/// </summary>
/// <param name="a">The first byte array.</param>
/// <param name="b">The second byte array.</param>
/// <returns>True if both byte arrays are equal. False otherwise.</returns>
private static bool SlowEquals(byte[] a, byte[] b)
{
uint diff = (uint)a.Length ^ (uint)b.Length;
for (int i = 0; i < a.Length && i < b.Length; i++)
diff |= (uint)(a[i] ^ b[i]);
return diff == 0;
}
/// <summary>
/// Computes the PBKDF2-SHA1 hash of a password.
/// </summary>
/// <param name="password">The password to hash.</param>
/// <param name="salt">The salt.</param>
/// <param name="iterations">The PBKDF2 iteration count.</param>
/// <param name="outputBytes">The length of the hash to generate, in bytes.</param>
/// <returns>A hash of the password.</returns>
private static byte[] PBKDF2(string password, byte[] salt, int iterations, int outputBytes)
{
Rfc2898DeriveBytes pbkdf2 = new Rfc2898DeriveBytes(password, salt);
pbkdf2.IterationCount = iterations;
return pbkdf2.GetBytes(outputBytes);
}
public static string GetUniqueKey(int length)
{
char[] chars = "abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ1234567890".ToCharArray();
byte[] bytes = new byte[length];
using (var rng = new RNGCryptoServiceProvider())
{
rng.GetNonZeroBytes(bytes);
}
var result = new StringBuilder(length);
foreach (byte b in bytes)
{
result.Append(chars[b % (chars.Length - 1)]);
}
return result.ToString();
}
public static string Base64Encode(string str)
{
return Convert.ToBase64String(System.Text.Encoding.UTF8.GetBytes(str));
}
public static string Base64Decode(string str)
{
return System.Text.Encoding.UTF8.GetString(Convert.FromBase64String(str));
}
public static string Base64EncodeGuid(Guid guid)
{
return Convert.ToBase64String(guid.ToByteArray());
}
public static Guid Base64DecodeGuid(string str)
{
return new Guid(Convert.FromBase64String(str));
}
}
然后,我执行类似以下操作来生成密码重置:
var token = CryptoUtil.GetUniqueKey(16);
var hashedToken = CryptoUtil.CreateHash(token);
var emailToken = CryptoUtil.Base64Encode(string.Format("{0}:{1}", email, token));
hashedToken
变量存储在您的数据库中,而 emailToken
是发送给您的用户的 URL 中的内容。关于处理 URL:
var parts = CryptoUtil.Base64Decode(emailToken).Split(':');
var email = parts[0];
var token = parts[1];
使用email
查找记录。然后比较使用:
CryptoUtil.ValidateHash(token, hashedTokenFromDatabase)