mirror of
https://gitee.com/ThingsGateway/ThingsGateway.git
synced 2025-10-20 10:50:48 +08:00
403 lines
15 KiB
C#
403 lines
15 KiB
C#
//------------------------------------------------------------------------------
|
||
// 此代码版权声明为全文件覆盖,如有原作者特别声明,会在下方手动补充
|
||
// 此代码版权(除特别声明外的代码)归作者本人Diego所有
|
||
// 源代码使用协议遵循本仓库的开源协议及附加协议
|
||
// Gitee源代码仓库:https://gitee.com/diego2098/ThingsGateway
|
||
// Github源代码仓库:https://github.com/kimdiego2098/ThingsGateway
|
||
// 使用文档:https://thingsgateway.cn/
|
||
// QQ群:605534569
|
||
//------------------------------------------------------------------------------
|
||
|
||
using Microsoft.AspNetCore.Authentication.Cookies;
|
||
using Microsoft.AspNetCore.Http;
|
||
|
||
using System.Security.Claims;
|
||
|
||
using ThingsGateway.DataEncryption;
|
||
using ThingsGateway.FriendlyException;
|
||
|
||
namespace ThingsGateway.Admin.Application;
|
||
|
||
public class AuthService : IAuthService
|
||
{
|
||
private readonly ISysDictService _configService;
|
||
private readonly ISysResourceService _sysResourceService;
|
||
private readonly IUserCenterService _userCenterService;
|
||
private readonly ISysUserService _sysUserService;
|
||
private readonly ISysOrgService _sysOrgService;
|
||
private IStringLocalizer<AuthService> _localizer;
|
||
private IVerificatInfoService _verificatInfoService;
|
||
private IAppService _appService;
|
||
|
||
public AuthService(ISysDictService configService, ISysResourceService sysResourceService,
|
||
ISysUserService userService, IUserCenterService userCenterService,
|
||
ISysOrgService sysOrgService,
|
||
IVerificatInfoService verificatInfoService, IAppService appService,
|
||
IStringLocalizer<AuthService> localizer)
|
||
{
|
||
_sysOrgService = sysOrgService;
|
||
_configService = configService;
|
||
_appService = appService;
|
||
_sysUserService = userService;
|
||
_sysResourceService = sysResourceService;
|
||
_userCenterService = userCenterService;
|
||
_localizer = localizer;
|
||
_verificatInfoService = verificatInfoService;
|
||
}
|
||
|
||
/// <summary>
|
||
/// 登录
|
||
/// </summary>
|
||
/// <param name="input">登录参数</param>
|
||
/// <param name="isCookie">cookie方式登录</param>
|
||
/// <returns>登录输出</returns>
|
||
public async Task<LoginOutput> LoginAsync(LoginInput input, bool isCookie = true)
|
||
{
|
||
var appConfig = await _configService.GetAppConfigAsync().ConfigureAwait(false);
|
||
|
||
//判断是否开启web访问
|
||
if (!appConfig.WebsitePolicy.WebStatus
|
||
&& input.Account != RoleConst.SuperAdmin)//如果禁用了网站并且不是超级管理员
|
||
{
|
||
throw Oops.Bah(appConfig.WebsitePolicy.CloseTip);
|
||
}
|
||
|
||
string? password = input.Password;
|
||
if (isCookie) //openApi登录不再需要解密
|
||
{
|
||
try
|
||
{
|
||
password = DESEncryption.Decrypt(input.Password);//解密
|
||
}
|
||
catch (Exception)
|
||
{
|
||
throw Oops.Bah(_localizer["MustDesc"]);
|
||
}
|
||
}
|
||
|
||
await BeforeLoginAsync(appConfig, input).ConfigureAwait(false);//登录前校验
|
||
|
||
var userInfo = await _sysUserService.GetUserByAccountAsync(input.Account, input.TenantId).ConfigureAwait(false);//获取用户信息
|
||
if (userInfo == null)
|
||
throw Oops.Bah(_localizer["UserNull", input.Account]);//用户不存在
|
||
|
||
if (userInfo.Password != password)
|
||
{
|
||
LoginError(appConfig.LoginPolicy, input.Account);//登录错误操作
|
||
}
|
||
var result = await ExecLogin(appConfig.LoginPolicy, input, userInfo, isCookie).ConfigureAwait(false);// 执行登录
|
||
return result;
|
||
}
|
||
|
||
/// <summary>
|
||
/// 注销当前用户
|
||
/// </summary>
|
||
public async Task LoginOutAsync()
|
||
{
|
||
if (UserManager.VerificatId == 0)
|
||
return;
|
||
var verificatId = UserManager.VerificatId;
|
||
//获取用户信息
|
||
var userinfo = await _sysUserService.GetUserByAccountAsync(UserManager.UserAccount, UserManager.TenantId).ConfigureAwait(false);
|
||
if (userinfo != null)
|
||
{
|
||
var loginEvent = new LoginEvent
|
||
{
|
||
Ip = _appService.RemoteIpAddress,
|
||
SysUser = userinfo,
|
||
VerificatId = verificatId
|
||
};
|
||
RemoveTokenFromCache(loginEvent);//移除verificat
|
||
}
|
||
await _appService.LoginOutAsync().ConfigureAwait(false);
|
||
}
|
||
|
||
#region 方法
|
||
|
||
/// <summary>
|
||
/// 登录之前执行的方法
|
||
/// </summary>
|
||
/// <param name="appConfig">配置</param>
|
||
/// <param name="input">input</param>
|
||
private async Task BeforeLoginAsync(AppConfig appConfig, LoginInput input)
|
||
{
|
||
var tenantEnable = App.GetOptions<TenantOptions>()?.Enable ?? false;
|
||
|
||
if (tenantEnable)
|
||
{
|
||
//如果租户ID为空表示用域名登录
|
||
if (input.TenantId == null)
|
||
{
|
||
//获取域名
|
||
var origin = App.HttpContext.Request.Headers["Origin"].ToString();
|
||
// 如果Origin头不存在,可以尝试使用Referer头作为备选
|
||
if (string.IsNullOrEmpty(origin))
|
||
origin = App.HttpContext.Request.Headers["Referer"].ToString();
|
||
//根据域名获取二级域名
|
||
var domain = origin.Split("//")[1].Split(".")[0];
|
||
//根据二级域名获取租户
|
||
var tenantList = await _sysOrgService.GetTenantListAsync().ConfigureAwait(false);
|
||
var tenant = tenantList.FirstOrDefault(x => x.Code.Equals(domain, StringComparison.OrdinalIgnoreCase));//获取租户默认是机构编码
|
||
if (tenant != null)
|
||
input.TenantId = tenant.Id;
|
||
else
|
||
input.TenantId = RoleConst.DefaultTenantId;
|
||
}
|
||
}
|
||
else
|
||
{
|
||
input.TenantId = RoleConst.DefaultTenantId;
|
||
}
|
||
|
||
var key = CacheConst.Cache_LoginErrorCount + input.Account + input.TenantId;//获取登录错误次数Key值
|
||
var errorCountCache = App.CacheService.Get<int>(key);//获取登录错误次数
|
||
|
||
if (errorCountCache >= appConfig.LoginPolicy.ErrorCount)
|
||
{
|
||
App.CacheService.SetExpire(key, TimeSpan.FromMinutes(appConfig.LoginPolicy.ErrorLockTime));//设置缓存
|
||
throw Oops.Bah(_localizer["PasswordError", appConfig.LoginPolicy.ErrorLockTime]);
|
||
}
|
||
}
|
||
|
||
/// <summary>
|
||
/// 执行登录
|
||
/// </summary>
|
||
/// <param name="loginPolicy">登录策略</param>
|
||
/// <param name="input">用户登录参数</param>
|
||
/// <param name="sysUser">用户信息</param>
|
||
/// <param name="isCookie">cookie方式登录</param>
|
||
/// <returns>登录输出结果</returns>
|
||
private async Task<LoginOutput> ExecLogin(LoginPolicy loginPolicy, LoginInput input, SysUser sysUser, bool isCookie = true)
|
||
{
|
||
if (sysUser.Status == false)
|
||
throw Oops.Bah(_localizer["UserDisable", sysUser.Account]);//账号已停用
|
||
|
||
var verificatId = CommonUtils.GetSingleId();
|
||
var expire = loginPolicy.VerificatExpireTime;
|
||
string accessToken = string.Empty;
|
||
string refreshToken = string.Empty;
|
||
if (!isCookie)
|
||
{
|
||
#region Token
|
||
|
||
//生成Token
|
||
accessToken = JWTEncryption.Encrypt(new Dictionary<string, object>
|
||
{
|
||
{
|
||
ClaimConst.UserId, sysUser.Id
|
||
},
|
||
{
|
||
ClaimConst.Account, sysUser.Account
|
||
},
|
||
{
|
||
ClaimConst.SuperAdmin, sysUser.RoleIdList.Contains(RoleConst.SuperAdminRoleId)
|
||
},
|
||
{
|
||
ClaimConst.VerificatId, verificatId
|
||
},
|
||
{
|
||
ClaimConst.OrgId, sysUser.OrgId
|
||
},
|
||
{
|
||
ClaimConst.TenantId, input.TenantId
|
||
}
|
||
});
|
||
// 生成刷新Token令牌
|
||
refreshToken = JWTEncryption.GenerateRefreshToken(accessToken, expire * 2);
|
||
// 设置Swagger自动登录
|
||
App.HttpContext?.SigninToSwagger(accessToken);
|
||
// 设置响应报文头
|
||
App.HttpContext?.SetTokensOfResponseHeaders(accessToken, refreshToken);
|
||
|
||
#endregion Token
|
||
}
|
||
else
|
||
{
|
||
if (sysUser.ModuleList.Count == 0)
|
||
throw Oops.Bah(_localizer["UserNoModule"]);//未分配模块
|
||
var org = await _sysOrgService.GetSysOrgByIdAsync(sysUser.OrgId).ConfigureAwait(false);//获取机构
|
||
if (!org.Status) throw Oops.Bah(_localizer["OrgDisable"]);//机构冻结
|
||
#region cookie
|
||
|
||
var identity = new ClaimsIdentity(CookieAuthenticationDefaults.AuthenticationScheme);
|
||
identity.AddClaim(new Claim(ClaimConst.VerificatId, verificatId.ToString()));
|
||
identity.AddClaim(new Claim(ClaimConst.UserId, sysUser.Id.ToString()));
|
||
identity.AddClaim(new Claim(ClaimConst.Account, sysUser.Account));
|
||
identity.AddClaim(new Claim(ClaimConst.SuperAdmin, sysUser.RoleIdList.Contains(RoleConst.SuperAdminRoleId).ToString()));
|
||
identity.AddClaim(new Claim(ClaimConst.OrgId, sysUser.OrgId.ToString()));
|
||
identity.AddClaim(new Claim(ClaimConst.TenantId, input.TenantId?.ToString() ?? "0"));
|
||
|
||
await _appService.LoginAsync(identity, expire).ConfigureAwait(false);
|
||
|
||
#endregion cookie
|
||
}
|
||
//登录事件参数
|
||
var logingEvent = new LoginEvent
|
||
{
|
||
Ip = _appService.RemoteIpAddress,
|
||
Device = _appService.UserAgent?.Platform ?? "Unknown",
|
||
Expire = expire,
|
||
SysUser = sysUser,
|
||
VerificatId = verificatId
|
||
};
|
||
await WriteTokenToCache(loginPolicy, logingEvent).ConfigureAwait(false);//写入verificat到cache
|
||
await UpdateUser(logingEvent).ConfigureAwait(false);
|
||
|
||
//返回结果
|
||
return new LoginOutput
|
||
{
|
||
VerificatId = verificatId,
|
||
Account = sysUser.Account,
|
||
Id = sysUser.Id,
|
||
AccessToken = accessToken,
|
||
RefreshToken = refreshToken
|
||
};
|
||
}
|
||
|
||
/// <summary>
|
||
/// 登录错误反馈
|
||
/// </summary>
|
||
/// <param name="loginPolicy">登录策略</param>
|
||
/// <param name="userName">用户名称</param>
|
||
private void LoginError(LoginPolicy loginPolicy, string userName)
|
||
{
|
||
var key = CacheConst.Cache_LoginErrorCount + userName;//获取登录错误次数Key值
|
||
App.CacheService.Increment(key, 1);// 登录错误次数+1
|
||
App.CacheService.SetExpire(key, TimeSpan.FromMinutes(loginPolicy.ErrorResetTime));//设置过期时间
|
||
var errorCountCache = App.CacheService.Get<int>(key);//获取登录错误次数
|
||
throw Oops.Bah(_localizer["AuthErrorMax", loginPolicy.ErrorCount, loginPolicy.ErrorLockTime, errorCountCache]);//账号密码错误
|
||
}
|
||
|
||
/// <summary>
|
||
/// 从cache删除用户verificat
|
||
/// </summary>
|
||
/// <param name="loginEvent">登录事件参数</param>
|
||
private void RemoveTokenFromCache(LoginEvent loginEvent)
|
||
{
|
||
//更新verificat列表
|
||
_verificatInfoService.Delete(loginEvent.VerificatId);
|
||
}
|
||
|
||
/// <summary>
|
||
/// 单用户登录通知用户下线
|
||
/// </summary>
|
||
/// <param name="userId">用户Id</param>
|
||
private async Task SingleLogin(long userId)
|
||
{
|
||
var clientIds = _verificatInfoService.GetClientIdListByUserId(userId);
|
||
await NoticeUtil.UserLoginOut(new UserLoginOutEvent
|
||
{
|
||
Message = _localizer["SingleLoginWarn"],
|
||
ClientIds = clientIds,
|
||
}).ConfigureAwait(false);
|
||
}
|
||
|
||
/// <summary>
|
||
/// 登录事件
|
||
/// </summary>
|
||
/// <param name="loginEvent"></param>
|
||
/// <returns></returns>
|
||
private async Task UpdateUser(LoginEvent loginEvent)
|
||
{
|
||
var sysUser = loginEvent.SysUser;
|
||
|
||
#region 登录/密码策略
|
||
|
||
var key = CacheConst.Cache_LoginErrorCount + sysUser.Account;//获取登录错误次数Key值
|
||
App.CacheService.Remove(key);//移除登录错误次数
|
||
|
||
//获取用户verificat列表
|
||
var userToken = _verificatInfoService.GetOne(loginEvent.VerificatId);
|
||
|
||
#endregion 登录/密码策略
|
||
|
||
#region 重新赋值属性,设置本次登录信息为最新的信息
|
||
|
||
sysUser.LastLoginIp = sysUser.LatestLoginIp;
|
||
sysUser.LastLoginTime = sysUser.LatestLoginTime;
|
||
sysUser.LatestLoginIp = loginEvent.Ip;
|
||
sysUser.LatestLoginTime = loginEvent.DateTime;
|
||
|
||
#endregion 重新赋值属性,设置本次登录信息为最新的信息
|
||
|
||
using var db = DbContext.GetDB<SysUser>();
|
||
//更新用户登录信息
|
||
if (await db.UpdateableT(sysUser).UpdateColumns(it => new
|
||
{
|
||
it.LastLoginIp,
|
||
it.LastLoginTime,
|
||
it.LatestLoginIp,
|
||
it.LatestLoginTime,
|
||
}).ExecuteCommandAsync().ConfigureAwait(false) > 0)
|
||
App.CacheService.HashAdd(CacheConst.Cache_SysUser, sysUser.Id.ToString(), sysUser);//更新Cache信息
|
||
}
|
||
|
||
/// <summary>
|
||
/// 写入用户verificat到cache
|
||
/// </summary>
|
||
/// <param name="loginPolicy">登录策略</param>
|
||
/// <param name="loginEvent">登录事件参数</param>
|
||
private async Task WriteTokenToCache(LoginPolicy loginPolicy, LoginEvent loginEvent)
|
||
{
|
||
//获取verificat列表
|
||
var tokenTimeout = loginEvent.DateTime.AddMinutes(loginEvent.Expire);
|
||
//生成verificat信息
|
||
var verificatInfo = new VerificatInfo
|
||
{
|
||
Device = loginEvent.Device ?? "Unknown",
|
||
Expire = loginEvent.Expire,
|
||
VerificatTimeout = tokenTimeout,
|
||
Id = loginEvent.VerificatId,
|
||
UserId = loginEvent.SysUser.Id,
|
||
LoginIp = loginEvent.Ip,
|
||
LoginTime = loginEvent.DateTime
|
||
};
|
||
//判断是否单用户登录
|
||
if (loginPolicy.SingleOpen)
|
||
{
|
||
await SingleLogin(loginEvent.SysUser.Id).ConfigureAwait(false);//单用户登录方法
|
||
}
|
||
|
||
//添加到verificat列表
|
||
_verificatInfoService.Add(verificatInfo);
|
||
}
|
||
|
||
#endregion 方法
|
||
}
|
||
|
||
/// <summary>
|
||
/// 登录事件参数
|
||
/// </summary>
|
||
public class LoginEvent
|
||
{
|
||
/// <summary>
|
||
/// 时间
|
||
/// </summary>
|
||
public DateTime DateTime = DateTime.Now;
|
||
|
||
/// <summary>
|
||
/// 过期时间
|
||
/// </summary>
|
||
public int Expire { get; set; }
|
||
|
||
/// <summary>
|
||
/// Ip地址
|
||
/// </summary>
|
||
public string? Ip { get; set; }
|
||
|
||
/// <summary>
|
||
/// 用户信息
|
||
/// </summary>
|
||
public SysUser SysUser { get; set; }
|
||
|
||
/// <summary>
|
||
/// VerificatId
|
||
/// </summary>
|
||
public long VerificatId { get; set; }
|
||
|
||
/// <summary>
|
||
/// 登录设备
|
||
/// </summary>
|
||
public string Device { get; set; }
|
||
}
|