244 lines
12 KiB
C#
244 lines
12 KiB
C#
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>();
|
||
}
|
||
|
||
}
|
||
}
|