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

331 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.ExceptionExtend;
using LiveForum.Code.JwtInfrastructure.Interface;
using LiveForum.Code.Redis.Contract;
using LiveForum.Code.Utility;
using Microsoft.Extensions.Logging;
using Microsoft.Extensions.Options;
using Microsoft.IdentityModel.Tokens;
using System;
using System.Collections.Generic;
using System.IdentityModel.Tokens.Jwt;
using System.Linq;
using System.Security.Claims;
using System.Security.Cryptography;
using System.Text;
using System.Text.Json;
using System.Threading.Tasks;
namespace LiveForum.Code.JwtInfrastructure
{
/// <summary>
/// 获取Redis存储的Jwt帮助类
/// </summary>
public class JwtRedisAuthManager : IJwtAuthManager
{
private readonly IRedisService _redisService;
private readonly IOptionsMonitor<JwtTokenConfig> _jwtTokenConfigMonitor;
private readonly ILogger<JwtRedisAuthManager> _logger;
private const string JWT_USER_PREFIX = "jwt:user:";
public JwtRedisAuthManager(IRedisService redisService, IOptionsMonitor<JwtTokenConfig> jwtTokenConfigMonitor, ILogger<JwtRedisAuthManager> logger)
{
_redisService = redisService;
_jwtTokenConfigMonitor = jwtTokenConfigMonitor;
_logger = logger;
}
/// <summary>
/// 获取当前 JWT 配置(每次调用都获取最新配置)
/// </summary>
private JwtTokenConfig GetCurrentConfig() => _jwtTokenConfigMonitor.CurrentValue;
public async Task<JwtAccessToken> GenerateTokensAsync(string username, List<Claim> claims, DateTime now)
{
try
{
// 获取最新配置
var config = GetCurrentConfig();
var tokenHandler = new JwtSecurityTokenHandler();
var key = Encoding.ASCII.GetBytes(config.Secret);
var expireAt = now.AddHours(config.AccessTokenExpiration);
//claims[]
if (claims == null)
{
claims = new List<Claim>();
}
claims.Add(new Claim("userName", username));
var tokenDescriptor = new SecurityTokenDescriptor
{
Subject = new ClaimsIdentity(claims),
Expires = expireAt,
Issuer = config.Issuer,
Audience = config.Audience,
SigningCredentials = new SigningCredentials(new SymmetricSecurityKey(key), SecurityAlgorithms.HmacSha256Signature)
};
var token = tokenHandler.CreateToken(tokenDescriptor);
var tokenString = tokenHandler.WriteToken(token);
var jwtAccessToken = new JwtAccessToken
{
UserName = username,
Token = tokenString,
ExpireAt = expireAt
};
var md5Str = tokenString.ToMD5();
// 存储到Redis
var userKey = $"{JWT_USER_PREFIX}:{username}";
//var expiration = expireAt - now; 过期时间不等于缓存时间
await _redisService.SetAsync(userKey, tokenString, TimeSpan.FromMinutes(60));
return jwtAccessToken;
}
catch (Exception ex)
{
_logger.LogError(ex, "生成JWT令牌失败用户名: {Username}", username);
throw;
}
}
public Task<DateTime> GetAccessTokenExpireAtAsync()
{
try
{
// 获取最新配置
var config = GetCurrentConfig();
return Task.FromResult(DateTime.Now.AddMinutes(config.AccessTokenExpiration));
}
catch (Exception ex)
{
_logger.LogError(ex, "获取访问令牌过期时间失败");
throw;
}
}
public Task<bool> VerificationJwtAccessTokenAsync(string token)
{
try
{
if (string.IsNullOrEmpty(token))
return Task.FromResult(false);
// 获取最新配置
var config = GetCurrentConfig();
// 验证JWT令牌格式和签名
var tokenHandler = new JwtSecurityTokenHandler();
var key = Encoding.ASCII.GetBytes(config.Secret);
try
{
tokenHandler.ValidateToken(token, new TokenValidationParameters
{
ValidateIssuerSigningKey = true,
IssuerSigningKey = new SymmetricSecurityKey(key),
ValidateIssuer = true,
ValidIssuer = config.Issuer,
ValidateAudience = true,
ValidAudience = config.Audience,
ValidateLifetime = true,
ClockSkew = TimeSpan.Zero
}, out SecurityToken validatedToken);
return Task.FromResult(true);
}
catch
{
return Task.FromResult(false);
}
}
catch (Exception ex)
{
_logger.LogError(ex, "验证JWT令牌失败");
return Task.FromResult(false);
}
}
/// <summary>
///
/// </summary>
/// <param name="username"></param>
/// <param name="token"></param>
/// <param name="expireAt"></param>
/// <returns></returns>
/// <exception cref="ArgumentException"></exception>
public async Task WriteJwtCacheAsync(string username, string token, DateTime expireAt)
{
var timespan = (expireAt - DateTime.Now);
if (timespan.TotalSeconds < 0)
{
throw new ArgumentException("令牌已过期");
}
//验证token是否在redis中存在
var userKey = $"{JWT_USER_PREFIX}{username}";
//每次缓存60分钟
await _redisService.SetAsync(userKey, token, TimeSpan.FromMinutes(60));
}
public async Task<JwtUserInfoModel> DecodeJwtTokenAsync(string token)
{
try
{
var userInfo = await DecodeBaseJwtTokenAsync(token);
//验证token是否在redis中存在
var userKey = $"{JWT_USER_PREFIX}{userInfo.UserName}";
var storedToken = await _redisService.GetAsync(userKey);
if (storedToken != token)
{
throw new RedisNullException("token无效或已被注销");
}
return userInfo;
}
catch (Exception ex)
{
_logger.LogError(ex, "解密JWT令牌失败");
throw;
}
}
public async Task<JwtUserInfoModel> DecodeBaseJwtTokenAsync(string token)
{
if (string.IsNullOrEmpty(token))
throw new ArgumentException("Token不能为空");
var isSuccess = await VerificationJwtAccessTokenAsync(token);
if (!isSuccess)
{
throw new ArgumentException("token已过期");
}
var tokenHandler = new JwtSecurityTokenHandler();
var jwtToken = tokenHandler.ReadJwtToken(token);
var username = jwtToken.Claims.FirstOrDefault(x => x.Type == "userName")?.Value ??
jwtToken.Claims.FirstOrDefault(x => x.Type == ClaimTypes.Name)?.Value ??
jwtToken.Claims.FirstOrDefault(x => x.Type == "sub")?.Value ??
string.Empty;
var claims = jwtToken.Claims.ToArray();
var expireAt = jwtToken.ValidTo;
if (string.IsNullOrEmpty(username))
{
throw new ArgumentException("无法从令牌中提取用户名");
}
return new JwtUserInfoModel() { UserName = username, Claims = claims.ToList(), ExpireAt = expireAt };
}
public async Task<List<JwtAccessToken>> GetJwtAccessTokenListAsync()
{
try
{
var db = await _redisService.GetDatabaseAsync();
var server = db.Multiplexer.GetServer(db.Multiplexer.GetEndPoints().First());
var keys = server.Keys(pattern: JWT_USER_PREFIX + "*");
var tokens = new List<JwtAccessToken>();
foreach (var key in keys)
{
var tokenString = await _redisService.GetAsync(key.ToString());
if (!string.IsNullOrEmpty(tokenString))
{
// 从token中解析用户名和过期时间
var tokenHandler = new JwtSecurityTokenHandler();
try
{
var jwtToken = tokenHandler.ReadJwtToken(tokenString);
var username = jwtToken.Claims.FirstOrDefault(x => x.Type == "userName")?.Value ??
jwtToken.Claims.FirstOrDefault(x => x.Type == ClaimTypes.Name)?.Value ??
jwtToken.Claims.FirstOrDefault(x => x.Type == "sub")?.Value ??
string.Empty;
var jwtAccessToken = new JwtAccessToken
{
UserName = username,
Token = tokenString,
ExpireAt = jwtToken.ValidTo
};
tokens.Add(jwtAccessToken);
}
catch (Exception ex)
{
_logger.LogWarning(ex, "解析JWT令牌失败跳过该令牌: {Token}", tokenString);
continue;
}
}
}
return tokens;
}
catch (Exception ex)
{
_logger.LogError(ex, "获取JWT令牌列表失败");
throw;
}
}
public async Task<int> GetJwtAccessTokenCountAsync()
{
try
{
var db = await _redisService.GetDatabaseAsync();
var server = db.Multiplexer.GetServer(db.Multiplexer.GetEndPoints().First());
var keys = server.Keys(pattern: JWT_USER_PREFIX + "*");
return keys.Count();
}
catch (Exception ex)
{
_logger.LogError(ex, "获取JWT令牌数量失败");
throw;
}
}
public async Task RemoveTokenByUserNameAsync(string userName)
{
try
{
if (string.IsNullOrEmpty(userName))
return;
var userKey = $"{JWT_USER_PREFIX}{userName}";
await _redisService.RemoveAsync(userKey);
_logger.LogInformation("已删除用户 {UserName} 的JWT令牌", userName);
}
catch (Exception ex)
{
_logger.LogError(ex, "按用户名删除JWT令牌失败用户名: {Username}", userName);
throw;
}
}
public async Task RemoveAllTokensAsync()
{
try
{
var db = await _redisService.GetDatabaseAsync();
var server = db.Multiplexer.GetServer(db.Multiplexer.GetEndPoints().First());
// 删除所有用户令牌映射
var userKeys = server.Keys(pattern: JWT_USER_PREFIX + "*");
var removedCount = 0;
foreach (var key in userKeys)
{
await _redisService.RemoveAsync(key.ToString());
removedCount++;
}
_logger.LogInformation("已删除所有JWT令牌共删除 {Count} 个令牌", removedCount);
}
catch (Exception ex)
{
_logger.LogError(ex, "删除所有JWT令牌失败");
throw;
}
}
}
}