using Microsoft.Extensions.Logging; using System.Security.Cryptography; using System.Text; using Microsoft.IdentityModel.Tokens; using System.IdentityModel.Tokens.Jwt; using YD_Event.Application.Client.Dtos; using YD_Event.Core.Infrastructure.WeChat; using YD_Event.Application.Auth; using Newtonsoft.Json.Linq; using Newtonsoft.Json; namespace YD_Event.Application.Client; /// /// 微信小程序认证接口 /// [ApiDescriptionSettings("微信小程序接口")] public class OAuthController : IDynamicApiController { private readonly WeChatService _weChatTool; private readonly ISqlSugarRepository _accountRepository; private readonly ISqlSugarRepository _appUserRepository; private readonly ISqlSugarRepository _roleRepository; private readonly ISqlSugarRepository _userRoleRepository; private readonly IEasyCachingProvider _easyCachingProvider; private readonly IIdGenerator _idGenerator; private readonly ILogger _logger; private const string SessionKeyCachePrefix = "wechat_session_"; public OAuthController( WeChatService weChatTool, ISqlSugarRepository accountRepository, ISqlSugarRepository appUserRepository, ISqlSugarRepository roleRepository, ISqlSugarRepository userRoleRepository, IEasyCachingProvider easyCachingProvider, IIdGenerator idGenerator, ILogger logger) { _weChatTool = weChatTool; _accountRepository = accountRepository; _appUserRepository = appUserRepository; _roleRepository = roleRepository; _userRoleRepository = userRoleRepository; _easyCachingProvider = easyCachingProvider; _idGenerator = idGenerator; _logger = logger; } /// /// 登录接口 /// /// /// [HttpPost("login")] [AllowAnonymous] public async Task Login([FromBody] WeChatLoginInput input) { // 1. code2Session var sessionResult = await _weChatTool.Code2Session(input.Code); string openid = sessionResult.OpenId; string sessionKey = sessionResult.SessionKey; // 缓存 session_key,用于后续解密 await _easyCachingProvider.SetAsync($"{SessionKeyCachePrefix}{openid}", sessionKey, TimeSpan.FromHours(2)); // 2. 校验签名 (可选,根据业务严谨性) // var signature2 = Hash(input.RawData + sessionKey); // if (signature2 != input.Signature) throw Oops.Bah("签名校验失败"); // 3. 查找用户 var account = await _accountRepository.GetFirstAsync(x => x.OAuthId == openid && x.Type == "WeChat"); string accessToken = null; string refreshToken = null; object userInfo = null; int expiresIn = 7200; if (account != null) { // 生成 Token (accessToken, refreshToken) = GenerateTokens(account); // 获取用户信息 var appUser = await _appUserRepository.GetFirstAsync(x => x.UserId == account.Id); if (appUser != null) { userInfo = new { appUser.Name, appUser.Id, appUser.Mobile, appUser.Avatar, appUser.NickName }; } else { userInfo = new { account.Name, account.Avatar }; } } return new WeChatLoginOutput { Openid = openid, SessionKey = sessionKey, // 需求要求返回 AccessToken = accessToken, RefreshToken = refreshToken, ExpiresIn = expiresIn, UserInfo = userInfo }; } /// /// 注册接口 /// /// /// [HttpPost("register")] [AllowAnonymous] [UnitOfWork] public async Task Register([FromBody] WeChatRegisterInput input) { // 1. 获取 session_key var cacheKey = $"{SessionKeyCachePrefix}{input.Openid}"; var sessionKeyCache = await _easyCachingProvider.GetAsync(cacheKey); if (!sessionKeyCache.HasValue) throw Oops.Bah("SessionKey已过期,请重新登录"); string sessionKey = sessionKeyCache.Value; // 2. 解密用户信息 string json = DecryptWeChatData(input.EncryptedData, sessionKey, input.Iv); var wxUser = JsonConvert.DeserializeObject(json); if (wxUser == null) throw Oops.Bah("解密用户信息失败"); // 验证 watermark var configAppId = App.Configuration["WeChat:AppId"]; if (!string.IsNullOrEmpty(configAppId) && wxUser.Watermark?.Appid != configAppId) { // 仅记录警告,防止配置不一致导致注册失败 _logger.LogWarning($"Watermark AppId mismatch: {wxUser.Watermark?.Appid} vs {configAppId}"); } // 3. 写入 AuthAccount // 检查是否已存在 if (await _accountRepository.IsAnyAsync(x => x.OAuthId == input.Openid && x.Type == "WeChat")) { throw Oops.Bah("该用户已注册"); } long accountId = _idGenerator.NewLong(); var account = new AuthAccount { Id = accountId, OAuthId = input.Openid, Type = "WeChat", Name = wxUser.NickName, Avatar = wxUser.AvatarUrl, Gender = (Gender)wxUser.Gender, // 假设 Gender 枚举兼容 CreatedTime = DateTime.Now, CreatedUserId = accountId // 自指 }; await _accountRepository.InsertAsync(account); // 4. 写入 AppUser (Profile) long appUserId = _idGenerator.NewLong(); var appUser = new YD_Event.Core.Entities.AppUser { Id = appUserId, UserId = accountId, Name = wxUser.NickName, NickName = wxUser.NickName, Avatar = wxUser.AvatarUrl, Gender = (Gender)wxUser.Gender, PersonType = PersonType.User, // 普通用户 Status = AvailabilityStatus.Enable, AuditStatus = AuditStatus.Pass, CreatedTime = DateTime.Now, CreatedUserId = accountId }; await _appUserRepository.InsertAsync(appUser); // 5. 初始化默认角色 // 查找默认角色,假设为 "OrdinaryUser" 或 "User" var defaultRole = await _roleRepository.GetFirstAsync(x => x.Code == "User" || x.Code == "OrdinaryUser"); if (defaultRole == null) { // 如果不存在,创建一个默认角色 defaultRole = new SysRole { Id = _idGenerator.NewLong(), Name = "普通用户", Code = "User", Status = AvailabilityStatus.Enable, CreatedTime = DateTime.Now }; await _roleRepository.InsertAsync(defaultRole); } // 绑定角色 (这里是 AppUser 还是 AuthAccount 绑定角色? SysUserRole 关联 UserId,通常指 AuthAccount.Id 或 SysUser.Id) // 假设 SysUserRole.UserId 对应 AuthAccount.Id (因为 AuthAccount 是登录主体) await _userRoleRepository.InsertAsync(new SysUserRole { Id = _idGenerator.NewLong(), UserId = accountId, RoleId = defaultRole.Id, CreatedTime = DateTime.Now }); return new WeChatRegisterOutput { UserId = accountId, Nickname = account.Name, AvatarUrl = account.Avatar, RegisterTime = account.CreatedTime }; } /// /// 找回密码接口 /// /// /// [HttpPost("reset-password")] [AllowAnonymous] public async Task ResetPassword([FromBody] WeChatResetPasswordInput input) { // 1. 获取 session_key var cacheKey = $"{SessionKeyCachePrefix}{input.Openid}"; var sessionKeyCache = await _easyCachingProvider.GetAsync(cacheKey); if (!sessionKeyCache.HasValue) throw Oops.Bah("SessionKey已过期,请重新登录"); string sessionKey = sessionKeyCache.Value; // 2. 验证微信身份 (尝试解密) try { string json = DecryptWeChatData(input.EncryptedData, sessionKey, input.Iv); var wxUser = JsonConvert.DeserializeObject(json); if (wxUser.OpenId != input.Openid) throw Oops.Bah("身份验证失败: OpenId不匹配"); } catch (Exception ex) { _logger.LogError(ex, "ResetPassword Decrypt Failed"); throw Oops.Bah("身份验证失败"); } // 3. 生成一次性 Token string resetToken = Guid.NewGuid().ToString("N"); // 4. 发送邮件/短信 (Mock) string sendChannel = "none"; if (!string.IsNullOrEmpty(input.Email)) { // Mock send email _logger.LogInformation($"Send Reset Token {resetToken} to Email {input.Email}"); sendChannel = "email"; } else if (!string.IsNullOrEmpty(input.SmsCode)) { // Verify Sms Code (Mock) // Mock send sms _logger.LogInformation($"Verify SMS Code {input.SmsCode} and generate token"); sendChannel = "sms"; } return new WeChatResetPasswordOutput { ResetToken = resetToken, ExpireTime = DateTime.Now.AddMinutes(15), SendChannel = sendChannel }; } /// /// AES-128-CBC 解密 /// private string DecryptWeChatData(string encryptedData, string sessionKey, string iv) { try { byte[] encryptedBytes = Convert.FromBase64String(encryptedData); byte[] keyBytes = Convert.FromBase64String(sessionKey); byte[] ivBytes = Convert.FromBase64String(iv); using var aes = Aes.Create(); aes.KeySize = 128; aes.BlockSize = 128; aes.Mode = CipherMode.CBC; aes.Padding = PaddingMode.PKCS7; aes.Key = keyBytes; aes.IV = ivBytes; using var decryptor = aes.CreateDecryptor(aes.Key, aes.IV); using var ms = new MemoryStream(encryptedBytes); using var cs = new CryptoStream(ms, decryptor, CryptoStreamMode.Read); using var sr = new StreamReader(cs); return sr.ReadToEnd(); } catch (Exception ex) { _logger.LogError(ex, "AES Decrypt Error"); throw Oops.Bah("数据解密失败"); } } /// /// 生成自定义 Token /// private (string AccessToken, string RefreshToken) GenerateTokens(AuthAccount account) { long uniqueId = _idGenerator.NewLong(); string accessToken = JWTEncryption.Encrypt(new Dictionary() { [AuthClaimsConst.AuthIdKey] = account.Id, [AuthClaimsConst.AccountKey] = account.OAuthId, [AuthClaimsConst.UuidKey] = uniqueId, [AuthClaimsConst.AuthPlatformTypeKey] = AuthPlatformType.WeChatApplet }); // 获取刷新 token string refreshToken = JWTEncryption.GenerateRefreshToken(accessToken); return (accessToken, refreshToken); } }