v10.10.2024.0601 优化Json序列化,支持DateOnly/TimeOnly,支持带时区的时间序列化
石头 编写于 2024-06-01 08:10:50
X
using NewLife.Security;

namespace NewLife.Web;

/// <summary>令牌提供者</summary>
/// <remarks>
/// 文档 https://newlifex.com/core/token_provider
/// </remarks>
public class TokenProvider
{
    #region 属性
    /// <summary>密钥。签发方用私钥,验证方用公钥</summary>
    public String? Key { get; set; }
    #endregion

    #region 方法
    /// <summary>读取密钥</summary>
    /// <param name="file">文件</param>
    /// <param name="generate">是否生成</param>
    /// <returns></returns>
    public Boolean ReadKey(String file, Boolean generate = false)
    {
        if (file.IsNullOrEmpty()) return false;

        file = file.GetBasePath();
        if (File.Exists(file))
        {
            Key = File.ReadAllText(file);

            if (!Key.IsNullOrEmpty()) return true;
        }

        if (!generate || !file.EndsWithIgnoreCase(".prvkey")) return false;

        var ss = DSAHelper.GenerateKey();
        File.WriteAllText(file.EnsureDirectory(true), ss[0]);
        file = Path.ChangeExtension(file, ".pubkey");
        File.WriteAllText(file, ss[1]);

        Key = ss[0];

        return true;
    }

    /// <summary>编码用户和有效期得到令牌</summary>
    /// <param name="user">用户</param>
    /// <param name="expire">有效期</param>
    /// <returns></returns>
    public String Encode(String user, DateTime expire)
    {
        if (user.IsNullOrEmpty()) throw new ArgumentNullException(nameof(user));
        if (expire.Year < 2000) throw new ArgumentOutOfRangeException(nameof(expire));
        if (Key.IsNullOrEmpty()) throw new ArgumentNullException(nameof(Key));

        var secs = expire.ToInt();

        // 拼接数据并签名
        var data = (user + "," + secs).GetBytes();
        var sig = DSAHelper.Sign(data, Key);

        // Base64拼接数据和签名
        return data.ToUrlBase64() + "." + sig.ToUrlBase64();
    }

    /// <summary>令牌解码得到用户和有效期</summary>
    /// <param name="token">令牌</param>
    /// <param name="expire">有效期</param>
    /// <returns></returns>
    [Obsolete("=>TryDecode")]
    public String? Decode(String token, out DateTime expire)
    {
        if (token.IsNullOrEmpty()) throw new ArgumentNullException(nameof(token));
        if (Key.IsNullOrEmpty()) throw new ArgumentNullException(nameof(Key));

        expire = DateTime.MinValue;

        // Base64拆分数据和签名
        var p = token.IndexOf('.');
        var data = token[..p].ToBase64();
        var sig = token[(p + 1)..].ToBase64();

        // 验证签名
        //if (!DSAHelper.Verify(data, Key, sig)) throw new InvalidOperationException("签名验证失败!");
        if (!DSAHelper.Verify(data, Key, sig)) return null;

        // 拆分数据和有效期
        var str = data.ToStr();
        p = str.LastIndexOf(',');

        var user = str[..p];
        var secs = str[(p + 1)..].ToInt();
        expire = secs.ToDateTime();

        return user;
    }

    /// <summary>尝试解码令牌,即使失败,也会返回用户信息和有效时间</summary>
    /// <param name="token">令牌</param>
    /// <param name="user">用户信息</param>
    /// <param name="expire">有效时间</param>
    /// <returns>解码结果,成功或失败</returns>
    public Boolean TryDecode(String token, out String user, out DateTime expire)
    {
        if (token.IsNullOrEmpty()) throw new ArgumentNullException(nameof(token));
        //if (Key.IsNullOrEmpty()) throw new ArgumentNullException(nameof(Key));

        // Base64拆分数据和签名
        var p = token.IndexOf('.');
        var data = token[..p].ToBase64();
        var sig = token[(p + 1)..].ToBase64();

        // 拆分数据和有效期
        var str = data.ToStr();
        p = str.LastIndexOf(',');

        user = str[..p];
        var secs = str[(p + 1)..].ToInt();
        expire = secs.ToDateTime();

        if (Key.IsNullOrEmpty()) return false;

        // 验证签名
        //if (!DSAHelper.Verify(data, Key, sig)) throw new InvalidOperationException("签名验证失败!");
        if (!DSAHelper.Verify(data, Key, sig)) return false;

        return true;
    }
    #endregion
}