微信登录
This commit is contained in:
parent
735fd67b75
commit
558ae72e50
133
YD_Event.Application/Client/Dtos/MiniProgramDtos.cs
Normal file
133
YD_Event.Application/Client/Dtos/MiniProgramDtos.cs
Normal file
@ -0,0 +1,133 @@
|
||||
using System.ComponentModel.DataAnnotations;
|
||||
|
||||
namespace YD_Event.Application.Client.Dtos;
|
||||
|
||||
public class WeChatLoginInput
|
||||
{
|
||||
/// <summary>
|
||||
/// 微信临时授权码
|
||||
/// </summary>
|
||||
[Required(ErrorMessage = "code不能为空")]
|
||||
public string Code { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// 加密数据
|
||||
/// </summary>
|
||||
[Required(ErrorMessage = "encryptedData不能为空")]
|
||||
public string EncryptedData { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// 初始向量
|
||||
/// </summary>
|
||||
[Required(ErrorMessage = "iv不能为空")]
|
||||
public string Iv { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// 原始数据
|
||||
/// </summary>
|
||||
[Required(ErrorMessage = "rawData不能为空")]
|
||||
public string RawData { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// 签名
|
||||
/// </summary>
|
||||
[Required(ErrorMessage = "signature不能为空")]
|
||||
public string Signature { get; set; }
|
||||
}
|
||||
|
||||
public class WeChatLoginOutput
|
||||
{
|
||||
public string Openid { get; set; }
|
||||
public string SessionKey { get; set; }
|
||||
public string AccessToken { get; set; }
|
||||
public string RefreshToken { get; set; }
|
||||
public int ExpiresIn { get; set; }
|
||||
public object UserInfo { get; set; } // 简化的用户信息对象
|
||||
}
|
||||
|
||||
public class WeChatRegisterInput
|
||||
{
|
||||
/// <summary>
|
||||
/// OpenId
|
||||
/// </summary>
|
||||
[Required(ErrorMessage = "openid不能为空")]
|
||||
public string Openid { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// 加密数据
|
||||
/// </summary>
|
||||
[Required(ErrorMessage = "encryptedData不能为空")]
|
||||
public string EncryptedData { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// 初始向量
|
||||
/// </summary>
|
||||
[Required(ErrorMessage = "iv不能为空")]
|
||||
public string Iv { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// 原始数据
|
||||
/// </summary>
|
||||
[Required(ErrorMessage = "rawData不能为空")]
|
||||
public string RawData { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// 签名
|
||||
/// </summary>
|
||||
[Required(ErrorMessage = "signature不能为空")]
|
||||
public string Signature { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// 邀请码 (可选)
|
||||
/// </summary>
|
||||
public string? InviteCode { get; set; }
|
||||
}
|
||||
|
||||
public class WeChatRegisterOutput
|
||||
{
|
||||
public long UserId { get; set; }
|
||||
public string Nickname { get; set; }
|
||||
public string AvatarUrl { get; set; }
|
||||
public DateTime RegisterTime { get; set; }
|
||||
}
|
||||
|
||||
public class WeChatResetPasswordInput
|
||||
{
|
||||
[Required(ErrorMessage = "openid不能为空")]
|
||||
public string Openid { get; set; }
|
||||
|
||||
[Required(ErrorMessage = "encryptedData不能为空")]
|
||||
public string EncryptedData { get; set; }
|
||||
|
||||
[Required(ErrorMessage = "iv不能为空")]
|
||||
public string Iv { get; set; }
|
||||
|
||||
public string? Email { get; set; }
|
||||
public string? SmsCode { get; set; }
|
||||
}
|
||||
|
||||
public class WeChatResetPasswordOutput
|
||||
{
|
||||
public string ResetToken { get; set; }
|
||||
public DateTime ExpireTime { get; set; }
|
||||
public string SendChannel { get; set; } // "email" or "sms"
|
||||
}
|
||||
|
||||
public class WeChatUserInfo
|
||||
{
|
||||
public string OpenId { get; set; }
|
||||
public string NickName { get; set; }
|
||||
public int Gender { get; set; }
|
||||
public string City { get; set; }
|
||||
public string Province { get; set; }
|
||||
public string Country { get; set; }
|
||||
public string AvatarUrl { get; set; }
|
||||
public string UnionId { get; set; }
|
||||
public Watermark Watermark { get; set; }
|
||||
}
|
||||
|
||||
public class Watermark
|
||||
{
|
||||
public long Timestamp { get; set; }
|
||||
public string Appid { get; set; }
|
||||
}
|
||||
@ -1,18 +0,0 @@
|
||||
namespace YD_Event.Application.Client.Dtos;
|
||||
|
||||
public class OAuthAccountDetailOutput
|
||||
{
|
||||
/// <summary>
|
||||
/// 用户ID
|
||||
/// </summary>
|
||||
public long Id { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// 昵称
|
||||
/// </summary>
|
||||
public string NickName { get; set; }
|
||||
/// <summary>
|
||||
/// 头像
|
||||
/// </summary>
|
||||
public string Avatar { get; set; }
|
||||
}
|
||||
@ -1,185 +1,319 @@
|
||||
/*
|
||||
using YD_Event.Application.Auth;
|
||||
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 MrHuo.OAuth.QQ;
|
||||
using YD_Event.Core.Utils;
|
||||
using YD_Event.Application.Auth;
|
||||
using Newtonsoft.Json.Linq;
|
||||
using Newtonsoft.Json;
|
||||
|
||||
namespace YD_Event.Application.Client;
|
||||
|
||||
/// <summary>
|
||||
/// 第三方授权登录
|
||||
/// 微信小程序认证接口
|
||||
/// </summary>
|
||||
[ApiDescriptionSettings("微信小程序接口")]
|
||||
[Route("api/v1/mini-program/auth")]
|
||||
public class OAuthController : IDynamicApiController
|
||||
{
|
||||
/// <summary>
|
||||
/// 第三方授权缓存
|
||||
/// </summary>
|
||||
private const string OAuthKey = "oauth.";
|
||||
/// <summary>
|
||||
/// 授权成功后回调页面缓存键
|
||||
/// </summary>
|
||||
private const string OAuthRedirectKey = "oauth.redirect.";
|
||||
|
||||
private readonly QQOAuth _qqoAuth;
|
||||
private readonly AuthManager _authManager;
|
||||
private readonly WeChatTool _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 IHttpContextAccessor _httpContextAccessor;
|
||||
private readonly IIdGenerator _idGenerator;
|
||||
private readonly ILogger<OAuthController> _logger;
|
||||
|
||||
public OAuthController(QQOAuth qqoAuth,
|
||||
AuthManager authManager,
|
||||
private const string SessionKeyCachePrefix = "wechat_session_";
|
||||
|
||||
public OAuthController(
|
||||
WeChatTool weChatTool,
|
||||
ISqlSugarRepository<AuthAccount> accountRepository,
|
||||
ISqlSugarRepository<YD_Event.Core.Entities.AppUser> appUserRepository,
|
||||
ISqlSugarRepository<SysRole> roleRepository,
|
||||
ISqlSugarRepository<SysUserRole> userRoleRepository,
|
||||
IEasyCachingProvider easyCachingProvider,
|
||||
IHttpContextAccessor httpContextAccessor,
|
||||
IIdGenerator idGenerator)
|
||||
IIdGenerator idGenerator,
|
||||
ILogger<OAuthController> logger)
|
||||
{
|
||||
_qqoAuth = qqoAuth;
|
||||
_authManager = authManager;
|
||||
_weChatTool = weChatTool;
|
||||
_accountRepository = accountRepository;
|
||||
_appUserRepository = appUserRepository;
|
||||
_roleRepository = roleRepository;
|
||||
_userRoleRepository = userRoleRepository;
|
||||
_easyCachingProvider = easyCachingProvider;
|
||||
_httpContextAccessor = httpContextAccessor;
|
||||
_idGenerator = idGenerator;
|
||||
_logger = logger;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// 获取授权地址
|
||||
/// 登录接口
|
||||
/// </summary>
|
||||
/// <param name="type">授权登录类型</param>
|
||||
/// <param name="referer">回跳地址</param>
|
||||
/// <param name="input"></param>
|
||||
/// <returns></returns>
|
||||
[HttpGet("{type}")]
|
||||
[HttpPost("login")]
|
||||
[AllowAnonymous]
|
||||
public async Task<string> Get(string type, [FromQuery][Required(ErrorMessage = "缺少参数")] string referer)
|
||||
public async Task<WeChatLoginOutput> Login([FromBody] WeChatLoginInput input)
|
||||
{
|
||||
string code = _idGenerator.Encode(_idGenerator.NewLong());
|
||||
var request = _httpContextAccessor.HttpContext!.Request;
|
||||
await _easyCachingProvider.SetAsync($"{OAuthRedirectKey}{code}", referer, TimeSpan.FromMinutes(5));
|
||||
string authUrl = type.ToLower() switch
|
||||
// 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)
|
||||
{
|
||||
"qq" => _qqoAuth.GetAuthorizeUrl(code),
|
||||
_ => throw Oops.Bah("无效请求")
|
||||
// 生成 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
|
||||
};
|
||||
return authUrl;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// 授权回调
|
||||
/// 注册接口
|
||||
/// </summary>
|
||||
/// <param name="type">授权类型</param>
|
||||
/// <param name="code"></param>
|
||||
/// <param name="state">缓存唯一ID</param>
|
||||
/// <param name="input"></param>
|
||||
/// <returns></returns>
|
||||
[HttpGet("{type}/callback")]
|
||||
[HttpPost("register")]
|
||||
[AllowAnonymous]
|
||||
public async Task<IActionResult> Callback(string type, [FromQuery] string code, [FromQuery] string state)
|
||||
[UnitOfWork]
|
||||
public async Task<WeChatRegisterOutput> Register([FromBody] WeChatRegisterInput input)
|
||||
{
|
||||
if (string.IsNullOrWhiteSpace(state) || !await _easyCachingProvider.ExistsAsync($"{OAuthRedirectKey}{state}"))
|
||||
{
|
||||
throw Oops.Oh("缺少参数");
|
||||
}
|
||||
AuthAccount account;
|
||||
switch (type.ToLower())
|
||||
{
|
||||
case "qq":
|
||||
var auth = await _qqoAuth.AuthorizeCallback(code, state);
|
||||
if (!auth.IsSccess)
|
||||
{
|
||||
throw Oops.Bah(auth.ErrorMessage);
|
||||
}
|
||||
var info = auth.UserInfo;
|
||||
string openId = await _qqoAuth.GetOpenId(auth.AccessToken.AccessToken);
|
||||
account = await _accountRepository.AsQueryable().FirstAsync(x => x.OAuthId == openId && SqlFunc.ToLower(x.Type) == "qq");
|
||||
var gender = info.Gender == "男" ? Gender.Male :
|
||||
info.Gender == "女" ? Gender.Female : Gender.Unknown;
|
||||
if (account != null)
|
||||
{
|
||||
await _accountRepository.UpdateAsync(x => new AuthAccount()
|
||||
{
|
||||
Avatar = string.IsNullOrWhiteSpace(info.QQ100Avatar) ? info.Avatar : info.QQ100Avatar,
|
||||
Name = info.Name,
|
||||
Gender = gender
|
||||
},
|
||||
x => x.OAuthId == openId && SqlFunc.ToLower(x.Type) == "qq");
|
||||
}
|
||||
else
|
||||
{
|
||||
account = await _accountRepository.InsertReturnEntityAsync(new AuthAccount()
|
||||
{
|
||||
Gender = gender,
|
||||
Avatar = info.Avatar,
|
||||
Name = info.Name,
|
||||
OAuthId = openId,
|
||||
Type = "QQ"
|
||||
});
|
||||
}
|
||||
break;
|
||||
// 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;
|
||||
|
||||
default:
|
||||
throw Oops.Bah("无效请求");
|
||||
// 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}");
|
||||
}
|
||||
|
||||
string key = $"{OAuthKey}{state}";
|
||||
await _easyCachingProvider.SetAsync(key, account, TimeSpan.FromMinutes(3));
|
||||
//登录成功后的回调页面
|
||||
string url = (await _easyCachingProvider.GetAsync<string>($"{OAuthRedirectKey}{state}")).Value;//App.Configuration["oauth:redirect_uri"]?.TrimEnd('/');
|
||||
string redirect = url.Contains("?") ? $"{url}&code={state}" : $"{url}?code={state}";
|
||||
return new RedirectResult(redirect);
|
||||
// 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="code"></param>
|
||||
/// <param name="input"></param>
|
||||
/// <returns></returns>
|
||||
[HttpPost("login/{code}")]
|
||||
[HttpPost("reset-password")]
|
||||
[AllowAnonymous]
|
||||
public async Task<string> Login(string code)
|
||||
public async Task<WeChatResetPasswordOutput> ResetPassword([FromBody] WeChatResetPasswordInput input)
|
||||
{
|
||||
string key = $"{OAuthKey}{code}", key2 = $"{OAuthRedirectKey}{code}";
|
||||
var value = await _easyCachingProvider.GetAsync<AuthAccount>(key);
|
||||
if (!value.HasValue)
|
||||
// 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
|
||||
{
|
||||
throw Oops.Bah("无效参数");
|
||||
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();
|
||||
var account = value.Value;
|
||||
string token = JWTEncryption.Encrypt(new Dictionary<string, object>()
|
||||
string accessToken = JWTEncryption.Encrypt(new Dictionary<string, object>()
|
||||
{
|
||||
[AuthClaimsConst.AuthIdKey] = account.Id,
|
||||
[AuthClaimsConst.AccountKey] = account.OAuthId,
|
||||
[AuthClaimsConst.UuidKey] = uniqueId,
|
||||
[AuthClaimsConst.AuthPlatformTypeKey] = AuthPlatformType.WeChatApplet
|
||||
});
|
||||
// 获取刷新 token
|
||||
var refreshToken = JWTEncryption.GenerateRefreshToken(token);
|
||||
// 设置响应报文头
|
||||
_httpContextAccessor.HttpContext!.Response.Headers["access-token"] = token;
|
||||
_httpContextAccessor.HttpContext.Response.Headers["x-access-token"] = refreshToken;
|
||||
string url = (await _easyCachingProvider.GetAsync<string>(key2)).Value;
|
||||
await _easyCachingProvider.RemoveAsync(key);
|
||||
await _easyCachingProvider.RemoveAsync(key2);
|
||||
return url;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// 获取用户信息
|
||||
/// </summary>
|
||||
/// <returns></returns>
|
||||
[HttpGet]
|
||||
public async Task<OAuthAccountDetailOutput> UserInfo()
|
||||
{
|
||||
long id = _authManager.UserId;
|
||||
var account = await _accountRepository.AsQueryable().FirstAsync(x => x.Id == id);
|
||||
if (account == null)
|
||||
{
|
||||
throw Oops.Bah("用户不存在");
|
||||
}
|
||||
return new OAuthAccountDetailOutput
|
||||
{
|
||||
Id = account.Id,
|
||||
Avatar = account.Avatar,
|
||||
NickName = account.Name
|
||||
};
|
||||
// 获取刷新 token
|
||||
string refreshToken = JWTEncryption.GenerateRefreshToken(accessToken);
|
||||
|
||||
return (accessToken, refreshToken);
|
||||
}
|
||||
}
|
||||
*/
|
||||
@ -1400,19 +1400,95 @@
|
||||
失败结果列表
|
||||
</summary>
|
||||
</member>
|
||||
<member name="P:YD_Event.Application.Client.Dtos.OAuthAccountDetailOutput.Id">
|
||||
<member name="P:YD_Event.Application.Client.Dtos.WeChatLoginInput.Code">
|
||||
<summary>
|
||||
用户ID
|
||||
微信临时授权码
|
||||
</summary>
|
||||
</member>
|
||||
<member name="P:YD_Event.Application.Client.Dtos.OAuthAccountDetailOutput.NickName">
|
||||
<member name="P:YD_Event.Application.Client.Dtos.WeChatLoginInput.EncryptedData">
|
||||
<summary>
|
||||
昵称
|
||||
加密数据
|
||||
</summary>
|
||||
</member>
|
||||
<member name="P:YD_Event.Application.Client.Dtos.OAuthAccountDetailOutput.Avatar">
|
||||
<member name="P:YD_Event.Application.Client.Dtos.WeChatLoginInput.Iv">
|
||||
<summary>
|
||||
头像
|
||||
初始向量
|
||||
</summary>
|
||||
</member>
|
||||
<member name="P:YD_Event.Application.Client.Dtos.WeChatLoginInput.RawData">
|
||||
<summary>
|
||||
原始数据
|
||||
</summary>
|
||||
</member>
|
||||
<member name="P:YD_Event.Application.Client.Dtos.WeChatLoginInput.Signature">
|
||||
<summary>
|
||||
签名
|
||||
</summary>
|
||||
</member>
|
||||
<member name="P:YD_Event.Application.Client.Dtos.WeChatRegisterInput.Openid">
|
||||
<summary>
|
||||
OpenId
|
||||
</summary>
|
||||
</member>
|
||||
<member name="P:YD_Event.Application.Client.Dtos.WeChatRegisterInput.EncryptedData">
|
||||
<summary>
|
||||
加密数据
|
||||
</summary>
|
||||
</member>
|
||||
<member name="P:YD_Event.Application.Client.Dtos.WeChatRegisterInput.Iv">
|
||||
<summary>
|
||||
初始向量
|
||||
</summary>
|
||||
</member>
|
||||
<member name="P:YD_Event.Application.Client.Dtos.WeChatRegisterInput.RawData">
|
||||
<summary>
|
||||
原始数据
|
||||
</summary>
|
||||
</member>
|
||||
<member name="P:YD_Event.Application.Client.Dtos.WeChatRegisterInput.Signature">
|
||||
<summary>
|
||||
签名
|
||||
</summary>
|
||||
</member>
|
||||
<member name="P:YD_Event.Application.Client.Dtos.WeChatRegisterInput.InviteCode">
|
||||
<summary>
|
||||
邀请码 (可选)
|
||||
</summary>
|
||||
</member>
|
||||
<member name="T:YD_Event.Application.Client.OAuthController">
|
||||
<summary>
|
||||
微信小程序认证接口
|
||||
</summary>
|
||||
</member>
|
||||
<member name="M:YD_Event.Application.Client.OAuthController.Login(YD_Event.Application.Client.Dtos.WeChatLoginInput)">
|
||||
<summary>
|
||||
登录接口
|
||||
</summary>
|
||||
<param name="input"></param>
|
||||
<returns></returns>
|
||||
</member>
|
||||
<member name="M:YD_Event.Application.Client.OAuthController.Register(YD_Event.Application.Client.Dtos.WeChatRegisterInput)">
|
||||
<summary>
|
||||
注册接口
|
||||
</summary>
|
||||
<param name="input"></param>
|
||||
<returns></returns>
|
||||
</member>
|
||||
<member name="M:YD_Event.Application.Client.OAuthController.ResetPassword(YD_Event.Application.Client.Dtos.WeChatResetPasswordInput)">
|
||||
<summary>
|
||||
找回密码接口
|
||||
</summary>
|
||||
<param name="input"></param>
|
||||
<returns></returns>
|
||||
</member>
|
||||
<member name="M:YD_Event.Application.Client.OAuthController.DecryptWeChatData(System.String,System.String,System.String)">
|
||||
<summary>
|
||||
AES-128-CBC 解密
|
||||
</summary>
|
||||
</member>
|
||||
<member name="M:YD_Event.Application.Client.OAuthController.GenerateTokens(YD_Event.Core.Entities.AuthAccount)">
|
||||
<summary>
|
||||
生成自定义 Token
|
||||
</summary>
|
||||
</member>
|
||||
<member name="T:YD_Event.Application.Coupon.CouponService">
|
||||
|
||||
@ -101,6 +101,11 @@
|
||||
"scope": "get_user_info"
|
||||
}
|
||||
},
|
||||
// 微信小程序配置
|
||||
"WeChat": {
|
||||
"AppId": "wx1234567890abcdef",
|
||||
"Secret": "1234567890abcdef1234567890abcdef"
|
||||
},
|
||||
// 雪花id 文档:https://github.com/yitter/IdGenerator
|
||||
"SnowId": {
|
||||
"Method": 1, //雪花计算方法(1-漂移算法|2-传统算法),默认1
|
||||
|
||||
@ -37,6 +37,18 @@ public class AppUser : Entity<long>, ISoftDelete, ICreatedUserId, ICreatedTime,
|
||||
[SugarColumn(IsNullable = true)]
|
||||
public int? CertificationStatus { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// 昵称 (来自微信等第三方)
|
||||
/// </summary>
|
||||
[SugarColumn(Length = 64, IsNullable = true)]
|
||||
public string? NickName { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// 头像 (来自微信等第三方)
|
||||
/// </summary>
|
||||
[SugarColumn(Length = 512, IsNullable = true)]
|
||||
public string? Avatar { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// 姓名
|
||||
/// </summary>
|
||||
|
||||
@ -1,4 +1,4 @@
|
||||
using YD_Event.Core.Options;
|
||||
using YD_Event.Core.Options;
|
||||
|
||||
namespace Easy.Core;
|
||||
|
||||
@ -16,6 +16,7 @@ public static class ServiceCollectionExtension
|
||||
service.AddConfigurableOptions<SnowIdOptions>();
|
||||
service.AddConfigurableOptions<SmsOptions>();
|
||||
service.AddConfigurableOptions<AliyunOcrOptions>();
|
||||
service.AddConfigurableOptions<WeChatOptions>();
|
||||
return service;
|
||||
}
|
||||
}
|
||||
10
YD_Event.Core/Options/WeChatOptions.cs
Normal file
10
YD_Event.Core/Options/WeChatOptions.cs
Normal file
@ -0,0 +1,10 @@
|
||||
using Furion.ConfigurableOptions;
|
||||
|
||||
namespace YD_Event.Core.Options;
|
||||
|
||||
[OptionsSettings("WeChat")]
|
||||
public class WeChatOptions : IConfigurableOptions
|
||||
{
|
||||
public string AppId { get; set; }
|
||||
public string Secret { get; set; }
|
||||
}
|
||||
93
YD_Event.Core/Utils/WeChatTool.cs
Normal file
93
YD_Event.Core/Utils/WeChatTool.cs
Normal file
@ -0,0 +1,93 @@
|
||||
using System.Net.Http.Json;
|
||||
using Microsoft.Extensions.Logging;
|
||||
using Microsoft.Extensions.Options;
|
||||
using Newtonsoft.Json;
|
||||
using Polly;
|
||||
using Polly.Retry;
|
||||
using YD_Event.Core.Options;
|
||||
using Furion.DependencyInjection;
|
||||
using System.Net.Http;
|
||||
using System.Threading.Tasks;
|
||||
using Furion.FriendlyException;
|
||||
|
||||
namespace YD_Event.Core.Utils;
|
||||
|
||||
public class WeChatTool : ITransient
|
||||
{
|
||||
private readonly IHttpClientFactory _httpClientFactory;
|
||||
private readonly ILogger<WeChatTool> _logger;
|
||||
private readonly WeChatOptions _options;
|
||||
|
||||
public WeChatTool(IHttpClientFactory httpClientFactory, ILogger<WeChatTool> logger, IOptions<WeChatOptions> options)
|
||||
{
|
||||
_httpClientFactory = httpClientFactory;
|
||||
_logger = logger;
|
||||
_options = options.Value;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// code换取session_key
|
||||
/// </summary>
|
||||
/// <param name="code"></param>
|
||||
/// <returns></returns>
|
||||
public async Task<WeChatCode2SessionResponse> Code2Session(string code)
|
||||
{
|
||||
var url = $"https://api.weixin.qq.com/sns/jscode2session?appid={_options.AppId}&secret={_options.Secret}&js_code={code}&grant_type=authorization_code";
|
||||
|
||||
// 定义重试策略:3秒超时,重试2次
|
||||
var retryPolicy = Policy
|
||||
.Handle<HttpRequestException>()
|
||||
.Or<TaskCanceledException>()
|
||||
.WaitAndRetryAsync(2, retryAttempt => TimeSpan.FromSeconds(1), (exception, timeSpan, retryCount, context) =>
|
||||
{
|
||||
_logger.LogWarning($"WeChat API Request Failed. Retry {retryCount}. Error: {exception.Message}");
|
||||
});
|
||||
|
||||
var timeoutPolicy = Policy.TimeoutAsync(3); // 3秒超时
|
||||
|
||||
var policyWrap = Policy.WrapAsync(retryPolicy, timeoutPolicy);
|
||||
|
||||
try
|
||||
{
|
||||
return await policyWrap.ExecuteAsync(async () =>
|
||||
{
|
||||
var client = _httpClientFactory.CreateClient();
|
||||
var response = await client.GetAsync(url);
|
||||
response.EnsureSuccessStatusCode();
|
||||
var content = await response.Content.ReadAsStringAsync();
|
||||
var result = JsonConvert.DeserializeObject<WeChatCode2SessionResponse>(content);
|
||||
|
||||
if (result == null || result.ErrCode != 0)
|
||||
{
|
||||
_logger.LogError($"WeChat Code2Session Error: {content}");
|
||||
throw Oops.Bah($"微信接口调用失败: {result?.ErrMsg ?? "未知错误"}");
|
||||
}
|
||||
|
||||
return result;
|
||||
});
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
_logger.LogError(ex, "WeChat Code2Session Exception");
|
||||
throw Oops.Bah("微信服务暂不可用,请稍后重试");
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
public class WeChatCode2SessionResponse
|
||||
{
|
||||
[JsonProperty("openid")]
|
||||
public string OpenId { get; set; }
|
||||
|
||||
[JsonProperty("session_key")]
|
||||
public string SessionKey { get; set; }
|
||||
|
||||
[JsonProperty("unionid")]
|
||||
public string UnionId { get; set; }
|
||||
|
||||
[JsonProperty("errcode")]
|
||||
public int ErrCode { get; set; }
|
||||
|
||||
[JsonProperty("errmsg")]
|
||||
public string ErrMsg { get; set; }
|
||||
}
|
||||
@ -19,8 +19,10 @@
|
||||
<PackageReference Include="Hashids.net" Version="1.7.0" />
|
||||
<PackageReference Include="Lazy.Captcha.Core" Version="2.2.2" />
|
||||
<PackageReference Include="log4net" Version="3.2.0" />
|
||||
<PackageReference Include="Microsoft.Extensions.Http" Version="10.0.5" />
|
||||
<PackageReference Include="MrHuo.OAuth.QQ" Version="1.1.1" />
|
||||
<PackageReference Include="OnceMi.AspNetCore.OSS" Version="1.2.0" />
|
||||
<PackageReference Include="Polly" Version="8.6.6" />
|
||||
<PackageReference Include="QuestPDF" Version="2025.12.1" />
|
||||
<PackageReference Include="QRCoder" Version="1.4.3" />
|
||||
<PackageReference Include="DocumentFormat.OpenXml" Version="2.20.0" />
|
||||
|
||||
@ -485,6 +485,16 @@
|
||||
认证状态(教练特有):0-未认证,1-已认证
|
||||
</summary>
|
||||
</member>
|
||||
<member name="P:YD_Event.Core.Entities.AppUser.NickName">
|
||||
<summary>
|
||||
昵称 (来自微信等第三方)
|
||||
</summary>
|
||||
</member>
|
||||
<member name="P:YD_Event.Core.Entities.AppUser.Avatar">
|
||||
<summary>
|
||||
头像 (来自微信等第三方)
|
||||
</summary>
|
||||
</member>
|
||||
<member name="P:YD_Event.Core.Entities.AppUser.Name">
|
||||
<summary>
|
||||
姓名
|
||||
@ -4503,6 +4513,13 @@
|
||||
子选项
|
||||
</summary>
|
||||
</member>
|
||||
<member name="M:YD_Event.Core.Utils.WeChatTool.Code2Session(System.String)">
|
||||
<summary>
|
||||
code换取session_key
|
||||
</summary>
|
||||
<param name="code"></param>
|
||||
<returns></returns>
|
||||
</member>
|
||||
<member name="T:YD_Event.Application.Sms.AliyunSmsService">
|
||||
<summary>
|
||||
阿里云短信服务实现
|
||||
|
||||
Loading…
x
Reference in New Issue
Block a user