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.Utils;
using YD_Event.Application.Auth;
using Newtonsoft.Json.Linq;
using Newtonsoft.Json;
namespace YD_Event.Application.Client;
///
/// 微信小程序认证接口
///
[ApiDescriptionSettings("微信小程序接口")]
public class OAuthController : IDynamicApiController
{
private readonly WeChatTool _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(
WeChatTool 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 = 0, // 普通用户
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);
}
}