平安校园
You can not select more than 25 topics Topics must start with a letter or number, can include dashes ('-') and can be up to 35 characters long.
 
 
 
 
 
 

503 lines
20 KiB

  1. //
  2. namespace SafeCampus.System.Services.Auth;
  3. /// <inheritdoc cref="IAuthService"/>
  4. public class AuthService : IAuthService
  5. {
  6. private readonly ISimpleCacheService _simpleCacheService;
  7. private readonly IEventPublisher _eventPublisher;
  8. private readonly IConfigService _configService;
  9. private readonly ISysUserService _userService;
  10. private readonly ISysOrgService _sysOrgService;
  11. public AuthService(ISimpleCacheService simpleCacheService, IEventPublisher eventPublisher, IConfigService configService,
  12. ISysUserService userService, ISysOrgService sysOrgService)
  13. {
  14. _simpleCacheService = simpleCacheService;
  15. _eventPublisher = eventPublisher;
  16. _configService = configService;
  17. _userService = userService;
  18. _sysOrgService = sysOrgService;
  19. }
  20. /// <inheritdoc/>
  21. public async Task<PicValidCodeOutPut> GetCaptchaInfo()
  22. {
  23. var config = await _configService.GetByConfigKey(CateGoryConst.CONFIG_LOGIN_POLICY, SysConfigConst.LOGIN_CAPTCHA_TYPE);
  24. var captchaType = (CaptchaType)Enum.Parse(typeof(CaptchaType), config.ConfigValue);
  25. //生成验证码
  26. var captchaInfo = CaptchaUtil.CreateCaptcha(captchaType, 4, 100, 38);
  27. //生成请求号,并将验证码放入缓存
  28. var reqNo = AddValidCodeToRedis(captchaInfo.Code);
  29. //返回验证码和请求号
  30. return new PicValidCodeOutPut
  31. {
  32. ValidCodeBase64 = captchaInfo.Base64Str,
  33. ValidCodeReqNo = reqNo
  34. };
  35. }
  36. /// <inheritdoc/>
  37. public async Task<string> GetPhoneValidCode(GetPhoneValidCodeInput input, LoginClientTypeEnum loginClientType)
  38. {
  39. await ValidPhoneValidCode(input, loginClientType);//校验手机号验证码
  40. var phoneValidCode = RandomHelper.CreateNum(6);//生产随机数字;
  41. #region 发送短信和记录数据库等操作
  42. //这里为什么不封装阿里云或者腾讯云短信接口,是因为短信发送是一个通用功能,一般每个公司都有自己封装好的短信组件来适配各种项目,这里再封装一次显得多余了。
  43. //这里不建议在该系统中封装短信发送接口,当然后续如果确实有需要也会增加
  44. //这里执行发送短信验证码代码,这里先默认就是年月日
  45. phoneValidCode = DateTime.Now.ToString("yyMMdd");
  46. #endregion 发送短信和记录数据库等操作
  47. //生成请求号,并将验证码放入缓存
  48. var reqNo = AddValidCodeToRedis(phoneValidCode);
  49. return reqNo;
  50. }
  51. /// <inheritdoc/>
  52. public async Task<LoginOutPut> Login(LoginInput input, LoginClientTypeEnum loginClientType)
  53. {
  54. await CheckCaptcha(input);//检查验证码
  55. await CheckWebOpen(input);//检查网站是否开启
  56. var password = CryptogramUtil.Sm2Decrypt(input.Password);//SM2解密
  57. //获取多租户配置
  58. var isTenant = await _configService.IsTenant();
  59. //获取登录策略
  60. var loginPolicy = await _configService.GetConfigsByCategory(CateGoryConst.CONFIG_LOGIN_POLICY);
  61. await BeforeLogin(loginPolicy, input, isTenant);//登录前校验
  62. // 根据账号获取用户信息,根据B端或C端判断
  63. if (loginClientType == LoginClientTypeEnum.B)//如果是B端
  64. {
  65. var userInfo = await _userService.GetUserByAccount(input.Account, input.TenantId);//获取用户信息
  66. if (userInfo == null) throw Oops.Bah("用户不存在");//用户不存在
  67. if (userInfo.Password != password)
  68. {
  69. LoginError(loginPolicy, input, isTenant);//登录错误操作
  70. throw Oops.Bah("账号密码错误");//账号密码错误
  71. }
  72. var result = await ExecLoginB(userInfo, input.Device, loginClientType, input.TenantId);// 执行B端登录
  73. return result;
  74. }
  75. //执行c端登录
  76. return null;
  77. }
  78. /// <inheritdoc/>
  79. public async Task<LoginOutPut> LoginByPhone(LoginByPhoneInput input, LoginClientTypeEnum loginClientType)
  80. {
  81. await ValidPhoneValidCode(input, loginClientType, false);//校验手机号和验证码,这里不删除Redis,防止输入错误又要重新输入验证码
  82. // 根据手机号获取用户信息,根据B端或C端判断
  83. if (loginClientType == LoginClientTypeEnum.B)//如果是B端
  84. {
  85. var userInfo = await _userService.GetUserByPhone(input.Phone);//获取信息
  86. if (userInfo == null) throw Oops.Bah("用户不存在");//用户不存在
  87. var result = await ExecLoginB(userInfo, input.Device, loginClientType, input.TenantId);// 执行B端登录
  88. RemoveValidCodeFromRedis(input.ValidCodeReqNo);//删除验证码
  89. return result;
  90. }
  91. //执行c端登录
  92. return null;
  93. }
  94. /// <inheritdoc/>
  95. public async Task LoginOut(string token, LoginClientTypeEnum loginClientType)
  96. {
  97. //获取用户信息
  98. var userinfo = await _userService.GetUserByAccount(UserManager.UserAccount, UserManager.TenantId);
  99. if (userinfo != null)
  100. {
  101. var loginEvent = new LoginEvent
  102. {
  103. Ip = App.HttpContext.GetRemoteIpAddressToIPv4(),
  104. SysUser = userinfo,
  105. Token = token
  106. };
  107. RemoveTokenFromRedis(loginEvent, loginClientType);//移除token
  108. //发布登出事件总线
  109. await _eventPublisher.PublishAsync(EventSubscriberConst.LOGIN_OUT_B, loginEvent);
  110. }
  111. }
  112. /// <inheritdoc/>
  113. public async Task<LoginUserOutput> GetLoginUser()
  114. {
  115. var userInfo = await _userService.GetUserByAccount(UserManager.UserAccount, UserManager.TenantId);//根据账号获取用户信息
  116. if (userInfo != null)
  117. {
  118. userInfo.Avatar = await _userService.GetUserAvatar(userInfo.Id);//获取头像
  119. return userInfo.Adapt<LoginUserOutput>();
  120. }
  121. return null;
  122. }
  123. #region 方法
  124. /// <summary>
  125. /// 检查验证码
  126. /// </summary>
  127. /// <param name="input"></param>
  128. /// <exception cref="AppFriendlyException"></exception>
  129. public async Task CheckCaptcha(LoginInput input)
  130. {
  131. //判断是否有验证码
  132. var sysBase = await _configService.GetByConfigKey(CateGoryConst.CONFIG_LOGIN_POLICY, SysConfigConst.LOGIN_CAPTCHA_OPEN);
  133. if (sysBase != null)//如果有这个配置项
  134. {
  135. if (sysBase.ConfigValue.ToBoolean())//如果需要验证码
  136. {
  137. //如果没填验证码,提示验证码不能为空
  138. if (string.IsNullOrEmpty(input.ValidCode) || string.IsNullOrEmpty(input.ValidCodeReqNo))
  139. throw Oops.Bah("验证码不能为空").StatusCode(410);
  140. ValidValidCode(input.ValidCode, input.ValidCodeReqNo);//校验验证码
  141. }
  142. }
  143. }
  144. /// <summary>
  145. /// 检查网站是否开启
  146. /// </summary>
  147. /// <param name="input"></param>
  148. /// <exception cref="AppFriendlyException"></exception>
  149. public async Task CheckWebOpen(LoginInput input)
  150. {
  151. //判断是否开启web访问
  152. var webStatus = await _configService.GetByConfigKey(CateGoryConst.CONFIG_SYS_BASE, SysConfigConst.SYS_WEB_STATUS);
  153. if (webStatus != null && webStatus.ConfigValue == CommonStatusConst.DISABLED
  154. && input.Account.ToLower() != SysRoleConst.SUPER_ADMIN.ToLower())//如果禁用了网站并且不是超级管理员
  155. {
  156. var closePrompt = await _configService.GetByConfigKey(CateGoryConst.CONFIG_SYS_BASE, SysConfigConst.SYS_WEB_CLOSE_PROMPT);
  157. throw Oops.Bah(closePrompt.ConfigValue);
  158. }
  159. }
  160. /// <summary>
  161. /// 登录之前执行的方法
  162. /// </summary>
  163. /// <param name="loginPolicy"></param>
  164. /// <param name="input"></param>
  165. /// <param name="isTenant"></param>
  166. public async Task BeforeLogin(List<SysConfig> loginPolicy, LoginInput input, bool isTenant)
  167. {
  168. var lockTime = loginPolicy.First(x => x.ConfigKey == SysConfigConst.LOGIN_ERROR_LOCK).ConfigValue.ToInt();//获取锁定时间
  169. var errorCount = loginPolicy.First(x => x.ConfigKey == SysConfigConst.LOGIN_ERROR_COUNT).ConfigValue.ToInt();//获取错误次数
  170. var key = SystemConst.CACHE_LOGIN_ERROR_COUNT;//获取登录错误次数Key值
  171. //如果是关闭多租户则直接用账号
  172. if (!isTenant)
  173. key += input.Account;
  174. else
  175. {
  176. //如果租户ID为空表示用域名登录
  177. if (input.TenantId == null)
  178. {
  179. //获取域名
  180. var origin = App.HttpContext.Request.Headers["Origin"].ToString();
  181. // 如果Origin头不存在,可以尝试使用Referer头作为备选
  182. if (string.IsNullOrEmpty(origin))
  183. origin = App.HttpContext.Request.Headers["Referer"].ToString();
  184. //根据域名获取二级域名
  185. var domain = origin.Split("//")[1].Split(".")[0];
  186. //根据二级域名获取租户
  187. var tenantList = await _sysOrgService.GetTenantList();
  188. var tenant = tenantList.FirstOrDefault(x => x.Code.ToLower() == domain);//获取租户默认是机构编码
  189. if (tenant != null)
  190. input.TenantId = tenant.Id;
  191. else
  192. throw Oops.Bah("租户不存在");
  193. }
  194. //如果是手动选择多租户则用账号+租户
  195. key += $"{input.TenantId}:{input.Account}";
  196. }
  197. var errorCountCache = _simpleCacheService.Get<int>(key);//获取登录错误次数
  198. if (errorCountCache >= errorCount)
  199. {
  200. _simpleCacheService.SetExpire(key, TimeSpan.FromMinutes(lockTime));//设置缓存
  201. throw Oops.Bah($"密码错误次数过多,请{lockTime}分钟后再试");
  202. }
  203. }
  204. /// <summary>
  205. /// 登录错误操作
  206. /// </summary>
  207. /// <param name="loginPolicy"></param>
  208. /// <param name="input"></param>
  209. /// <param name="isTenant"></param>
  210. public void LoginError(List<SysConfig> loginPolicy, LoginInput input, bool isTenant)
  211. {
  212. var resetTime = loginPolicy.First(x => x.ConfigKey == SysConfigConst.LOGIN_ERROR_RESET_TIME).ConfigValue.ToInt();//获取重置时间
  213. var key = SystemConst.CACHE_LOGIN_ERROR_COUNT;//获取登录错误次数Key值
  214. //如果是关闭多租户则直接用账号
  215. if (!isTenant)
  216. key += input.Account;
  217. else
  218. {
  219. //如果是手动选择多租户则用账号+租户
  220. key += $"{input.TenantId}:{input.Account}";
  221. }
  222. _simpleCacheService.Increment(key, 1);// 登录错误次数+1
  223. _simpleCacheService.SetExpire(key, TimeSpan.FromMinutes(resetTime));//设置过期时间
  224. }
  225. /// <summary>
  226. /// 校验验证码方法
  227. /// </summary>
  228. /// <param name="validCode">验证码</param>
  229. /// <param name="validCodeReqNo">请求号</param>
  230. /// <param name="isDelete">是否从Redis删除</param>
  231. public void ValidValidCode(string validCode, string validCodeReqNo, bool isDelete = true)
  232. {
  233. var key = SystemConst.CACHE_CAPTCHA + validCodeReqNo;//获取验证码Key值
  234. var code = _simpleCacheService.Get<string>(key);//从缓存拿数据
  235. if (isDelete) RemoveValidCodeFromRedis(validCodeReqNo);//如果需要删除验证码
  236. if (code != null && validCode != null)//如果有
  237. {
  238. //验证码如果不匹配直接抛错误,这里忽略大小写
  239. if (validCode.ToLower() != code.ToLower()) throw Oops.Bah("验证码错误");
  240. }
  241. else
  242. {
  243. throw Oops.Bah("验证码不能为空");//抛出验证码不能为空
  244. }
  245. }
  246. /// <summary>
  247. /// 从Redis中删除验证码
  248. /// </summary>
  249. /// <param name="validCodeReqNo"></param>
  250. public void RemoveValidCodeFromRedis(string validCodeReqNo)
  251. {
  252. var key = SystemConst.CACHE_CAPTCHA + validCodeReqNo;//获取验证码Key值
  253. _simpleCacheService.Remove(key);//删除验证码
  254. }
  255. /// <summary>
  256. /// 校验手机验证码
  257. /// </summary>
  258. /// <param name="input">输入参数</param>
  259. /// <param name="loginClientType">登录端类型</param>
  260. /// <param name="isDelete">是否删除</param>
  261. /// <returns></returns>
  262. public async Task ValidPhoneValidCode(GetPhoneValidCodeInput input, LoginClientTypeEnum loginClientType, bool isDelete = true)
  263. {
  264. ValidValidCode(input.ValidCode, input.ValidCodeReqNo, isDelete);//校验验证码
  265. if (loginClientType == LoginClientTypeEnum.B)//B端登录
  266. {
  267. var userId = await _userService.GetIdByPhone(input.Phone);//获取用户名
  268. if (userId == 0)
  269. {
  270. throw Oops.Bah("手机号不存在");//手机号不存在
  271. }
  272. }
  273. else
  274. {
  275. //校验C段用户手机号
  276. throw Oops.Bah("手机号不存在");//抛出验证码不能为空
  277. }
  278. }
  279. /// <summary>
  280. /// 添加验证码到缓存
  281. /// </summary>
  282. /// <param name="code">验证码</param>
  283. /// <param name="expire">过期时间</param>
  284. /// <returns>验证码请求号</returns>
  285. public string AddValidCodeToRedis(string code, int expire = 5)
  286. {
  287. //生成请求号
  288. var reqNo = CommonUtils.GetSingleId().ToString();
  289. //插入缓存
  290. _simpleCacheService.Set(SystemConst.CACHE_CAPTCHA + reqNo, code, TimeSpan.FromMinutes(expire));
  291. return reqNo;
  292. }
  293. /// <summary>
  294. /// 执行B端登录
  295. /// </summary>
  296. /// <param name="sysUser">用户信息</param>
  297. /// <param name="device">登录设备</param>
  298. /// <param name="loginClientType">登录类型</param>
  299. /// <param name="tenantId">租户id</param>
  300. /// <returns></returns>
  301. public async Task<LoginOutPut> ExecLoginB(SysUser sysUser, AuthDeviceTypeEnum device, LoginClientTypeEnum loginClientType,
  302. long? tenantId)
  303. {
  304. if (sysUser.Status == CommonStatusConst.DISABLED)
  305. throw Oops.Bah("账号已停用");//账号冻结
  306. if (sysUser.ModuleList.Count == 0) throw Oops.Bah("该账号未分配模块,请联系管理员");//没有分配菜单权限
  307. var org = await _sysOrgService.GetSysOrgById(sysUser.OrgId);//获取机构
  308. if (org.Status == CommonStatusConst.DISABLED) throw Oops.Bah("所属公司/部门已停用,请联系管理员");//机构冻结
  309. //生成Token
  310. var accessToken = JWTEncryption.Encrypt(new Dictionary<string, object>
  311. {
  312. {
  313. ClaimConst.USER_ID, sysUser.Id
  314. },
  315. {
  316. ClaimConst.ACCOUNT, sysUser.Account
  317. },
  318. {
  319. ClaimConst.NAME, sysUser.Name
  320. },
  321. {
  322. ClaimConst.IS_SUPER_ADMIN, sysUser.RoleCodeList.Contains(SysRoleConst.SUPER_ADMIN)
  323. },
  324. {
  325. ClaimConst.ORG_ID, sysUser.OrgId
  326. },
  327. {
  328. ClaimConst.TENANT_ID, tenantId
  329. }
  330. });
  331. var expire = App.GetConfig<int>("JWTSettings:ExpiredTime");//获取过期时间(分钟)
  332. // 生成刷新Token令牌
  333. var refreshToken = JWTEncryption.GenerateRefreshToken(accessToken, expire * 2);
  334. // 设置Swagger自动登录
  335. App.HttpContext.SigninToSwagger(accessToken);
  336. // 设置响应报文头
  337. App.HttpContext.SetTokensOfResponseHeaders(accessToken, refreshToken);
  338. //登录事件参数
  339. var loginEvent = new LoginEvent
  340. {
  341. Ip = App.HttpContext.GetRemoteIpAddressToIPv4(),
  342. Device = device,
  343. Expire = expire,
  344. SysUser = sysUser,
  345. Token = accessToken
  346. };
  347. await WriteTokenToRedis(loginEvent, loginClientType);//写入token到缓存
  348. await _eventPublisher.PublishAsync(EventSubscriberConst.LOGIN_B, loginEvent);//发布登录事件总线
  349. //返回结果
  350. return new LoginOutPut
  351. {
  352. Token = accessToken,
  353. Account = sysUser.Account,
  354. Name = sysUser.Name,
  355. DefaultModule = sysUser.DefaultModule,
  356. ModuleList = sysUser.ModuleList
  357. };
  358. }
  359. /// <summary>
  360. /// 写入用户token到缓存
  361. /// </summary>
  362. /// <param name="loginEvent">登录事件参数</param>
  363. /// <param name="loginClientType">登录类型</param>
  364. private async Task WriteTokenToRedis(LoginEvent loginEvent, LoginClientTypeEnum loginClientType)
  365. {
  366. //获取token列表
  367. var tokenInfos = GetTokenInfos(loginEvent.SysUser.Id);
  368. var tokenTimeout = loginEvent.DateTime.AddMinutes(loginEvent.Expire);
  369. //生成token信息
  370. var tokenInfo = new TokenInfo
  371. {
  372. Device = loginEvent.Device.ToString(),
  373. Expire = loginEvent.Expire,
  374. TokenTimeout = tokenTimeout,
  375. LoginClientType = loginClientType,
  376. Token = loginEvent.Token
  377. };
  378. //如果缓存有数据
  379. if (tokenInfos != null)
  380. {
  381. var isSingle = false;//默认不开启单用户登录
  382. var singleConfig = await _configService.GetByConfigKey(CateGoryConst.CONFIG_LOGIN_POLICY, SysConfigConst.LOGIN_SINGLE_OPEN);//获取系统单用户登录选项
  383. if (singleConfig != null)
  384. isSingle = singleConfig.ConfigValue.ToBoolean();//如果配置不为空则设置单用户登录选项为系统配置的值
  385. //判断是否单用户登录
  386. if (isSingle)
  387. {
  388. await SingleLogin(loginEvent.SysUser.Id.ToString(), tokenInfos.Where(it => it.LoginClientType == loginClientType).ToList());//单用户登录方法
  389. tokenInfos = tokenInfos.Where(it => it.LoginClientType != loginClientType).ToList();//去掉当前登录类型的token
  390. tokenInfos.Add(tokenInfo);//添加到列表
  391. }
  392. else
  393. {
  394. tokenInfos.Add(tokenInfo);
  395. }
  396. }
  397. else
  398. {
  399. tokenInfos = new List<TokenInfo>
  400. {
  401. tokenInfo
  402. };//直接就一个
  403. }
  404. //添加到token列表
  405. _simpleCacheService.HashAdd(CacheConst.CACHE_USER_TOKEN, loginEvent.SysUser.Id.ToString(), tokenInfos);
  406. }
  407. /// <summary>
  408. /// 缓存删除用户token
  409. /// </summary>
  410. /// <param name="loginEvent">登录事件参数</param>
  411. /// <param name="loginClientType">登录类型</param>
  412. private void RemoveTokenFromRedis(LoginEvent loginEvent, LoginClientTypeEnum loginClientType)
  413. {
  414. //获取token列表
  415. var tokenInfos = GetTokenInfos(loginEvent.SysUser.Id);
  416. if (tokenInfos != null)
  417. {
  418. //获取当前用户的token
  419. var token = tokenInfos.Where(it => it.Token == loginEvent.Token && it.LoginClientType == loginClientType).FirstOrDefault();
  420. if (token != null)
  421. tokenInfos.Remove(token);
  422. if (tokenInfos.Count > 0)
  423. {
  424. //更新token列表
  425. _simpleCacheService.HashAdd(CacheConst.CACHE_USER_TOKEN, loginEvent.SysUser.Id.ToString(), tokenInfos);
  426. }
  427. else
  428. {
  429. //从列表中删除
  430. _simpleCacheService.HashDel<List<TokenInfo>>(CacheConst.CACHE_USER_TOKEN, loginEvent.SysUser.Id.ToString());
  431. }
  432. }
  433. }
  434. /// <summary>
  435. /// 获取用户token列表
  436. /// </summary>
  437. /// <param name="userId">用户ID</param>
  438. /// <returns>token列表</returns>
  439. private List<TokenInfo> GetTokenInfos(long userId)
  440. {
  441. //缓存获取用户token列表
  442. var tokenInfos = _simpleCacheService.HashGetOne<List<TokenInfo>>(CacheConst.CACHE_USER_TOKEN, userId.ToString());
  443. if (tokenInfos != null)
  444. {
  445. tokenInfos = tokenInfos.Where(it => it.TokenTimeout > DateTime.Now).ToList();//去掉登录超时的
  446. }
  447. return tokenInfos;
  448. }
  449. /// <summary>
  450. /// 单用户登录通知用户下线
  451. /// </summary>
  452. /// <param name="userId">用户ID</param>
  453. /// <param name="tokenInfos">Token列表</param>
  454. private async Task SingleLogin(string userId, List<TokenInfo> tokenInfos)
  455. {
  456. await _eventPublisher.PublishAsync(EventSubscriberConst.USER_LOGIN_OUT, new UserLoginOutEvent
  457. {
  458. Message = "您的账号已在别处登录!",
  459. TokenInfos = tokenInfos,
  460. UserId = userId
  461. });//通知用户下线
  462. }
  463. #endregion 方法
  464. }