fix 登录授权的问题
This commit is contained in:
@ -11,6 +11,9 @@ namespace QYZH.InteractiveMagazine.Infrastructure.Auth;
|
||||
/// </summary>
|
||||
public static class JwtHelper
|
||||
{
|
||||
public const string AdminTokenKeyPrefix = "InteractiveMagazine:AdminAuth:Token";
|
||||
public const string WeChatTokenKeyPrefix = "InteractiveMagazine:WeChatAuth:Token";
|
||||
|
||||
/// <summary>
|
||||
/// 生成JWT令牌(管理端 / 单用户场景)
|
||||
/// </summary>
|
||||
@ -60,6 +63,26 @@ public static class JwtHelper
|
||||
/// </summary>
|
||||
public const string WxUserIdClaimType = "WxUserId";
|
||||
|
||||
public static string BuildAdminTokenKey(string userId)
|
||||
{
|
||||
return $"{AdminTokenKeyPrefix}:{userId}";
|
||||
}
|
||||
|
||||
public static string BuildAdminTokenKey(long userId)
|
||||
{
|
||||
return BuildAdminTokenKey(userId.ToString());
|
||||
}
|
||||
|
||||
public static string BuildWeChatTokenKey(string wxUserId, string userId)
|
||||
{
|
||||
return $"{WeChatTokenKeyPrefix}:{wxUserId}:{userId}";
|
||||
}
|
||||
|
||||
public static string BuildWeChatTokenKey(long wxUserId, long userId)
|
||||
{
|
||||
return BuildWeChatTokenKey(wxUserId.ToString(), userId.ToString());
|
||||
}
|
||||
|
||||
private static string BuildToken(Claim[] claims, JwtSettings settings)
|
||||
{
|
||||
var key = new SymmetricSecurityKey(Encoding.UTF8.GetBytes(settings.SecretKey!));
|
||||
|
||||
@ -7,6 +7,7 @@ using Microsoft.Extensions.Hosting;
|
||||
using Microsoft.Extensions.Logging;
|
||||
using Microsoft.Extensions.Options;
|
||||
using Microsoft.IdentityModel.Tokens;
|
||||
using QYZH.InteractiveMagazine.Infrastructure.Auth;
|
||||
using QYZH.InteractiveMagazine.Infrastructure.Middleware;
|
||||
using QYZH.InteractiveMagazine.Models.Settings;
|
||||
using System.Security.Claims;
|
||||
@ -16,34 +17,23 @@ using System.Text.Encodings.Web;
|
||||
namespace QYZH.InteractiveMagazine.Infrastructure.Extensions;
|
||||
|
||||
/// <summary>
|
||||
/// 统一服务注册扩展
|
||||
/// Infrastructure service registration extensions.
|
||||
/// </summary>
|
||||
public static class DependencyInjectionExtensions
|
||||
{
|
||||
/// <summary>
|
||||
/// 注册基础设施服务
|
||||
/// Registers infrastructure services.
|
||||
/// </summary>
|
||||
/// <param name="services">服务集合</param>
|
||||
/// <param name="configuration">配置</param>
|
||||
/// <param name="environment">运行环境</param>
|
||||
public static void AddInfrastructureServices(this IServiceCollection services, IConfiguration configuration, IWebHostEnvironment? environment = null)
|
||||
{
|
||||
AddJwtAuthentication(services, configuration, environment);
|
||||
|
||||
services.AddTransient<GlobalExceptionMiddleware>();
|
||||
services.AddTransient<OperationLogMiddleware>();
|
||||
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// 配置JWT认证
|
||||
/// </summary>
|
||||
/// <param name="services">服务集合</param>
|
||||
/// <param name="configuration">配置</param>
|
||||
/// <param name="environment">运行环境</param>
|
||||
private static void AddJwtAuthentication(IServiceCollection services, IConfiguration configuration, IWebHostEnvironment? environment = null)
|
||||
{
|
||||
// 开发环境下跳过 JWT 验证
|
||||
if (environment?.IsDevelopment() == true)
|
||||
{
|
||||
services.AddAuthentication("NoAuth")
|
||||
@ -74,37 +64,56 @@ public static class DependencyInjectionExtensions
|
||||
{
|
||||
OnTokenValidated = async context =>
|
||||
{
|
||||
var currentToken = GetBearerToken(context);
|
||||
if (string.IsNullOrEmpty(currentToken))
|
||||
{
|
||||
context.Fail("Invalid token");
|
||||
return;
|
||||
}
|
||||
|
||||
var userId = context.Principal?.FindFirst(ClaimTypes.NameIdentifier)?.Value;
|
||||
if (string.IsNullOrEmpty(userId))
|
||||
{
|
||||
context.Fail("无效的 Token");
|
||||
context.Fail("Invalid token");
|
||||
return;
|
||||
}
|
||||
|
||||
// 检查管理后台 Token
|
||||
var adminToken = await RedisHelper.GetAsync($"InteractiveMagazine:AdminAuth:Token:{userId}");
|
||||
if (!string.IsNullOrEmpty(adminToken))
|
||||
var adminToken = await RedisHelper.GetAsync(JwtHelper.BuildAdminTokenKey(userId));
|
||||
var wxUserId = context.Principal?.FindFirst(JwtHelper.WxUserIdClaimType)?.Value;
|
||||
if (string.IsNullOrEmpty(wxUserId) && !string.IsNullOrEmpty(adminToken))
|
||||
{
|
||||
return;
|
||||
}
|
||||
|
||||
// 检查微信端 Token
|
||||
var wechatToken = await RedisHelper.GetAsync($"InteractiveMagazine:WeChatAuth:Token:{userId}");
|
||||
if (!string.IsNullOrEmpty(wechatToken))
|
||||
if (!string.IsNullOrEmpty(wxUserId))
|
||||
{
|
||||
var wechatToken = await RedisHelper.GetAsync(JwtHelper.BuildWeChatTokenKey(wxUserId, userId));
|
||||
if (wechatToken == currentToken)
|
||||
{
|
||||
return;
|
||||
}
|
||||
}
|
||||
|
||||
// Redis 中不存在任何 Token,认证失败
|
||||
context.Fail("Token 已失效,请重新登录");
|
||||
context.Fail("Token expired, please login again");
|
||||
}
|
||||
};
|
||||
});
|
||||
}
|
||||
|
||||
private static string? GetBearerToken(TokenValidatedContext context)
|
||||
{
|
||||
var authHeader = context.HttpContext.Request.Headers.Authorization.FirstOrDefault();
|
||||
if (string.IsNullOrWhiteSpace(authHeader) || !authHeader.StartsWith("Bearer ", StringComparison.OrdinalIgnoreCase))
|
||||
{
|
||||
return null;
|
||||
}
|
||||
|
||||
return authHeader.Substring("Bearer ".Length).Trim();
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// 开发环境免认证处理器
|
||||
/// Authentication handler used only in development.
|
||||
/// </summary>
|
||||
public class NoAuthHandler : AuthenticationHandler<AuthenticationSchemeOptions>
|
||||
{
|
||||
@ -115,14 +124,13 @@ public class NoAuthHandler : AuthenticationHandler<AuthenticationSchemeOptions>
|
||||
|
||||
protected override Task<AuthenticateResult> HandleAuthenticateAsync()
|
||||
{
|
||||
// 开发环境下始终认证成功
|
||||
var claims = new[]
|
||||
{
|
||||
new System.Security.Claims.Claim(System.Security.Claims.ClaimTypes.Name, "DevUser"),
|
||||
new System.Security.Claims.Claim(System.Security.Claims.ClaimTypes.NameIdentifier, "0")
|
||||
new Claim(ClaimTypes.Name, "DevUser"),
|
||||
new Claim(ClaimTypes.NameIdentifier, "0")
|
||||
};
|
||||
var identity = new System.Security.Claims.ClaimsIdentity(claims, Scheme.Name);
|
||||
var principal = new System.Security.Claims.ClaimsPrincipal(identity);
|
||||
var identity = new ClaimsIdentity(claims, Scheme.Name);
|
||||
var principal = new ClaimsPrincipal(identity);
|
||||
var ticket = new AuthenticationTicket(principal, Scheme.Name);
|
||||
|
||||
return Task.FromResult(AuthenticateResult.Success(ticket));
|
||||
|
||||
@ -3,21 +3,17 @@ using Microsoft.Extensions.Configuration;
|
||||
using QYZH.InteractiveMagazine.Infrastructure.Auth;
|
||||
using QYZH.InteractiveMagazine.Models.Settings;
|
||||
using System.IdentityModel.Tokens.Jwt;
|
||||
using System.Security.Claims;
|
||||
|
||||
namespace QYZH.InteractiveMagazine.Infrastructure.Middleware;
|
||||
|
||||
/// <summary>
|
||||
/// JWT 自动刷新中间件
|
||||
/// 功能:
|
||||
/// 每次请求都刷新 Redis 中 Token 的过期时间(保持会话活跃)
|
||||
/// 前端永远使用同一个 Token,无需处理 Token 刷新
|
||||
/// Refreshes the Redis session TTL for the current JWT when it is still the active token.
|
||||
/// </summary>
|
||||
public class JwtAutoRefreshMiddleware
|
||||
{
|
||||
private readonly RequestDelegate _next;
|
||||
private readonly IConfiguration _configuration;
|
||||
private const string AdminTokenKeyPrefix = "InteractiveMagazine:AdminAuth:Token";
|
||||
private const string WeChatTokenKeyPrefix = "InteractiveMagazine:WeChatAuth:Token";
|
||||
|
||||
public JwtAutoRefreshMiddleware(RequestDelegate next, IConfiguration configuration)
|
||||
{
|
||||
@ -31,13 +27,13 @@ public class JwtAutoRefreshMiddleware
|
||||
if (!string.IsNullOrEmpty(authHeader) && authHeader.StartsWith("Bearer "))
|
||||
{
|
||||
var token = authHeader.Substring("Bearer ".Length).Trim();
|
||||
await TryRefreshRedisTokenExpiryAsync(context, token);
|
||||
await TryRefreshRedisTokenExpiryAsync(token);
|
||||
}
|
||||
|
||||
await _next(context);
|
||||
}
|
||||
|
||||
private async Task TryRefreshRedisTokenExpiryAsync(HttpContext context, string token)
|
||||
private async Task TryRefreshRedisTokenExpiryAsync(string token)
|
||||
{
|
||||
try
|
||||
{
|
||||
@ -47,60 +43,57 @@ public class JwtAutoRefreshMiddleware
|
||||
return;
|
||||
}
|
||||
|
||||
var expiryTime = jwtToken.ValidTo;
|
||||
var remainingTime = expiryTime - DateTime.Now;
|
||||
|
||||
// JWT 已过期,不处理
|
||||
if (remainingTime <= TimeSpan.Zero)
|
||||
if (jwtToken.ValidTo <= DateTime.UtcNow)
|
||||
{
|
||||
return;
|
||||
}
|
||||
|
||||
var userId = jwtToken.Claims.FirstOrDefault(c => c.Type == System.Security.Claims.ClaimTypes.NameIdentifier)?.Value;
|
||||
var userId = GetClaimValue(jwtToken, ClaimTypes.NameIdentifier, JwtRegisteredClaimNames.NameId);
|
||||
if (string.IsNullOrEmpty(userId))
|
||||
{
|
||||
return;
|
||||
}
|
||||
var wxUserId = jwtToken.Claims.FirstOrDefault(c => c.Type == JwtHelper.WxUserIdClaimType)?.Value;
|
||||
if (string.IsNullOrEmpty(wxUserId))
|
||||
{
|
||||
return;
|
||||
}
|
||||
|
||||
var jwtSettings = _configuration.GetSection("JwtSettings").Get<JwtSettings>();
|
||||
if (jwtSettings == null || jwtSettings.ExpiryMinutes <= 0)
|
||||
{
|
||||
return;
|
||||
}
|
||||
|
||||
// 每次请求都刷新 Redis 中 Token 的过期时间
|
||||
await RefreshRedisTokenExpiryAsync(wxUserId,userId, token, jwtSettings.ExpiryMinutes);
|
||||
var wxUserId = GetClaimValue(jwtToken, JwtHelper.WxUserIdClaimType);
|
||||
await RefreshRedisTokenExpiryAsync(wxUserId, userId, token, jwtSettings.ExpiryMinutes);
|
||||
}
|
||||
catch
|
||||
{
|
||||
// 忽略自动刷新异常,由后续认证中间件处理
|
||||
// Ignore refresh failures. Authentication middleware will validate the request later.
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// 刷新 Redis 中已存在 Token 的过期时间(保持会话活跃)
|
||||
/// </summary>
|
||||
private async Task RefreshRedisTokenExpiryAsync(string wxUserId,string userId, string currentToken, int expiryMinutes)
|
||||
private static async Task RefreshRedisTokenExpiryAsync(string? wxUserId, string userId, string currentToken, int expiryMinutes)
|
||||
{
|
||||
// 检查 Admin Token
|
||||
var adminTokenKey = $"{AdminTokenKeyPrefix}:{userId}";
|
||||
var adminTokenKey = JwtHelper.BuildAdminTokenKey(userId);
|
||||
var adminToken = await RedisHelper.GetAsync(adminTokenKey);
|
||||
if (!string.IsNullOrEmpty(adminToken))
|
||||
if (string.IsNullOrEmpty(wxUserId) && !string.IsNullOrEmpty(adminToken))
|
||||
{
|
||||
await RedisHelper.SetAsync(adminTokenKey, currentToken, TimeSpan.FromMinutes(expiryMinutes));
|
||||
await RedisHelper.SetAsync(adminTokenKey, adminToken, TimeSpan.FromMinutes(expiryMinutes));
|
||||
return;
|
||||
}
|
||||
|
||||
// 检查 WeChat Token
|
||||
var wechatTokenKey = $"{WeChatTokenKeyPrefix}:{wxUserId}{userId}";
|
||||
if (string.IsNullOrEmpty(wxUserId))
|
||||
{
|
||||
return;
|
||||
}
|
||||
|
||||
var wechatTokenKey = JwtHelper.BuildWeChatTokenKey(wxUserId, userId);
|
||||
var wechatToken = await RedisHelper.GetAsync(wechatTokenKey);
|
||||
if (!string.IsNullOrEmpty(wechatToken))
|
||||
if (wechatToken == currentToken)
|
||||
{
|
||||
await RedisHelper.SetAsync(wechatTokenKey, currentToken, TimeSpan.FromMinutes(expiryMinutes));
|
||||
}
|
||||
}
|
||||
|
||||
private static string? GetClaimValue(JwtSecurityToken jwtToken, params string[] claimTypes)
|
||||
{
|
||||
return jwtToken.Claims.FirstOrDefault(c => claimTypes.Contains(c.Type))?.Value;
|
||||
}
|
||||
}
|
||||
|
||||
@ -24,7 +24,6 @@ public class WeChatAuthService(
|
||||
IPetService petService)
|
||||
: BaseRepository<WxUser>, IWeChatAuthService
|
||||
{
|
||||
private const string TokenKeyPrefix = "InteractiveMagazine:WeChatAuth:Token";
|
||||
private const string AccessTokenCacheKey = "InteractiveMagazine:WeChat:AccessToken";
|
||||
private const string Code2SessionUrl = "https://api.weixin.qq.com/sns/jscode2session?appid={0}&secret={1}&js_code={2}&grant_type=authorization_code";
|
||||
private const string GetAccessTokenUrl = "https://api.weixin.qq.com/cgi-bin/token?grant_type=client_credential&appid={0}&secret={1}";
|
||||
@ -85,10 +84,10 @@ public class WeChatAuthService(
|
||||
UpdatedAt = DateTime.Now
|
||||
};
|
||||
|
||||
var wxUserId = await wxUserRepository.Insertable(wxUser).ExecuteReturnIdentityAsync();
|
||||
wxUser.Id = wxUserId;
|
||||
await wxUserRepository.InsertAsync(wxUser);
|
||||
|
||||
logger.LogInformation("WxUser 创建成功,WxUserId: {WxUserId}, OpenId: {OpenId}", wxUserId, wxResponse.OpenId);
|
||||
|
||||
logger.LogInformation("WxUser 创建成功,WxUserId: {WxUserId}, OpenId: {OpenId}", wxUser.Id, wxResponse.OpenId);
|
||||
}
|
||||
else
|
||||
{
|
||||
@ -201,8 +200,8 @@ public class WeChatAuthService(
|
||||
var token = JwtHelper.GenerateToken(wxUserId, refreshedUser.Id, refreshedUser.Name ?? string.Empty, jwtSettings);
|
||||
|
||||
// 清除旧 Redis Token,写入新 Token
|
||||
await RedisHelper.DelAsync($"{TokenKeyPrefix}:{currentUserId}");
|
||||
await RedisHelper.SetAsync($"{TokenKeyPrefix}:{refreshedUser.Id}", token, TimeSpan.FromMinutes(jwtSettings.ExpiryMinutes));
|
||||
await RedisHelper.DelAsync(JwtHelper.BuildWeChatTokenKey(wxUserId, currentUserId));
|
||||
await RedisHelper.SetAsync(JwtHelper.BuildWeChatTokenKey(wxUserId, refreshedUser.Id), token, TimeSpan.FromMinutes(jwtSettings.ExpiryMinutes));
|
||||
|
||||
return new WeChatSwitchUserOutput
|
||||
{
|
||||
@ -327,7 +326,7 @@ public class WeChatAuthService(
|
||||
var jwtSettings = GetJwtSettings();
|
||||
var token = JwtHelper.GenerateToken(wxUser.Id, userId, userName, jwtSettings);
|
||||
|
||||
await RedisHelper.SetAsync($"{TokenKeyPrefix}:{wxUser.Id}{userId}", token, TimeSpan.FromMinutes(jwtSettings.ExpiryMinutes));
|
||||
await RedisHelper.SetAsync(JwtHelper.BuildWeChatTokenKey(wxUser.Id, userId), token, TimeSpan.FromMinutes(jwtSettings.ExpiryMinutes));
|
||||
|
||||
return new WeChatLoginOutput
|
||||
{
|
||||
|
||||
@ -6,7 +6,8 @@
|
||||
"Issuer": "QYZH.InteractiveMagazine",
|
||||
"Audience": "QYZH.InteractiveMagazine",
|
||||
"SecretKey": "zG7pLqR9xVw2bN8fYtHk3mPc5sA1dF6eUjW4gXhC7vB",
|
||||
"ExpiryMinutes": 120
|
||||
"ExpiryMinutes": 120,
|
||||
"JwtTokenExpiryDays": 30
|
||||
},
|
||||
"RedisSettings": {
|
||||
"ConnectionString": "192.168.20.150:16379,defaultDatabase=5",
|
||||
|
||||
Reference in New Issue
Block a user