319 lines
12 KiB
C#
Raw Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

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 = 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
};
}
/// <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);
}
}