diff --git a/Admin.NET/Admin.NET.Core/Const/CacheConst.cs b/Admin.NET/Admin.NET.Core/Const/CacheConst.cs index f84a01a2..2fface7b 100644 --- a/Admin.NET/Admin.NET.Core/Const/CacheConst.cs +++ b/Admin.NET/Admin.NET.Core/Const/CacheConst.cs @@ -11,6 +11,11 @@ namespace Admin.NET.Core; /// public class CacheConst { + /// + /// 用户Token版本缓存 + /// + public const string KeyUserToken = "sys_user_token:"; + /// /// 用户接口缓存(接口集合) /// @@ -81,16 +86,6 @@ public class CacheConst /// public const string KeyOpenAccessNonce = "sys_open_access_nonce:"; - /// - /// 用户黑名单 - /// - public const string KeyUserBlacklist = "sys_user_blacklist:"; - - /// - /// token黑名单 - /// - public const string KeyTokenBlacklist = "sys_token_blacklist:"; - /// /// 系统配置缓存 /// diff --git a/Admin.NET/Admin.NET.Core/SeedData/SysConfigSeedData.cs b/Admin.NET/Admin.NET.Core/SeedData/SysConfigSeedData.cs index 28c99c76..1b45f3b6 100644 --- a/Admin.NET/Admin.NET.Core/SeedData/SysConfigSeedData.cs +++ b/Admin.NET/Admin.NET.Core/SeedData/SysConfigSeedData.cs @@ -25,8 +25,8 @@ public class SysConfigSeedData : ISqlSugarEntitySeedData new SysConfig{ Id=1300000000131, Name="日志保留天数", Code=ConfigConst.SysLogRetentionDays, Value="180", SysFlag=YesNoEnum.Y, Remark="日志保留天数(天)", OrderNo=40, GroupCode=ConfigConst.SysDefaultGroup, CreateTime=DateTime.Parse("2022-02-10 00:00:00") }, new SysConfig{ Id=1300000000141, Name="记录操作日志", Code=ConfigConst.SysOpLog, Value="True", SysFlag=YesNoEnum.Y, Remark="是否记录操作日志", OrderNo=50, GroupCode=ConfigConst.SysDefaultGroup, CreateTime=DateTime.Parse("2022-02-10 00:00:00") }, new SysConfig{ Id=1300000000151, Name="单设备登录", Code=ConfigConst.SysSingleLogin, Value="False", SysFlag=YesNoEnum.Y, Remark="是否开启单设备登录", OrderNo=60, GroupCode=ConfigConst.SysDefaultGroup, CreateTime=DateTime.Parse("2022-02-10 00:00:00") }, - new SysConfig{ Id=1300000000161, Name="Token过期时间", Code=ConfigConst.SysTokenExpire, Value="10080", SysFlag=YesNoEnum.Y, Remark="Token过期时间(分钟)", OrderNo=90, GroupCode=ConfigConst.SysDefaultGroup, CreateTime=DateTime.Parse("2022-02-10 00:00:00") }, - new SysConfig{ Id=1300000000171, Name="RefreshToken过期时间", Code=ConfigConst.SysRefreshTokenExpire, Value="20160", SysFlag=YesNoEnum.Y, Remark="刷新Token过期时间(分钟)(一般 refresh_token 的有效时间 > 2 * access_token 的有效时间)", OrderNo=100, GroupCode=ConfigConst.SysDefaultGroup, CreateTime=DateTime.Parse("2022-02-10 00:00:00") }, + new SysConfig{ Id=1300000000161, Name="Token过期时间", Code=ConfigConst.SysTokenExpire, Value="60", SysFlag=YesNoEnum.Y, Remark="Token过期时间(分钟)", OrderNo=90, GroupCode=ConfigConst.SysDefaultGroup, CreateTime=DateTime.Parse("2022-02-10 00:00:00") }, + new SysConfig{ Id=1300000000171, Name="RefreshToken过期时间", Code=ConfigConst.SysRefreshTokenExpire, Value="120", SysFlag=YesNoEnum.Y, Remark="刷新Token过期时间(分钟)(一般 refresh_token 的有效时间 > 2 * access_token 的有效时间)", OrderNo=100, GroupCode=ConfigConst.SysDefaultGroup, CreateTime=DateTime.Parse("2022-02-10 00:00:00") }, new SysConfig{ Id=1300000000181, Name="发送异常日志邮件", Code=ConfigConst.SysErrorMail, Value="False", SysFlag=YesNoEnum.Y, Remark="是否发送异常日志邮件", OrderNo=110, GroupCode=ConfigConst.SysDefaultGroup, CreateTime=DateTime.Parse("2022-02-10 00:00:00") }, new SysConfig{ Id=1300000000191, Name="域登录验证", Code=ConfigConst.SysDomainLogin, Value="False", SysFlag=YesNoEnum.Y, Remark="是否开启域登录验证", OrderNo=120, GroupCode=ConfigConst.SysDefaultGroup, CreateTime=DateTime.Parse("2022-02-10 00:00:00") }, //new SysConfig{ Id=1300000000201, Name="行政区划同步层级", Code=ConfigConst.SysRegionSyncLevel, Value="3", SysFlag=YesNoEnum.Y, Remark="行政区划同步层级 1-省级,2-市级,3-区县级,4-街道级,5-村级", OrderNo=150, GroupCode=ConfigConst.SysDefaultGroup, CreateTime=DateTime.Parse("2022-02-10 00:00:00") }, diff --git a/Admin.NET/Admin.NET.Core/Service/Auth/SysAuthService.cs b/Admin.NET/Admin.NET.Core/Service/Auth/SysAuthService.cs index 82443323..25837814 100644 --- a/Admin.NET/Admin.NET.Core/Service/Auth/SysAuthService.cs +++ b/Admin.NET/Admin.NET.Core/Service/Auth/SysAuthService.cs @@ -122,7 +122,7 @@ public class SysAuthService : IDynamicApiController, ITransient _ = user ?? throw Oops.Oh(ErrorCodeEnum.D0009); // 判断账号是否被冻结 - if (user.Status == StatusEnum.Disable) throw Oops.Oh(ErrorCodeEnum.D1017); + if (user.Status != StatusEnum.Enable) throw Oops.Oh(ErrorCodeEnum.D1017); // 判断租户是否存在及状态 var tenant = await _sysUserRep.ChangeRepository>().GetFirstAsync(u => u.Id == user.TenantId); @@ -258,16 +258,19 @@ public class SysAuthService : IDynamicApiController, ITransient user.LastLoginIp = _httpContextAccessor.HttpContext.GetRemoteIpAddressToIPv4(true); (user.LastLoginAddress, double? longitude, double? latitude) = CommonUtil.GetIpAddress(user.LastLoginIp); user.LastLoginTime = DateTime.Now; - user.LastLoginDevice = CommonUtil.GetClientDeviceInfo(_httpContextAccessor.HttpContext?.Request?.Headers?.UserAgent); + user.LastLoginDevice = CommonUtil.GetClientDeviceInfo(_httpContextAccessor.HttpContext?.Request?.Headers?.UserAgent); await _sysUserRep.AsUpdateable(user).UpdateColumns(u => new { u.LastLoginIp, u.LastLoginAddress, u.LastLoginTime, u.LastLoginDevice, - }).ExecuteCommandAsync(); - - // 发布系统登录事件 + }).ExecuteCommandAsync(); + + // 缓存用户Token版本 + _sysCacheService.Set($"{CacheConst.KeyUserToken}{user.Id}", $"{user.TokenVersion}"); + + // 发布系统登录事件 await _eventPublisher.PublishAsync(UserEventTypeEnum.Login, user); return new LoginOutput @@ -341,12 +344,16 @@ public class SysAuthService : IDynamicApiController, ITransient public async void Logout() { if (string.IsNullOrWhiteSpace(_userManager.Account)) - throw Oops.Oh(ErrorCodeEnum.D1011); - - // 增加无效Token黑名单 - var tokenExpire = await _sysConfigService.GetTokenExpire(); - var accessToken = _httpContextAccessor.HttpContext.Request.Headers.Authorization.ToString(); - _sysCacheService.Set($"{CacheConst.KeyTokenBlacklist}{MD5Encryption.Encrypt(accessToken)}", $"{_userManager.UserId}-{_userManager.Account}-{_userManager.RealName}-{DateTime.Now}", TimeSpan.FromMinutes(tokenExpire)); + throw Oops.Oh(ErrorCodeEnum.D1011); + + //// 更新用户Token版本号 + //await _sysUserRep.AsUpdateable() + // .SetColumns(u => u.TokenVersion == u.TokenVersion + 1) + // .Where(u => u.Id == _userManager.UserId) + // .ExecuteCommandAsync(); + + // 更新用户Token版本缓存 + _sysCacheService.Set($"{CacheConst.KeyUserToken}{_userManager.UserId}", $"{_userManager.TokenVersion + 1}"); // 发布系统退出事件 await _eventPublisher.PublishAsync(UserEventTypeEnum.Logout, _userManager); diff --git a/Admin.NET/Admin.NET.Core/Service/User/SysUserService.cs b/Admin.NET/Admin.NET.Core/Service/User/SysUserService.cs index f7f9190d..7c219835 100644 --- a/Admin.NET/Admin.NET.Core/Service/User/SysUserService.cs +++ b/Admin.NET/Admin.NET.Core/Service/User/SysUserService.cs @@ -140,19 +140,24 @@ public class SysUserService : IDynamicApiController, ITransient if (await query.AnyAsync(u => u.Account == input.Account)) throw Oops.Oh(ErrorCodeEnum.D1003); if (!string.IsNullOrWhiteSpace(input.Phone) && await query.AnyAsync(u => u.Phone == input.Phone)) throw Oops.Oh(ErrorCodeEnum.D1032); - await _sysUserRep.AsUpdateable(input.Adapt()).IgnoreColumns(true) - .IgnoreColumns(u => new { u.Password, u.Status, u.TenantId }).ExecuteCommandAsync(); + input.TokenVersion++; + var user = input.Adapt(); + await _sysUserRep.AsUpdateable(user).IgnoreColumns(true).IgnoreColumns(u => new { u.Password, u.Status, u.TenantId }).ExecuteCommandAsync(); + // 更新用户附属机构 await UpdateRoleAndExtOrg(input); // 删除用户机构缓存 SqlSugarFilter.DeleteUserOrgCache(input.Id, _sysUserRep.Context.CurrentConnectionConfig.ConfigId.ToString()); // 若账号的角色和组织架构发生变化,则强制下线账号进行权限更新 - var user = await _sysUserRep.AsQueryable().ClearFilter().FirstAsync(u => u.Id == input.Id); var roleIds = await _sysUserRoleService.GetUserRoleIdList(input.Id); if (input.OrgId != user.OrgId || !input.RoleIdList.OrderBy(u => u).SequenceEqual(roleIds.OrderBy(u => u))) - await _sysOnlineUserService.ForceOfflineByUserId(input.Id); + { + // 强制下线账号和失效Token + await OfflineAndExpireToken(user); + } + // 更新域账号 await _sysUserLdapService.AddUserLdap(user.TenantId!.Value, user.Id, user.Account, input.DomainAccount); @@ -194,12 +199,10 @@ public class SysUserService : IDynamicApiController, ITransient var isOpenAccessUser = await _sysUserRep.ChangeRepository>().IsAnyAsync(u => u.BindUserId == input.Id); if (isOpenAccessUser) throw Oops.Oh(ErrorCodeEnum.D1030); - // 设置账号Token黑名单 - await SetUserBlackList(user, StatusEnum.Disable); - - // 强制账号下线 - await _sysOnlineUserService.ForceOfflineByUserId(user.Id); + // 强制下线账号和失效Token + await OfflineAndExpireToken(user); + // 删除用户 await _sysUserRep.DeleteAsync(user); // 删除用户角色 @@ -258,8 +261,11 @@ public class SysUserService : IDynamicApiController, ITransient if (!Enum.IsDefined(typeof(StatusEnum), input.Status)) throw Oops.Oh(ErrorCodeEnum.D3005); - // 设置账号Token黑名单 - await SetUserBlackList(user, input.Status); + if (input.Status != StatusEnum.Enable) + { + // 强制下线账号和失效Token + await OfflineAndExpireToken(user); + } user.Status = input.Status; var rows = await _sysUserRep.AsUpdateable(user).UpdateColumns(u => new { u.Status }).ExecuteCommandAsync(); @@ -270,28 +276,6 @@ public class SysUserService : IDynamicApiController, ITransient return rows; } - /// - /// 设置账号Token黑名单 - /// - /// - /// - /// - [NonAction] - private async Task SetUserBlackList(SysUser user, StatusEnum status) - { - // 禁用账号则增加黑名单,启用账号则移除黑名单 - var sysCacheService = App.GetRequiredService(); - if (status != StatusEnum.Enable) - { - sysCacheService.Set($"{CacheConst.KeyUserBlacklist}{user.Id}", $"{user.RealName}-{user.Phone}"); - await _sysOnlineUserService.ForceOfflineByUserId(user.Id); // 强制账号下线 - } - else - { - sysCacheService.Remove($"{CacheConst.KeyUserBlacklist}{user.Id}"); - } - } - /// /// 授权用户角色 🔖 /// @@ -301,14 +285,12 @@ public class SysUserService : IDynamicApiController, ITransient [DisplayName("授权用户角色")] public async Task GrantRole(UserRoleInput input) { - //var user = await _sysUserRep.GetByIdAsync(input.UserId) ?? throw Oops.Oh(ErrorCodeEnum.D0009); - //if (user.AccountType == AccountTypeEnum.SuperAdmin) - // throw Oops.Oh(ErrorCodeEnum.D1022); + var user = await _sysUserRep.GetByIdAsync(input.UserId) ?? throw Oops.Oh(ErrorCodeEnum.D0009); await _sysUserRoleService.GrantUserRole(input); - // 强制账号下线 - await _sysOnlineUserService.ForceOfflineByUserId(input.UserId); + // 强制下线账号和失效Token + await OfflineAndExpireToken(user); // 发布更新用户角色事件 await _eventPublisher.PublishAsync(UserEventTypeEnum.UpdateRole, input); @@ -367,14 +349,15 @@ public class SysUserService : IDynamicApiController, ITransient // 更新密码和最新修改时间 user.LastChangePasswordTime = DateTime.Now; - var rows = await _sysUserRep.AsUpdateable(user).UpdateColumns(u => new { u.Password, u.LastChangePasswordTime }).ExecuteCommandAsync(); + user.TokenVersion = user.TokenVersion + 1; + var rows = await _sysUserRep.AsUpdateable(user).UpdateColumns(u => new { u.Password, u.LastChangePasswordTime, u.TokenVersion }).ExecuteCommandAsync(); + + // 强制下线账号和失效Token + await OfflineAndExpireToken(user); // 发布修改用户密码事件 await _eventPublisher.PublishAsync(UserEventTypeEnum.ChangePwd, input); - // 强制账号下线 - await _sysOnlineUserService.ForceOfflineByUserId(user.Id); - return rows; } @@ -390,14 +373,14 @@ public class SysUserService : IDynamicApiController, ITransient var password = await _sysConfigService.GetConfigValueByCode(ConfigConst.SysPassword); user.Password = CryptogramUtil.Encrypt(password); user.LastChangePasswordTime = null; - await _sysUserRep.AsUpdateable(user).UpdateColumns(u => new { u.Password, u.LastChangePasswordTime }).ExecuteCommandAsync(); + user.TokenVersion = user.TokenVersion + 1; + await _sysUserRep.AsUpdateable(user).UpdateColumns(u => new { u.Password, u.LastChangePasswordTime, u.TokenVersion }).ExecuteCommandAsync(); - // 清空密码错误次数 - var keyPasswordErrorTimes = $"{CacheConst.KeyPasswordErrorTimes}{user.Account}"; - _sysCacheService.Remove(keyPasswordErrorTimes); + // 清空密码错误次数缓存 + _sysCacheService.Remove($"{CacheConst.KeyPasswordErrorTimes}{user.Account}"); - // 强制账号下线 - await _sysOnlineUserService.ForceOfflineByUserId(user.Id); + // 强制下线账号和失效Token + await OfflineAndExpireToken(user); // 发布重置用户密码事件 await _eventPublisher.PublishAsync(UserEventTypeEnum.ResetPwd, input); @@ -471,4 +454,17 @@ public class SysUserService : IDynamicApiController, ITransient { return await _sysUserExtOrgService.GetUserExtOrgList(userId); } + + /// + /// 强制下线账号和失效Token + /// + /// + private async Task OfflineAndExpireToken(SysUser user) + { + // 更新Token版本缓存 + _sysCacheService.Set($"{CacheConst.KeyUserToken}{user.Id}", $"{user.TokenVersion + 1}"); + + // 强制下线账号 + await _sysOnlineUserService.ForceOfflineByUserId(user.Id); + } } \ No newline at end of file diff --git a/Admin.NET/Admin.NET.Web.Core/Handlers/JwtHandler.cs b/Admin.NET/Admin.NET.Web.Core/Handlers/JwtHandler.cs index ab053d0c..21d793bd 100644 --- a/Admin.NET/Admin.NET.Web.Core/Handlers/JwtHandler.cs +++ b/Admin.NET/Admin.NET.Web.Core/Handlers/JwtHandler.cs @@ -38,26 +38,17 @@ namespace Admin.NET.Web.Core { // var serviceProvider = context.GetCurrentHttpContext().RequestServices; using var serviceScope = _serviceProvider.CreateScope(); - - // 验证账号黑名单有则授权失败 var sysCacheService = serviceScope.ServiceProvider.GetRequiredService(); - if (sysCacheService.ExistKey($"{CacheConst.KeyUserBlacklist}{context.User.FindFirst(ClaimConst.UserId)?.Value}")) - { - context.Fail(new AuthorizationFailureReason(this, "账号已经黑名单,请联系相关管理人员。")); - context.GetCurrentHttpContext().SignoutToSwagger(); - return; - } - - // 验证Token黑名单有则授权失败 - var accessToken = httpContext.Request.Headers.Authorization.ToString(); - if (sysCacheService.ExistKey($"{CacheConst.KeyTokenBlacklist}{MD5Encryption.Encrypt(accessToken)}")) - { - context.Fail(new AuthorizationFailureReason(this, "Token已失效,请重新登录。")); - context.GetCurrentHttpContext().SignoutToSwagger(); - return; - } // 验证Token版本号 + var userId = context.User.FindFirst(ClaimConst.UserId)?.Value; + var tokenVersion = context.User.FindFirst(ClaimConst.TokenVersion)?.Value; + if (sysCacheService.Get($"{CacheConst.KeyUserToken}{userId}") != tokenVersion) + { + context.Fail(new AuthorizationFailureReason(this, "令牌已失效,请重新登录。")); + context.GetCurrentHttpContext().SignoutToSwagger(); + return; + } // 验证租户有效期 var tenantId = context.User.FindFirst(ClaimConst.TenantId)?.Value; @@ -66,7 +57,7 @@ namespace Admin.NET.Web.Core var tenant = sysCacheService.Get>(CacheConst.KeyTenant)?.FirstOrDefault(u => u.Id == long.Parse(tenantId)); if (tenant != null && tenant.ExpirationTime != null && DateTime.Now > tenant.ExpirationTime) { - context.Fail(new AuthorizationFailureReason(this, "租户已过期,请联系相关管理人员。")); + context.Fail(new AuthorizationFailureReason(this, "租户已过期,请联系管理员。")); context.GetCurrentHttpContext().SignoutToSwagger(); return; }