如何生成一个在24小时后过期的唯一令牌?

50

我有一个WCF Web服务,用于检查用户是否有效。

如果用户有效,我想生成一个在24小时后过期的令牌。

public bool authenticateUserManual(string userName, string password,string language,string token)
{
    if (Membership.ValidateUser(userName,password))
    {
        //////////
        string token = ???? 
        //////////

        return true;
    }
    else 
    {
        return false;
    }
}   

2
我们将生成一个令牌并将其与时间戳一起存储,下次检查它是否在24小时内使用,否则显示已过期的消息。 - V4Vendetta
你能给我一个简短的例子吗?我不知道如何生成唯一的令牌。 - Eray Geveci
6个回答

160

有两种可能的方法:一种是创建一个唯一值并将其与创建时间一起存储在某处,例如数据库中;另一种是将创建时间放入令牌中,以便稍后解码并查看其创建时间。

要创建一个唯一令牌:

string token = Convert.ToBase64String(Guid.NewGuid().ToByteArray());

创建包含时间戳的唯一令牌的基本示例:

byte[] time = BitConverter.GetBytes(DateTime.UtcNow.ToBinary());
byte[] key = Guid.NewGuid().ToByteArray();
string token = Convert.ToBase64String(time.Concat(key).ToArray());

解码令牌以获取创建时间:

byte[] data = Convert.FromBase64String(token);
DateTime when = DateTime.FromBinary(BitConverter.ToInt64(data, 0));
if (when < DateTime.UtcNow.AddHours(-24)) {
  // too old
}

注意: 如果您需要时间戳的令牌更加安全,您需要对其进行加密。否则,用户可能会弄清楚它包含什么并创建一个伪造的令牌。


Convert.ToBase64String(data) 会包含时间戳,对吗? - SuperFrog
6
是的,该令牌包含时间戳。您是指您想获取GUID密钥吗?那么您将使用 new GUID(data.Skip(8).ToArray()) - Guffa
2
使用这个时间解决方案和这种加密 http://msdn.microsoft.com/en-us/library/system.security.cryptography.rijndael.aspx,您可以创建一个漂亮的安全令牌。 - Piotr Kula
2
@Guffa 使用GUID在令牌中安全吗? - Dragon
1
Guid.NewGuid()是可预测的,请查看https://dev59.com/bEfRa4cB1Zd3GeqP82-- - Toolkit
显示剩余9条评论

35

我喜欢Guffa的答案,由于我无法评论,我会在此提供对Udil问题的回答。

我需要类似的东西,但我想要我的令牌中有特定的逻辑,我想要:

  1. 查看令牌的过期时间
  2. 使用guid来掩盖验证(全球应用程序guid或用户guid)
  3. 查看是否为我创建令牌提供了目的(不可重复使用..)
  4. 查看我发送令牌给的用户是否是我正在为其验证的用户

现在点1-3是固定长度的,所以很容易,下面是我的代码:

这是我的生成令牌代码:

public string GenerateToken(string reason, MyUser user)
{
    byte[] _time     = BitConverter.GetBytes(DateTime.UtcNow.ToBinary());
    byte[] _key      = Guid.Parse(user.SecurityStamp).ToByteArray();
    byte[] _Id       = GetBytes(user.Id.ToString());
    byte[] _reason   = GetBytes(reason);
    byte[] data       = new byte[_time.Length + _key.Length + _reason.Length+_Id.Length];

    System.Buffer.BlockCopy(_time, 0, data, 0, _time.Length);
    System.Buffer.BlockCopy(_key , 0, data, _time.Length, _key.Length);
    System.Buffer.BlockCopy(_reason, 0, data, _time.Length + _key.Length, _reason.Length);
    System.Buffer.BlockCopy(_Id, 0, data, _time.Length + _key.Length + _reason.Length, _Id.Length);

    return Convert.ToBase64String(data.ToArray());
}

这是我用来获取生成的令牌字符串并验证它的代码:

public TokenValidation ValidateToken(string reason, MyUser user, string token)
{
    var result = new TokenValidation();
    byte[] data     = Convert.FromBase64String(token);
    byte[] _time     = data.Take(8).ToArray();
    byte[] _key      = data.Skip(8).Take(16).ToArray();
    byte[] _reason   = data.Skip(24).Take(2).ToArray();
    byte[] _Id       = data.Skip(26).ToArray();

    DateTime when = DateTime.FromBinary(BitConverter.ToInt64(_time, 0));
    if (when < DateTime.UtcNow.AddHours(-24))
    {
        result.Errors.Add( TokenValidationStatus.Expired);
    }
    
    Guid gKey = new Guid(_key);
    if (gKey.ToString() != user.SecurityStamp)
    {
        result.Errors.Add(TokenValidationStatus.WrongGuid);
    }

    if (reason != GetString(_reason))
    {
        result.Errors.Add(TokenValidationStatus.WrongPurpose);
    }

    if (user.Id.ToString() != GetString(_Id))
    {
        result.Errors.Add(TokenValidationStatus.WrongUser);
    }
    
    return result;
}

private static string GetString(byte[] reason) => Encoding.ASCII.GetString(reason);

private static byte[] GetBytes(string reason) => Encoding.ASCII.GetBytes(reason);

TokenValidation类的样子如下:

public class TokenValidation
{
    public bool Validated { get { return Errors.Count == 0; } }
    public readonly List<TokenValidationStatus> Errors = new List<TokenValidationStatus>();
}

public enum TokenValidationStatus
{
    Expired,
    WrongUser,
    WrongPurpose,
    WrongGuid
}

现在我有一种简单的方法来验证一个令牌,无需将其保存在列表中约24个小时左右。 以下是我的测试用例:

