331 lines
12 KiB
C#
331 lines
12 KiB
C#
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;
|
||
}
|
||
}
|
||
|
||
|
||
}
|
||
}
|