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;
}