using FreeSql; using LiveForum.Code.ExceptionExtend; using LiveForum.Code.JwtInfrastructure; using LiveForum.Code.JwtInfrastructure.Interface; using LiveForum.Code.Utility; using LiveForum.IService.Auth; using LiveForum.IService.Users; using LiveForum.Model; using LiveForum.Model.Dto.Others; using LiveForum.Model.Enum; using LiveForum.Model.Enum.Users; using Microsoft.AspNetCore.Http; using Microsoft.Extensions.Options; using System.Security.Claims; using System.Security.Cryptography; using System.Text; using SystemConvert = System.Convert; namespace LiveForum.Service.Auth { /// /// 登录服务 /// public class LoginService : ILoginService { private readonly IJwtAuthManager _jwtAuthManager; private readonly IBaseRepository _userRepository; private readonly IBaseRepository _userTokensRepository; private readonly IBaseRepository _wechatMiniProgramLoginsRepository; private readonly IBaseRepository _accountPasswordLoginsRepository; private readonly JwtUserInfoModel _userInfoModel; private readonly IHttpContextAccessor _httpContextAccessor; private readonly IOptionsSnapshot _appSettingsSnapshot; private readonly IUserInfoService _userInfoService; private readonly IBaseRepository _identityGroupsRepository; private readonly IBaseRepository _userIdentityGroupsRepository; /// /// 注意,要先创建私有对象,在构造函数中进行赋值 /// /// jwt管理对象 /// 用户仓储类 /// 用户token仓储类 /// 微信小程序登录仓储类 /// 账号密码登录仓储类 /// 在验证jwt的时候进行 /// HTTP上下文访问器 /// 应用配置快照(支持配置热更新) /// 用户信息服务 /// 身份组仓储类 /// 用户身份组关联仓储类 public LoginService(IJwtAuthManager jwtAuthManager, IBaseRepository userRepository, IBaseRepository userTokensRepository, IBaseRepository wechatMiniProgramLoginsRepository, IBaseRepository accountPasswordLoginsRepository, JwtUserInfoModel userInfoModel, IHttpContextAccessor httpContextAccessor, IOptionsSnapshot appSettingsSnapshot, IUserInfoService userInfoService, IBaseRepository identityGroupsRepository, IBaseRepository userIdentityGroupsRepository) { _jwtAuthManager = jwtAuthManager; _userRepository = userRepository; _userTokensRepository = userTokensRepository; _wechatMiniProgramLoginsRepository = wechatMiniProgramLoginsRepository; _accountPasswordLoginsRepository = accountPasswordLoginsRepository; _userInfoModel = userInfoModel; _httpContextAccessor = httpContextAccessor; _appSettingsSnapshot = appSettingsSnapshot; _userInfoService = userInfoService; _identityGroupsRepository = identityGroupsRepository; _userIdentityGroupsRepository = userIdentityGroupsRepository; } /// /// 微信小程序登录 /// 实现步奏: /// 1. 根据 openId 去查询T_WechatMiniProgramLogins用户是否存在 /// 2. 如果存在,获取对应的用户信息,生成JwtAccessToken返回(生成的jwt要看一下JwtUserInfoModel实体是否能满足) /// 3. 如果不存在,创建用户,并保存T_WechatMiniProgramLogins记录,然后生成JwtAccessToken返回 /// 4. 将生成的token保存到T_UserTokens中 /// 5. 返回JwtAccessToken /// /// /// /// /// /// public async Task WechatMpLogin(string openId, string sessionKey = "", string unionId = "") { if (string.IsNullOrEmpty(openId)) { throw new ArgumentNullException(nameof(openId), "openId 不能为空"); } var now = DateTime.Now; var clientIp = _httpContextAccessor.GetClientIpAddress(); T_WechatMiniProgramLogins wechatLogin; T_Users user; // 1. 根据 openId 查询微信小程序登录记录 wechatLogin = await _wechatMiniProgramLoginsRepository.Select .Where(x => x.OpenId == openId) .FirstAsync(); if (wechatLogin != null) { // 2. 如果存在,获取对应的用户信息 user = await _userRepository.Select .Where(x => x.Id == wechatLogin.UserId) .FirstAsync(); if (user == null) { // 用户已被删除(如管理后台删除),重新创建用户 var appSettings1 = _appSettingsSnapshot.Value; var defaultNickName1 = string.IsNullOrEmpty(appSettings1.UserDefaultName) ? "用户" : appSettings1.UserDefaultName; var defaultAvatar1 = string.IsNullOrEmpty(appSettings1.UserDefaultIcon) ? "" : appSettings1.UserDefaultIcon; user = new T_Users { NickName = $"{defaultNickName1}_{openId.Substring(0, 8)}", Avatar = defaultAvatar1, Experience = 0, LevelId = 1, Status = UserStatusEnum.Normal, CertifiedType = 0, // 0表示未认证 CertifiedStatus = CertifiedStatusEnum.未认证, IsCertified = false, IsVip = false, UID = await _userInfoService.GenerateUniqueUIDAsync(), CreatedAt = now, UpdatedAt = now, RegisterIp = clientIp, LastLoginIp = clientIp, LastLoginTime = now }; await _userRepository.InsertAsync(user); // 新用户自动关联默认身份组 await AssignDefaultIdentityGroupAsync(user.Id); // 更新微信登录记录关联到新用户 wechatLogin.UserId = user.Id; } // 更新最后登录时间和IP wechatLogin.LastLoginTime = now; wechatLogin.LastLoginIp = clientIp; await _wechatMiniProgramLoginsRepository.UpdateAsync(wechatLogin); // 更新用户最后登录信息 user.LastLoginTime = now; user.LastLoginIp = clientIp; await _userRepository.UpdateAsync(user); } else { // 3. 如果不存在,创建新用户 // 使用配置中的默认头像和昵称(从 IOptionsSnapshot 获取最新配置) var appSettings = _appSettingsSnapshot.Value; var defaultNickName = string.IsNullOrEmpty(appSettings.UserDefaultName) ? "用户" : appSettings.UserDefaultName; var defaultAvatar = string.IsNullOrEmpty(appSettings.UserDefaultIcon) ? "" : appSettings.UserDefaultIcon; user = new T_Users { NickName = $"{defaultNickName}_{openId.Substring(0, 8)}", Avatar = defaultAvatar, Experience = 0, LevelId = 1, Status = UserStatusEnum.Normal, CertifiedType = 0, // 0表示未认证 CertifiedStatus = CertifiedStatusEnum.未认证, IsCertified = false, IsVip = false, UID = await _userInfoService.GenerateUniqueUIDAsync(), CreatedAt = now, UpdatedAt = now, RegisterIp = clientIp, LastLoginIp = clientIp, LastLoginTime = now }; await _userRepository.InsertAsync(user); // 新用户自动关联默认身份组 await AssignDefaultIdentityGroupAsync(user.Id); // 创建微信登录记录 wechatLogin = new T_WechatMiniProgramLogins { OpenId = openId, SessionKey = sessionKey, UnionId = unionId ?? "", UserId = user.Id, LastLoginIp = clientIp, LastLoginTime = now, CreatedAt = now, UpdatedAt = now, }; await _wechatMiniProgramLoginsRepository.InsertAsync(wechatLogin); } // 4. 生成JWT Token var claims = new List { new Claim("userId", user.Id.ToString()), new Claim("loginType",((int)LoginTypeEnum.WechatLogin).ToString()) }; var jwtToken = await _jwtAuthManager.GenerateTokensAsync(user.Id.ToString(), claims, now); // 3. 删除T_UserTokens中的token信息 await _userTokensRepository.Select .Where(x => x.UserId == user.Id).ToDelete().ExecuteDeletedAsync(); // 5. 保存Token到数据库 var userToken = new T_UserTokens { UserId = user.Id, AccessToken = jwtToken.Token, LoginType = LoginTypeEnum.WechatLogin, LoginId = wechatLogin.Id, ClientIp = clientIp, UserAgent = _httpContextAccessor.GetUserAgent(), ExpiresAt = jwtToken.ExpireAt, TokenType = "Bearer", CreatedAt = now, UpdatedAt = now }; await _userTokensRepository.InsertAsync(userToken); return jwtToken; } /// /// 刷新token /// 1.根据注入的userInfoModel对象,获取当前用户信息 /// 2.生成新的JwtAccessToken /// 3.更新T_UserTokens中的token信息 /// 4.旧的token作废,去除redis缓存(RemoveTokenByUserNameAsync) /// 5.返回新的JwtAccessToken /// /// /// public async Task RefreshToken() { if (_userInfoModel == null || string.IsNullOrEmpty(_userInfoModel.UserName)) { throw new Exception("用户未登录"); } var now = DateTime.Now; // 1. 根据注入的userInfoModel对象,获取当前用户信息 if (!long.TryParse(_userInfoModel.UserName, out long userId)) { throw new Exception("无效的用户ID"); } var user = await _userRepository.Select .Where(x => x.Id == userId) .FirstAsync(); if (user == null) { throw new Exception("用户不存在"); } //if (_userInfoModel.ExpireAt < DateTime.Now.AddDays(3)) //{ //} // 2. 生成新的JwtAccessToken var claims = new List { new Claim("userId", user.Id.ToString()), new Claim("loginType",_userInfoModel.LoginType.ToString()) }; var newJwtToken = await _jwtAuthManager.GenerateTokensAsync(user.Id.ToString(), claims, now); // 3. 删除T_UserTokens中的token信息 await _userTokensRepository.Select .Where(x => x.UserId == userId).ToDelete().ExecuteDeletedAsync(); // 3. 更新T_UserTokens中的token信息(将旧的token标记为无效,插入新token) //var oldTokens = await _userTokensRepository.Select // .Where(x => x.UserId == userId) // .ToListAsync(); //foreach (var oldToken in oldTokens) //{ // oldToken.UpdatedAt = now; // await _userTokensRepository.UpdateAsync(oldToken); //} // 插入新的token记录 var clientIp = _httpContextAccessor.GetClientIpAddress(); var newUserToken = new T_UserTokens { UserId = user.Id, AccessToken = newJwtToken.Token, LoginType = (LoginTypeEnum)_userInfoModel.LoginType, // 可以根据实际情况调整 ClientIp = clientIp, UserAgent = _httpContextAccessor.GetUserAgent(), ExpiresAt = newJwtToken.ExpireAt, TokenType = "Bearer", CreatedAt = now, UpdatedAt = now, TokenMD5 = newJwtToken.Token.ToMD5() }; await _userTokensRepository.InsertAsync(newUserToken); // 4. 旧的token作废,去除redis缓存 生成的时候好像会覆盖掉 //await _jwtAuthManager.RemoveTokenByUserNameAsync(_userInfoModel.UserName); // 5. 返回新的JwtAccessToken return newJwtToken; } /// /// 账号密码登录 /// /// 账号(用户名/手机号/邮箱) /// 密码 /// 账号类型:1-用户名,2-手机号,3-邮箱 /// public async Task AccountLogin(string account, string password, int accountType = 1) { // 1. 根据账号查找登录记录 var loginRecord = await _accountPasswordLoginsRepository.Select .Where(x => x.LoginAccount == account && x.AccountType == accountType && !x.IsDeleted) .FirstAsync(); if (loginRecord == null) { throw new LoginErrorException("账号不存在或密码错误"); } // 2. 验证密码 if (!VerifyPassword(password, loginRecord.PasswordHash, loginRecord.PasswordSalt)) { throw new LoginErrorException("账号不存在或密码错误"); } // 3. 获取用户信息 var user = await _userRepository.Select.Where(x => x.Id == loginRecord.UserId).FirstAsync(); if (user == null || user.Status != UserStatusEnum.Normal) { throw new LoginErrorException("用户状态异常"); } // 4. 更新登录信息 var clientIp = _httpContextAccessor.GetClientIpAddress(); ; loginRecord.LastLoginTime = DateTime.Now; loginRecord.LastLoginIp = clientIp; await _accountPasswordLoginsRepository.UpdateAsync(loginRecord); // 5. 更新用户最后登录信息 user.LastLoginTime = DateTime.Now; user.LastLoginIp = clientIp; await _userRepository.UpdateAsync(user); // 6. 生成JWT Token var claims = new List { new Claim(ClaimTypes.NameIdentifier, user.Id.ToString()), new Claim(ClaimTypes.Name, user.NickName), new Claim("loginType", "2"), // 2-账号密码登录 new Claim("userId", loginRecord.Id.ToString()) }; var jwtToken = await _jwtAuthManager.GenerateTokensAsync(user.Id.ToString(), claims.ToList(), DateTime.Now); // 3. 删除T_UserTokens中的token信息 await _userTokensRepository.Select .Where(x => x.UserId == user.Id).ToDelete().ExecuteDeletedAsync(); // 7. 保存Token到数据库 var userToken = new T_UserTokens { UserId = user.Id, AccessToken = jwtToken.Token, TokenType = "Bearer", ExpiresAt = jwtToken.ExpireAt, LoginType = LoginTypeEnum.AccountPasswordLogin, // 2-账号密码登录 LoginId = loginRecord.Id, ClientIp = clientIp, UserAgent = _httpContextAccessor.GetUserAgent(), CreatedAt = DateTime.Now, UpdatedAt = DateTime.Now, TokenMD5 = jwtToken.Token.ToMD5() }; await _userTokensRepository.InsertAsync(userToken); return jwtToken; } /// /// 账号密码注册 /// /// 登录账号(用户名/手机号/邮箱) /// 账号类型:1-用户名,2-手机号,3-邮箱 /// 密码 /// 昵称 /// 验证码(手机号/邮箱注册时必填) /// public async Task AccountRegister(string loginAccount, int accountType, string password, string nickName, string verifyCode = "") { // 1. 验证账号是否已存在 var existingLogin = await _accountPasswordLoginsRepository.Select .Where(x => x.LoginAccount == loginAccount && x.AccountType == accountType && !x.IsDeleted) .FirstAsync(); if (existingLogin != null) { throw new LoginErrorException("该账号已存在"); } // 2. 验证验证码(如果需要) if (accountType == 2 || accountType == 3) // 手机号或邮箱注册 { if (string.IsNullOrEmpty(verifyCode)) { throw new LoginErrorException("验证码不能为空"); } // TODO: 实现验证码验证逻辑 } // 3. 密码加密 var (passwordHash, passwordSalt) = HashPassword(password); // 4. 创建用户记录 var clientIp = _httpContextAccessor.GetClientIpAddress(); ; var now = DateTime.Now; // 使用配置中的默认头像(从 IOptionsSnapshot 获取最新配置) var appSettings = _appSettingsSnapshot.Value; var defaultAvatar = string.IsNullOrEmpty(appSettings.UserDefaultIcon) ? "" : appSettings.UserDefaultIcon; var user = new T_Users { NickName = nickName, Avatar = defaultAvatar, LevelId = 1, // 默认等级 Experience = 0, Status = UserStatusEnum.Normal, CertifiedType = 0, // 0表示未认证 CertifiedStatus = CertifiedStatusEnum.未认证, IsCertified = false, IsVip = false, UID = await _userInfoService.GenerateUniqueUIDAsync(), RegisterIp = clientIp, LastLoginIp = clientIp, LastLoginTime = now, CreatedAt = now, UpdatedAt = now }; await _userRepository.InsertAsync(user); // 新用户自动关联默认身份组 await AssignDefaultIdentityGroupAsync(user.Id); // 5. 创建登录记录 var loginRecord = new T_AccountPasswordLogins { UserId = user.Id, LoginAccount = loginAccount, AccountType = (byte)accountType, PasswordHash = passwordHash, PasswordSalt = passwordSalt, IsPrimary = true, // 第一个登录方式为主登录方式 CreatedAt = now, UpdatedAt = now }; await _accountPasswordLoginsRepository.InsertAsync(loginRecord); // 6. 生成JWT Token var claims = new List { new Claim(ClaimTypes.NameIdentifier, user.Id.ToString()), new Claim(ClaimTypes.Name, user.NickName), new Claim("loginType", "2"), // 2-账号密码登录 new Claim("userId", user.Id.ToString()) }; var jwtToken = await _jwtAuthManager.GenerateTokensAsync(user.Id.ToString(), claims.ToList(), DateTime.Now); // 7. 保存Token到数据库 var userToken = new T_UserTokens { UserId = user.Id, AccessToken = jwtToken.Token, TokenType = "Bearer", ExpiresAt = jwtToken.ExpireAt, LoginType = LoginTypeEnum.AccountPasswordLogin, // 2-账号密码登录 LoginId = loginRecord.Id, ClientIp = clientIp, UserAgent = _httpContextAccessor.GetUserAgent(), CreatedAt = now, UpdatedAt = now, TokenMD5 = jwtToken.Token.ToMD5() }; await _userTokensRepository.InsertAsync(userToken); return jwtToken; } /// /// 微信手机号登录 /// 用户匹配逻辑:手机号 → openId → 新用户 /// public async Task WechatPhoneLogin(string openId, string phoneNumber, string sessionKey = "", string unionId = "") { if (string.IsNullOrEmpty(openId)) throw new ArgumentNullException(nameof(openId)); if (string.IsNullOrEmpty(phoneNumber)) throw new ArgumentNullException(nameof(phoneNumber)); var now = DateTime.Now; var clientIp = _httpContextAccessor.GetClientIpAddress(); T_Users user = null; T_WechatMiniProgramLogins wechatLogin = null; // 1. 根据手机号查询用户 user = await _userRepository.Select .Where(x => x.PhoneNumber == phoneNumber) .FirstAsync(); if (user != null) { // 找到手机号用户,确保绑定/更新 OpenID // 先按 UserId 查当前用户的微信记录 wechatLogin = await _wechatMiniProgramLoginsRepository.Select .Where(x => x.UserId == user.Id) .FirstAsync(); if (wechatLogin == null) { // 当前用户没有微信记录,检查该 OpenId 是否已被其他用户占用 var existingByOpenId = await _wechatMiniProgramLoginsRepository.Select .Where(x => x.OpenId == openId) .FirstAsync(); if (existingByOpenId != null) { // OpenId 已存在,将其重新关联到当前手机号用户 existingByOpenId.UserId = user.Id; existingByOpenId.SessionKey = sessionKey; existingByOpenId.LastLoginTime = now; existingByOpenId.LastLoginIp = clientIp; existingByOpenId.UpdatedAt = now; await _wechatMiniProgramLoginsRepository.UpdateAsync(existingByOpenId); wechatLogin = existingByOpenId; } else { wechatLogin = new T_WechatMiniProgramLogins { OpenId = openId, SessionKey = sessionKey, UnionId = unionId ?? "", UserId = user.Id, LastLoginIp = clientIp, LastLoginTime = now, CreatedAt = now, UpdatedAt = now, }; await _wechatMiniProgramLoginsRepository.InsertAsync(wechatLogin); } } else { wechatLogin.OpenId = openId; wechatLogin.SessionKey = sessionKey; wechatLogin.LastLoginTime = now; wechatLogin.LastLoginIp = clientIp; wechatLogin.UpdatedAt = now; await _wechatMiniProgramLoginsRepository.UpdateAsync(wechatLogin); } } else { // 2. 根据 OpenID 查询 wechatLogin = await _wechatMiniProgramLoginsRepository.Select .Where(x => x.OpenId == openId) .FirstAsync(); if (wechatLogin != null) { user = await _userRepository.Select .Where(x => x.Id == wechatLogin.UserId) .FirstAsync(); if (user != null) { // 绑定手机号 user.PhoneNumber = phoneNumber; await _userRepository.UpdateAsync(user); wechatLogin.SessionKey = sessionKey; wechatLogin.LastLoginTime = now; wechatLogin.LastLoginIp = clientIp; await _wechatMiniProgramLoginsRepository.UpdateAsync(wechatLogin); } } } // 3. 新用户注册 if (user == null) { var appSettings = _appSettingsSnapshot.Value; var defaultNickName = string.IsNullOrEmpty(appSettings.UserDefaultName) ? "用户" : appSettings.UserDefaultName; var defaultAvatar = string.IsNullOrEmpty(appSettings.UserDefaultIcon) ? "" : appSettings.UserDefaultIcon; user = new T_Users { NickName = $"{defaultNickName}_{openId.Substring(0, Math.Min(8, openId.Length))}", Avatar = defaultAvatar, PhoneNumber = phoneNumber, Experience = 0, LevelId = 1, Status = UserStatusEnum.Normal, CertifiedType = 0, CertifiedStatus = CertifiedStatusEnum.未认证, IsCertified = false, IsVip = false, UID = await _userInfoService.GenerateUniqueUIDAsync(), CreatedAt = now, UpdatedAt = now, RegisterIp = clientIp, LastLoginIp = clientIp, LastLoginTime = now }; await _userRepository.InsertAsync(user); // 新用户自动关联默认身份组 await AssignDefaultIdentityGroupAsync(user.Id); wechatLogin = new T_WechatMiniProgramLogins { OpenId = openId, SessionKey = sessionKey, UnionId = unionId ?? "", UserId = user.Id, LastLoginIp = clientIp, LastLoginTime = now, CreatedAt = now, UpdatedAt = now, }; await _wechatMiniProgramLoginsRepository.InsertAsync(wechatLogin); } // 更新用户最后登录信息 user.LastLoginTime = now; user.LastLoginIp = clientIp; await _userRepository.UpdateAsync(user); // 生成JWT Token var claims = new List { new Claim("userId", user.Id.ToString()), new Claim("loginType", ((int)LoginTypeEnum.WechatLogin).ToString()) }; var jwtToken = await _jwtAuthManager.GenerateTokensAsync(user.Id.ToString(), claims, now); await _userTokensRepository.Select .Where(x => x.UserId == user.Id).ToDelete().ExecuteDeletedAsync(); var userToken = new T_UserTokens { UserId = user.Id, AccessToken = jwtToken.Token, LoginType = LoginTypeEnum.WechatLogin, LoginId = wechatLogin.Id, ClientIp = clientIp, UserAgent = _httpContextAccessor.GetUserAgent(), ExpiresAt = jwtToken.ExpireAt, TokenType = "Bearer", CreatedAt = now, UpdatedAt = now }; await _userTokensRepository.InsertAsync(userToken); return jwtToken; } /// /// 为新用户自动关联默认身份组 /// /// 新用户ID private async Task AssignDefaultIdentityGroupAsync(long userId) { var defaultGroup = await _identityGroupsRepository.Select .Where(x => x.IsDefault == true) .FirstAsync(); if (defaultGroup != null) { await _userIdentityGroupsRepository.InsertAsync(new T_UserIdentityGroups { UserId = userId, IdentityGroupId = defaultGroup.Id, CreatedAt = DateTime.Now }); } } /// /// 密码加密 /// /// 原始密码 /// 加密后的密码哈希和盐值 private (string hash, string salt) HashPassword(string password) { // 生成随机盐值 var saltBytes = new byte[32]; using (var rng = RandomNumberGenerator.Create()) { rng.GetBytes(saltBytes); } var salt = SystemConvert.ToBase64String(saltBytes); // 使用PBKDF2进行密码哈希 using (var pbkdf2 = new Rfc2898DeriveBytes(password, saltBytes, 10000, HashAlgorithmName.SHA256)) { var hashBytes = pbkdf2.GetBytes(32); var hash = SystemConvert.ToBase64String(hashBytes); return (hash, salt); } } /// /// 验证密码 /// /// 原始密码 /// 存储的密码哈希 /// 存储的盐值 /// 密码是否正确 private bool VerifyPassword(string password, string hash, string salt) { try { var saltBytes = SystemConvert.FromBase64String(salt); using (var pbkdf2 = new Rfc2898DeriveBytes(password, saltBytes, 10000, HashAlgorithmName.SHA256)) { var hashBytes = pbkdf2.GetBytes(32); var computedHash = SystemConvert.ToBase64String(hashBytes); return computedHash == hash; } } catch { return false; } } } }