2026-03-14 06:13:06 +08:00
|
|
|
|
using Microsoft.Extensions.Logging;
|
|
|
|
|
|
using System.Security.Cryptography;
|
|
|
|
|
|
using System.Text;
|
|
|
|
|
|
using Microsoft.IdentityModel.Tokens;
|
|
|
|
|
|
using System.IdentityModel.Tokens.Jwt;
|
2025-12-17 11:12:13 +08:00
|
|
|
|
using YD_Event.Application.Client.Dtos;
|
2026-03-14 06:13:06 +08:00
|
|
|
|
using YD_Event.Core.Utils;
|
|
|
|
|
|
using YD_Event.Application.Auth;
|
|
|
|
|
|
using Newtonsoft.Json.Linq;
|
|
|
|
|
|
using Newtonsoft.Json;
|
2025-12-17 10:32:52 +08:00
|
|
|
|
|
2025-12-17 11:12:13 +08:00
|
|
|
|
namespace YD_Event.Application.Client;
|
2026-03-14 06:13:06 +08:00
|
|
|
|
|
2025-12-17 10:32:52 +08:00
|
|
|
|
/// <summary>
|
2026-03-14 06:13:06 +08:00
|
|
|
|
/// 微信小程序认证接口
|
2025-12-17 10:32:52 +08:00
|
|
|
|
/// </summary>
|
2025-12-27 12:53:54 +08:00
|
|
|
|
[ApiDescriptionSettings("微信小程序接口")]
|
2025-12-17 10:32:52 +08:00
|
|
|
|
public class OAuthController : IDynamicApiController
|
|
|
|
|
|
{
|
2026-03-14 06:13:06 +08:00
|
|
|
|
private readonly WeChatTool _weChatTool;
|
2025-12-17 10:32:52 +08:00
|
|
|
|
private readonly ISqlSugarRepository<AuthAccount> _accountRepository;
|
2026-03-14 06:13:06 +08:00
|
|
|
|
private readonly ISqlSugarRepository<YD_Event.Core.Entities.AppUser> _appUserRepository;
|
|
|
|
|
|
private readonly ISqlSugarRepository<SysRole> _roleRepository;
|
|
|
|
|
|
private readonly ISqlSugarRepository<SysUserRole> _userRoleRepository;
|
2025-12-17 10:32:52 +08:00
|
|
|
|
private readonly IEasyCachingProvider _easyCachingProvider;
|
|
|
|
|
|
private readonly IIdGenerator _idGenerator;
|
2026-03-14 06:13:06 +08:00
|
|
|
|
private readonly ILogger<OAuthController> _logger;
|
|
|
|
|
|
|
|
|
|
|
|
private const string SessionKeyCachePrefix = "wechat_session_";
|
2025-12-17 10:32:52 +08:00
|
|
|
|
|
2026-03-14 06:13:06 +08:00
|
|
|
|
public OAuthController(
|
|
|
|
|
|
WeChatTool weChatTool,
|
2025-12-17 10:32:52 +08:00
|
|
|
|
ISqlSugarRepository<AuthAccount> accountRepository,
|
2026-03-14 06:13:06 +08:00
|
|
|
|
ISqlSugarRepository<YD_Event.Core.Entities.AppUser> appUserRepository,
|
|
|
|
|
|
ISqlSugarRepository<SysRole> roleRepository,
|
|
|
|
|
|
ISqlSugarRepository<SysUserRole> userRoleRepository,
|
2025-12-17 10:32:52 +08:00
|
|
|
|
IEasyCachingProvider easyCachingProvider,
|
2026-03-14 06:13:06 +08:00
|
|
|
|
IIdGenerator idGenerator,
|
|
|
|
|
|
ILogger<OAuthController> logger)
|
2025-12-17 10:32:52 +08:00
|
|
|
|
{
|
2026-03-14 06:13:06 +08:00
|
|
|
|
_weChatTool = weChatTool;
|
2025-12-17 10:32:52 +08:00
|
|
|
|
_accountRepository = accountRepository;
|
2026-03-14 06:13:06 +08:00
|
|
|
|
_appUserRepository = appUserRepository;
|
|
|
|
|
|
_roleRepository = roleRepository;
|
|
|
|
|
|
_userRoleRepository = userRoleRepository;
|
2025-12-17 10:32:52 +08:00
|
|
|
|
_easyCachingProvider = easyCachingProvider;
|
|
|
|
|
|
_idGenerator = idGenerator;
|
2026-03-14 06:13:06 +08:00
|
|
|
|
_logger = logger;
|
2025-12-17 10:32:52 +08:00
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
/// <summary>
|
2026-03-14 06:13:06 +08:00
|
|
|
|
/// 登录接口
|
2025-12-17 10:32:52 +08:00
|
|
|
|
/// </summary>
|
2026-03-14 06:13:06 +08:00
|
|
|
|
/// <param name="input"></param>
|
2025-12-17 10:32:52 +08:00
|
|
|
|
/// <returns></returns>
|
2026-03-14 06:13:06 +08:00
|
|
|
|
[HttpPost("login")]
|
2025-12-17 10:32:52 +08:00
|
|
|
|
[AllowAnonymous]
|
2026-03-14 06:13:06 +08:00
|
|
|
|
public async Task<WeChatLoginOutput> Login([FromBody] WeChatLoginInput input)
|
2025-12-17 10:32:52 +08:00
|
|
|
|
{
|
2026-03-14 06:13:06 +08:00
|
|
|
|
// 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)
|
2025-12-17 10:32:52 +08:00
|
|
|
|
{
|
2026-03-14 06:13:06 +08:00
|
|
|
|
// 生成 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
|
2025-12-17 10:32:52 +08:00
|
|
|
|
};
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
/// <summary>
|
2026-03-14 06:13:06 +08:00
|
|
|
|
/// 注册接口
|
2025-12-17 10:32:52 +08:00
|
|
|
|
/// </summary>
|
2026-03-14 06:13:06 +08:00
|
|
|
|
/// <param name="input"></param>
|
2025-12-17 10:32:52 +08:00
|
|
|
|
/// <returns></returns>
|
2026-03-14 06:13:06 +08:00
|
|
|
|
[HttpPost("register")]
|
2025-12-17 10:32:52 +08:00
|
|
|
|
[AllowAnonymous]
|
2026-03-14 06:13:06 +08:00
|
|
|
|
[UnitOfWork]
|
|
|
|
|
|
public async Task<WeChatRegisterOutput> Register([FromBody] WeChatRegisterInput input)
|
2025-12-17 10:32:52 +08:00
|
|
|
|
{
|
2026-03-14 06:13:06 +08:00
|
|
|
|
// 1. 获取 session_key
|
|
|
|
|
|
var cacheKey = $"{SessionKeyCachePrefix}{input.Openid}";
|
|
|
|
|
|
var sessionKeyCache = await _easyCachingProvider.GetAsync<string>(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<WeChatUserInfo>(json);
|
|
|
|
|
|
if (wxUser == null) throw Oops.Bah("解密用户信息失败");
|
|
|
|
|
|
|
|
|
|
|
|
// 验证 watermark
|
|
|
|
|
|
var configAppId = App.Configuration["WeChat:AppId"];
|
|
|
|
|
|
if (!string.IsNullOrEmpty(configAppId) && wxUser.Watermark?.Appid != configAppId)
|
2025-12-17 10:32:52 +08:00
|
|
|
|
{
|
2026-03-14 06:13:06 +08:00
|
|
|
|
// 仅记录警告,防止配置不一致导致注册失败
|
|
|
|
|
|
_logger.LogWarning($"Watermark AppId mismatch: {wxUser.Watermark?.Appid} vs {configAppId}");
|
2025-12-17 10:32:52 +08:00
|
|
|
|
}
|
2026-03-14 06:13:06 +08:00
|
|
|
|
|
|
|
|
|
|
// 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);
|
2025-12-17 10:32:52 +08:00
|
|
|
|
}
|
|
|
|
|
|
|
2026-03-14 06:13:06 +08:00
|
|
|
|
// 绑定角色 (这里是 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
|
|
|
|
|
|
};
|
2025-12-17 10:32:52 +08:00
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
/// <summary>
|
2026-03-14 06:13:06 +08:00
|
|
|
|
/// 找回密码接口
|
2025-12-17 10:32:52 +08:00
|
|
|
|
/// </summary>
|
2026-03-14 06:13:06 +08:00
|
|
|
|
/// <param name="input"></param>
|
2025-12-17 10:32:52 +08:00
|
|
|
|
/// <returns></returns>
|
2026-03-14 06:13:06 +08:00
|
|
|
|
[HttpPost("reset-password")]
|
2025-12-17 10:32:52 +08:00
|
|
|
|
[AllowAnonymous]
|
2026-03-14 06:13:06 +08:00
|
|
|
|
public async Task<WeChatResetPasswordOutput> ResetPassword([FromBody] WeChatResetPasswordInput input)
|
2025-12-17 10:32:52 +08:00
|
|
|
|
{
|
2026-03-14 06:13:06 +08:00
|
|
|
|
// 1. 获取 session_key
|
|
|
|
|
|
var cacheKey = $"{SessionKeyCachePrefix}{input.Openid}";
|
|
|
|
|
|
var sessionKeyCache = await _easyCachingProvider.GetAsync<string>(cacheKey);
|
|
|
|
|
|
if (!sessionKeyCache.HasValue) throw Oops.Bah("SessionKey已过期,请重新登录");
|
|
|
|
|
|
string sessionKey = sessionKeyCache.Value;
|
|
|
|
|
|
|
|
|
|
|
|
// 2. 验证微信身份 (尝试解密)
|
|
|
|
|
|
try
|
2025-12-17 10:32:52 +08:00
|
|
|
|
{
|
2026-03-14 06:13:06 +08:00
|
|
|
|
string json = DecryptWeChatData(input.EncryptedData, sessionKey, input.Iv);
|
|
|
|
|
|
var wxUser = JsonConvert.DeserializeObject<WeChatUserInfo>(json);
|
|
|
|
|
|
if (wxUser.OpenId != input.Openid) throw Oops.Bah("身份验证失败: OpenId不匹配");
|
2025-12-17 10:32:52 +08:00
|
|
|
|
}
|
2026-03-14 06:13:06 +08:00
|
|
|
|
catch (Exception ex)
|
2025-12-17 10:32:52 +08:00
|
|
|
|
{
|
2026-03-14 06:13:06 +08:00
|
|
|
|
_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
|
|
|
|
|
|
};
|
2025-12-17 10:32:52 +08:00
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
/// <summary>
|
2026-03-14 06:13:06 +08:00
|
|
|
|
/// AES-128-CBC 解密
|
2025-12-17 10:32:52 +08:00
|
|
|
|
/// </summary>
|
2026-03-14 06:13:06 +08:00
|
|
|
|
private string DecryptWeChatData(string encryptedData, string sessionKey, string iv)
|
2025-12-17 10:32:52 +08:00
|
|
|
|
{
|
2026-03-14 06:13:06 +08:00
|
|
|
|
try
|
2026-02-26 18:29:08 +08:00
|
|
|
|
{
|
2026-03-14 06:13:06 +08:00
|
|
|
|
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();
|
2026-02-26 18:29:08 +08:00
|
|
|
|
}
|
2026-03-14 06:13:06 +08:00
|
|
|
|
catch (Exception ex)
|
2026-02-26 18:29:08 +08:00
|
|
|
|
{
|
2026-03-14 06:13:06 +08:00
|
|
|
|
_logger.LogError(ex, "AES Decrypt Error");
|
|
|
|
|
|
throw Oops.Bah("数据解密失败");
|
|
|
|
|
|
}
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
/// <summary>
|
|
|
|
|
|
/// 生成自定义 Token
|
|
|
|
|
|
/// </summary>
|
|
|
|
|
|
private (string AccessToken, string RefreshToken) GenerateTokens(AuthAccount account)
|
|
|
|
|
|
{
|
|
|
|
|
|
long uniqueId = _idGenerator.NewLong();
|
|
|
|
|
|
string accessToken = JWTEncryption.Encrypt(new Dictionary<string, object>()
|
|
|
|
|
|
{
|
|
|
|
|
|
[AuthClaimsConst.AuthIdKey] = account.Id,
|
|
|
|
|
|
[AuthClaimsConst.AccountKey] = account.OAuthId,
|
|
|
|
|
|
[AuthClaimsConst.UuidKey] = uniqueId,
|
|
|
|
|
|
[AuthClaimsConst.AuthPlatformTypeKey] = AuthPlatformType.WeChatApplet
|
|
|
|
|
|
});
|
|
|
|
|
|
|
|
|
|
|
|
// 获取刷新 token
|
|
|
|
|
|
string refreshToken = JWTEncryption.GenerateRefreshToken(accessToken);
|
|
|
|
|
|
|
|
|
|
|
|
return (accessToken, refreshToken);
|
2025-12-17 10:32:52 +08:00
|
|
|
|
}
|
2026-03-02 11:19:25 +08:00
|
|
|
|
}
|