Лично я бы рассмотрел третий вариант: создать параллельную таблицу управления доступом для этой страницы. Другими словами, у вас будет что-то вроде:
public class PageAccess
{
public string Email { get; set; }
public string Token { get; set; }
public DateTime Expiration { get; set; }
}
Когда администратор хочет предоставить доступ к странице, он сообщает адрес электронной почты пользователя, у которого должен быть доступ (Email
). Затем будет сгенерирован случайный токен (сохраненный в хешированном виде как Token
). Затем пользователю будет отправлено электронное письмо на его адрес электронной почты с URL-адресом страницы, который будет включать параметр, состоящий из адреса электронной почты и токена, а затем закодированный по базе 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));
}
}
Затем я делаю что-то вроде следующего для создания сброса пароля:
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)
02.02.2015