forked from gongxuegit/tiku-backend.net
feat: add wechat authentication
This commit is contained in:
115
Tiku.Infrastructure/Auth/WechatOAuthClient.cs
Normal file
115
Tiku.Infrastructure/Auth/WechatOAuthClient.cs
Normal file
@@ -0,0 +1,115 @@
|
||||
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;
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user