using LiveForum.Code.Base;
using LiveForum.Code.ExceptionExtend;
using LiveForum.Code.JwtInfrastructure.Interface;
using LiveForum.Code.Redis.Contract;
using LiveForum.Code.Utility;
using Mapster;
using Microsoft.AspNetCore.Authentication.JwtBearer;
using Microsoft.AspNetCore.Builder;
using Microsoft.AspNetCore.Http;
using Microsoft.Extensions.DependencyInjection;
using Microsoft.Extensions.Hosting;
using Microsoft.Extensions.Options;
using Microsoft.IdentityModel.Tokens;
using Newtonsoft.Json.Linq;
using System;
using System.Collections.Generic;
using System.Linq;
using System.Security.Claims;
using System.Text;
using System.Threading.Tasks;
namespace LiveForum.Code.JwtInfrastructure
{
///
///
///
public static class JwtTokenManageExtension
{
///
/// 添加jwt安全验证,配置
///
///
///
///
public static void AddJwtConfig(this IHostApplicationBuilder builder, Func func)
{
// 使用 Configure 注册 JWT 配置(支持热更新)
builder.Services.Configure(builder.Configuration.GetSection("JwtTokenConfig"));
// 注册 JWT 配置的 PostConfigure,设置默认值
builder.Services.PostConfigure(options =>
{
// 如果 Secret 为空,生成一个默认值
if (string.IsNullOrEmpty(options.Secret))
{
options.Secret = Guid.NewGuid().ToString("N");
}
// 如果 AccessTokenExpiration 小于等于 0,设置默认值
if (options.AccessTokenExpiration <= 0)
{
options.AccessTokenExpiration = 60;
}
});
// 注册一个向后兼容的单例服务(使用 IOptionsMonitor 获取最新配置)
builder.Services.AddSingleton(sp =>
{
var monitor = sp.GetRequiredService>();
return monitor.CurrentValue;
});
//
builder.Services.AddAuthentication(options =>
{
options.DefaultAuthenticateScheme = JwtBearerDefaults.AuthenticationScheme;
options.DefaultChallengeScheme = JwtBearerDefaults.AuthenticationScheme;
}).AddJwtBearer(options =>
{
options.RequireHttpsMetadata = true;
options.SaveToken = true;
// 从配置中读取初始值(用于中间件初始化)
// 注意:TokenValidationParameters 在中间件初始化时设置,但我们可以通过事件处理器使用最新配置进行额外验证
var secret = builder.Configuration["JwtTokenConfig:Secret"]?.tostrnotempty(Guid.NewGuid().ToString("N")) ?? string.Empty;
var issuer = builder.Configuration["JwtTokenConfig:Issuer"] ?? string.Empty;
var audience = builder.Configuration["JwtTokenConfig:Audience"] ?? string.Empty;
//调试使用
//options.Events = new JwtDebugBearerEvents().GetJwtBearerEvents();
options.TokenValidationParameters = new TokenValidationParameters
{
//是否验证颁发者
ValidateIssuer = true,
//是否验证受众
ValidateAudience = true,
//指定是否验证令牌的生存期。设置为 true 表示要进行验证。
ValidateLifetime = true,
//指定是否验证颁发者签名密钥。设置为 true 表示要进行验证。
ValidateIssuerSigningKey = true,
//颁发者(使用初始配置)
ValidIssuer = issuer,
//受众(使用初始配置)
ValidAudience = audience,
//指定用于验证颁发者签名的密钥(使用初始配置)
IssuerSigningKey = new SymmetricSecurityKey(Encoding.UTF8.GetBytes(secret)),
//指定允许令牌的时钟偏移。允许令牌的过期时间与实际时间之间存在的时间差。在这里设置为 5 分钟,表示允许令牌的时钟偏移为 5 分钟。
ClockSkew = TimeSpan.FromMinutes(30)
};
options.Events = new JwtBearerEvents
{
OnMessageReceived = context =>
{
var rawAuth = context.Request.Headers["Authorization"].ToString();
if (string.IsNullOrWhiteSpace(rawAuth))
{
return Task.CompletedTask;
}
if (rawAuth.StartsWith("Bearer ", StringComparison.OrdinalIgnoreCase))
{
var tokenValue = rawAuth.Substring(7).Trim();
if (string.IsNullOrEmpty(tokenValue) || tokenValue.Equals("undefined", StringComparison.OrdinalIgnoreCase))
{
context.NoResult();
}
}
return Task.CompletedTask;
},
OnTokenValidated = async context =>
{
// 注意:中间件的 TokenValidationParameters 在初始化时设置,但 JwtRedisAuthManager 会使用 IOptionsMonitor 获取最新配置
// 因此新生成的 token 和验证逻辑都会使用最新配置
var token = context.Request.Headers.GetAuthorization();
if (string.IsNullOrEmpty(token))
{
context.Fail("非法请求接口");
return;
}
var tokenMd5 = token.ToMD5();
var manage = context.HttpContext.RequestServices.GetRequiredService();
var redis = context.HttpContext.RequestServices.GetRequiredService();
var userInfo = context.HttpContext.RequestServices.GetRequiredService();
string blackUserKey = $"jwt:expire:user:{tokenMd5}";
//判断token是否被加入黑名单
var isUserExpire = await redis.GetAsync(blackUserKey);
if (isUserExpire != null && isUserExpire > 2)
{
context.Fail(new LoginExpiredException(ResponseCode.Unauthorized, "token已失效"));
return;
}
try
{
var tokenInfo = await manage.DecodeJwtTokenAsync(token);
if (userInfo == null)
{
userInfo = new JwtUserInfoModel();
}
tokenInfo.Adapt(userInfo);
//userInfo.UserName= tokenInfo.UserName;
//userInfo.Claims= tokenInfo.Claims;
//userInfo.ExpireAt= tokenInfo.ExpireAt;
//
}
catch (RedisNullException)
{
//context.Fail(lex);
var dataToken = func(token, tokenMd5, context.HttpContext.RequestServices);
if (string.IsNullOrEmpty(dataToken))
{
if (isUserExpire == null)
{
isUserExpire = 0;
}
isUserExpire++;
await redis.SetAsync(blackUserKey, isUserExpire.Value, TimeSpan.FromMinutes(5));
context.Fail(new LoginExpiredException(ResponseCode.Unauthorized, "未登录"));
return;
}
var databaseUserInfo = await manage.DecodeBaseJwtTokenAsync(dataToken);
databaseUserInfo.Adapt(userInfo);
//重新写入缓存
await manage.WriteJwtCacheAsync(userInfo.UserName, token, userInfo.ExpireAt);
}
catch (Exception ex)
{
context.Fail(new LoginExpiredException(ResponseCode.Forbidden, ex.Message));
return;
}
},
// 处理认证失败的事件
OnAuthenticationFailed = context =>
{
var response = context.Response;
if (response.HasStarted)
{
return Task.CompletedTask;
}
response.Clear(); // 清空现有响应内容
response.StatusCode = StatusCodes.Status200OK;
response.ContentType = "application/json";
var payload = context.Exception is LoginExpiredException loginExpired
? loginExpired.ToString()
: new BaseResponse