diff --git a/Directory.Packages.props b/Directory.Packages.props index 59a5902..d27b894 100644 --- a/Directory.Packages.props +++ b/Directory.Packages.props @@ -30,6 +30,9 @@ + + + diff --git a/Tiku.Infrastructure/Auth/WechatOAuthClient.cs b/Tiku.Infrastructure/Auth/WechatOAuthClient.cs index d2183f0..384c009 100644 --- a/Tiku.Infrastructure/Auth/WechatOAuthClient.cs +++ b/Tiku.Infrastructure/Auth/WechatOAuthClient.cs @@ -1,49 +1,65 @@ -using System.Net.Http.Json; using System.Text.Json; +using Senparc.Weixin.MP.AdvancedAPIs; +using Senparc.Weixin.WxOpen.AdvancedAPIs.Sns; using Tiku.Application.Auth; namespace Tiku.Infrastructure.Auth; -public sealed class WechatOAuthClient(HttpClient httpClient) : IWechatOAuthClient +public sealed class WechatOAuthClient : IWechatOAuthClient { - private static readonly Uri WebAccessTokenEndpoint = new("https://api.weixin.qq.com/sns/oauth2/access_token"); - private static readonly Uri WebUserInfoEndpoint = new("https://api.weixin.qq.com/sns/userinfo"); - private static readonly Uri MiniAppCode2SessionEndpoint = new("https://api.weixin.qq.com/sns/jscode2session"); - public async Task ExchangeWebCodeAsync( WechatProviderOptions options, string code, CancellationToken cancellationToken = default) { - var tokenUri = BuildUri(WebAccessTokenEndpoint, new Dictionary - { - ["appid"] = options.AppId, - ["secret"] = options.AppSecret, - ["code"] = code, - ["grant_type"] = "authorization_code" - }); - using var tokenDocument = await GetWechatJsonAsync(tokenUri, cancellationToken); - var token = tokenDocument.RootElement; - var accessToken = RequiredString(token, "access_token", "wechat_access_token_missing"); - var openId = RequiredString(token, "openid", "wechat_openid_missing"); - var unionId = OptionalString(token, "unionid"); + cancellationToken.ThrowIfCancellationRequested(); - var userInfoUri = BuildUri(WebUserInfoEndpoint, new Dictionary + try { - ["access_token"] = accessToken, - ["openid"] = openId, - ["lang"] = "zh_CN" - }); - using var userDocument = await GetWechatJsonAsync(userInfoUri, cancellationToken); - var user = userDocument.RootElement; + var token = await OAuthApi.GetAccessTokenAsync( + options.AppId, + options.AppSecret, + code, + "authorization_code"); + cancellationToken.ThrowIfCancellationRequested(); + EnsureSuccess(token.ErrorCodeValue, token.errmsg); - return new WechatIdentity( - openId, - OptionalString(user, "unionid") ?? unionId, - OptionalString(user, "nickname"), - OptionalString(user, "headimgurl"), - null, - user.GetRawText()); + if (string.IsNullOrWhiteSpace(token.access_token)) + { + throw new InvalidCredentialsException("wechat_access_token_missing"); + } + + if (string.IsNullOrWhiteSpace(token.openid)) + { + throw new InvalidCredentialsException("wechat_openid_missing"); + } + + var user = await OAuthApi.GetUserInfoAsync( + token.access_token, + token.openid, + Senparc.Weixin.Language.zh_CN); + cancellationToken.ThrowIfCancellationRequested(); + + return new WechatIdentity( + token.openid.Trim(), + FirstNonBlank(user.unionid, token.unionid), + NullIfBlank(user.nickname), + NullIfBlank(user.headimgurl), + null, + SerializeRaw(new + { + accessToken = token, + user + })); + } + catch (InvalidCredentialsException) + { + throw; + } + catch (Exception exception) + { + throw new InvalidCredentialsException(MapWechatException(exception)); + } } public async Task ExchangeMiniAppCodeAsync( @@ -51,65 +67,78 @@ public sealed class WechatOAuthClient(HttpClient httpClient) : IWechatOAuthClien string code, CancellationToken cancellationToken = default) { - var uri = BuildUri(MiniAppCode2SessionEndpoint, new Dictionary - { - ["appid"] = options.AppId, - ["secret"] = options.AppSecret, - ["js_code"] = code, - ["grant_type"] = "authorization_code" - }); - using var document = await GetWechatJsonAsync(uri, cancellationToken); - var root = document.RootElement; + cancellationToken.ThrowIfCancellationRequested(); - return new WechatIdentity( - RequiredString(root, "openid", "wechat_openid_missing"), - OptionalString(root, "unionid"), - null, - null, - RequiredString(root, "session_key", "wechat_session_key_missing"), - root.GetRawText()); + try + { + var result = await SnsApi.JsCode2JsonAsync( + options.AppId, + options.AppSecret, + code, + "authorization_code"); + cancellationToken.ThrowIfCancellationRequested(); + EnsureSuccess(result.ErrorCodeValue, result.errmsg); + + if (string.IsNullOrWhiteSpace(result.openid)) + { + throw new InvalidCredentialsException("wechat_openid_missing"); + } + + if (string.IsNullOrWhiteSpace(result.session_key)) + { + throw new InvalidCredentialsException("wechat_session_key_missing"); + } + + return new WechatIdentity( + result.openid.Trim(), + NullIfBlank(result.unionid), + null, + null, + result.session_key.Trim(), + SerializeRaw(result)); + } + catch (InvalidCredentialsException) + { + throw; + } + catch (Exception exception) + { + throw new InvalidCredentialsException(MapWechatException(exception)); + } } - private async Task GetWechatJsonAsync( - Uri uri, - CancellationToken cancellationToken) + private static void EnsureSuccess(int errorCode, string? errorMessage) { - using var response = await httpClient.GetAsync(uri, cancellationToken); - response.EnsureSuccessStatusCode(); - var document = await response.Content.ReadFromJsonAsync(cancellationToken) - ?? throw new InvalidCredentialsException("wechat_empty_response"); - - if (document.RootElement.TryGetProperty("errcode", out var errcode) && - errcode.ValueKind == JsonValueKind.Number && - errcode.GetInt32() != 0) + if (errorCode == 0) { - throw new InvalidCredentialsException("wechat_code_exchange_failed"); + return; } - return document; + throw new InvalidCredentialsException( + string.IsNullOrWhiteSpace(errorMessage) + ? "wechat_code_exchange_failed" + : $"wechat_code_exchange_failed:{errorCode}"); } - private static Uri BuildUri(Uri endpoint, IReadOnlyDictionary query) + private static string SerializeRaw(T value) { - var builder = new UriBuilder(endpoint); - builder.Query = string.Join( - '&', - query.Select(pair => - $"{Uri.EscapeDataString(pair.Key)}={Uri.EscapeDataString(pair.Value)}")); - return builder.Uri; + return JsonSerializer.Serialize(value); } - private static string RequiredString(JsonElement element, string property, string errorCode) + private static string? FirstNonBlank(params string?[] values) { - return OptionalString(element, property) ?? throw new InvalidCredentialsException(errorCode); + return values.Select(NullIfBlank).FirstOrDefault(value => value is not null); } - private static string? OptionalString(JsonElement element, string property) + private static string? NullIfBlank(string? value) { - return element.TryGetProperty(property, out var value) && - value.ValueKind == JsonValueKind.String && - !string.IsNullOrWhiteSpace(value.GetString()) - ? value.GetString()!.Trim() - : null; + return string.IsNullOrWhiteSpace(value) ? null : value.Trim(); + } + + private static string MapWechatException(Exception exception) + { + return exception is HttpRequestException + ? "wechat_http_error" + : "wechat_code_exchange_failed"; } } diff --git a/Tiku.Infrastructure/DependencyInjection.cs b/Tiku.Infrastructure/DependencyInjection.cs index 5f11a11..55e5971 100644 --- a/Tiku.Infrastructure/DependencyInjection.cs +++ b/Tiku.Infrastructure/DependencyInjection.cs @@ -40,7 +40,7 @@ public static class DependencyInjection services.AddScoped(); services.AddScoped(); services.AddScoped(); - services.AddHttpClient(); + services.AddScoped(); services.AddScoped(); services.AddScoped(); services.AddScoped(); diff --git a/Tiku.Infrastructure/Tiku.Infrastructure.csproj b/Tiku.Infrastructure/Tiku.Infrastructure.csproj index 800c076..9092d95 100644 --- a/Tiku.Infrastructure/Tiku.Infrastructure.csproj +++ b/Tiku.Infrastructure/Tiku.Infrastructure.csproj @@ -14,6 +14,9 @@ + + +