Merge pull request '使用缓存管理用户session信息、优化日志记录逻辑' (#414) from jasondom/Admin.NET.Pro:v2-1 into v2

Reviewed-on: https://code.adminnet.top/Admin.NET/Admin.NET.Pro/pulls/414
This commit is contained in:
zuohuaijun 2025-08-24 15:33:22 +08:00
commit b04324848c
27 changed files with 926 additions and 419 deletions

View File

@ -24,6 +24,7 @@ public class AppAuthService : IDynamicApiController, ITransient
private readonly SysRoleService _sysRoleService; private readonly SysRoleService _sysRoleService;
private readonly SysOnlineUserService _sysOnlineUserService; private readonly SysOnlineUserService _sysOnlineUserService;
private readonly SysConfigService _sysConfigService; private readonly SysConfigService _sysConfigService;
private readonly SysAuthService _sysAuthService;
private readonly ICaptcha _captcha; private readonly ICaptcha _captcha;
private readonly SysCacheService _sysCacheService; private readonly SysCacheService _sysCacheService;
@ -33,9 +34,11 @@ public class AppAuthService : IDynamicApiController, ITransient
SysRoleService sysRoleService, SysRoleService sysRoleService,
SysOnlineUserService sysOnlineUserService, SysOnlineUserService sysOnlineUserService,
SysConfigService sysConfigService, SysConfigService sysConfigService,
SysAuthService sysAuthService,
ICaptcha captcha, ICaptcha captcha,
SysCacheService sysCacheService) SysCacheService sysCacheService)
{ {
_sysAuthService = sysAuthService;
_appUserManager = appUserManager; _appUserManager = appUserManager;
_sysUserRep = sysUserRep; _sysUserRep = sysUserRep;
_httpContextAccessor = httpContextAccessor; _httpContextAccessor = httpContextAccessor;
@ -100,8 +103,7 @@ public class AppAuthService : IDynamicApiController, ITransient
// 登录成功则清空密码错误次数 // 登录成功则清空密码错误次数
_sysCacheService.Remove(keyPasswordErrorTimes); _sysCacheService.Remove(keyPasswordErrorTimes);
return await _sysAuthService.CreateToken(user, LoginModeEnum.APP);
return await CreateToken(user);
} }
/// <summary> /// <summary>
@ -149,61 +151,14 @@ public class AppAuthService : IDynamicApiController, ITransient
// 账号是否存在 // 账号是否存在
var user = await _sysUserRep.AsQueryable().Includes(u => u.SysOrg).IgnoreTenant().FirstAsync(u => u.Phone.Equals(input.Phone)); var user = await _sysUserRep.AsQueryable().Includes(u => u.SysOrg).IgnoreTenant().FirstAsync(u => u.Phone.Equals(input.Phone));
_ = user ?? throw Oops.Oh(ErrorCodeEnum.D0009); _ = user ?? throw Oops.Oh(ErrorCodeEnum.D0009);
return await _sysAuthService.CreateToken(user, LoginModeEnum.APP);
return await CreateToken(user);
} }
/// <summary> /// <summary>
/// 生成Token令牌 🔖 /// 获取当前登陆用户信息 🔖
/// </summary>
/// <param name="user"></param>
/// <param name="loginMode"></param>
/// <returns></returns>
[NonAction]
public virtual async Task<LoginOutput> CreateToken(SysUser user, LoginModeEnum loginMode = LoginModeEnum.APP)
{
// 单用户登录
await _sysOnlineUserService.SingleLogin(user.Id, loginMode);
// 生成Token令牌
var tokenExpire = await _sysConfigService.GetTokenExpire();
var accessToken = JWTEncryption.Encrypt(new Dictionary<string, object>
{
{ AppClaimConst.UserId, user.Id },
{ AppClaimConst.TenantId, user.TenantId },
{ AppClaimConst.Account, user.Account },
{ AppClaimConst.RealName, user.RealName },
{ AppClaimConst.AccountType, user.AccountType },
{ AppClaimConst.OrgId, user.OrgId },
{ AppClaimConst.OrgName, user.SysOrg?.Name },
{ AppClaimConst.OrgType, user.SysOrg?.Type },
{ AppClaimConst.OrgLevel, user.SysOrg?.Level },
{ ClaimConst.LoginMode, loginMode },
{ ClaimConst.TokenVersion, user.TokenVersion },
}, tokenExpire);
// 生成刷新Token令牌
var refreshTokenExpire = await _sysConfigService.GetRefreshTokenExpire();
var refreshToken = JWTEncryption.GenerateRefreshToken(accessToken, refreshTokenExpire);
// 设置响应报文头
_httpContextAccessor.HttpContext.SetTokensOfResponseHeaders(accessToken, refreshToken);
// 缓存用户Token版本
_sysCacheService.Set($"{CacheConst.KeyUserToken}{user.Id}", $"{user.TokenVersion}");
return new LoginOutput
{
AccessToken = accessToken,
RefreshToken = refreshToken
};
}
/// <summary>
/// 获取登录账号 🔖
/// </summary> /// </summary>
/// <returns></returns> /// <returns></returns>
[DisplayName("获取登录账号")] [DisplayName("获取当前登陆用户信息")]
public virtual async Task<LoginUserOutput> GetUserInfo() public virtual async Task<LoginUserOutput> GetUserInfo()
{ {
var user = await _sysUserRep.GetByIdAsync(_appUserManager.UserId) ?? throw Oops.Oh(ErrorCodeEnum.D1011).StatusCode(401); var user = await _sysUserRep.GetByIdAsync(_appUserManager.UserId) ?? throw Oops.Oh(ErrorCodeEnum.D1011).StatusCode(401);
@ -261,6 +216,7 @@ public class AppAuthService : IDynamicApiController, ITransient
if (string.IsNullOrWhiteSpace(_appUserManager.Account)) if (string.IsNullOrWhiteSpace(_appUserManager.Account))
throw Oops.Oh(ErrorCodeEnum.D1011); throw Oops.Oh(ErrorCodeEnum.D1011);
_appUserManager.RemoveSession(_appUserManager.UserId);
_httpContextAccessor.HttpContext.SignoutToSwagger(); _httpContextAccessor.HttpContext.SignoutToSwagger();
} }

View File

@ -6,16 +6,25 @@
using Microsoft.AspNetCore.Http; using Microsoft.AspNetCore.Http;
namespace Admin.NET.Application.Service.App; namespace Admin.NET.Application;
public class AppUserManager : UserManager /// <summary>
/// 业务层用户Session管理
/// </summary>
/// <param name="sysCacheService"></param>
/// <param name="httpContextAccessor"></param>
public class AppUserManager(
SysCacheService sysCacheService,
IHttpContextAccessor httpContextAccessor)
: UserManager(sysCacheService, httpContextAccessor)
{
// 扩展属性
}
/// <summary>
/// 用户会话信息
/// </summary>
public partial class UserSessionDao
{ {
private readonly IHttpContextAccessor _httpContextAccessor;
public AppUserManager(IHttpContextAccessor httpContextAccessor) : base(httpContextAccessor)
{
_httpContextAccessor = httpContextAccessor;
}
// 扩展属性 // 扩展属性
} }

View File

@ -16,12 +16,18 @@ namespace Admin.NET.Core.ApiKeyAuth;
public class ApiKeyAuthenticationHandler : AuthenticationHandler<ApiKeyAuthenticationOptions> public class ApiKeyAuthenticationHandler : AuthenticationHandler<ApiKeyAuthenticationOptions>
{ {
public const string AuthenticationScheme = "ApiKey"; public const string AuthenticationScheme = "ApiKey";
private readonly SysAuthService _sysAuthService;
private readonly UserManager _userManager;
public ApiKeyAuthenticationHandler(IOptionsMonitor<ApiKeyAuthenticationOptions> options, public ApiKeyAuthenticationHandler(IOptionsMonitor<ApiKeyAuthenticationOptions> options,
ILoggerFactory logger, ILoggerFactory logger,
UrlEncoder encoder) UrlEncoder encoder,
UserManager userManager,
SysAuthService sysAuthService)
: base(options, logger, encoder) : base(options, logger, encoder)
{ {
_userManager = userManager;
_sysAuthService = sysAuthService;
} }
protected override async Task<AuthenticateResult> HandleAuthenticateAsync() protected override async Task<AuthenticateResult> HandleAuthenticateAsync()
@ -39,15 +45,12 @@ public class ApiKeyAuthenticationHandler : AuthenticationHandler<ApiKeyAuthentic
identity.AddClaims( identity.AddClaims(
[ [
new Claim(ClaimConst.UserId, acsInfo.BindUserId + ""), new Claim(ClaimConst.UserId, acsInfo.BindUserId + ""),
new Claim(ClaimConst.TenantId, acsInfo.BindTenantId + ""),
new Claim(ClaimConst.Account, acsInfo.BindUser.Account + ""),
new Claim(ClaimConst.RealName, acsInfo.BindUser.RealName),
new Claim(ClaimConst.AccountType, ((int)acsInfo.BindUser.AccountType).ToString()),
new Claim(ClaimConst.OrgId, acsInfo.BindUser.OrgId + ""),
new Claim(ClaimConst.OrgName, acsInfo.BindUser.SysOrg?.Name + ""),
new Claim(ClaimConst.OrgType, acsInfo.BindUser.SysOrg?.Type + ""),
new Claim(ClaimConst.TokenVersion, "1") new Claim(ClaimConst.TokenVersion, "1")
]); ]);
// 设置用户Session缓存
if (!_userManager.ExistSession(acsInfo.Id)) await _sysAuthService.CreateToken(acsInfo.BindUser);
var user = new ClaimsPrincipal(identity); var user = new ClaimsPrincipal(identity);
return AuthenticateResult.Success(new AuthenticationTicket(user, AuthenticationScheme)); return AuthenticateResult.Success(new AuthenticationTicket(user, AuthenticationScheme));
} }

View File

