Files
tiku-backend.net/Tiku.Infrastructure/Auth/WechatOAuthClient.cs
2026-07-26 13:12:53 +08:00

116 lines
4.4 KiB
C#

using System.Net.Http.Json;
using System.Text.Json;
using Tiku.Application.Auth;
namespace Tiku.Infrastructure.Auth;
public sealed class WechatOAuthClient(HttpClient httpClient) : 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<WechatIdentity> ExchangeWebCodeAsync(
WechatProviderOptions options,
string code,
CancellationToken cancellationToken = default)
{
var tokenUri = BuildUri(WebAccessTokenEndpoint, new Dictionary<string, string>
{
["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");
var userInfoUri = BuildUri(WebUserInfoEndpoint, new Dictionary<string, string>
{
["access_token"] = accessToken,
["openid"] = openId,
["lang"] = "zh_CN"
});
using var userDocument = await GetWechatJsonAsync(userInfoUri, cancellationToken);
var user = userDocument.RootElement;
return new WechatIdentity(
openId,
OptionalString(user, "unionid") ?? unionId,
OptionalString(user, "nickname"),
OptionalString(user, "headimgurl"),
null,
user.GetRawText());
}
public async Task<WechatIdentity> ExchangeMiniAppCodeAsync(
WechatProviderOptions options,
string code,
CancellationToken cancellationToken = default)
{
var uri = BuildUri(MiniAppCode2SessionEndpoint, new Dictionary<string, string>
{
["appid"] = options.AppId,
["secret"] = options.AppSecret,
["js_code"] = code,
["grant_type"] = "authorization_code"
});
using var document = await GetWechatJsonAsync(uri, cancellationToken);
var root = document.RootElement;
return new WechatIdentity(
RequiredString(root, "openid", "wechat_openid_missing"),
OptionalString(root, "unionid"),
null,
null,
RequiredString(root, "session_key", "wechat_session_key_missing"),
root.GetRawText());
}
private async Task<JsonDocument> GetWechatJsonAsync(
Uri uri,
CancellationToken cancellationToken)
{
using var response = await httpClient.GetAsync(uri, cancellationToken);
response.EnsureSuccessStatusCode();
var document = await response.Content.ReadFromJsonAsync<JsonDocument>(cancellationToken)
?? throw new InvalidCredentialsException("wechat_empty_response");
if (document.RootElement.TryGetProperty("errcode", out var errcode) &&
errcode.ValueKind == JsonValueKind.Number &&
errcode.GetInt32() != 0)
{
throw new InvalidCredentialsException("wechat_code_exchange_failed");
}
return document;
}
private static Uri BuildUri(Uri endpoint, IReadOnlyDictionary<string, string> query)
{
var builder = new UriBuilder(endpoint);
builder.Query = string.Join(
'&',
query.Select(pair =>
$"{Uri.EscapeDataString(pair.Key)}={Uri.EscapeDataString(pair.Value)}"));
return builder.Uri;
}
private static string RequiredString(JsonElement element, string property, string errorCode)
{
return OptionalString(element, property) ?? throw new InvalidCredentialsException(errorCode);
}
private static string? OptionalString(JsonElement element, string property)
{
return element.TryGetProperty(property, out var value) &&
value.ValueKind == JsonValueKind.String &&
!string.IsNullOrWhiteSpace(value.GetString())
? value.GetString()!.Trim()
: null;
}
}