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 @@
+
+
+