@ -11,6 +11,11 @@ namespace Admin.NET.Core;
/// </summary> /// </summary>
public class CacheConst public class CacheConst
{ {
/// <summary>
/// 用户会话缓存
/// </summary>
public const string KeyUserSession = "sys_user_session:";
/// <summary> /// <summary>
/// 用户Token版本缓存 /// 用户Token版本缓存
/// </summary> /// </summary>

View File

@ -11,73 +11,85 @@ namespace Admin.NET.Core;
/// </summary> /// </summary>
public class ClaimConst public class ClaimConst
{ {
/// <summary>
/// 应用Id
/// </summary>
public const string AppId = "AppId";
/// <summary>
/// 租户Id
/// </summary>
public const string TenantId = "TenantId";
/// <summary> /// <summary>
/// 用户Id /// 用户Id
/// </summary> /// </summary>
public const string UserId = "UserId"; public const string UserId = "UserId";
/// <summary>
/// 账号
/// </summary>
public const string Account = "Account";
/// <summary>
/// 真实姓名
/// </summary>
public const string RealName = "RealName";
/// <summary>
/// 昵称
/// </summary>
public const string NickName = "NickName";
/// <summary>
/// 账号类型
/// </summary>
public const string AccountType = "AccountType";
/// <summary>
/// 组织机构Id
/// </summary>
public const string OrgId = "OrgId";
/// <summary>
/// 组织机构名称
/// </summary>
public const string OrgName = "OrgName";
/// <summary>
/// 组织机构类型
/// </summary>
public const string OrgType = "OrgType";
/// <summary>
/// 组织机构级别
/// </summary>
public const string OrgLevel = "OrgLevel";
/// <summary>
/// 登录模式PC、APP
/// </summary>
public const string LoginMode = "LoginMode";
/// <summary> /// <summary>
/// Token版本号 /// Token版本号
/// </summary> /// </summary>
public const string TokenVersion = "TokenVersion"; public const string TokenVersion = "TokenVersion";
/// <summary>
/// 应用Id
/// </summary>
[Obsolete("参数已从请求上下文中移除请通过UserManger获取")]
public const string AppId = "AppId";
/// <summary>
/// 租户Id
/// </summary>
[Obsolete("参数已从请求上下文中移除请通过UserManger获取")]
public const string TenantId = "TenantId";
/// <summary>
/// 账号
/// </summary>
[Obsolete("参数已从请求上下文中移除请通过UserManger获取")]
public const string Account = "Account";
/// <summary>
/// 真实姓名
/// </summary>
[Obsolete("参数已从请求上下文中移除请通过UserManger获取")]
public const string RealName = "RealName";
/// <summary>
/// 昵称
/// </summary>
[Obsolete("参数已从请求上下文中移除请通过UserManger获取")]
public const string NickName = "NickName";
/// <summary>
/// 账号类型
/// </summary>
[Obsolete("参数已从请求上下文中移除请通过UserManger获取")]
public const string AccountType = "AccountType";
/// <summary>
/// 组织机构Id
/// </summary>
[Obsolete("参数已从请求上下文中移除请通过UserManger获取")]
public const string OrgId = "OrgId";
/// <summary>
/// 组织机构名称
/// </summary>
[Obsolete("参数已从请求上下文中移除请通过UserManger获取")]
public const string OrgName = "OrgName";
/// <summary>
/// 组织机构类型
/// </summary>
[Obsolete("参数已从请求上下文中移除请通过UserManger获取")]
public const string OrgType = "OrgType";
/// <summary>
/// 组织机构级别
/// </summary>
[Obsolete("参数已从请求上下文中移除请通过UserManger获取")]
public const string OrgLevel = "OrgLevel";
/// <summary>
/// 登录模式PC、APP
/// </summary>
[Obsolete("参数已从请求上下文中移除请通过UserManger获取")]
public const string LoginMode = "LoginMode";
/// <summary> /// <summary>
/// 微信OpenId /// 微信OpenId
/// </summary> /// </summary>
[Obsolete("参数已从请求上下文中移除请通过UserManger获取")]
public const string OpenId = "OpenId"; public const string OpenId = "OpenId";
} }

View File

@ -15,6 +15,7 @@ public class CommonConst
/// <summary> /// <summary>
/// 系统管理员角色编码 /// 系统管理员角色编码
/// </summary> /// </summary>
[Description("系统管理员角色编码")]
public const string SysAdminRole = "sys_admin"; public const string SysAdminRole = "sys_admin";
/// <summary> /// <summary>

View File

@ -12,7 +12,7 @@ namespace Admin.NET.Core;
[SugarTable(null, "系统差异日志表")] [SugarTable(null, "系统差异日志表")]
[SysTable] [SysTable]
[LogTable] [LogTable]
public partial class SysLogDiff : EntityTenant public partial class SysLogDiff : EntityTenantId
{ {
/// <summary> /// <summary>
/// 操作前记录 /// 操作前记录
@ -55,4 +55,23 @@ public partial class SysLogDiff : EntityTenant
/// </summary> /// </summary>
[SugarColumn(ColumnDescription = "耗时")] [SugarColumn(ColumnDescription = "耗时")]
public long? Elapsed { get; set; } public long? Elapsed { get; set; }
/// <summary>
/// 创建时间
/// </summary>
[SugarColumn(ColumnDescription = "创建时间", IsOnlyIgnoreUpdate = true)]
public virtual DateTime CreateTime { get; set; }
/// <summary>
/// 创建者Id
/// </summary>
[OwnerUser]
[SugarColumn(ColumnDescription = "创建者Id", IsOnlyIgnoreUpdate = true)]
public virtual long? CreateUserId { get; set; }
/// <summary>
/// 创建者姓名
/// </summary>
[SugarColumn(ColumnDescription = "创建者姓名", Length = 64, IsOnlyIgnoreUpdate = true)]
public virtual string? CreateUserName { get; set; }
} }

View File

@ -12,7 +12,7 @@ namespace Admin.NET.Core;
[SugarTable(null, "系统消息日志表")] [SugarTable(null, "系统消息日志表")]
[SysTable] [SysTable]
[LogTable] [LogTable]
public partial class SysLogMsg : EntityTenant public partial class SysLogMsg : EntityTenantId
{ {
/// <summary> /// <summary>
/// 消息类型 /// 消息类型
@ -125,4 +125,23 @@ public partial class SysLogMsg : EntityTenant
[SugarColumn(ColumnDescription = "发送者设备", Length = 256)] [SugarColumn(ColumnDescription = "发送者设备", Length = 256)]
[MaxLength(256)] [MaxLength(256)]
public string? SendDevice { get; set; } public string? SendDevice { get; set; }
/// <summary>
/// 创建时间
/// </summary>
[SugarColumn(ColumnDescription = "创建时间", IsNullable = true, IsOnlyIgnoreUpdate = true)]
public virtual DateTime CreateTime { get; set; }
/// <summary>
/// 创建者Id
/// </summary>
[OwnerUser]
[SugarColumn(ColumnDescription = "创建者Id", IsOnlyIgnoreUpdate = true)]
public virtual long? CreateUserId { get; set; }
/// <summary>
/// 创建者姓名
/// </summary>
[SugarColumn(ColumnDescription = "创建者姓名", Length = 64, IsOnlyIgnoreUpdate = true)]
public virtual string? CreateUserName { get; set; }
} }

View File

@ -12,7 +12,7 @@ namespace Admin.NET.Core;
[SugarTable(null, "系统访问日志表")] [SugarTable(null, "系统访问日志表")]
[SysTable] [SysTable]
[LogTable] [LogTable]
public partial class SysLogVis : EntityTenant public partial class SysLogVis : EntityTenantId
{ {
/// <summary> /// <summary>
/// 模块名称 /// 模块名称
@ -113,4 +113,24 @@ public partial class SysLogVis : EntityTenant
[SugarColumn(ColumnDescription = "真实姓名", Length = 32)] [SugarColumn(ColumnDescription = "真实姓名", Length = 32)]
[MaxLength(32)] [MaxLength(32)]
public string? RealName { get; set; } public string? RealName { get; set; }
/// <summary>
/// 创建时间
/// </summary>
[SugarColumn(ColumnDescription = "创建时间", IsOnlyIgnoreUpdate = true)]
public virtual DateTime CreateTime { get; set; }
/// <summary>
/// 创建者Id
/// </summary>
[OwnerUser]
[SugarColumn(ColumnDescription = "创建者Id", IsOnlyIgnoreUpdate = true)]
public virtual long? CreateUserId { get; set; }
/// <summary>
/// 创建者姓名
/// </summary>
[SugarColumn(ColumnDescription = "创建者姓名", Length = 64, IsOnlyIgnoreUpdate = true)]
public virtual string? CreateUserName { get; set; }
} }

View File

@ -19,10 +19,14 @@ public class OnlineUserHub : Hub<IOnlineUserHub>
private readonly SysMessageService _sysMessageService; private readonly SysMessageService _sysMessageService;
private readonly IHubContext<OnlineUserHub, IOnlineUserHub> _onlineUserHubContext; private readonly IHubContext<OnlineUserHub, IOnlineUserHub> _onlineUserHubContext;
private readonly UserManager _userManager;
public OnlineUserHub(SysMessageService sysMessageService, public OnlineUserHub(
UserManager userManager,
SysMessageService sysMessageService,
IHubContext<OnlineUserHub, IOnlineUserHub> onlineUserHubContext) IHubContext<OnlineUserHub, IOnlineUserHub> onlineUserHubContext)
{ {
_userManager = userManager;
_sysMessageService = sysMessageService; _sysMessageService = sysMessageService;
_onlineUserHubContext = onlineUserHubContext; _onlineUserHubContext = onlineUserHubContext;
} }
@ -34,28 +38,22 @@ public class OnlineUserHub : Hub<IOnlineUserHub>
public override async Task OnConnectedAsync() public override async Task OnConnectedAsync()
{ {
var httpContext = Context.GetHttpContext(); var httpContext = Context.GetHttpContext();
var userId = (httpContext.User.FindFirst(ClaimConst.UserId)?.Value).ToLong(); if (_userManager.UserId < 0 || string.IsNullOrWhiteSpace(_userManager.Account)) return;
var account = httpContext.User.FindFirst(ClaimConst.Account)?.Value; var loginMode = _userManager.LoginMode == 0 ? LoginModeEnum.PC : LoginModeEnum.APP; // 默认PC登录模式
if (userId < 0 || string.IsNullOrWhiteSpace(account)) return;
var realName = httpContext.User.FindFirst(ClaimConst.RealName)?.Value;
var tenantId = (httpContext.User.FindFirst(ClaimConst.TenantId)?.Value).ToLong();
var loginMode = (LoginModeEnum)(httpContext.User.FindFirst(ClaimConst.LoginMode)?.Value).ToInt();
loginMode = loginMode == 0 ? LoginModeEnum.PC : loginMode; // 默认PC登录模式
var device = httpContext.GetClientDeviceInfo().Trim(); var device = httpContext.GetClientDeviceInfo().Trim();
var ipAddress = httpContext.Request.Headers["X-Forwarded-For"].FirstOrDefault(); var ipAddress = httpContext.Request.Headers["X-Forwarded-For"].FirstOrDefault();
var user = new OnlineUser var user = new OnlineUser
{ {
ConnectionId = Context.ConnectionId, ConnectionId = Context.ConnectionId,
UserId = userId, UserId = _userManager.UserId,
UserName = account, UserName = _userManager.Account,
RealName = realName, RealName = _userManager.RealName,
Time = DateTime.Now, Time = DateTime.Now,
Ip = string.IsNullOrWhiteSpace(ipAddress) ? httpContext.GetRemoteIpAddressToIPv4(true) : ipAddress, Ip = string.IsNullOrWhiteSpace(ipAddress) ? httpContext.GetRemoteIpAddressToIPv4(true) : ipAddress,
Browser = httpContext.GetClientBrowser(), Browser = httpContext.GetClientBrowser(),
Os = httpContext.GetClientOs(), Os = httpContext.GetClientOs(),
TenantId = tenantId, TenantId = _userManager.TenantId ?? 0,
LoginMode = loginMode, LoginMode = loginMode,
Device = device Device = device
}; };
@ -66,7 +64,7 @@ public class OnlineUserHub : Hub<IOnlineUserHub>
await _onlineUserHubContext.Groups.AddToGroupAsync(Context.ConnectionId, groupName); await _onlineUserHubContext.Groups.AddToGroupAsync(Context.ConnectionId, groupName);
// 更新在线用户列表 // 更新在线用户列表
var userList = SysCacheService.HashGetAll<OnlineUser>(CacheConst.KeyUserOnline).Values.Where(u => u.TenantId == tenantId).ToList(); var userList = SysCacheService.HashGetAll<OnlineUser>(CacheConst.KeyUserOnline).Values.Where(u => u.TenantId == _userManager.TenantId).ToList();
await _onlineUserHubContext.Clients.Groups(groupName).OnlineUserList(new OnlineUserList await _onlineUserHubContext.Clients.Groups(groupName).OnlineUserList(new OnlineUserList
{ {
RealName = user.RealName, RealName = user.RealName,

View File

@ -13,13 +13,18 @@ namespace Admin.NET.Core;
/// </summary> /// </summary>
public class DatabaseLoggingWriter : IDatabaseLoggingWriter, IDisposable public class DatabaseLoggingWriter : IDatabaseLoggingWriter, IDisposable
{ {
private readonly IServiceScope _serviceScope; private static readonly Lazy<UserManager> _userManager = new(() => App.GetService<UserManager>());
private readonly IEventPublisher _eventPublisher; private static readonly Lazy<SqlSugarRepository<SysUser>> _sysUserRep = new(() => App.GetService<SqlSugarRepository<SysUser>>());
private readonly ILogger<DatabaseLoggingWriter> _logger; private readonly ILogger<DatabaseLoggingWriter> _logger;
private readonly SysConfigService _sysConfigService; private readonly SysConfigService _sysConfigService;
private readonly IEventPublisher _eventPublisher;
private readonly IServiceScope _serviceScope;
private readonly SqlSugarScopeProvider _db; private readonly SqlSugarScopeProvider _db;
public DatabaseLoggingWriter(IServiceScopeFactory serviceScopeFactory, IEventPublisher eventPublisher, ILogger<DatabaseLoggingWriter> logger) public DatabaseLoggingWriter(
IServiceScopeFactory serviceScopeFactory,
ILogger<DatabaseLoggingWriter> logger,
IEventPublisher eventPublisher)
{ {
_serviceScope = serviceScopeFactory.CreateScope(); _serviceScope = serviceScopeFactory.CreateScope();
_sysConfigService = _serviceScope.ServiceProvider.GetRequiredService<SysConfigService>(); _sysConfigService = _serviceScope.ServiceProvider.GetRequiredService<SysConfigService>();
@ -94,33 +99,24 @@ public class DatabaseLoggingWriter : IDatabaseLoggingWriter, IDisposable
return; return;
} }
var loggingMonitor = JSON.Deserialize<dynamic>(jsonStr);
// 获取当前操作者 // 获取当前操作者
string account = "", realName = "", userId = "", tenantId = ""; var loggingMonitor = JSON.Deserialize<LoggingMonitorDto>(jsonStr);
if (loggingMonitor.authorizationClaims != null) var userInfo = GetUserInfo(loggingMonitor);
{
var authDict = (loggingMonitor.authorizationClaims as IEnumerable<dynamic>)!.ToDictionary(u => u.type.ToString(), u => u.value.ToString());
account = authDict?.GetValueOrDefault(ClaimConst.Account);
realName = authDict?.GetValueOrDefault(ClaimConst.RealName);
tenantId = authDict?.GetValueOrDefault(ClaimConst.TenantId);
userId = authDict?.GetValueOrDefault(ClaimConst.UserId);
}
// 优先获取 X-Forwarded-For 头部信息携带的IP地址如nginx代理配置转发 // 优先获取 X-Forwarded-For 头部信息携带的IP地址如nginx代理配置转发
var remoteIPv4 = ((JArray)loggingMonitor.requestHeaders).OfType<JObject>() var reqHeaders = loggingMonitor.RequestHeaders.ToDictionary(u => u.Key, u => u.Value);
.FirstOrDefault(header => (string)header["key"] == "X-Forwarded-For")?["value"]?.ToString(); var remoteIPv4 = reqHeaders.GetValueOrDefault("X-Forwarded-For")?.ToString();
// 获取IP地理位置 // 获取IP地理位置
if (string.IsNullOrEmpty(remoteIPv4)) if (string.IsNullOrEmpty(remoteIPv4)) remoteIPv4 = loggingMonitor.RemoteIPv4;
remoteIPv4 = loggingMonitor.remoteIPv4;
(string ipLocation, double? longitude, double? latitude) = CommonHelper.GetIpAddress(remoteIPv4); (string ipLocation, double? longitude, double? latitude) = CommonHelper.GetIpAddress(remoteIPv4);
// 获取设备信息 // 获取设备信息
var browser = "";
var os = ""; var os = "";
if (loggingMonitor.userAgent != null) var browser = "";
if (loggingMonitor.UserAgent != null)
{ {
var client = Parser.GetDefault().Parse(loggingMonitor.userAgent.ToString()); var client = Parser.GetDefault().Parse(loggingMonitor.UserAgent);
browser = $"{client.UA.Family} {client.UA.Major}.{client.UA.Minor} / {client.Device.Family}"; browser = $"{client.UA.Family} {client.UA.Major}.{client.UA.Minor} / {client.Device.Family}";
os = $"{client.OS.Family} {client.OS.Major} {client.OS.Minor}"; os = $"{client.OS.Family} {client.OS.Major} {client.OS.Minor}";
} }
@ -128,120 +124,119 @@ public class DatabaseLoggingWriter : IDatabaseLoggingWriter, IDisposable
// 捕捉异常,否则会由于 unhandled exception 导致程序崩溃 // 捕捉异常,否则会由于 unhandled exception 导致程序崩溃
try try
{ {
// 记录异常日志-发送邮件 var logEntity = new SysLogOp
if (logMsg.Exception != null || loggingMonitor.exception != null)
{ {
await _db.Insertable(new SysLogEx ControllerName = loggingMonitor.DisplayName,
{ ActionName = loggingMonitor.ActionTypeName,
ControllerName = loggingMonitor.controllerName, DisplayTitle = loggingMonitor.DisplayTitle,
ActionName = loggingMonitor.actionTypeName, Status = loggingMonitor.ReturnInformation?.HttpStatusCode?.ToString(),
DisplayTitle = loggingMonitor.displayTitle,
Status = loggingMonitor.returnInformation?.httpStatusCode,
RemoteIp = remoteIPv4, RemoteIp = remoteIPv4,
Location = ipLocation, Location = ipLocation,
Longitude = longitude, Longitude = longitude,
Latitude = latitude, Latitude = latitude,
Browser = browser, // loggingMonitor.userAgent, Browser = browser,
Os = os, // loggingMonitor.osDescription + " " + loggingMonitor.osArchitecture, Os = os,
Elapsed = loggingMonitor.timeOperationElapsedMilliseconds, Elapsed = loggingMonitor.TimeOperationElapsedMilliseconds,
Message = logMsg.Message,
HttpMethod = loggingMonitor.HttpMethod,
RequestUrl = loggingMonitor.RequestUrl,
RequestParam = loggingMonitor.Parameters is { Count: > 0 } ? JSON.Serialize(loggingMonitor.Parameters[0].Value) : null,
ReturnResult = loggingMonitor.ReturnInformation?.Value != null ? JSON.Serialize(loggingMonitor.ReturnInformation?.Value) : null,
Exception = loggingMonitor.Exception == null ? JSON.Serialize(logMsg.Exception) : JSON.Serialize(loggingMonitor.Exception),
LogDateTime = logMsg.LogDateTime, LogDateTime = logMsg.LogDateTime,
Account = account,
RealName = realName,
HttpMethod = loggingMonitor.httpMethod,
RequestUrl = loggingMonitor.requestUrl,
RequestParam = (loggingMonitor.parameters == null || loggingMonitor.parameters.Count == 0) ? null : JSON.Serialize(loggingMonitor.parameters[0].value),
ReturnResult = loggingMonitor.returnInformation == null ? null : JSON.Serialize(loggingMonitor.returnInformation),
EventId = logMsg.EventId.Id, EventId = logMsg.EventId.Id,
ThreadId = logMsg.ThreadId, ThreadId = logMsg.ThreadId,
TraceId = logMsg.TraceId, TraceId = logMsg.TraceId,
Exception = JSON.Serialize(loggingMonitor.exception), Account = userInfo.Account,
Message = logMsg.Message, RealName = userInfo.RealName,
CreateUserId = string.IsNullOrWhiteSpace(userId) ? 0 : long.Parse(userId), CreateUserId = userInfo.UserId,
TenantId = string.IsNullOrWhiteSpace(tenantId) ? 0 : long.Parse(tenantId), CreateUserName = userInfo.RealName,
LogLevel = logMsg.LogLevel TenantId = userInfo.TenantId,
}).ExecuteCommandAsync(); LogLevel = logMsg.LogLevel,
};
// 记录异常日志-发送邮件
if (logMsg.Exception != null || loggingMonitor.Exception != null)
{
await _db.Insertable(logEntity.Adapt<SysLogEx>()).ExecuteCommandAsync();
// 将异常日志发送到邮件 // 将异常日志发送到邮件
await _eventPublisher.PublishAsync(CommonConst.SendErrorMail, logMsg.Exception ?? loggingMonitor.exception); await _eventPublisher.PublishAsync(CommonConst.SendErrorMail, logMsg.Exception ?? loggingMonitor.Exception);
return; return;
} }
// 记录访问日志-登录退出 // 记录访问日志-登录退出
if (loggingMonitor.actionName == "login" || loggingMonitor.actionName == "loginPhone" || loggingMonitor.actionName == "logout") if (loggingMonitor.ActionName == "login" || loggingMonitor.ActionName == "loginPhone" || loggingMonitor.ActionName == "logout")
{ {
if (loggingMonitor.actionName != "logout") await _db.Insertable(logEntity.Adapt<SysLogVis>()).ExecuteCommandAsync();
{
dynamic para = Clay.Parse((loggingMonitor.parameters == null) ? null : JSON.Serialize(loggingMonitor.parameters[0].value));
if (loggingMonitor.actionName == "login")
account = para.account;
else if (loggingMonitor.actionName == "loginPhone")
account = para.phone;
}
await _db.Insertable(new SysLogVis
{
ControllerName = loggingMonitor.displayName,
ActionName = loggingMonitor.actionTypeName,
DisplayTitle = loggingMonitor.displayTitle,
Status = loggingMonitor.returnInformation?.httpStatusCode,
RemoteIp = remoteIPv4,
Location = ipLocation,
Longitude = longitude,
Latitude = latitude,
Browser = browser, // loggingMonitor.userAgent,
Os = os, // loggingMonitor.osDescription + " " + loggingMonitor.osArchitecture,
Elapsed = loggingMonitor.timeOperationElapsedMilliseconds,
LogDateTime = logMsg.LogDateTime,
Account = account,
RealName = realName,
CreateUserId = string.IsNullOrWhiteSpace(userId) ? 0 : long.Parse(userId),
TenantId = string.IsNullOrWhiteSpace(tenantId) ? 0 : long.Parse(tenantId),
LogLevel = logMsg.LogLevel
}).ExecuteCommandAsync();
return; return;
} }
// 记录操作日志 // 记录操作日志
if (!await _sysConfigService.GetConfigValueByCode<bool>(ConfigConst.SysOpLog)) return; if (!await _sysConfigService.GetConfigValueByCode<bool>(ConfigConst.SysOpLog)) return;
await _db.Insertable(new SysLogOp await _db.Insertable(logEntity.Adapt<SysLogOp>()).ExecuteCommandAsync();
{
ControllerName = loggingMonitor.controllerName,
ActionName = loggingMonitor.actionTypeName,
DisplayTitle = loggingMonitor.displayTitle,
Status = loggingMonitor.returnInformation?.httpStatusCode,
RemoteIp = remoteIPv4,
Location = ipLocation,
Longitude = longitude,
Latitude = latitude,
Browser = browser, // loggingMonitor.userAgent,
Os = os, // loggingMonitor.osDescription + " " + loggingMonitor.osArchitecture,
Elapsed = loggingMonitor.timeOperationElapsedMilliseconds,
LogDateTime = logMsg.LogDateTime,
Account = account,
RealName = realName,
HttpMethod = loggingMonitor.httpMethod,
RequestUrl = loggingMonitor.requestUrl,
RequestParam = (loggingMonitor.parameters == null || loggingMonitor.parameters.Count == 0) ? null : JSON.Serialize(loggingMonitor.parameters[0].value),
ReturnResult = loggingMonitor.returnInformation == null ? null : JSON.Serialize(loggingMonitor.returnInformation),
EventId = logMsg.EventId.Id,
ThreadId = logMsg.ThreadId,
TraceId = logMsg.TraceId,
Exception = loggingMonitor.exception == null ? null : JSON.Serialize(loggingMonitor.exception),
Message = logMsg.Message,
CreateUserId = string.IsNullOrWhiteSpace(userId) ? 0 : long.Parse(userId),
TenantId = string.IsNullOrWhiteSpace(tenantId) ? 0 : long.Parse(tenantId),
LogLevel = logMsg.LogLevel
}).ExecuteCommandAsync();
await Task.Delay(50); // 延迟 0.05 秒写入数据库,有效减少高频写入数据库导致死锁问题 await Task.Delay(50); // 延迟 0.05 秒写入数据库,有效减少高频写入数据库导致死锁问题
} }
catch (Exception ex) catch (Exception ex)
{ {
_logger.LogError(ex, "操作日志入库"); _logger.LogError(ex, "操作日志入库");
// 将异常日志发送到邮件
await _eventPublisher.PublishAsync(CommonConst.SendErrorMail, ex);
} }
} }
/// <summary>
/// 从日志消息中获取用户信息
/// </summary>
/// <param name="loggingMonitor"></param>
/// <returns></returns>
private LoggingUserInfo GetUserInfo(LoggingMonitorDto loggingMonitor)
{
LoggingUserInfo result = new();
if (loggingMonitor.AuthorizationClaims != null)
{
result.UserId = long.TryParse(loggingMonitor.AuthorizationClaims
.ToDictionary(u => u.Type, u => u.Value)
.GetValueOrDefault(ClaimConst.UserId) ?? ""
, out var temp) ? temp : null;
var userSession = _userManager.Value.GetSession(result.UserId);
result.TenantId = long.TryParse(userSession?.TenantId?.ToString() ?? "", out temp) ? temp : null;
result.RealName = userSession?.RealName;
result.Account = userSession?.Account;
}
// 退出登陆时没有session尝试从数据库中获取
if (string.IsNullOrWhiteSpace(result.Account) && result.UserId != null)
{
var user = _sysUserRep.Value.GetById(result.UserId);
result.Account = user?.TenantId?.ToString();
result.RealName = user?.RealName;
result.Account = user?.Account;
}
// 用户登陆时没有userId需要根据入参获取
if (loggingMonitor.ActionName == "login" && loggingMonitor.Parameters is { Count: > 0 })
{
try
{
result.Account = (loggingMonitor.Parameters[0].Value as JObject)!.GetValue("account")?.ToString();
if (!string.IsNullOrEmpty(result.Account))
{
var user = _sysUserRep.Value.AsQueryable().First(u => u.Account == result.Account);
result.TenantId = user?.TenantId;
result.RealName = user?.RealName;
result.UserId = user?.Id;
}
}
catch
{
// ignored
}
}
return result;
}
/// <summary> /// <summary>
/// 释放服务作用域 /// 释放服务作用域
/// </summary> /// </summary>

View File

@ -15,12 +15,15 @@ public class ElasticSearchLoggingWriter : IDatabaseLoggingWriter, IDisposable
{ {
private readonly IServiceScope _serviceScope; private readonly IServiceScope _serviceScope;
private readonly ElasticsearchClient _esClient; private readonly ElasticsearchClient _esClient;
private readonly SysCacheService _sysCacheService;
private readonly SysConfigService _sysConfigService; private readonly SysConfigService _sysConfigService;
private static readonly Lazy<UserManager> _userManager = new(() => App.GetService<UserManager>());
public ElasticSearchLoggingWriter(IServiceScopeFactory scopeFactory) public ElasticSearchLoggingWriter(IServiceScopeFactory scopeFactory)
{ {
_serviceScope = scopeFactory.CreateScope(); _serviceScope = scopeFactory.CreateScope();
_esClient = _serviceScope.ServiceProvider.GetRequiredService<ElasticsearchClient>(); _esClient = _serviceScope.ServiceProvider.GetRequiredService<ElasticsearchClient>();
_sysCacheService = _serviceScope.ServiceProvider.GetRequiredService<SysCacheService>();
_sysConfigService = _serviceScope.ServiceProvider.GetRequiredService<SysConfigService>(); _sysConfigService = _serviceScope.ServiceProvider.GetRequiredService<SysConfigService>();
} }
@ -43,17 +46,17 @@ public class ElasticSearchLoggingWriter : IDatabaseLoggingWriter, IDisposable
string account = "", realName = "", userId = "", tenantId = ""; string account = "", realName = "", userId = "", tenantId = "";
if (loggingMonitor.authorizationClaims != null) if (loggingMonitor.authorizationClaims != null)
{ {
UserSessionDao userSession = null;
foreach (var item in loggingMonitor.authorizationClaims) foreach (var item in loggingMonitor.authorizationClaims)
{ {
if (item.type == ClaimConst.Account) if (item.type != ClaimConst.UserId) continue;
account = item.value;
if (item.type == ClaimConst.RealName)
realName = item.value;
if (item.type == ClaimConst.TenantId)
tenantId = item.value;
if (item.type == ClaimConst.UserId)
userId = item.value; userId = item.value;
userSession = _userManager.Value?.GetSession(userId);
break;
} }
tenantId = userSession?.TenantId.ToString();
realName = userSession?.RealName;
account = userSession?.Account;
} }
string remoteIPv4 = loggingMonitor.remoteIPv4; string remoteIPv4 = loggingMonitor.remoteIPv4;

View File

@ -0,0 +1,309 @@
// Admin.NET 项目的版权、商标、专利和其他相关权利均受相应法律法规的保护。使用本项目应遵守相关法律法规和许可证的要求。
//
// 本项目主要遵循 MIT 许可证和 Apache 许可证(版本 2.0)进行分发和使用。许可证位于源代码树根目录中的 LICENSE-MIT 和 LICENSE-APACHE 文件。
//
// 不得利用本项目从事危害国家安全、扰乱社会秩序、侵犯他人合法权益等法律法规禁止的活动!任何基于本项目二次开发而产生的一切法律纠纷和责任,我们不承担任何责任!
namespace Admin.NET.Core;
/// <summary>
/// 日志监控信息输出参数
/// </summary>
public class LoggingMonitorDto
{
/// <summary>
/// 标题
/// </summary>
public string Title { get; set; }
/// <summary>
/// 控制器名称
/// </summary>
public string ControllerName { get; set; }
/// <summary>
/// 控制器类型名称
/// </summary>
public string ControllerTypeName { get; set; }
/// <summary>
/// 操作方法名称
/// </summary>
public string ActionName { get; set; }
/// <summary>
/// 操作方法类型名称
/// </summary>
public string ActionTypeName { get; set; }
/// <summary>
/// 区域名称Area
/// </summary>
public string AreaName { get; set; }
/// <summary>
/// 显示名称(全路径)
/// </summary>
public string DisplayName { get; set; }
/// <summary>
/// 显示标题
/// </summary>
public string DisplayTitle { get; set; }
/// <summary>
/// 本地IPv4地址
/// </summary>
public string LocalIPv4 { get; set; }
/// <summary>
/// 本地端口
/// </summary>
public int? LocalPort { get; set; }
/// <summary>
/// 远程IPv4地址
/// </summary>
public string RemoteIPv4 { get; set; }
/// <summary>
/// 远程端口
/// </summary>
public int? RemotePort { get; set; }
/// <summary>
/// HTTP请求方法如GET、POST
/// </summary>
public string HttpMethod { get; set; }
/// <summary>
/// 分布式追踪IDTraceId
/// </summary>
public string TraceId { get; set; }
/// <summary>
/// 线程ID
/// </summary>
public int? ThreadId { get; set; }
/// <summary>
/// 请求URL
/// </summary>
public string RequestUrl { get; set; }
/// <summary>
/// 协议版本如HTTP/1.1
/// </summary>
public string Protocol { get; set; }
/// <summary>
/// 引用页面URLReferer
/// </summary>
public string RefererUrl { get; set; }
/// <summary>
/// 用户代理User-Agent
/// </summary>
public string UserAgent { get; set; }
/// <summary>
/// 接受的语言Accept-Language
/// </summary>
public string AcceptLanguage { get; set; }
/// <summary>
/// 请求来源client、server等
/// </summary>
public string RequestFrom { get; set; }
/// <summary>
/// 请求头中的Cookies
/// </summary>
public string RequestHeaderCookies { get; set; }
/// <summary>
/// 操作耗时(毫秒)
/// </summary>
public long? TimeOperationElapsedMilliseconds { get; set; }
/// <summary>
/// 访问令牌AccessToken
/// </summary>
public string AccessToken { get; set; }
/// <summary>
/// 响应头中的Cookies
/// </summary>
public string ResponseHeaderCookies { get; set; }
/// <summary>
/// 操作系统描述
/// </summary>
public string OsDescription { get; set; }
/// <summary>
/// 操作系统架构如X64
/// </summary>
public string OsArchitecture { get; set; }
/// <summary>
/// 框架描述(如.NET 8.0.18
/// </summary>
public string FrameworkDescription { get; set; }
/// <summary>
/// 基础框架名称如Furion.Pure
/// </summary>
public string BasicFramework { get; set; }
/// <summary>
/// 基础框架版本
/// </summary>
public string BasicFrameworkVersion { get; set; }
/// <summary>
/// 入口程序集名称
/// </summary>
public string EntryAssemblyName { get; set; }
/// <summary>
/// 进程名称
/// </summary>
public string ProcessName { get; set; }
/// <summary>
/// 部署服务器如Kestrel
/// </summary>
public string DeployServer { get; set; }
/// <summary>
/// 启动监听地址
/// </summary>
public string StartUrls { get; set; }
/// <summary>
/// 环境如Development、Production
/// </summary>
public string Environment { get; set; }
/// <summary>
/// 授权声明集合
/// </summary>
public List<LoggingAuthorizationClaimsDto> AuthorizationClaims { get; set; }
/// <summary>
/// 请求头集合
/// </summary>
public List<KeyValuePair<string, object>> RequestHeaders { get; set; }
/// <summary>
/// 请求参数集合
/// </summary>
public List<LoggingParametersDto> Parameters { get; set; }
/// <summary>
/// 返回信息
/// </summary>
public LoggingReturnInformationDto ReturnInformation { get; set; }
/// <summary>
/// 异常信息
/// </summary>
public object Exception { get; set; }
/// <summary>
/// 验证信息
/// </summary>
public object Validation { get; set; }
}
public class LoggingAuthorizationClaimsDto
{
/// <summary>
/// 类型名
/// </summary>
public string Type { get; set; }
/// <summary>
/// 值类型
/// </summary>
public string ValueType { get; set; }
/// <summary>
/// 值
/// </summary>
public string Value { get; set; }
}
/// <summary>
/// 输入参数
/// </summary>
public class LoggingParametersDto
{
/// <summary>
/// 输入类型
/// </summary>
public string Name { get; set; }
/// <summary>
/// 输入类型
/// </summary>
public string Type { get; set; }
/// <summary>
/// 实际输入数据
/// </summary>
public object Value { get; set; }
}
/// <summary>
/// 返回信息详情
/// </summary>
public class LoggingReturnInformationDto
{
/// <summary>
/// 返回类型如X.Core.XResult<System.Object>
/// </summary>
public string Type { get; set; }
/// <summary>
/// HTTP状态码
/// </summary>
public int? HttpStatusCode { get; set; }
/// <summary>
/// 实际返回类型如Task<List<T>>
/// </summary>
public string ActType { get; set; }
/// <summary>
/// 实际返回数据
/// </summary>
public object Value { get; set; }
}
/// <summary>
/// 用户信息
/// </summary>
public class LoggingUserInfo
{
/// <summary>
/// 用户Id
/// </summary>
public long? UserId { get; set; }
/// <summary>
/// 账号
/// </summary>
public string Account { get; set; }
/// <summary>
/// 真实姓名
/// </summary>
public string RealName { get; set; }
/// <summary>
/// 租户Id
/// </summary>
public long? TenantId { get; set; }
}

View File

@ -233,17 +233,24 @@ public class SysAuthService : IDynamicApiController, ITransient
var accessToken = JWTEncryption.Encrypt(new Dictionary<string, object> var accessToken = JWTEncryption.Encrypt(new Dictionary<string, object>
{ {
{ ClaimConst.UserId, user.Id }, { ClaimConst.UserId, user.Id },
{ ClaimConst.TenantId, user.TenantId },
{ ClaimConst.Account, user.Account },
{ ClaimConst.RealName, user.RealName },
{ ClaimConst.AccountType, user.AccountType },
{ ClaimConst.OrgId, user.OrgId },
{ ClaimConst.OrgName, user.SysOrg?.Name },
{ ClaimConst.OrgType, user.SysOrg?.Type },
{ ClaimConst.OrgLevel, user.SysOrg?.Level },
{ ClaimConst.TokenVersion, user.TokenVersion }, { ClaimConst.TokenVersion, user.TokenVersion },
}, tokenExpire); }, tokenExpire);
// 缓存用户Session
_userManager.SetSession(new()
{
UserId = user.Id,
TenantId = user.TenantId,
Account = user.Account,
RealName = user.RealName,
AccountType = user.AccountType,
OrgId = user.OrgId,
OrgName = user.SysOrg?.Name,
OrgType = user.SysOrg?.Type,
OrgLevel = user.SysOrg?.Level,
TokenVersion = user.TokenVersion,
}, TimeSpan.FromMinutes(tokenExpire));
// 生成刷新Token令牌 // 生成刷新Token令牌
var refreshTokenExpire = await _sysConfigService.GetRefreshTokenExpire(); var refreshTokenExpire = await _sysConfigService.GetRefreshTokenExpire();
var refreshToken = JWTEncryption.GenerateRefreshToken(accessToken, refreshTokenExpire); var refreshToken = JWTEncryption.GenerateRefreshToken(accessToken, refreshTokenExpire);
@ -281,10 +288,10 @@ public class SysAuthService : IDynamicApiController, ITransient
} }
/// <summary> /// <summary>
/// 获取登录账号 🔖 /// 获取当前登陆用户信息 🔖
/// </summary> /// </summary>
/// <returns></returns> /// <returns></returns>
[DisplayName("获取登录账号")] [DisplayName("获取当前登陆用户信息")]
public virtual async Task<LoginUserOutput> GetUserInfo() public virtual async Task<LoginUserOutput> GetUserInfo()
{ {
var user = await _sysUserRep.GetByIdAsync(_userManager.UserId) ?? throw Oops.Oh(ErrorCodeEnum.D1011).StatusCode(401); var user = await _sysUserRep.GetByIdAsync(_userManager.UserId) ?? throw Oops.Oh(ErrorCodeEnum.D1011).StatusCode(401);
@ -348,16 +355,18 @@ public class SysAuthService : IDynamicApiController, ITransient
{ {
var httpContext = _httpContextAccessor.HttpContext ?? throw Oops.Oh(ErrorCodeEnum.D1016); var httpContext = _httpContextAccessor.HttpContext ?? throw Oops.Oh(ErrorCodeEnum.D1016);
var accessToken = httpContext.Request.Headers.Authorization.ToString(); var userId = httpContext.User.FindFirst(ClaimConst.UserId)?.Value;
if (string.IsNullOrWhiteSpace(accessToken) || string.IsNullOrWhiteSpace(_userManager.Account)) var version = httpContext.User.FindFirst(ClaimConst.TokenVersion)?.Value;
if (string.IsNullOrWhiteSpace(userId) || string.IsNullOrWhiteSpace(version) || string.IsNullOrWhiteSpace(_userManager.Account))
throw Oops.Oh(ErrorCodeEnum.D1011); throw Oops.Oh(ErrorCodeEnum.D1011);
// 写入Token黑名单 // 写入Token黑名单
var tokenExpire = await _sysConfigService.GetTokenExpire(); var tokenExpire = await _sysConfigService.GetTokenExpire();
_sysCacheService.Set($"{CacheConst.KeyTokenBlacklist}:{accessToken}", _userManager.Account, TimeSpan.FromMinutes(tokenExpire)); _sysCacheService.Set($"{CacheConst.KeyTokenBlacklist}:{userId}:{version}", _userManager.Account, TimeSpan.FromMinutes(tokenExpire));
// 发布系统退出事件 // 发布系统退出事件
await _eventPublisher.PublishAsync(UserEventTypeEnum.Logout, _userManager); await _eventPublisher.PublishAsync(UserEventTypeEnum.Logout, _userManager);
_userManager.RemoveSession(_userManager.UserId);
// 退出Swagger/设置无效Token响应头 // 退出Swagger/设置无效Token响应头
_httpContextAccessor.HttpContext.SignoutToSwagger(); _httpContextAccessor.HttpContext.SignoutToSwagger();

View File

@ -12,10 +12,13 @@ namespace Admin.NET.Core.Service;
[ApiDescriptionSettings(Order = 400, Description = "系统缓存")] [ApiDescriptionSettings(Order = 400, Description = "系统缓存")]
public class SysCacheService : IDynamicApiController, ISingleton public class SysCacheService : IDynamicApiController, ISingleton
{ {
private readonly Lazy<UserManager> _userManager = new(() => App.GetService<UserManager>());
private static ICacheProvider _cacheProvider; private static ICacheProvider _cacheProvider;
private readonly CacheOptions _cacheOptions; private readonly CacheOptions _cacheOptions;
public SysCacheService(ICacheProvider cacheProvider, IOptions<CacheOptions> cacheOptions) public SysCacheService(
ICacheProvider cacheProvider,
IOptions<CacheOptions> cacheOptions)
{ {
_cacheProvider = cacheProvider; _cacheProvider = cacheProvider;
_cacheOptions = cacheOptions.Value; _cacheOptions = cacheOptions.Value;
@ -427,10 +430,20 @@ public class SysCacheService : IDynamicApiController, ISingleton
[DisplayName("清空所有缓存")] [DisplayName("清空所有缓存")]
[ApiDescriptionSettings(Name = "Clear"), HttpPost] [ApiDescriptionSettings(Name = "Clear"), HttpPost]
public void Clear() public void Clear()
{
// 超管用户操作,清空所有缓存
if (_userManager.Value.SuperAdmin)
{ {
_cacheProvider.Cache.Clear(); _cacheProvider.Cache.Clear();
Cache.Default.Clear(); Cache.Default.Clear();
return;
}
// 排除非本租户、以及超管的Session缓存
var sysUserRep = App.GetService<SqlSugarRepository<SysUser>>();
var userIds = sysUserRep.AsQueryable().Where(u => u.AccountType != AccountTypeEnum.SuperAdmin).Select(u => u.Id).ToList().Select(u => u.ToString()).ToList();
var keys = _cacheProvider.Cache.Keys.Where(key => !key.StartsWith(CacheConst.KeyUserSession) || userIds.Any(key.EndsWith)).ToList();
keys.ForEach(key => _cacheProvider.Cache.Remove(key));
} }
/// <summary> /// <summary>

View File

@ -17,15 +17,22 @@ public class SysOpenAccessService : IDynamicApiController, ITransient
{ {
private readonly SqlSugarRepository<SysOpenAccess> _sysOpenAccessRep; private readonly SqlSugarRepository<SysOpenAccess> _sysOpenAccessRep;
private readonly SysCacheService _sysCacheService; private readonly SysCacheService _sysCacheService;
private readonly SysAuthService _sysAuthService;
private readonly UserManager _userManager;
/// <summary> /// <summary>
/// 开放接口身份服务构造函数 /// 开放接口身份服务构造函数
/// </summary> /// </summary>
public SysOpenAccessService(SqlSugarRepository<SysOpenAccess> sysOpenAccessRep, public SysOpenAccessService(
SysCacheService sysCacheService) SqlSugarRepository<SysOpenAccess> sysOpenAccessRep,
SysCacheService sysCacheService,
SysAuthService sysAuthService,
UserManager userManager)
{ {
_sysOpenAccessRep = sysOpenAccessRep; _sysOpenAccessRep = sysOpenAccessRep;
_sysCacheService = sysCacheService; _sysCacheService = sysCacheService;
_sysAuthService = sysAuthService;
_userManager = userManager;
} }
/// <summary> /// <summary>
@ -156,6 +163,17 @@ public class SysOpenAccessService : IDynamicApiController, ITransient
); );
} }
/// <summary>
/// 根据 Key 获取对象
/// </summary>
/// <param name="openAccess"></param>
/// <returns></returns>
[NonAction]
private async Task TryCreateSession(SysOpenAccess openAccess)
{
if (!_userManager.ExistSession(openAccess.Id)) await _sysAuthService.CreateToken(openAccess.BindUser);
}
/// <summary> /// <summary>
/// Signature 身份验证事件默认实现 /// Signature 身份验证事件默认实现
/// </summary> /// </summary>
@ -188,15 +206,9 @@ public class SysOpenAccessService : IDynamicApiController, ITransient
identity.AddClaims( identity.AddClaims(
[ [
new Claim(ClaimConst.UserId, openAccess.BindUserId + ""), new Claim(ClaimConst.UserId, openAccess.BindUserId + ""),
new Claim(ClaimConst.TenantId, openAccess.BindTenantId + ""),
new Claim(ClaimConst.Account, openAccess.BindUser.Account + ""),
new Claim(ClaimConst.RealName, openAccess.BindUser.RealName),
new Claim(ClaimConst.AccountType, ((int)openAccess.BindUser.AccountType).ToString()),
new Claim(ClaimConst.OrgId, openAccess.BindUser.OrgId + ""),
new Claim(ClaimConst.OrgName, openAccess.BindUser.SysOrg?.Name + ""),
new Claim(ClaimConst.OrgType, openAccess.BindUser.SysOrg?.Type + ""),
new Claim(ClaimConst.TokenVersion, openAccess.BindUser.TokenVersion + ""), new Claim(ClaimConst.TokenVersion, openAccess.BindUser.TokenVersion + ""),
]); ]);
openAccessService.TryCreateSession(openAccess).GetAwaiter().GetResult();
return Task.CompletedTask; return Task.CompletedTask;
} }
}; };

View File

@ -172,7 +172,7 @@ public class SysSerialService : IDynamicApiController, ITransient
public async Task<string> NextSeqNo(SerialTypeEnum? type, bool isGlobal = false, bool isTran = true) public async Task<string> NextSeqNo(SerialTypeEnum? type, bool isGlobal = false, bool isTran = true)
{ {
// 获取租户Id, 以及分布式锁缓存键名 // 获取租户Id, 以及分布式锁缓存键名
long tenantId = isGlobal ? SqlSugarConst.DefaultTenantId : _userManager.TenantId; long? tenantId = isGlobal ? SqlSugarConst.DefaultTenantId : _userManager.TenantId;
string cacheKey = $"{CacheConst.KeySerialLock}:{tenantId}:{type}"; string cacheKey = $"{CacheConst.KeySerialLock}:{tenantId}:{type}";
// 获取分布式锁 // 获取分布式锁

View File

@ -27,6 +27,7 @@ public class SysTenantService : IDynamicApiController, ITransient
private readonly SysConfigService _sysConfigService; private readonly SysConfigService _sysConfigService;
private readonly SysCacheService _sysCacheService; private readonly SysCacheService _sysCacheService;
private readonly IEventPublisher _eventPublisher; private readonly IEventPublisher _eventPublisher;
private readonly UserManager _userManager;
public SysTenantService(SqlSugarRepository<SysTenant> sysTenantRep, public SysTenantService(SqlSugarRepository<SysTenant> sysTenantRep,
SqlSugarRepository<SysOrg> sysOrgRep, SqlSugarRepository<SysOrg> sysOrgRep,
@ -560,7 +561,7 @@ public class SysTenantService : IDynamicApiController, ITransient
//// 还可以根据域名判断租户 //// 还可以根据域名判断租户
//var host = App.HttpContext.Request.Host.ToString(); //var host = App.HttpContext.Request.Host.ToString();
if (tenantId < 1) tenantId = long.Parse(App.User?.FindFirst(ClaimConst.TenantId)?.Value ?? "0"); if (tenantId < 1) tenantId = _userManager?.TenantId ?? 0;
if (tenantId < 1) tenantId = SqlSugarConst.DefaultTenantId; if (tenantId < 1) tenantId = SqlSugarConst.DefaultTenantId;
var tenant = await _sysTenantRep.GetFirstAsync(u => u.Id == tenantId) ?? throw Oops.Oh($"租户信息不存在:{tenantId}"); var tenant = await _sysTenantRep.GetFirstAsync(u => u.Id == tenantId) ?? throw Oops.Oh($"租户信息不存在:{tenantId}");
@ -613,7 +614,7 @@ public class SysTenantService : IDynamicApiController, ITransient
[DisplayName("保存系统信息")] [DisplayName("保存系统信息")]
public async Task SaveSysInfo(SysInfoInput input) public async Task SaveSysInfo(SysInfoInput input)
{ {
input.TenantId = input.TenantId < 1 ? long.Parse(App.User?.FindFirst(ClaimConst.TenantId)?.Value ?? "0") : input.TenantId; input.TenantId = input.TenantId < 1 ? (_userManager.TenantId ?? 0) : input.TenantId;
var tenant = await _sysTenantRep.GetFirstAsync(u => u.Id == input.TenantId) ?? throw Oops.Oh(ErrorCodeEnum.D1002); var tenant = await _sysTenantRep.GetFirstAsync(u => u.Id == input.TenantId) ?? throw Oops.Oh(ErrorCodeEnum.D1002);
tenant = input.Adapt<SysTenant>(); tenant = input.Adapt<SysTenant>();
tenant.Id = input.TenantId; tenant.Id = input.TenantId;

View File

@ -9,92 +9,120 @@ namespace Admin.NET.Core;
/// <summary> /// <summary>
/// 当前登录用户信息 /// 当前登录用户信息
/// </summary> /// </summary>
public class UserManager : IScoped public class UserManager (
SysCacheService sysCacheService,
IHttpContextAccessor httpContextAccessor) : UserSessionDao, IScoped
{ {
private readonly IHttpContextAccessor _httpContextAccessor; /// <summary>
/// 用户Session
/// </summary>
[System.Text.Json.Serialization.JsonIgnore]
[Newtonsoft.Json.JsonIgnore]
private UserSessionDao Session { get; set; }
/// <summary> /// <summary>
/// 应用Id /// 代理对象
/// </summary> /// </summary>
public long AppId => (_httpContextAccessor.HttpContext?.User.FindFirst(ClaimConst.AppId)?.Value).ToLong(); [System.Text.Json.Serialization.JsonIgnore]
[Newtonsoft.Json.JsonIgnore]
/// <summary> protected UserSessionDao session => Session ??= sysCacheService.Get<UserSessionDao>(CacheConst.KeyUserSession + UserId);
/// 租户Id
/// </summary>
public long TenantId => (_httpContextAccessor.HttpContext?.User.FindFirst(ClaimConst.TenantId)?.Value).ToLong();
/// <summary> /// <summary>
/// 用户Id /// 用户Id
/// </summary> /// </summary>
public long UserId => (_httpContextAccessor.HttpContext?.User.FindFirst(ClaimConst.UserId)?.Value).ToLong(); [System.Text.Json.Serialization.JsonIgnore]
[Newtonsoft.Json.JsonIgnore]
public override long UserId => (httpContextAccessor.HttpContext?.User.FindFirst(nameof(UserId))?.Value).ToLong();
/// <summary>
/// 应用Id
/// </summary>
public override long? AppId => session?.AppId;
/// <summary>
/// 租户Id
/// </summary>
public override long? TenantId => session?.TenantId;
/// <summary> /// <summary>
/// 用户账号 /// 用户账号
/// </summary> /// </summary>
public string Account => _httpContextAccessor.HttpContext?.User.FindFirst(ClaimConst.Account)?.Value; public override string Account => session?.Account;
/// <summary> /// <summary>
/// 真实姓名 /// 真实姓名
/// </summary> /// </summary>
public string RealName => _httpContextAccessor.HttpContext?.User.FindFirst(ClaimConst.RealName)?.Value; public override string RealName => session?.RealName;
/// <summary> /// <summary>
/// 昵称 /// 昵称
/// </summary> /// </summary>
public string NickName => _httpContextAccessor.HttpContext?.User.FindFirst(ClaimConst.NickName)?.Value; public override string NickName => session?.NickName;
/// <summary> /// <summary>
/// 账号类型 /// 账号类型
/// </summary> /// </summary>
public AccountTypeEnum? AccountType => int.TryParse(_httpContextAccessor.HttpContext?.User.FindFirst(ClaimConst.AccountType)?.Value, out var val) ? (AccountTypeEnum?)val : null; public override AccountTypeEnum? AccountType => session?.AccountType;
/// <summary>
/// 是否超级管理员
/// </summary>
public bool SuperAdmin => _httpContextAccessor.HttpContext?.User.FindFirst(ClaimConst.AccountType)?.Value == ((int)AccountTypeEnum.SuperAdmin).ToString();
/// <summary>
/// 是否系统管理员
/// </summary>
public bool SysAdmin => _httpContextAccessor.HttpContext?.User.FindFirst(ClaimConst.AccountType)?.Value == ((int)AccountTypeEnum.SysAdmin).ToString();
/// <summary> /// <summary>
/// 组织机构Id /// 组织机构Id
/// </summary> /// </summary>
public long OrgId => (_httpContextAccessor.HttpContext?.User.FindFirst(ClaimConst.OrgId)?.Value).ToLong(); public override long OrgId => session?.OrgId ?? 0;
/// <summary> /// <summary>
/// 组织机构名称 /// 组织机构名称
/// </summary> /// </summary>
public string OrgName => (_httpContextAccessor.HttpContext?.User.FindFirst(ClaimConst.OrgName)?.Value); public override string OrgName => session?.OrgName;
/// <summary> /// <summary>
/// 组织机构Id /// 组织机构Id
/// </summary> /// </summary>
public string OrgType => (_httpContextAccessor.HttpContext?.User.FindFirst(ClaimConst.OrgType)?.Value); public override string OrgType => session?.OrgType;
/// <summary> /// <summary>
/// 组织机构级别 /// 组织机构级别
/// </summary> /// </summary>
public int OrgLevel => (_httpContextAccessor.HttpContext?.User.FindFirst(ClaimConst.OrgLevel)?.Value).ToInt(); public override int? OrgLevel => session?.OrgLevel;
/// <summary> /// <summary>
/// 登录模式 /// 登录模式
/// </summary> /// </summary>
public int LoginMode => (_httpContextAccessor.HttpContext?.User.FindFirst(ClaimConst.LoginMode)?.Value).ToInt(); public override LoginModeEnum? LoginMode => session?.LoginMode;
/// <summary>
/// Token版本号
/// </summary>
public int TokenVersion => (_httpContextAccessor.HttpContext?.User.FindFirst(ClaimConst.TokenVersion)?.Value).ToInt();
/// <summary> /// <summary>
/// 微信OpenId /// 微信OpenId
/// </summary> /// </summary>
public string OpenId => _httpContextAccessor.HttpContext?.User.FindFirst(ClaimConst.OpenId)?.Value; public override string OpenId => session?.OpenId;
public UserManager(IHttpContextAccessor httpContextAccessor) /// <summary>
/// 用户Session是否存在
/// </summary>
public bool ExistSession(long userId)
{ {
_httpContextAccessor = httpContextAccessor; return sysCacheService.ExistKey(CacheConst.KeyUserSession + userId);
}
/// <summary>
/// 设置用户Session
/// </summary>
public void SetSession(UserSessionDao userSession, TimeSpan expire)
{
sysCacheService.Set(CacheConst.KeyUserSession + userSession.UserId, userSession, expire);
}
/// <summary>
/// 清除指定用户Session
/// </summary>
public void RemoveSession(long userId)
{
sysCacheService.Remove(CacheConst.KeyUserSession + userId);
}
/// <summary>
/// 获取指定用户Session
/// </summary>
public UserSessionDao GetSession(dynamic userId)
{
return sysCacheService.Get<UserSessionDao>(CacheConst.KeyUserSession + userId);
} }
} }

View File

@ -0,0 +1,97 @@
// Admin.NET 项目的版权、商标、专利和其他相关权利均受相应法律法规的保护。使用本项目应遵守相关法律法规和许可证的要求。
//
// 本项目主要遵循 MIT 许可证和 Apache 许可证(版本 2.0)进行分发和使用。许可证位于源代码树根目录中的 LICENSE-MIT 和 LICENSE-APACHE 文件。
//
// 不得利用本项目从事危害国家安全、扰乱社会秩序、侵犯他人合法权益等法律法规禁止的活动!任何基于本项目二次开发而产生的一切法律纠纷和责任,我们不承担任何责任!
namespace Admin.NET.Core;
/// <summary>
/// 用户会话信息
/// </summary>
public partial class UserSessionDao
{
/// <summary>
/// 用户Id
/// </summary>
public virtual long UserId { get; set; }
/// <summary>
/// 平台应用Id
/// </summary>
public virtual long? AppId { get; set; }
/// <summary>
/// 租户Id
/// </summary>
public virtual long? TenantId { get; set; }
/// <summary>
/// 账号
/// </summary>
public virtual string Account { get; set; }
/// <summary>
/// 真实姓名
/// </summary>
public virtual string RealName { get; set; }
/// <summary>
/// 昵称
/// </summary>
public virtual string NickName { get; set; }
/// <summary>
/// 账户类型
/// </summary>
public virtual AccountTypeEnum? AccountType { get; set; }
/// <summary>
/// 是否超级管理员
/// </summary>
[System.Text.Json.Serialization.JsonIgnore]
[Newtonsoft.Json.JsonIgnore]
public bool SuperAdmin => AccountType == AccountTypeEnum.SuperAdmin;
/// <summary>
/// 是否系统管理员
/// </summary>
[System.Text.Json.Serialization.JsonIgnore]
[Newtonsoft.Json.JsonIgnore]
public bool SysAdmin => AccountType == AccountTypeEnum.SysAdmin;
/// <summary>
/// 组织机构Id
/// </summary>
public virtual long OrgId { get; set; }
/// <summary>
/// 组织机构名称
/// </summary>
public virtual string OrgName { get; set; }
/// <summary>
/// 组织机构类型
/// </summary>
public virtual string OrgType { get; set; }
/// <summary>
/// 组织机构级别
/// </summary>
public virtual int? OrgLevel { get; set; }
/// <summary>
/// 登录模式
/// </summary>
public virtual LoginModeEnum? LoginMode { get; set; }
/// <summary>
/// 微信OpenId
/// </summary>
public virtual string OpenId { get; set; }
/// <summary>
/// token版本
/// </summary>
public virtual long TokenVersion { get; set; }
}

View File

@ -13,16 +13,20 @@ namespace Admin.NET.Core.Service;
public class SysWechatService : IDynamicApiController, ITransient public class SysWechatService : IDynamicApiController, ITransient
{ {
private readonly SqlSugarRepository<SysOAuthUser> _sysOAuthUserRep; private readonly SqlSugarRepository<SysOAuthUser> _sysOAuthUserRep;
private readonly SysConfigService _sysConfigService;
private readonly WechatApiClientFactory _wechatApiClientFactory; private readonly WechatApiClientFactory _wechatApiClientFactory;
private readonly WechatApiClient _wechatApiClient; private readonly WechatApiClient _wechatApiClient;
private readonly SysAuthService _sysAuthService;
public SysWechatService(SqlSugarRepository<SysOAuthUser> sysOAuthUserRep,
public SysWechatService(
UserManager userManager,
SysAuthService sysAuthService,
SysConfigService sysConfigService, SysConfigService sysConfigService,
SqlSugarRepository<SysOAuthUser> sysOAuthUserRep,
WechatApiClientFactory wechatApiClientFactory) WechatApiClientFactory wechatApiClientFactory)
{ {
_sysAuthService = sysAuthService;
_sysOAuthUserRep = sysOAuthUserRep; _sysOAuthUserRep = sysOAuthUserRep;
_sysConfigService = sysConfigService;
_wechatApiClientFactory = wechatApiClientFactory; _wechatApiClientFactory = wechatApiClientFactory;
_wechatApiClient = wechatApiClientFactory.CreateWechatClient(); _wechatApiClient = wechatApiClientFactory.CreateWechatClient();
} }
@ -92,20 +96,14 @@ public class SysWechatService : IDynamicApiController, ITransient
[DisplayName("微信用户登录OpenId")] [DisplayName("微信用户登录OpenId")]
public async Task<dynamic> OpenIdLogin(WechatUserLogin input) public async Task<dynamic> OpenIdLogin(WechatUserLogin input)
{ {
var wxUser = await _sysOAuthUserRep.GetFirstAsync(p => p.OpenId == input.OpenId); var wxUser = await _sysOAuthUserRep.AsQueryable().Includes(u => u.SysUser, u => u.SysOrg).FirstAsync(p => p.OpenId == input.OpenId);
if (wxUser == null) if (wxUser == null) throw Oops.Oh("微信用户登录OpenId错误");
throw Oops.Oh("微信用户登录OpenId错误"); if (wxUser.SysUser == null) throw Oops.Oh("微信用户未关联");
var tokenInfo = await _sysAuthService.CreateToken(wxUser.SysUser);
var tokenExpire = await _sysConfigService.GetTokenExpire();
return new return new
{ {
wxUser.Avatar, wxUser.Avatar,
accessToken = JWTEncryption.Encrypt(new Dictionary<string, object> accessToken = tokenInfo.AccessToken
{
{ ClaimConst.UserId, wxUser.Id },
{ ClaimConst.NickName, wxUser.NickName },
{ ClaimConst.LoginMode, LoginModeEnum.APP },
}, tokenExpire)
}; };
} }

View File

@ -13,21 +13,22 @@ namespace Admin.NET.Core.Service;
public class SysWxOpenService : IDynamicApiController, ITransient public class SysWxOpenService : IDynamicApiController, ITransient
{ {
private readonly SqlSugarRepository<SysOAuthUser> _sysOAuthUserRep; private readonly SqlSugarRepository<SysOAuthUser> _sysOAuthUserRep;
private readonly SysConfigService _sysConfigService; private readonly SysAuthService _sysAuthService;
private readonly WechatApiClient _wechatApiClient; private readonly WechatApiClient _wechatApiClient;
private readonly SysFileService _sysFileService; private readonly SysFileService _sysFileService;
private readonly WechatApiClientFactory _wechatApiClientFactory; private readonly WechatApiClientFactory _wechatApiClientFactory;
public SysWxOpenService(SqlSugarRepository<SysOAuthUser> sysOAuthUserRep, public SysWxOpenService(
SysConfigService sysConfigService, SqlSugarRepository<SysOAuthUser> sysOAuthUserRep,
SysAuthService sysAuthService,
WechatApiClientFactory wechatApiClientFactory, WechatApiClientFactory wechatApiClientFactory,
SysFileService sysFileService) SysFileService sysFileService)
{ {
_sysOAuthUserRep = sysOAuthUserRep; _sysOAuthUserRep = sysOAuthUserRep;
_sysConfigService = sysConfigService; _sysAuthService = sysAuthService;
_wechatApiClient = wechatApiClientFactory.CreateWxOpenClient();
_sysFileService = sysFileService; _sysFileService = sysFileService;
_wechatApiClientFactory = wechatApiClientFactory; _wechatApiClientFactory = wechatApiClientFactory;
_wechatApiClient = wechatApiClientFactory.CreateWxOpenClient();
} }
/// <summary> /// <summary>
@ -119,17 +120,14 @@ public class SysWxOpenService : IDynamicApiController, ITransient
[DisplayName("微信小程序登录OpenId")] [DisplayName("微信小程序登录OpenId")]
public async Task<dynamic> WxOpenIdLogin(WxOpenIdLoginInput input) public async Task<dynamic> WxOpenIdLogin(WxOpenIdLoginInput input)
{ {
var wxUser = await _sysOAuthUserRep.GetFirstAsync(p => p.OpenId == input.OpenId) ?? throw Oops.Oh("微信小程序登录失败"); var wxUser = await _sysOAuthUserRep.AsQueryable().Includes(u => u.SysUser, u => u.SysOrg).FirstAsync(p => p.OpenId == input.OpenId) ?? throw Oops.Oh("微信小程序登录失败");
var tokenExpire = await _sysConfigService.GetTokenExpire(); if (wxUser.SysUser == null) throw Oops.Oh("微信未关联该用户");
var tokenInfo = await _sysAuthService.CreateToken(wxUser.SysUser, LoginModeEnum.APP);
return new return new
{ {
wxUser.Avatar, wxUser.Avatar,
accessToken = JWTEncryption.Encrypt(new Dictionary<string, object> accessToken = tokenInfo.AccessToken
{
{ ClaimConst.UserId, wxUser.Id },
{ ClaimConst.RealName, wxUser.NickName },
{ ClaimConst.LoginMode, LoginModeEnum.APP },
}, tokenExpire)
}; };
} }

View File

@ -12,6 +12,8 @@ namespace Admin.NET.Core;
/// <typeparam name="T"></typeparam> /// <typeparam name="T"></typeparam>
public class SqlSugarRepository<T> : SimpleClient<T>, ISqlSugarRepository<T> where T : class, new() public class SqlSugarRepository<T> : SimpleClient<T>, ISqlSugarRepository<T> where T : class, new()
{ {
private static readonly Lazy<UserManager> _userManager = new(() => App.GetService<UserManager>());
public SqlSugarRepository() public SqlSugarRepository()
{ {
var iTenant = SqlSugarSetup.ITenant; var iTenant = SqlSugarSetup.ITenant;
@ -37,7 +39,7 @@ public class SqlSugarRepository<T> : SimpleClient<T>, ISqlSugarRepository<T> whe
return; return;
// 若未贴任何表特性或当前未登录或是默认租户Id则返回默认库连接 // 若未贴任何表特性或当前未登录或是默认租户Id则返回默认库连接
var tenantId = App.User?.FindFirst(ClaimConst.TenantId)?.Value; var tenantId = _userManager.Value.TenantId?.ToString();
if (string.IsNullOrWhiteSpace(tenantId) || tenantId == SqlSugarConst.MainConfigId) return; if (string.IsNullOrWhiteSpace(tenantId) || tenantId == SqlSugarConst.MainConfigId) return;
// 根据租户Id切换库连接, 为空则返回默认库连接 // 根据租户Id切换库连接, 为空则返回默认库连接

View File

@ -212,6 +212,7 @@ public static class SqlSugarSetup
}; };
// 数据审计 // 数据审计
Lazy<UserManager> userManager = new(() => App.GetService<UserManager>());
dbProvider.Aop.DataExecuting = (oldValue, entityInfo) => dbProvider.Aop.DataExecuting = (oldValue, entityInfo) =>
{ {
// 新增/插入 操作 // 新增/插入 操作
@ -249,7 +250,7 @@ public static class SqlSugarSetup
if (entityInfo.PropertyName == nameof(EntityTenantId.TenantId)) if (entityInfo.PropertyName == nameof(EntityTenantId.TenantId))
{ {
if (entityValue.TenantId == null || entityValue.TenantId == 0) if (entityValue.TenantId == null || entityValue.TenantId == 0)
entityInfo.SetValue(App.User.FindFirst(ClaimConst.TenantId)?.Value ?? SqlSugarConst.DefaultTenantId.ToString()); entityInfo.SetValue(userManager.Value.TenantId?.ToString() ?? SqlSugarConst.DefaultTenantId.ToString());
} }
else if (entityInfo.PropertyName == nameof(EntityBase.CreateUserId)) else if (entityInfo.PropertyName == nameof(EntityBase.CreateUserId))
{ {
@ -259,17 +260,17 @@ public static class SqlSugarSetup
else if (entityInfo.PropertyName == nameof(EntityBase.CreateUserName)) else if (entityInfo.PropertyName == nameof(EntityBase.CreateUserName))
{ {
if (string.IsNullOrWhiteSpace(entityValue.CreateUserName)) if (string.IsNullOrWhiteSpace(entityValue.CreateUserName))
entityInfo.SetValue(App.User.FindFirst(ClaimConst.RealName)?.Value); entityInfo.SetValue(userManager.Value.RealName);
} }
else if (entityInfo.PropertyName == nameof(EntityBaseData.CreateOrgId)) else if (entityInfo.PropertyName == nameof(EntityBaseData.CreateOrgId))
{ {
if (entityValue.CreateOrgId == null || entityValue.CreateOrgId == 0) if (entityValue.CreateOrgId == null || entityValue.CreateOrgId == 0)
entityInfo.SetValue(App.User.FindFirst(ClaimConst.OrgId)?.Value); entityInfo.SetValue(userManager.Value.OrgId);
} }
else if (entityInfo.PropertyName == nameof(EntityBaseData.CreateOrgName)) else if (entityInfo.PropertyName == nameof(EntityBaseData.CreateOrgName))
{ {
if (string.IsNullOrWhiteSpace(entityValue.CreateOrgName)) if (string.IsNullOrWhiteSpace(entityValue.CreateOrgName))
entityInfo.SetValue(App.User.FindFirst(ClaimConst.OrgName)?.Value); entityInfo.SetValue(userManager.Value.OrgName);
} }
} }
// 编辑/更新 操作 // 编辑/更新 操作
@ -280,7 +281,7 @@ public static class SqlSugarSetup
else if (entityInfo.PropertyName == nameof(EntityBase.UpdateUserId)) else if (entityInfo.PropertyName == nameof(EntityBase.UpdateUserId))
entityInfo.SetValue(App.User?.FindFirst(ClaimConst.UserId)?.Value); entityInfo.SetValue(App.User?.FindFirst(ClaimConst.UserId)?.Value);
else if (entityInfo.PropertyName == nameof(EntityBase.UpdateUserName)) else if (entityInfo.PropertyName == nameof(EntityBase.UpdateUserName))
entityInfo.SetValue(App.User?.FindFirst(ClaimConst.RealName)?.Value); entityInfo.SetValue(userManager.Value.RealName);
} }
// 设置绑定简称字段数据 // 设置绑定简称字段数据
@ -294,14 +295,13 @@ public static class SqlSugarSetup
}; };
// 超管不受任何过滤器限制 // 超管不受任何过滤器限制
if (App.User?.FindFirst(ClaimConst.AccountType)?.Value == ((int)AccountTypeEnum.SuperAdmin).ToString()) if (userManager.Value.AccountType == AccountTypeEnum.SuperAdmin)return;
return;
// 配置假删除过滤器 // 配置假删除过滤器
dbProvider.QueryFilter.AddTableFilter<IDeletedFilter>(u => u.IsDelete == false); dbProvider.QueryFilter.AddTableFilter<IDeletedFilter>(u => u.IsDelete == false);
// 配置租户过滤器 // 配置租户过滤器
var tenantId = App.User?.FindFirst(ClaimConst.TenantId)?.Value; var tenantId = userManager.Value.TenantId?.ToString();
if (!string.IsNullOrWhiteSpace(tenantId)) if (!string.IsNullOrWhiteSpace(tenantId))
dbProvider.QueryFilter.AddTableFilter<ITenantIdFilter>(u => u.TenantId == long.Parse(tenantId)); dbProvider.QueryFilter.AddTableFilter<ITenantIdFilter>(u => u.TenantId == long.Parse(tenantId));

View File

@ -23,6 +23,7 @@ namespace Admin.NET.Web.Core
public class JwtHandler : AppAuthorizeHandler public class JwtHandler : AppAuthorizeHandler
{ {
private readonly IServiceProvider _serviceProvider; private readonly IServiceProvider _serviceProvider;
private static readonly Lazy<UserManager> _userManager = new(() => App.GetService<UserManager>());
public JwtHandler(IServiceProvider serviceProvider) public JwtHandler(IServiceProvider serviceProvider)
{ {
@ -50,6 +51,7 @@ namespace Admin.NET.Web.Core
else else
{ {
context.Fail(new AuthorizationFailureReason(this, "登录已过期,请重新登录。")); context.Fail(new AuthorizationFailureReason(this, "登录已过期,请重新登录。"));
context.StatusCode(StatusCodes.Status401Unauthorized);
var currentHttpContext = context.GetCurrentHttpContext(); var currentHttpContext = context.GetCurrentHttpContext();
// 跳过签名 SignatureAuthentication 引发的失败 // 跳过签名 SignatureAuthentication 引发的失败
if (currentHttpContext.Items.ContainsKey(SignatureAuthenticationDefaults.AuthenticateFailMsgKey)) return; if (currentHttpContext.Items.ContainsKey(SignatureAuthenticationDefaults.AuthenticateFailMsgKey)) return;
@ -58,16 +60,17 @@ namespace Admin.NET.Web.Core
} }
// 验证Token黑名单 // 验证Token黑名单
var accessToken = httpContext.Request.Headers.Authorization.ToString(); var userId = httpContext.User.FindFirst(ClaimConst.UserId)?.Value;
if (sysCacheService.ExistKey($"{CacheConst.KeyTokenBlacklist}:{accessToken}")) var version = httpContext.User.FindFirst(ClaimConst.TokenVersion)?.Value;
if (sysCacheService.ExistKey($"{CacheConst.KeyTokenBlacklist}:{userId}:{version}") || !sysCacheService.ExistKey($"{CacheConst.KeyUserSession}{userId}"))
{ {
context.Fail(new AuthorizationFailureReason(this, "令牌已失效,请重新登录。")); context.Fail(new AuthorizationFailureReason(this, "令牌已失效,请重新登录。"));
context.StatusCode(StatusCodes.Status401Unauthorized);
context.GetCurrentHttpContext().SignoutToSwagger(); context.GetCurrentHttpContext().SignoutToSwagger();
return; return;
} }
// 验证Token版本号 // 验证Token版本号
var userId = httpContext.User.FindFirst(ClaimConst.UserId)?.Value;
var tokenVersion1 = httpContext.User.FindFirst(ClaimConst.TokenVersion)?.Value; var tokenVersion1 = httpContext.User.FindFirst(ClaimConst.TokenVersion)?.Value;
var tokenVersion2 = sysCacheService.Get<string>($"{CacheConst.KeyUserToken}{userId}"); var tokenVersion2 = sysCacheService.Get<string>($"{CacheConst.KeyUserToken}{userId}");
if (string.IsNullOrWhiteSpace(tokenVersion2) && !string.IsNullOrWhiteSpace(userId)) if (string.IsNullOrWhiteSpace(tokenVersion2) && !string.IsNullOrWhiteSpace(userId))
@ -77,6 +80,7 @@ namespace Admin.NET.Web.Core
if (user == null || user.Status == StatusEnum.Disable) if (user == null || user.Status == StatusEnum.Disable)
{ {
context.Fail(new AuthorizationFailureReason(this, "账号不存在或已被停用,请联系管理员。")); context.Fail(new AuthorizationFailureReason(this, "账号不存在或已被停用,请联系管理员。"));
context.StatusCode(StatusCodes.Status401Unauthorized);
context.GetCurrentHttpContext().SignoutToSwagger(); context.GetCurrentHttpContext().SignoutToSwagger();
return; return;
} }
@ -86,18 +90,20 @@ namespace Admin.NET.Web.Core
if (string.IsNullOrWhiteSpace(tokenVersion1) || tokenVersion1 != tokenVersion2) if (string.IsNullOrWhiteSpace(tokenVersion1) || tokenVersion1 != tokenVersion2)
{ {
context.Fail(new AuthorizationFailureReason(this, "令牌已失效,请重新登录。")); context.Fail(new AuthorizationFailureReason(this, "令牌已失效,请重新登录。"));
context.StatusCode(StatusCodes.Status401Unauthorized);
context.GetCurrentHttpContext().SignoutToSwagger(); context.GetCurrentHttpContext().SignoutToSwagger();
return; return;
} }
// 验证租户有效期 // 验证租户有效期
var tenantId = httpContext.User.FindFirst(ClaimConst.TenantId)?.Value; var tenantId = _userManager.Value.TenantId?.ToString();
if (!string.IsNullOrWhiteSpace(tenantId)) if (!string.IsNullOrWhiteSpace(tenantId))
{ {
var tenant = sysCacheService.Get<List<SysTenant>>(CacheConst.KeyTenant)?.FirstOrDefault(u => u.Id == long.Parse(tenantId)); var tenant = sysCacheService.Get<List<SysTenant>>(CacheConst.KeyTenant)?.FirstOrDefault(u => u.Id == long.Parse(tenantId));
if (tenant != null && tenant.ExpirationTime != null && DateTime.Now > tenant.ExpirationTime) if (tenant != null && tenant.ExpirationTime != null && DateTime.Now > tenant.ExpirationTime)
{ {
context.Fail(new AuthorizationFailureReason(this, "租户已过期,请联系管理员。")); context.Fail(new AuthorizationFailureReason(this, "租户已过期,请联系管理员。"));
context.StatusCode(StatusCodes.Status401Unauthorized);
context.GetCurrentHttpContext().SignoutToSwagger(); context.GetCurrentHttpContext().SignoutToSwagger();
} }
} }
@ -117,7 +123,7 @@ namespace Admin.NET.Web.Core
private static async Task<bool> CheckAuthorizeAsync(DefaultHttpContext httpContext) private static async Task<bool> CheckAuthorizeAsync(DefaultHttpContext httpContext)
{ {
// 排除超管权限判断 // 排除超管权限判断
if (App.User.FindFirst(ClaimConst.AccountType)?.Value == ((int)AccountTypeEnum.SuperAdmin).ToString()) if (_userManager.Value?.AccountType == AccountTypeEnum.SuperAdmin)
return true; return true;
var serviceScope = httpContext.RequestServices.CreateScope(); var serviceScope = httpContext.RequestServices.CreateScope();
@ -126,7 +132,7 @@ namespace Admin.NET.Web.Core
var path = httpContext.Request.Path.ToString(); var path = httpContext.Request.Path.ToString();
// 移动端接口权限判断 // 移动端接口权限判断
if (App.User.FindFirst(ClaimConst.LoginMode)?.Value == ((int)LoginModeEnum.APP).ToString()) if (_userManager.Value?.LoginMode == LoginModeEnum.APP)
{ {
var appApiList = serviceScope.ServiceProvider.GetRequiredService<SysCommonService>().GetAppApiList(); var appApiList = serviceScope.ServiceProvider.GetRequiredService<SysCommonService>().GetAppApiList();
return appApiList.Exists(u => path.EndsWith(u, StringComparison.CurrentCultureIgnoreCase)); return appApiList.Exists(u => path.EndsWith(u, StringComparison.CurrentCultureIgnoreCase));

View File

@ -37,7 +37,7 @@ namespace @Model.NameSpace;
public partial class @(@Model.ClassName)Service : IDynamicApiController, ITransient public partial class @(@Model.ClassName)Service : IDynamicApiController, ITransient
{ {
private readonly SysCacheService _sysCacheService;//默认CacheService private readonly SysCacheService _sysCacheService;//默认CacheService
private readonly UserManager _userManager;//默认用户管理 private readonly UserSession _userManager;//默认用户管理
private readonly IEventPublisher _eventPublisher;//默认事件总线 private readonly IEventPublisher _eventPublisher;//默认事件总线
private readonly SqlSugarRepository<@(@Model.ClassName)> _@(@Model.LowerClassName)Rep; private readonly SqlSugarRepository<@(@Model.ClassName)> _@(@Model.LowerClassName)Rep;
@foreach (var column in Model.TableField){ @foreach (var column in Model.TableField){
@ -51,7 +51,7 @@ public partial class @(@Model.ClassName)Service : IDynamicApiController, ITransi
public @(@Model.ClassName)Service(SqlSugarRepository<@(@Model.ClassName)> @(@Model.LowerClassName)Rep public @(@Model.ClassName)Service(SqlSugarRepository<@(@Model.ClassName)> @(@Model.LowerClassName)Rep
,SysCacheService sysCacheService ,SysCacheService sysCacheService
, UserManager userManager , UserSession userManager
,IEventPublisher eventPublisher ,IEventPublisher eventPublisher
@foreach (var column in Model.TableField){ @foreach (var column in Model.TableField){
if(@column.EffectType == "ForeignKey"||@column.EffectType == "ApiTreeSelector"){ if(@column.EffectType == "ForeignKey"||@column.EffectType == "ApiTreeSelector"){

View File

@ -18,6 +18,7 @@ namespace Admin.NET.Plugin.ReZero.Service;
/// </summary> /// </summary>
public class SuperApiAop : DefaultSuperApiAop public class SuperApiAop : DefaultSuperApiAop
{ {
private static readonly Lazy<UserManager> _userManager = new(() => App.GetService<UserManager>());
public override async Task OnExecutingAsync(InterfaceContext aopContext) public override async Task OnExecutingAsync(InterfaceContext aopContext)
{ {
////if (aopContext.InterfaceType == InterfaceType.DynamicApi) ////if (aopContext.InterfaceType == InterfaceType.DynamicApi)
@ -59,13 +60,6 @@ public class SuperApiAop : DefaultSuperApiAop
var api = aopContext.InterfaceInfo; var api = aopContext.InterfaceInfo;
var context = aopContext.HttpContext; var context = aopContext.HttpContext;
var accessToken = context.Request.Headers.Authorization.ToString();
if (!string.IsNullOrWhiteSpace(accessToken) && accessToken.StartsWith("Bearer "))
accessToken = accessToken.Replace("Bearer ", "");
var claims = JWTEncryption.ReadJwtToken(accessToken)?.Claims;
var userName = claims?.FirstOrDefault(u => u.Type == ClaimConst.Account)?.Value;
var realName = claims?.FirstOrDefault(u => u.Type == ClaimConst.RealName)?.Value;
var paths = api.Url.Split('/'); var paths = api.Url.Split('/');
var actionName = paths[paths.Length - 1]; var actionName = paths[paths.Length - 1];
@ -86,13 +80,13 @@ public class SuperApiAop : DefaultSuperApiAop
{ {
new new
{ {
type = ClaimConst.Account, type = _userManager.Value?.Account,
value = userName value = _userManager.Value?.RealName
}, },
new new
{ {
type = ClaimConst.RealName, type = _userManager.Value?.RealName,
value = realName value = _userManager.Value?.RealName
}, },
}, },
exception = aopContext.Exception == null ? null : JSON.Serialize(aopContext.Exception) exception = aopContext.Exception == null ? null : JSON.Serialize(aopContext.Exception)