安全地允许一次性访问 ASP.NET MVC 5 应用程序的一部分

Rob*_*Rob 0 asp.net security authentication asp.net-mvc asp.net-identity

我正在构建的应用程序的一部分要求管理员用户可以让员工访问应用程序的一个页面来执行任务。员工完成该任务后,他们没有理由返回应用程序。

此应用程序是在线托管的,因此需要通过登录来保护员工访问权限。

我的问题是,向只会使用系统一次的用户提供登录帐户的最佳方法是什么?

在我看来,我有两个选择:

  1. 为管理员用户提供一个永久的员工登录帐户,该帐户可以为每个员工重复使用(我需要为每个员工提供一个额外的密码,以便系统可以查找并查看他们的真实身份)

  2. 在需要访问时为每个员工创建一个登录帐户,然后在使用后删除该登录帐户。对于这个用户名,我会将一个常用词(例如公司名称)与一个唯一的 ID(可能是他们任务的 ID)连接起来

选项 2 似乎在安全性方面最有意义。这种方法是否存在任何缺陷,或者是否有其他解决方案?

Chr*_*att 5

就个人而言,我会考虑第三种选择:为此页面创建一个并行访问控制表。换句话说,你会有类似的东西:

public class PageAccess
{
    public string Email { get; set; }
    public string Token { get; set; }
    public DateTime Expiration { get; set; }
}
Run Code Online (Sandbox Code Playgroud)

当管理员想要授予对页面的访问权限时,他们会提供应该具有访问权限的用户的电子邮件 ( Email)。然后会生成一个随机令牌(散列保存为Token)。然后,用户将收到一封电子邮件,其中包含指向该页面的 URL 的电子邮件地址,该 URL 将包含由电子邮件地址和令牌组成的参数,然后进行 base 64 编码。

单击链接后,用户将被带到页面,首先,参数将被验证:base 64 解码、拆分电子邮件和令牌、通过电子邮件查找访问记录、散列令牌并与存储的令牌进行比较,以及(可选)将到期日期与现在进行比较(这样您就可以防止人们尝试访问几个月或几年前发送的电子邮件中的 URL)。

如果一切都符合 kosher 标准,则向用户显示该页面。当他们完成他们需要执行的任何操作时,您将删除访问记录。

这本质上与密码重置所采用的过程相同,只是在这里,您只是使用它来授予一次性访问权限,而不是允许他们更改密码。

更新

以下是我使用的实用程序类。我不是安全专家,但我做了一些广泛的阅读,并大量借用了我在某个时候、某个地方发现的 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));
    }
}
Run Code Online (Sandbox Code Playgroud)

然后,我执行以下操作来生成密码重置:

var token = CryptoUtil.GetUniqueKey(16);
var hashedToken = CryptoUtil.CreateHash(token);
var emailToken = CryptoUtil.Base64Encode(string.Format("{0}:{1}", email, token));
Run Code Online (Sandbox Code Playgroud)

hashedToken变量存储在您的数据库中,而emailToken放入发送给您的用户的 URL 中的内容。在处理 URL 的操作上:

var parts = CryptoUtil.Base64Decode(emailToken).Split(':');
var email = parts[0];
var token = parts[1];
Run Code Online (Sandbox Code Playgroud)

使用 查找记录email。然后比较使用:

CryptoUtil.ValidateHash(token, hashedTokenFromDatabase)
Run Code Online (Sandbox Code Playgroud)