live-forum/server/webapi/LiveForum/LiveForum.Code/JwtInfrastructure/JwtTokenManageExtension.cs
2026-03-24 11:27:37 +08:00

244 lines
12 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.

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
{
/// <summary>
///
/// </summary>
public static class JwtTokenManageExtension
{
/// <summary>
/// 添加jwt安全验证配置
/// </summary>
/// <param name="server"></param>
/// <param name="configuration"></param>
/// <returns></returns>
public static void AddJwtConfig(this IHostApplicationBuilder builder, Func<string, string, IServiceProvider, string?> func)
{
// 使用 Configure 注册 JWT 配置(支持热更新)
builder.Services.Configure<JwtTokenConfig>(builder.Configuration.GetSection("JwtTokenConfig"));
// 注册 JWT 配置的 PostConfigure设置默认值
builder.Services.PostConfigure<JwtTokenConfig>(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<JwtTokenConfig>(sp =>
{
var monitor = sp.GetRequiredService<IOptionsMonitor<JwtTokenConfig>>();
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<IJwtAuthManager>();
var redis = context.HttpContext.RequestServices.GetRequiredService<IRedisService>();
var userInfo = context.HttpContext.RequestServices.GetRequiredService<JwtUserInfoModel>();
string blackUserKey = $"jwt:expire:user:{tokenMd5}";
//判断token是否被加入黑名单
var isUserExpire = await redis.GetAsync<int?>(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<object>(ResponseCode.ParamError, "", null).ToString();
return response.WriteAsync(payload);
},
// 在认证失败并被 Challenge 时触发该事件
OnChallenge = context =>
{
context.HandleResponse(); // 确保不再执行默认的挑战响应
var response = context.Response;
if (response.HasStarted)
{
return Task.CompletedTask;
}
response.Clear(); // 清空现有响应内容
response.StatusCode = StatusCodes.Status200OK;
response.ContentType = "application/json";
string payload;
if (context.AuthenticateFailure is LoginExpiredException loginExpired)
{
payload = loginExpired.ToString();
}
else
{
var result = new BaseResponse<object>(ResponseCode.Unauthorized, "用户未登录", null);
if (context.AuthenticateFailure != null)
{
result.Message = context.AuthenticateFailure.Message;
}
payload = result.ToString();
}
return response.WriteAsync(payload);
}
};
});
//注册一个JwtAuthManager的单例服务
builder.Services.AddScoped<IJwtAuthManager, JwtRedisAuthManager>();
builder.Services.AddScoped<JwtUserInfoModel>();
}
}
}