安全地允许一次性访问ASP.NET MVC 5应用程序的某个部分
我正在构建的应用程序的一部分要求管理员用户可以让员工访问应用程序的一个页面来执行任务。在员工完成该任务后,他们没有理由返回应用程序 此应用程序是在线托管的,因此需要通过登录来保护员工的访问权限 我的问题是,向只使用一次系统的用户提供登录帐户的最佳方法是什么 在我看来,我有两个选择:安全地允许一次性访问ASP.NET MVC 5应用程序的某个部分,asp.net,asp.net-mvc,security,authentication,asp.net-identity,Asp.net,Asp.net Mvc,Security,Authentication,Asp.net Identity,我正在构建的应用程序的一部分要求管理员用户可以让员工访问应用程序的一个页面来执行任务。在员工完成该任务后,他们没有理由返回应用程序 此应用程序是在线托管的,因此需要通过登录来保护员工的访问权限 我的问题是,向只使用一次系统的用户提供登录帐户的最佳方法是什么 在我看来,我有两个选择: 为管理员用户提供一个员工的永久登录帐户,可以为每个员工重复使用(我需要为每个员工提供一个额外的密码,以便系统可以查找并了解他们的真实身份) 在每个员工需要访问时为他们创建一个登录帐户,然后在使用该登录帐户后将其删除。
选择2在安全方面似乎最有意义。这个方法有什么缺陷,或者有其他的解决方案吗? < P>个人,我会考虑第三个选项:为这个页面创建一个并行访问控制表。换言之,你会有类似于:
public class PageAccess
{
public string Email { get; set; }
public string Token { get; set; }
public DateTime Expiration { get; set; }
}
当管理员想要授予对页面的访问权限时,他们会给应该有访问权限的用户发送电子邮件(电子邮件
)。然后将生成一个随机令牌(散列保存为令牌
)。然后,用户将在其电子邮件地址处发送一封电子邮件,其中包含一个指向页面的URL,该URL将包含一个由电子邮件地址和令牌组成的参数,然后进行base 64编码
单击链接后,用户将进入页面,首先验证参数:base 64解码、拆分电子邮件和令牌、通过电子邮件查找访问记录、哈希令牌并与存储的令牌进行比较,以及(可选)将过期日期与现在进行比较(这样你就可以防止人们试图从几个月或几年前发送的电子邮件中访问URL)
如果一切都符合犹太教,那么页面会显示用户。当用户完成需要执行的任何操作时,您将删除访问记录
这与密码重置所采用的过程基本相同,只是在这里,您只是使用它授予一次性访问权限,而不是允许他们更改密码
更新
下面是我使用的一个实用程序类。我不是一个安全专家,但我做了一些广泛的阅读,大量借用了我在某个地方找到的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));
}
}
hashedToken
变量存储在数据库中,而emailToken
是放在发送给用户的URL中的内容。在处理URL的操作中:
var parts = CryptoUtil.Base64Decode(emailToken).Split(':');
var email = parts[0];
var token = parts[1];
使用电子邮件
查找记录。然后使用以下方法进行比较:
CryptoUtil.ValidateHash(token, hashedTokenFromDatabase)
P>个人,我会考虑第三个选项:为这个页面创建一个并行访问控制表。换言之,你会有这样的事情:
public class PageAccess
{
public string Email { get; set; }
public string Token { get; set; }
public DateTime Expiration { get; set; }
}
当管理员想要授予对页面的访问权限时,他们会给应该有访问权限的用户发送电子邮件(电子邮件
)。然后会生成一个随机令牌(散列保存为令牌
)。然后,用户将在其电子邮件地址处收到一封电子邮件,其中包含一个指向该页面的URL,该页面将包含一个由电子邮件地址和令牌组成的参数,然后进行base 64编码
单击链接后,用户将进入页面,首先验证参数:base 64解码、拆分电子邮件和令牌、通过电子邮件查找访问记录、哈希令牌并与存储的令牌进行比较,以及(可选)将过期日期与现在进行比较(这样你就可以防止人们试图从几个月或几年前发送的电子邮件中访问URL)
如果一切都符合犹太教,那么页面会显示用户。当用户完成需要执行的任何操作时,您将删除访问记录
这与密码重置所采用的过程基本相同,只是在这里,您只是使用它授予一次性访问权限,而不是允许他们更改密码
更新
下面是我使用的一个实用程序类。我不是一个安全专家,但我做了一些广泛的阅读,大量借用了我在某个地方找到的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));
}
}
hashedToken
变量存储在数据库中,而emailToken
是放在发送给用户的URL中的内容。在处理URL的操作中:
var parts = CryptoUtil.Base64Decode(emailToken).Split(':');
var email = parts[0];
var token = parts[1];
使用电子邮件
查找记录。然后使用以下方法进行比较:
CryptoUtil.ValidateHash(token, hashedTokenFromDatabase)
这种方法似乎很理想,但我不知道从哪里开始。我如何为一件事生成令牌?看看account controller中的Forgotten Password操作,它利用了UserManager上的一个函数,这对于我想在这里做的事情是不可用的。感谢您为这一伟大的解释付出了努力Chris。我已经有了很多现在一切正常。这种方法似乎很理想,但我不知道从哪里开始。我如何为一件事生成令牌?查看account controller中的Forgotten Password操作,它使用了UserManager上的一个函数,这对于我想在这里做的事情是不可用的。感谢您为这一伟大的解释付出了努力。我现在的工作都很顺利。