JWT 本身是無狀態的,這意味著服務器不會保存任何關于 Token 的狀態信息。但為了支持 JWT 的狀態管理(例如:強制使某些 Token 失效),可以借助 Redis 這樣的外部存儲來維護一個黑名單或白名單。
- 安裝必要的 NuGet 包
首先需要安裝以下 NuGet 包:
StackExchange.Redis;//用于與 Redis 數據庫交互。
Microsoft.AspNetCore.Authentication.JwtBearer;//用于處理 JWT 認證。
- 配置 Redis 連接
在 appsettings.json 文件中添加 Redis 配置:
{"Redis": {"ConnectionString": "localhost:6379"}
}
3.創建 Redis 緩存服務
接口
public interface IRedisCacheService{/// <summary>/// 設置redis/// </summary>/// <param name="key"></param>/// <param name="value"></param>/// <param name="expiry"></param>void Set(string key, string value, TimeSpan expiry);/// <summary>/// 刪除/// </summary>/// <param name="key"></param>void Remove(string key);/// <summary>/// 獲取/// </summary>/// <param name="key"></param>/// <returns></returns>string Get(string key);/// <summary>/// 判斷是否存在/// </summary>/// <param name="key"></param>/// <returns></returns>bool Exists(string key);}
實現類
public class RedisCacheService : IRedisCacheService{private readonly IDatabase _redisDb;public RedisCacheService(IConnectionMultiplexer redis){_redisDb = redis.GetDatabase();}public void Set(string key, string value, TimeSpan expiry){_redisDb.StringSet(key, value, expiry);}public void Remove(string key){_redisDb.KeyDelete(key);}public string Get(string key){return _redisDb.StringGet(key);}public bool Exists(string key){return _redisDb.KeyExists(key);}}
自定義一個擴展類
public static IServiceCollection AddRedisCacheService(this IServiceCollection services){return services.AddSingleton<IRedisCacheService, RedisCacheService>();}
- 注冊依賴注入服務
在Program.cs注冊服務
在這里插入代碼片builder.Services.AddControllers(opt =>{opt.Filters.Add<Filter.JWTAuthorizationFilter>(); // 添加 JWT 授權過濾器});// 配置 Redis服務 builder.Services.Configure<RedisSetting>(builder.Configuration.GetSection("RedisConnection")); // 注入 Redis 配置var redisSetting = builder.Configuration.GetSection("RedisConnection").Get<RedisSetting>();builder.Services.AddSingleton<IConnectionMultiplexer>(ConnectionMultiplexer.Connect(redisSetting.ConnectionString));
builder.Services.AddRedisCacheService();// 創建個DTO保存redis配置public class RedisSetting{public string ConnectionString { get; set; }}
- 登錄時保存Redis信息
a.登錄成功后,將用戶的 JWTVersion 寫入 Redis。每次登錄版本號不一致,可以解決跨瀏覽器登錄和跨地區登錄。
b.如果登錄人狀態被禁用了,重新設置下Redis的值即可。
/// <summary>/// 登錄/// </summary>/// <param name="login"></param>/// <returns></returns>[HttpPost][NoAuthAttribute]public async Task<IActionResult> Login([FromBody] LoginModel login){var user = await _userManager.FindByNameAsync(login.UserName);if (user == null){return new JsonResult(new { Code = 400, Message = "用戶不存在" });}if (!await _userManager.CheckPasswordAsync(user, login.Password)){return new JsonResult(new { Code = 400, Message = "密碼錯誤" });}var guid = Guid.NewGuid().ToString();var redisKey = $"{user.Id}_user.Id";var roles = await _userManager.GetRolesAsync(user);var token = _jwtService.GenerateToken(user.Id, user.UserName, guid, roles.ToList());// 設置redis和redis1小時過期_redisDb.Set(redisKey, guid, TimeSpan.FromHours(1));return new JsonResult(new { Code = 200, Message = "登錄成功", Token = token });}
// 使用自定義注解,去掉登錄時的過濾攔截public class NoAuthAttribute : Attribute{}
- 使用過濾器來過濾請求中的數據是否有效
public class JWTAuthorizationFilter : IAsyncActionFilter
{private readonly UserManager<MyUser> userManager;private readonly IRedisCacheService redisDb;public JWTAuthorizationFilter(UserManager<MyUser> userManager, IRedisCacheService redisCache){this.userManager = userManager;this.redisDb = redisCache;}public async Task OnActionExecutionAsync(ActionExecutingContext context, ActionExecutionDelegate next){// 排除掉登錄不需要校驗的問題var controllerAttr = context.Controller.GetType().GetCustomAttributes(typeof(NoAuthAttribute), true).Any();if (controllerAttr){await next();return;}var haveNoAuth = context.ActionDescriptor.EndpointMetadata.Any(p => p is NoAuthAttribute || p is AllowAnonymousAttribute);if (haveNoAuth){await next();return;}// 這里可以添加 JWT 驗證邏輯// 如果驗證失敗,可以返回 401// 如果驗證成功,繼續執行下一個操作if (!context.HttpContext.Request.Headers.TryGetValue("Authorization", out var token)){context.Result = new ObjectResult("請求頭,沒有Authorization參數") { StatusCode = 401 };return;}token = token.ToString().Replace("Bearer ", "");if (string.IsNullOrEmpty(token)){context.Result = new ObjectResult("請求頭,token為空") { StatusCode = 401 };return;}var jwtVersion = context.HttpContext.User.FindFirstValue("JWTVersion");var userId = context.HttpContext.User.FindFirstValue(ClaimTypes.NameIdentifier);if (string.IsNullOrEmpty(jwtVersion) || string.IsNullOrEmpty(userId)){context.Result = new ObjectResult("JWTVersion或NameIdentifier為空") { StatusCode = 401 };return;}var redisKey = $"{userId}_user.Id";var sourceGuid = redisDb.Get(redisKey);// 判斷當前的版本號是否一致,不一致校驗token失效if (string.IsNullOrEmpty(sourceGuid) || sourceGuid != jwtVersion){context.Result = new ObjectResult("token已失效") { StatusCode = 401 };return;}await next();}
}