安全地允许一次性访问ASP.NET MVC 5应用程序的一部分 [英] Securely allowing one-time access to a section of an ASP.NET MVC 5 app

查看:39
本文介绍了安全地允许一次性访问ASP.NET MVC 5应用程序的一部分的处理方法,对大家解决问题具有一定的参考价值,需要的朋友们下面随着小编来一起学习吧!

问题描述

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

Part of an app i'm building requires that admin users can let an employee access one page of the app to perform a task. After the employee has completed that task, they have no reason to return to the app.

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

This app is hosted online and so the employee access needs to be secured with a logon.

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

My question is, what is the best approach regarding providing a login account to a user who would only use the system once?

如我所见,我有两个选择:

As I see it, I have two options:

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

  1. Provide the admin users with one permanent login account for employees, which can be re-used for each employee (i would need to provide each employee with an extra passcode so that the system can look it up and see who they really are)

在需要访问权限时为每个员工创建一个登录帐户,然后在使用该登录帐户后将其删除.对于此用户名,我将用一个唯一的ID(可能是其任务的ID)连接一个常用词(例如公司名称)

Create a login account for each employee as and when they need access, and then delete the login account after it has been used. For this username I would concatenate a common word (company name for example) with a unique id (possibly the id of their task)

在安全性方面,选项2似乎最有意义.这种方法有什么陷阱吗?还是有其他替代解决方案?

Option 2 seems to make the most sense in terms of security. Are there any pitfalls with this approach, or are there any alternative solutions?

推荐答案

我个人考虑第三个选择:为此页面创建一个并行访问控制表.换句话说,您将拥有类似的东西:

Personally, I would consider a third option: create a parallel access control table for this page. In other words, you'd have something like:

public class PageAccess
{
    public string Email { get; set; }
    public string Token { get; set; }
    public DateTime Expiration { get; set; }
}

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

When an admin wants to grant access to the page, they would give the email of the user who should have access (Email). A random token would then be generated (saved hashed as Token). Then the user would be sent an email at their email address with a URL to the page which would include a parameter composed of the email address and token, and then base 64 encoded.

点击链接后,用户将转到页面,首先将对该参数进行验证:base 64解码,拆分电子邮件和令牌,通过电子邮件,哈希令牌查找访问记录并与存储的令牌进行比较,以及(可选)将到期日期与现在进行比较(以便使人们避免尝试从几个月或几年前发送的电子邮件中访问URL).

Upon clicking the link the user would be taken to the page, where first, the parameter would be validated: base 64 decode, split email and token, lookup the access record by email, hash token and compare to stored token, and (optionally) compare the expiration date with now (so that you can keep people from trying to access a URL from an email sent months or years ago).

如果一切正常,则会向用户显示该页面.当他们完成需要执行的所有操作后,您将删除访问记录.

If everything is kosher, the user is shown the page. When they complete whatever action they need to make, you delete the access record.

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

This is essentially the same process employed by a password reset, only here, you're just using it to grant one-time access instead of allowing them to change their password.

更新

以下是我使用的实用程序类.我不是安全专家,但是我做了一些广泛的阅读,并大量借鉴了我在某个地方某个地方发现的StackExchange代码,这些代码要么不再公开存在,要么规避了我的搜索技能.

The following is a utility class that I use. I'm not a security expert, but I did some extensive reading and borrowed heavily from StackExchange code I found at some point, somewhere, which either doesn't exist publicly anymore, or evades my search skills.

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));
    }
}

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

Then, I do something like the following for generating password resets:

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

hashedToken变量存储在数据库中,而emailToken是发送到用户的URL中的内容.关于处理URL的操作:

The hashedToken variable gets stored in your database, while emailToken is what is put in the URL that is sent to your user. On the action that handles the URL:

var parts = CryptoUtil.Base64Decode(emailToken).Split(':');
var email = parts[0];
var token = parts[1];

使用email查找记录.然后使用:

Look up the record using email. Then compare using:

CryptoUtil.ValidateHash(token, hashedTokenFromDatabase)

这篇关于安全地允许一次性访问ASP.NET MVC 5应用程序的一部分的文章就介绍到这了,希望我们推荐的答案对大家有所帮助,也希望大家多多支持IT屋!

查看全文
登录 关闭
扫码关注1秒登录
发送“验证码”获取 | 15天全站免登陆