Files
ThingsGateway/src/Admin/ThingsGateway.Furion/JWT/JWTEncryption.cs
2025-05-14 18:52:19 +08:00

534 lines
21 KiB
C#
Raw Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

// ------------------------------------------------------------------------
// 版权信息
// 版权归百小僧及百签科技(广东)有限公司所有。
// 所有权利保留。
// 官方网站https://baiqian.com
//
// 许可证信息
// 项目主要遵循 MIT 许可证和 Apache 许可证(版本 2.0)进行分发和使用。
// 许可证的完整文本可以在源代码树根目录中的 LICENSE-APACHE 和 LICENSE-MIT 文件中找到。
// ------------------------------------------------------------------------
using Microsoft.AspNetCore.Authentication;
using Microsoft.AspNetCore.Authorization;
using Microsoft.AspNetCore.Http;
using Microsoft.Extensions.Caching.Distributed;
using Microsoft.Extensions.DependencyInjection;
using Microsoft.Extensions.Primitives;
using Microsoft.IdentityModel.JsonWebTokens;
using Microsoft.IdentityModel.Tokens;
using System.Diagnostics;
using System.IdentityModel.Tokens.Jwt;
using System.Reflection;
using System.Runtime.Loader;
using System.Security.Claims;
using System.Security.Cryptography;
using System.Text;
using System.Text.Encodings.Web;
using System.Text.Json;
using ThingsGateway.Authorization;
using JwtRegisteredClaimNames = Microsoft.IdentityModel.JsonWebTokens.JwtRegisteredClaimNames;
namespace ThingsGateway.DataEncryption;
/// <summary>
/// JWT 加解密
/// </summary>
public class JWTEncryption
{
/// <summary>
/// 刷新 Token 身份标识
/// </summary>
private static readonly string[] _refreshTokenClaims = new[] { "f", "e", "s", "l", "k" };
/// <summary>
/// 生成 Token
/// </summary>
/// <param name="payload"></param>
/// <param name="expiredTime">过期时间(分钟),最大支持 13 年</param>
/// <returns></returns>
public static string Encrypt(IDictionary<string, object> payload, long? expiredTime = null)
{
var (Payload, JWTSettings) = CombinePayload(payload, expiredTime);
return Encrypt(JWTSettings.IssuerSigningKey, Payload, JWTSettings.Algorithm);
}
private static readonly JsonSerializerOptions _jsonSerializerOptions = new JsonSerializerOptions
{
Encoder = JavaScriptEncoder.UnsafeRelaxedJsonEscaping
};
/// <summary>
/// 生成 Token
/// </summary>
/// <param name="issuerSigningKey"></param>
/// <param name="payload"></param>
/// <param name="algorithm"></param>
/// <returns></returns>
public static string Encrypt(string issuerSigningKey, IDictionary<string, object> payload, string algorithm = SecurityAlgorithms.HmacSha256)
{
// 处理 JwtPayload 序列化不一致问题
var stringPayload = payload is JwtPayload jwtPayload ? jwtPayload.SerializeToJson() : JsonSerializer.Serialize(payload, _jsonSerializerOptions);
return Encrypt(issuerSigningKey, stringPayload, algorithm);
}
/// <summary>
/// 生成 Token
/// </summary>
/// <param name="issuerSigningKey"></param>
/// <param name="payload"></param>
/// <param name="algorithm"></param>
/// <returns></returns>
public static string Encrypt(string issuerSigningKey, string payload, string algorithm = SecurityAlgorithms.HmacSha256)
{
SigningCredentials credentials = null;
if (!string.IsNullOrWhiteSpace(issuerSigningKey))
{
var securityKey = new SymmetricSecurityKey(Encoding.UTF8.GetBytes(issuerSigningKey));
credentials = new SigningCredentials(securityKey, algorithm);
}
var tokenHandler = new JsonWebTokenHandler();
return credentials == null ? tokenHandler.CreateToken(payload) : tokenHandler.CreateToken(payload, credentials);
}
/// <summary>
/// 生成刷新 Token
/// </summary>
/// <param name="accessToken"></param>
/// <param name="expiredTime">刷新 Token 有效期(分钟),最大支持 13 年</param>
/// <returns></returns>
public static string GenerateRefreshToken(string accessToken, int expiredTime = 43200)
{
// 分割Token
var tokenParagraphs = accessToken.Split('.', StringSplitOptions.RemoveEmptyEntries);
var s = RandomNumberGenerator.GetInt32(10, tokenParagraphs[1].Length / 2 + 2);
var l = RandomNumberGenerator.GetInt32(3, 13);
var payload = new Dictionary<string, object>
{
{ "f",tokenParagraphs[0] },
{ "e",tokenParagraphs[2] },
{ "s",s },
{ "l",l },
{ "k",tokenParagraphs[1].Substring(s,l) }
};
return Encrypt(payload, expiredTime);
}
/// <summary>
/// 通过过期Token 和 刷新Token 换取新的 Token
/// </summary>
/// <param name="expiredToken"></param>
/// <param name="refreshToken"></param>
/// <param name="expiredTime">过期时间(分钟),最大支持 13 年</param>
/// <param name="clockSkew">刷新token容差值秒做单位</param>
/// <returns></returns>
public static async Task<string> Exchange(string expiredToken, string refreshToken, long? expiredTime = null, long clockSkew = 5)
{
// 交换刷新Token 必须原Token 已过期
var (_isValid, _, _) = await Validate(expiredToken).ConfigureAwait(false);
if (_isValid) return default;
// 判断刷新Token 是否过期
var (isValid, refreshTokenObj, _) = await Validate(refreshToken).ConfigureAwait(false);
if (!isValid) return default;
// 解析 HttpContext
var httpContext = GetCurrentHttpContext();
// 判断这个刷新Token 是否已刷新过
var blacklistRefreshKey = "BLACKLIST_REFRESH_TOKEN:" + refreshToken;
var distributedCache = httpContext?.RequestServices?.GetRequiredService<IDistributedCache>();
// 处理token并发容错问题
var nowTime = DateTimeOffset.UtcNow;
var cachedValue = await distributedCache.GetStringAsync(blacklistRefreshKey).ConfigureAwait(false);
var isRefresh = !string.IsNullOrWhiteSpace(cachedValue); // 判断是否刷新过
if (isRefresh)
{
var refreshTime = new DateTimeOffset(long.Parse(cachedValue), TimeSpan.Zero);
// 处理并发时容差值
if ((nowTime - refreshTime).TotalSeconds > clockSkew) return default;
}
// 分割过期Token
var tokenParagraphs = expiredToken.Split('.', StringSplitOptions.RemoveEmptyEntries);
if (tokenParagraphs.Length < 3) return default;
// 判断各个部分是否匹配
if (!refreshTokenObj.GetPayloadValue<string>("f").Equals(tokenParagraphs[0])) return default;
if (!refreshTokenObj.GetPayloadValue<string>("e").Equals(tokenParagraphs[2])) return default;
if (!tokenParagraphs[1].Substring(refreshTokenObj.GetPayloadValue<int>("s"), refreshTokenObj.GetPayloadValue<int>("l")).Equals(refreshTokenObj.GetPayloadValue<string>("k"))) return default;
// 获取过期 Token 的存储信息
var jwtSecurityToken = SecurityReadJwtToken(expiredToken);
var payload = jwtSecurityToken.Payload;
// 移除 IatNbfExp
foreach (var innerKey in DateTypeClaimTypes)
{
if (!payload.ContainsKey(innerKey)) continue;
payload.Remove(innerKey);
}
// 交换成功后登记刷新Token标记失效
if (!isRefresh)
{
await distributedCache.SetStringAsync(blacklistRefreshKey, nowTime.Ticks.ToString(), new DistributedCacheEntryOptions
{
AbsoluteExpiration = DateTimeOffset.FromUnixTimeSeconds(refreshTokenObj.GetPayloadValue<long>(JwtRegisteredClaimNames.Exp))
}).ConfigureAwait(false);
}
return Encrypt(payload, expiredTime);
}
/// <summary>
/// 自动刷新 Token 信息
/// </summary>
/// <param name="context"></param>
/// <param name="httpContext"></param>
/// <param name="expiredTime">新 Token 过期时间(分钟),最大支持 13 年</param>
/// <param name="refreshTokenExpiredTime">新刷新 Token 有效期(分钟)</param>
/// <param name="tokenPrefix"></param>
/// <param name="clockSkew"></param>
/// <param name="onRefreshing">当刷新时触发</param>
/// <returns></returns>
public static async Task<bool> AutoRefreshToken(AuthorizationHandlerContext context, DefaultHttpContext httpContext, long? expiredTime = null, int refreshTokenExpiredTime = 43200, string tokenPrefix = "Bearer ", long clockSkew = 5, Action<string, string> onRefreshing = null)
{
// 如果验证有效,则跳过刷新
if (context.User.Identity.IsAuthenticated)
{
// 禁止使用刷新 Token 进行单独校验
if (_refreshTokenClaims.All(k => context.User.Claims.Any(c => c.Type == k)))
{
return false;
}
return true;
}
// 判断是否含有匿名特性
if (httpContext.GetEndpoint()?.Metadata?.GetMetadata<AllowAnonymousAttribute>() != null) return true;
// 获取过期Token 和 刷新Token
var expiredToken = GetJwtBearerToken(httpContext, tokenPrefix: tokenPrefix);
var refreshToken = GetJwtBearerToken(httpContext, "X-Authorization", tokenPrefix: tokenPrefix);
if (string.IsNullOrWhiteSpace(expiredToken) || string.IsNullOrWhiteSpace(refreshToken)) return false;
// 交换新的 Token
var accessToken = await Exchange(expiredToken, refreshToken, expiredTime, clockSkew).ConfigureAwait(false);
if (string.IsNullOrWhiteSpace(accessToken)) return false;
// 读取新的 Token Clamis
var claims = ReadJwtToken(accessToken)?.Claims;
if (claims == null) return false;
// 创建身份信息
var claimIdentity = new ClaimsIdentity("AuthenticationTypes.Federation");
claimIdentity.AddClaims(claims);
var claimsPrincipal = new ClaimsPrincipal(claimIdentity);
// 设置 HttpContext.User 并登录
httpContext.User = claimsPrincipal;
await httpContext.SignInAsync(claimsPrincipal).ConfigureAwait(false);
string accessTokenKey = "access-token"
, xAccessTokenKey = "x-access-token"
, accessControlExposeKey = "Access-Control-Expose-Headers";
// 返回新的 Token
httpContext.Response.Headers[accessTokenKey] = accessToken;
// 返回新的 刷新Token
var refreshAccessToken = GenerateRefreshToken(accessToken, refreshTokenExpiredTime); ;
httpContext.Response.Headers[xAccessTokenKey] = refreshAccessToken;
// 调用刷新后回调函数
onRefreshing?.Invoke(accessToken, refreshAccessToken);
// 处理 axios 问题
httpContext.Response.Headers.TryGetValue(accessControlExposeKey, out var acehs);
httpContext.Response.Headers[accessControlExposeKey] = string.Join(',', StringValues.Concat(acehs, new StringValues(new[] { accessTokenKey, xAccessTokenKey })).Distinct());
return true;
}
/// <summary>
/// 验证 Token
/// </summary>
/// <param name="accessToken"></param>
/// <returns></returns>
public static async Task<(bool IsValid, JsonWebToken Token, TokenValidationResult validationResult)> Validate(string accessToken)
{
var jwtSettings = GetJWTSettings();
if (jwtSettings == null) return (false, default, default);
// 加密Key
var key = new SymmetricSecurityKey(Encoding.UTF8.GetBytes(jwtSettings.IssuerSigningKey));
var creds = new SigningCredentials(key, jwtSettings.Algorithm);
// 创建Token验证参数
var tokenValidationParameters = CreateTokenValidationParameters(jwtSettings);
tokenValidationParameters.IssuerSigningKey ??= creds.Key;
// 验证 Token
var tokenHandler = new JsonWebTokenHandler();
try
{
var tokenValidationResult = await tokenHandler.ValidateTokenAsync(accessToken, tokenValidationParameters).ConfigureAwait(false);
if (!tokenValidationResult.IsValid) return (false, null, tokenValidationResult);
var jsonWebToken = tokenValidationResult.SecurityToken as JsonWebToken;
return (true, jsonWebToken, tokenValidationResult);
}
catch
{
return (false, default, default);
}
}
/// <summary>
/// 验证 Token
/// </summary>
/// <param name="httpContext"></param>
/// <param name="headerKey"></param>
/// <param name="tokenPrefix"></param>
/// <returns></returns>
public static async Task<(bool, JsonWebToken)> ValidateJwtBearerToken(DefaultHttpContext httpContext, string headerKey = "Authorization", string tokenPrefix = "Bearer ")
{
// 获取 token
var accessToken = GetJwtBearerToken(httpContext, headerKey, tokenPrefix);
if (string.IsNullOrWhiteSpace(accessToken))
{
return (false, null);
}
// 验证token
var (IsValid, Token, _) = await Validate(accessToken).ConfigureAwait(false);
var token = IsValid ? Token : null;
return (IsValid, token);
}
/// <summary>
/// 读取 Token不含验证
/// </summary>
/// <param name="accessToken"></param>
/// <returns></returns>
public static JsonWebToken ReadJwtToken(string accessToken)
{
var tokenHandler = new JsonWebTokenHandler();
if (tokenHandler.CanReadToken(accessToken))
{
return tokenHandler.ReadJsonWebToken(accessToken);
}
return default;
}
/// <summary>
/// 读取 Token不含验证
/// </summary>
/// <param name="accessToken"></param>
/// <returns></returns>
public static JwtSecurityToken SecurityReadJwtToken(string accessToken)
{
var jwtSecurityTokenHandler = new JwtSecurityTokenHandler();
var jwtSecurityToken = jwtSecurityTokenHandler.ReadJwtToken(accessToken);
return jwtSecurityToken;
}
/// <summary>
/// 获取 JWT Bearer Token
/// </summary>
/// <param name="httpContext"></param>
/// <param name="headerKey"></param>
/// <param name="tokenPrefix"></param>
/// <returns></returns>
public static string GetJwtBearerToken(DefaultHttpContext httpContext, string headerKey = "Authorization", string tokenPrefix = "Bearer ")
{
// 判断请求报文头中是否有 "Authorization" 报文头
var bearerToken = httpContext.Request.Headers[headerKey].ToString();
if (string.IsNullOrWhiteSpace(bearerToken)) return default;
var prefixLenght = tokenPrefix.Length;
return bearerToken.StartsWith(tokenPrefix, true, null) && bearerToken.Length > prefixLenght ? bearerToken[prefixLenght..].Trim() : default;
}
/// <summary>
/// 获取 JWT 配置
/// </summary>
/// <returns></returns>
public static JWTSettingsOptions GetJWTSettings()
{
// 获取框架上下文
_ = GetFrameworkContext(Assembly.GetCallingAssembly());
if (FrameworkApp == null)
{
Debug.WriteLine("No register the code `services.AddJwt()` on Startup.cs.");
}
var jwtSettingsOptions = FrameworkApp.GetMethod("GetOptions").MakeGenericMethod(typeof(JWTSettingsOptions)).Invoke(null, new object[] { null }) as JWTSettingsOptions;
if (jwtSettingsOptions.Algorithm == null && jwtSettingsOptions.ExpiredTime == null)
{
SetDefaultJwtSettings(jwtSettingsOptions);
}
return jwtSettingsOptions;
}
/// <summary>
/// 生成Token验证参数
/// </summary>
/// <param name="jwtSettings"></param>
/// <returns></returns>
public static TokenValidationParameters CreateTokenValidationParameters(JWTSettingsOptions jwtSettings)
{
return new TokenValidationParameters
{
// 验证签发方密钥
ValidateIssuerSigningKey = jwtSettings.ValidateIssuerSigningKey.Value,
// 签发方密钥
IssuerSigningKey = new SymmetricSecurityKey(Encoding.UTF8.GetBytes(jwtSettings.IssuerSigningKey)),
// 验证签发方
ValidateIssuer = jwtSettings.ValidateIssuer.Value,
// 设置签发方
ValidIssuer = jwtSettings.ValidIssuer,
// 验证签收方
ValidateAudience = jwtSettings.ValidateAudience.Value,
// 设置接收方
ValidAudience = jwtSettings.ValidAudience,
// 验证生存期
ValidateLifetime = jwtSettings.ValidateLifetime.Value,
// 过期时间容错值
ClockSkew = TimeSpan.FromSeconds(jwtSettings.ClockSkew.Value),
// 验证过期时间,设置 false 永不过期
RequireExpirationTime = jwtSettings.RequireExpirationTime
};
}
/// <summary>
/// 组合 Claims 负荷
/// </summary>
/// <param name="payload"></param>
/// <param name="expiredTime">过期时间,单位:分钟,最大支持 13 年</param>
/// <returns></returns>
private static (IDictionary<string, object> Payload, JWTSettingsOptions JWTSettings) CombinePayload(IDictionary<string, object> payload, long? expiredTime = null)
{
var jwtSettings = GetJWTSettings();
var datetimeOffset = DateTimeOffset.UtcNow;
if (!payload.ContainsKey(JwtRegisteredClaimNames.Iat))
{
payload.Add(JwtRegisteredClaimNames.Iat, datetimeOffset.ToUnixTimeSeconds());
}
if (!payload.ContainsKey(JwtRegisteredClaimNames.Nbf))
{
payload.Add(JwtRegisteredClaimNames.Nbf, datetimeOffset.ToUnixTimeSeconds());
}
if (!payload.ContainsKey(JwtRegisteredClaimNames.Exp))
{
var minute = expiredTime ?? jwtSettings?.ExpiredTime ?? 20;
payload.Add(JwtRegisteredClaimNames.Exp, DateTimeOffset.UtcNow.AddMinutes(minute).ToUnixTimeSeconds());
}
if (!payload.ContainsKey(JwtRegisteredClaimNames.Iss))
{
payload.Add(JwtRegisteredClaimNames.Iss, jwtSettings?.ValidIssuer);
}
if (!payload.ContainsKey(JwtRegisteredClaimNames.Aud))
{
payload.Add(JwtRegisteredClaimNames.Aud, jwtSettings?.ValidAudience);
}
return (payload, jwtSettings);
}
/// <summary>
/// 设置默认 Jwt 配置
/// </summary>
/// <param name="options"></param>
/// <returns></returns>
internal static JWTSettingsOptions SetDefaultJwtSettings(JWTSettingsOptions options)
{
options.ValidateIssuerSigningKey ??= true;
if (options.ValidateIssuerSigningKey == true)
{
options.IssuerSigningKey ??= "U2FsdGVkX1+6H3D8Q//yQMhInzTdRZI9DbUGetbyaag=";
}
options.ValidateIssuer ??= true;
if (options.ValidateIssuer == true)
{
options.ValidIssuer ??= "diego";
}
options.ValidateAudience ??= true;
if (options.ValidateAudience == true)
{
options.ValidAudience ??= "powerby ThingsGateway";
}
options.ValidateLifetime ??= true;
if (options.ValidateLifetime == true)
{
options.ClockSkew ??= 10;
}
options.ExpiredTime ??= 20;
options.Algorithm ??= SecurityAlgorithms.HmacSha256;
return options;
}
/// <summary>
/// 获取当前的 HttpContext
/// </summary>
/// <returns></returns>
private static HttpContext GetCurrentHttpContext()
{
return FrameworkApp.GetProperty("HttpContext").GetValue(null) as HttpContext;
}
/// <summary>
/// 日期类型的 Claim 类型
/// </summary>
private static readonly string[] DateTypeClaimTypes = new[] { JwtRegisteredClaimNames.Iat, JwtRegisteredClaimNames.Nbf, JwtRegisteredClaimNames.Exp };
/// <summary>
/// 框架 App 静态类
/// </summary>
internal static Type FrameworkApp { get; set; }
/// <summary>
/// 获取框架上下文
/// </summary>
/// <returns></returns>
internal static Assembly GetFrameworkContext(Assembly callAssembly)
{
if (FrameworkApp != null) return FrameworkApp.Assembly;
// 修复不注册 AddJwt 服务不能使用 JWT 加密问题
var executeAssembly = callAssembly == typeof(JWTEncryption).Assembly
? Assembly.GetEntryAssembly()
: callAssembly;
// 获取 程序集名称
var thingsGatewayAssemblyName = executeAssembly.GetReferencedAssemblies()
.FirstOrDefault(u => u.Name == "ThingsGateway" || u.Name == "ThingsGateway.Furion")
?? throw new InvalidOperationException("No `ThingsGateway` assembly installed in the current project was detected.");
// 加载 程序集
var thingsGatewayAssembly = AssemblyLoadContext.Default.LoadFromAssemblyName(thingsGatewayAssemblyName);
// 获取 App 静态类
FrameworkApp = thingsGatewayAssembly.GetType("ThingsGateway.App");
return thingsGatewayAssembly;
}
}