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; }
/// <summary>时钟容差。允许签发方与验证方之间的时钟偏差,默认30秒</summary>
public TimeSpan ClockSkew { get; set; } = TimeSpan.FromSeconds(30);
#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.ToUniversalTime().ToInt();
// 拼接数据并签名
var data = (user + "," + secs).GetBytes();
var sig = DSAHelper.Sign(data, Key);
// Base64拼接数据和签名
return data.ToUrlBase64() + "." + sig.ToUrlBase64();
}
/// <summary>尝试解码令牌,即使失败,也会返回用户信息和有效时间</summary>
/// <remarks>输入畸形(Base64 非法等)返回 false 而不抛异常;签名有效且未过期才返回 true,过期判断留 <see cref="ClockSkew"/> 时钟容差,容忍签发方与验证方的轻微时钟偏差</remarks>
/// <param name="token">令牌</param>
/// <param name="user">用户信息</param>
/// <param name="expire">有效时间</param>
/// <returns>true=签名有效且未过期;false=格式非法、签名无效或已过期</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('.');
// 无分隔符视为非法令牌,避免切片越界
if (p < 0)
{
user = "";
expire = default;
return false;
}
// Base64 畸形(非法字符/坏填充)按无效令牌处理:输入来自调用方或攻击者,不能让异常穿透
Byte[] data;
Byte[] sig;
try
{
data = token[..p].ToBase64();
sig = token[(p + 1)..].ToBase64();
}
catch (FormatException)
{
user = "";
expire = default;
return false;
}
// 拆分数据和有效期
var str = data.ToStr();
p = str.LastIndexOf(',');
if (p < 0)
{
user = "";
expire = default;
return false;
}
user = str[..p];
var secs = str[(p + 1)..].ToInt();
var expireUtc = secs.ToDateTime();
expire = expireUtc.ToLocalTime();
if (Key.IsNullOrEmpty()) return false;
// 验证签名
//if (!DSAHelper.Verify(data, Key, sig)) throw new InvalidOperationException("签名验证失败!");
if (!DSAHelper.Verify(data, Key, sig)) return false;
// 签名有效还需未过期,早于当前时间超过容差即视为过期。按UTC比较,避免本地时区与夏令时切换造成误判
if (expireUtc < DateTime.UtcNow - ClockSkew) return false;
return true;
}
#endregion
}
|