初始化 monorepo: Go后端(7微服务) + Unity客户端(9模块) + 启动器 HTML5原型: Three.js 3D体素世界, Perlin噪声地形, 原版材质, 22种方块 Minecraft创造模式背包: 双栏布局, 拖拽移动物品, 方向性元件引脚 AI助搭策划文档 + 客户端/服务端骨架 + Docker Compose + CI
This commit is contained in:
@@ -0,0 +1,40 @@
|
||||
using System.Text.Json.Serialization;
|
||||
using PCL.Core.Utils.Exts;
|
||||
|
||||
namespace PCL.Core.Minecraft.IdentityModel.OAuth;
|
||||
|
||||
public record AuthorizeResult
|
||||
{
|
||||
public bool IsError => !Error.IsNullOrEmpty();
|
||||
/// <summary>
|
||||
/// 错误类型 (e.g. invalid_request)
|
||||
/// </summary>
|
||||
[JsonPropertyName("error")] public string? Error { get; init; }
|
||||
/// <summary>
|
||||
/// 描述此错误的文本
|
||||
/// </summary>
|
||||
[JsonPropertyName("error_description")] public string? ErrorDescription { get; init; }
|
||||
|
||||
// 不用 SecureString,因为这东西依赖 DPAPI,不是最佳实践
|
||||
|
||||
/// <summary>
|
||||
/// 访问令牌
|
||||
/// </summary>
|
||||
[JsonPropertyName("access_token")] public string? AccessToken { get; init; }
|
||||
/// <summary>
|
||||
/// 刷新令牌
|
||||
/// </summary>
|
||||
[JsonPropertyName("refresh_token")] public string? RefreshToken { get; init; }
|
||||
/// <summary>
|
||||
/// ID Token
|
||||
/// </summary>
|
||||
[JsonPropertyName("id_token")] public string? IdToken { get; init; }
|
||||
/// <summary>
|
||||
/// 令牌类型
|
||||
/// </summary>
|
||||
[JsonPropertyName("token_type")] public string? TokenType { get; init; }
|
||||
/// <summary>
|
||||
/// 过期时间
|
||||
/// </summary>
|
||||
[JsonPropertyName("expires_in")] public int? ExpiresIn { get; init; }
|
||||
}
|
||||
@@ -0,0 +1,155 @@
|
||||
using System;
|
||||
using System.Text;
|
||||
using System.Net.Http;
|
||||
using System.Text.Json;
|
||||
using System.Threading.Tasks;
|
||||
using System.Collections.Generic;
|
||||
using System.Threading;
|
||||
using PCL.Core.IO.Net.Http;
|
||||
using PCL.Core.Minecraft.IdentityModel;
|
||||
|
||||
namespace PCL.Core.Minecraft.IdentityModel.OAuth;
|
||||
|
||||
/// <summary>
|
||||
/// OAuth 客户端实现,配合 Polly 食用效果更佳
|
||||
/// </summary>
|
||||
/// <param name="options">OAuth 参数</param>
|
||||
public sealed class SimpleOAuthClient(OAuthClientOptions options):IOAuthClient
|
||||
{
|
||||
/// <summary>
|
||||
/// 获取授权 Url
|
||||
/// </summary>
|
||||
/// <param name="scopes">访问权限列表</param>
|
||||
/// <param name="state"></param>
|
||||
/// <param name="extData"></param>
|
||||
/// <returns></returns>
|
||||
public string GetAuthorizeUrl(string[] scopes,string state,Dictionary<string,string>? extData = null)
|
||||
{
|
||||
ArgumentException.ThrowIfNullOrWhiteSpace(options.Meta.AuthorizeEndpoint);
|
||||
var sb = new StringBuilder();
|
||||
sb.Append(options.Meta.AuthorizeEndpoint);
|
||||
sb.Append($"?response_type=code&scope={Uri.EscapeDataString(string.Join(" ", scopes))}");
|
||||
sb.Append($"&redirect_uri={Uri.EscapeDataString(options.RedirectUri)}");
|
||||
sb.Append($"&client_id={options.ClientId}&state={state}");
|
||||
if (extData is null) return sb.ToString();
|
||||
foreach (var kvp in extData)
|
||||
sb.Append($"&{kvp.Key}={Uri.EscapeDataString(kvp.Value)}");
|
||||
return sb.ToString();
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// 使用授权代码获取令牌
|
||||
/// </summary>
|
||||
/// <param name="code">授权代码</param>
|
||||
/// <param name="extData">附加属性,不应该包含必须参数和预定义字段 (e.g. client_id、grant_type)</param>
|
||||
/// <param name="token"></param>
|
||||
/// <returns></returns>
|
||||
public async Task<AuthorizeResult?> AuthorizeWithCodeAsync(
|
||||
string code,CancellationToken token,Dictionary<string,string>? extData = null
|
||||
)
|
||||
{
|
||||
extData ??= new Dictionary<string, string>();
|
||||
extData["client_id"] = options.ClientId;
|
||||
extData["grant_type"] = "authorization_code";
|
||||
extData["code"] = code;
|
||||
var client = options.GetClient.Invoke();
|
||||
using var content = new FormUrlEncodedContent(extData);
|
||||
using var response = await HttpRequest
|
||||
.CreatePost(options.Meta.TokenEndpoint)
|
||||
.WithContent(content)
|
||||
.WithHeaders(options.Headers ?? [])
|
||||
.SendAsync(client, cancellationToken: token)
|
||||
.ConfigureAwait(false);
|
||||
return await response
|
||||
.AsJsonAsync<AuthorizeResult>(cancellationToken: token)
|
||||
.ConfigureAwait(false);
|
||||
}
|
||||
/// <summary>
|
||||
/// 获取设备代码对
|
||||
/// </summary>
|
||||
/// <param name="scopes"></param>
|
||||
/// <param name="token"></param>
|
||||
/// <param name="extData"></param>
|
||||
/// <returns></returns>
|
||||
public async Task<DeviceCodeData?> GetCodePairAsync
|
||||
(string[] scopes,CancellationToken token, Dictionary<string, string>? extData = null)
|
||||
{
|
||||
ArgumentException.ThrowIfNullOrEmpty(options.Meta.DeviceEndpoint);
|
||||
var client = options.GetClient.Invoke();
|
||||
extData ??= new Dictionary<string, string>();
|
||||
extData["scope"] = string.Join(" ", scopes);
|
||||
extData["client_id"] = options.ClientId;
|
||||
var content = new FormUrlEncodedContent(extData);
|
||||
|
||||
using var response = await HttpRequest
|
||||
.CreatePost(options.Meta.DeviceEndpoint)
|
||||
.WithContent(content)
|
||||
.WithHeaders(options.Headers ?? [])
|
||||
.SendAsync(client, cancellationToken: token)
|
||||
.ConfigureAwait(false);
|
||||
|
||||
return await response
|
||||
.AsJsonAsync<DeviceCodeData>(cancellationToken: token)
|
||||
.ConfigureAwait(false);
|
||||
}
|
||||
/// <summary>
|
||||
/// 验证用户授权状态 <br/>
|
||||
/// 注:此方法不会检查是否过去了 Interval 秒,请自行处理
|
||||
/// </summary>
|
||||
/// <param name="data"></param>
|
||||
/// <param name="token"></param>
|
||||
/// <param name="extData"></param>
|
||||
/// <returns></returns>
|
||||
/// <exception cref="IdentityModelAuthenticationException">认证服务器返回设备授权错误</exception>
|
||||
public async Task<AuthorizeResult?> AuthorizeWithDeviceAsync
|
||||
(DeviceCodeData data,CancellationToken token,Dictionary<string,string>? extData = null)
|
||||
{
|
||||
if (data.IsError) throw new IdentityModelAuthenticationException(data.Error, data.ErrorDescription);
|
||||
var client = options.GetClient.Invoke();
|
||||
extData ??= new Dictionary<string, string>();
|
||||
extData["client_id"] = options.ClientId;
|
||||
extData["grant_type"] = "urn:ietf:params:oauth:grant-type:device_code";
|
||||
extData["device_code"] = data.DeviceCode!;
|
||||
|
||||
using var content = new FormUrlEncodedContent(extData);
|
||||
using var response = await HttpRequest
|
||||
.CreatePost(options.Meta.TokenEndpoint)
|
||||
.WithContent(content)
|
||||
.WithHeaders(options.Headers ?? [])
|
||||
.SendAsync(client, cancellationToken: token)
|
||||
.ConfigureAwait(false);
|
||||
|
||||
return await response
|
||||
.AsJsonAsync<AuthorizeResult>(cancellationToken: token)
|
||||
.ConfigureAwait(false);
|
||||
}
|
||||
/// <summary>
|
||||
/// 刷新登录
|
||||
/// </summary>
|
||||
/// <param name="data"></param>
|
||||
/// <param name="token"></param>
|
||||
/// <param name="extData"></param>
|
||||
/// <returns></returns>
|
||||
/// <exception cref="IdentityModelAuthenticationException">认证服务器返回刷新授权错误</exception>
|
||||
public async Task<AuthorizeResult?> AuthorizeWithSilentAsync
|
||||
(AuthorizeResult data,CancellationToken token,Dictionary<string,string>? extData = null)
|
||||
{
|
||||
var client = options.GetClient.Invoke();
|
||||
if (data.IsError) throw new IdentityModelAuthenticationException(data.Error, data.ErrorDescription);
|
||||
extData ??= [];
|
||||
extData["refresh_token"] = data.RefreshToken!;
|
||||
extData["grant_type"] = "refresh_token";
|
||||
extData["client_id"] = options.ClientId;
|
||||
using var content = new FormUrlEncodedContent(extData);
|
||||
using var response = await HttpRequest
|
||||
.CreatePost(options.Meta.TokenEndpoint)
|
||||
.WithHeaders(options.Headers ?? [])
|
||||
.WithContent(content)
|
||||
.SendAsync(client, cancellationToken: token)
|
||||
.ConfigureAwait(false);
|
||||
|
||||
return await response
|
||||
.AsJsonAsync<AuthorizeResult>(cancellationToken: token)
|
||||
.ConfigureAwait(false);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,26 @@
|
||||
using System;
|
||||
using System.Collections.Generic;
|
||||
using System.Net.Http;
|
||||
|
||||
namespace PCL.Core.Minecraft.IdentityModel.OAuth;
|
||||
|
||||
public record OAuthClientOptions
|
||||
{
|
||||
/// <summary>
|
||||
/// 请求头
|
||||
/// </summary>
|
||||
public Dictionary<string,string>? Headers { get; set; }
|
||||
/// <summary>
|
||||
/// 端点数据
|
||||
/// </summary>
|
||||
public required EndpointMeta Meta { get; set; }
|
||||
public required Func<HttpClient> GetClient { get; set; }
|
||||
/// <summary>
|
||||
/// 重定向 Uri
|
||||
/// </summary>
|
||||
public required string RedirectUri { get; set; }
|
||||
/// <summary>
|
||||
/// 客户端 ID
|
||||
/// </summary>
|
||||
public required string ClientId { get; set; }
|
||||
}
|
||||
@@ -0,0 +1,49 @@
|
||||
using System.Text.Json.Serialization;
|
||||
using PCL.Core.Utils.Exts;
|
||||
|
||||
namespace PCL.Core.Minecraft.IdentityModel.OAuth;
|
||||
|
||||
public record DeviceCodeData
|
||||
{
|
||||
public bool IsError => !Error.IsNullOrEmpty();
|
||||
/// <summary>
|
||||
/// 错误类型
|
||||
/// </summary>
|
||||
[JsonPropertyName("error")]
|
||||
public string? Error { get; init; }
|
||||
/// <summary>
|
||||
/// 错误描述
|
||||
/// </summary>
|
||||
[JsonPropertyName("error_description")]
|
||||
public string? ErrorDescription { get; init; }
|
||||
/// <summary>
|
||||
/// 用户授权码
|
||||
/// </summary>
|
||||
[JsonPropertyName("user_code")]
|
||||
public string? UserCode { get; init; }
|
||||
/// <summary>
|
||||
/// 设备授权码
|
||||
/// </summary>
|
||||
[JsonPropertyName("device_code")]
|
||||
public string? DeviceCode { get; init; }
|
||||
/// <summary>
|
||||
/// 验证 Uri
|
||||
/// </summary>
|
||||
[JsonPropertyName("verification_uri")]
|
||||
public string? VerificationUri { get; init; }
|
||||
/// <summary>
|
||||
/// 验证 Uri (自动填充代码)
|
||||
/// </summary>
|
||||
[JsonPropertyName("verification_uri_complete")]
|
||||
public string? VerificationUriComplete { get; init; }
|
||||
/// <summary>
|
||||
/// 轮询间隔
|
||||
/// </summary>
|
||||
[JsonPropertyName("interval")]
|
||||
public int? Interval { get; init; }
|
||||
/// <summary>
|
||||
/// 过期时间
|
||||
/// </summary>
|
||||
[JsonPropertyName("expires_in")]
|
||||
public int? ExpiresIn { get; init; }
|
||||
}
|
||||
@@ -0,0 +1,17 @@
|
||||
namespace PCL.Core.Minecraft.IdentityModel.OAuth;
|
||||
|
||||
public record EndpointMeta
|
||||
{
|
||||
/// <summary>
|
||||
/// 设备授权端点
|
||||
/// </summary>
|
||||
public string? DeviceEndpoint { get; set; }
|
||||
/// <summary>
|
||||
/// 授权端点
|
||||
/// </summary>
|
||||
public required string AuthorizeEndpoint { get; set; }
|
||||
/// <summary>
|
||||
/// 令牌端点
|
||||
/// </summary>
|
||||
public required string TokenEndpoint { get; set; }
|
||||
}
|
||||
@@ -0,0 +1,14 @@
|
||||
using System.Threading;
|
||||
using System.Threading.Tasks;
|
||||
using System.Collections.Generic;
|
||||
|
||||
namespace PCL.Core.Minecraft.IdentityModel.OAuth;
|
||||
|
||||
public interface IOAuthClient
|
||||
{
|
||||
public string GetAuthorizeUrl(string[] scopes,string state,Dictionary<string,string>? extData);
|
||||
public Task<AuthorizeResult?> AuthorizeWithCodeAsync(string code,CancellationToken token,Dictionary<string,string>? extData = null);
|
||||
public Task<DeviceCodeData?> GetCodePairAsync(string[] scopes,CancellationToken token, Dictionary<string, string>? extData = null);
|
||||
public Task<AuthorizeResult?> AuthorizeWithDeviceAsync(DeviceCodeData data,CancellationToken token,Dictionary<string,string>? extData = null);
|
||||
public Task<AuthorizeResult?> AuthorizeWithSilentAsync(AuthorizeResult data,CancellationToken token,Dictionary<string,string>? extData = null);
|
||||
}
|
||||
Reference in New Issue
Block a user