private const string ResetPasswordTokenPurpose = "RP";
private const string ConfirmEmailTokenPurpose  = "EC";//change here change bit length for reason  section (2 per char)

[TestMethod]
public void GenerateTokenTest()
{
    MyUser user         = CreateTestUser("name");
    user.Id             = 123;
    user.SecurityStamp  = Guid.NewGuid().ToString();
    var token   = sit.GenerateToken(ConfirmEmailTokenPurpose, user);
    var validation    = sit.ValidateToken(ConfirmEmailTokenPurpose, user, token);
    Assert.IsTrue(validation.Validated,"Token validated for user 123");
}

您可以轻松地将代码适应到其他业务情况中。

编码愉快!

Walter


这是一个非常棒的答案;)我进行了一些微调以适应我的逻辑。 - Vikneshwar
1
GetString()怎么样?你能发一下函数体吗? - Jmocke
1
要使用GetString()和GetBytes(),请设置命名空间为System.Text,然后使用Encoding.UTF8.GetString() / Encoding.UTF8.GetBytes()。 - McDee
7
我有这种感觉,这个例子完全省略了任何安全层。似乎没有加密签名可以允许令牌通过不安全的通道传输,并确保不被篡改。在我看来,任何看到这个令牌的人都可以创建有效的变体,通过验证。注意,这个答案似乎是不安全的! - Sven
你能详细解释一下如何跳过和获取数据吗? - Aruna
已修复上面的错误代码,并提供工作示例:https://dotnetfiddle.net/VWagqD - GFoley83

3

使用Dictionary<string, DateTime>存储带有时间戳的令牌:

static Dictionary<string, DateTime> dic = new Dictionary<string, DateTime>();

每当您创建新的令牌时,请添加带有时间戳的令牌:

dic.Add("yourToken", DateTime.Now);

有一个定时器正在运行,用于将任何过期的token从字典中删除:

 timer = new Timer(1000*60); //assume run in 1 minute
 timer.Elapsed += timer_Elapsed;

 static void timer_Elapsed(object sender, ElapsedEventArgs e)
    {
        var expiredTokens = dic.Where(p => p.Value.AddDays(1) <= DateTime.Now)
                              .Select(p => p.Key);

        foreach (var key in expiredTokens)
            dic.Remove(key);
    }

因此,在验证令牌时,只需检查令牌是否存在于字典中即可。

计时器 = 新的计时器(1000*60); 这里有什么遗漏吗? - Eray Geveci
如果有什么遗漏,可以使用 System.Timers.Timer。 - cuongle

0

我也遇到了类似但略有不同的问题。我需要一个令牌,可以在另一台服务器上在2分钟内使用。ServerA和serverB共享一个私钥。当然,在用户浏览器中,令牌是公开的。

enter image description here

Web服务器A创建一个包含日期时间的字符串,然后对其进行哈希处理。用户在浏览器中使用令牌。当服务器B接收到令牌时,执行相同的哈希处理并比较结果。 ServerA和serverB运行C#代码。

问题类似但不完全相同,但也许这段代码可以帮助某些人解决问题....

public class TokenHelper
{

    private static byte[] GetHash(string inputString)
    {
        using (HashAlgorithm algorithm = MD5.Create())
            return algorithm.ComputeHash(Encoding.UTF8.GetBytes(inputString));
    }

    private static string GetHashString(string inputString)
    {
        StringBuilder sb = new StringBuilder();
        foreach (byte b in GetHash(inputString))
            sb.Append(b.ToString("X2"));

        return sb.ToString();
    }

    public static Guid GetTokenForGuest(EnumWebsiteName website, string privateKey)
    {            
        string datetime = DateTime.Now.ToString("dd/MM/yyyy hh:mm");
        var hashString = GetHashString($"{datetime}|{website}|{privateKey}");            
        return new Guid(hashString);
    }

    public static bool  CheckTokenForGuest(Guid token, EnumWebsiteName website, string privateKey)
    {
        string datetime = DateTime.Now.ToString("dd/MM/yyyy hh:mm");
        var hashString = GetHashString($"{datetime}|{website}|{privateKey}");
        var test1 = new Guid(hashString);
        if (test1.CompareTo(token)==0) {
            return true;
        }

        string datetime2 = DateTime.Now.AddMinutes(-1).ToString("dd/MM/yyyy hh:mm");
        var hashString2 = GetHashString($"{datetime2}|{website}|{privateKey}");
        var test2 = new Guid(hashString2);
        if (test2.CompareTo(token) == 0)
        {
            return true;
        }

        return false;
    }

}

对我来说有两个红旗:1. MD5不安全 2.在2分钟内比较令牌的方式太过复杂 - J.Luan
感谢您的评论。 1)是的,MD5不安全,但我使用它是因为它很快,并且对于我的项目来说足够了。无论如何,选择另一个哈希算法很容易。 2)对于“硬煮”的部分,这个类只有46行。服务器A调用一个方法,服务器B调用另一个方法。由于令牌时间跨度仅为2分钟,所以想法是使用哈希函数而不是解密字符串。 我不确定哈希一个字符串是否比解密它更快。 - Francesco

0

在第一次注册时创建令牌时,您需要存储令牌。当您从登录表中检索数据时,如果输入日期与当前日期相差超过1天(24小时),则需要显示消息,例如您的令牌已过期。

要生成密钥,请参考此处


-2

这样,一个令牌将存在长达24小时。以下是生成有效期为24小时的令牌的代码。我们使用此代码,但我并没有编写它。

public static string GenerateToken()
{
    int month = DateTime.Now.Month;
    int day = DateTime.Now.Day;
    string token = ((day * 100 + month) * 700 + day * 13).ToString();
    return token;
}

网页内容由stack overflow 提供, 点击上面的
可以查看英文原文,
原文链接