319 lines
12 KiB
C#
319 lines
12 KiB
C#
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;
|
||
|
||
/// <summary>
|
||
/// 微信小程序认证接口
|
||
/// </summary>
|
||
[ApiDescriptionSettings("微信小程序接口")]
|
||
public class OAuthController : IDynamicApiController
|
||
{
|
||
private readonly WeChatService _weChatTool;
|
||
private readonly ISqlSugarRepository<AuthAccount> _accountRepository;
|
||
private readonly ISqlSugarRepository<YD_Event.Core.Entities.AppUser> _appUserRepository;
|
||
private readonly ISqlSugarRepository<SysRole> _roleRepository;
|
||
private readonly ISqlSugarRepository<SysUserRole> _userRoleRepository;
|
||
private readonly IEasyCachingProvider _easyCachingProvider;
|
||
private readonly IIdGenerator _idGenerator;
|
||
private readonly ILogger<OAuthController> _logger;
|
||
|
||
private const string SessionKeyCachePrefix = "wechat_session_";
|
||
|
||
public OAuthController(
|
||
WeChatService weChatTool,
|
||
ISqlSugarRepository<AuthAccount> accountRepository,
|
||
ISqlSugarRepository<YD_Event.Core.Entities.AppUser> appUserRepository,
|
||
ISqlSugarRepository<SysRole> roleRepository,
|
||
ISqlSugarRepository<SysUserRole> userRoleRepository,
|
||
IEasyCachingProvider easyCachingProvider,
|
||
IIdGenerator idGenerator,
|
||
ILogger<OAuthController> logger)
|
||
{
|
||
_weChatTool = weChatTool;
|
||
_accountRepository = accountRepository;
|
||
_appUserRepository = appUserRepository;
|
||
_roleRepository = roleRepository;
|
||
_userRoleRepository = userRoleRepository;
|
||
_easyCachingProvider = easyCachingProvider;
|
||
_idGenerator = idGenerator;
|
||
_logger = logger;
|
||
}
|
||
|
||
/// <summary>
|
||
/// 登录接口
|
||
/// </summary>
|
||
/// <param name="input"></param>
|
||
/// <returns></returns>
|
||
[HttpPost("login")]
|
||
[AllowAnonymous]
|
||
public async Task<WeChatLoginOutput> 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
|
||
};
|
||
}
|
||
|
||
/// <summary>
|
||
/// 注册接口
|
||
/// </summary>
|
||
/// <param name="input"></param>
|
||
/// <returns></returns>
|
||
[HttpPost("register")]
|
||
[AllowAnonymous]
|
||
[UnitOfWork]
|
||
public async Task<WeChatRegisterOutput> Register([FromBody] WeChatRegisterInput input)
|
||
{
|
||
// 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)
|
||
{
|
||
// 仅记录警告,防止配置不一致导致注册失败
|
||
_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
|
||
};
|
||
}
|
||
|
||
/// <summary>
|
||
/// 找回密码接口
|
||
/// </summary>
|
||
/// <param name="input"></param>
|
||
/// <returns></returns>
|
||
[HttpPost("reset-password")]
|
||
[AllowAnonymous]
|
||
public async Task<WeChatResetPasswordOutput> ResetPassword([FromBody] WeChatResetPasswordInput input)
|
||
{
|
||
// 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
|
||
{
|
||
string json = DecryptWeChatData(input.EncryptedData, sessionKey, input.Iv);
|
||
var wxUser = JsonConvert.DeserializeObject<WeChatUserInfo>(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
|
||
};
|
||
}
|
||
|
||
/// <summary>
|
||
/// AES-128-CBC 解密
|
||
/// </summary>
|
||
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("数据解密失败");
|
||
}
|
||
}
|
||
|
||
/// <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);
|
||
}
|
||
}
|