Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
26 commits
Select commit Hold shift + click to select a range
a1a9100
我们孤身闯入这世界里 找寻名为宝藏的通关奖励
copytiao Feb 15, 2026
f10b822
越过深渊里迷人的金币 最后发现 珍贵是我和你
copytiao Feb 15, 2026
aa9fd4e
终于你可以说起那些心碎的过去 不过是 一场 狂风暴雨
copytiao Feb 16, 2026
59c5ffa
传说中偏爱少年勇敢举起的手臂 我爱你 笨拙 的心
copytiao Feb 16, 2026
81ebe6a
从蹒跚 到奔袭 从沙漠 到荆棘 你已赤脚穿过 这片陆地
copytiao Feb 16, 2026
44a5847
习惯了 空欢喜 学会了不哭泣 每颗珍珠都曾是 痛过的沙粒
copytiao Feb 16, 2026
a4b9a31
我在等你 找到你 一直到太阳升起 多少次坠下谷底 也能抱住自己
copytiao Feb 16, 2026
aa49183
山上的风 地心的力 生命向上长成了自己 那时你会看到春野满地
copytiao Feb 16, 2026
ecf2387
从失眠 到失意 从失落 到失去 多少痛的潮汐 曾吻过你
copytiao Feb 16, 2026
1b5b9a9
推开门 走出去 世界也在等着你 你是珍珠要亲手捧出你自己
copytiao Feb 21, 2026
3e0ab3f
此刻的你 找到你 我看见大雾散去 感谢你无数次拼命地拉住了自己
copytiao Feb 21, 2026
e46c27d
肩上的风 和手心的力 会化作你掌纹的痕迹 每个日出拥抱崭新的你
copytiao Feb 21, 2026
b3bfeb6
我要陪你 走下去 一直到无边天际 感谢我们无数次交付彼此的勇气
copytiao Feb 21, 2026
1f4a815
好的天气 坏的运气 都是值得庆祝的相遇 当我们开始真的爱自己
copytiao Feb 21, 2026
0e15c9d
传说的宝藏就是你
copytiao Feb 21, 2026
53917bf
Tun on the light 跟上这节拍
copytiao Feb 21, 2026
d446d78
Dance all right 梦驱散阴霾
copytiao Feb 21, 2026
8b08e12
要全世界喝彩 搭建我的舞台 就现在
copytiao Feb 23, 2026
4fd5e91
Merge branch 'dev' into feat/IdentityModel
copytiao Feb 23, 2026
385e990
种子在发芽像梦想给出回答
copytiao Feb 23, 2026
72a9077
I will be paying 一定会实现它
copytiao Feb 23, 2026
9586a8e
阳光倾洒 像希望就在脚下
copytiao Feb 23, 2026
b66d3ae
I wll be paying 烦恼都归于蒸发
copytiao Feb 23, 2026
6939e4b
Merge branch 'dev' into feat/IdentityModel
copytiao Feb 23, 2026
7c6ec77
被雨水浇灌的花 才会更鲜艳更无价
copytiao Feb 23, 2026
ad78cbc
就像舞台上的你啊
copytiao Feb 23, 2026
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Original file line number Diff line number Diff line change
@@ -0,0 +1,9 @@
using Microsoft.IdentityModel.Tokens;
using System.Text.Json.Serialization;

namespace PCL.Core.Minecraft.IdentityModel.Extensions.JsonWebToken;

public record JsonWebKeys
{
[JsonPropertyName("keys")] public required JsonWebKey[] Keys;
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,272 @@
using System;
using System.Collections.Generic;
using System.IdentityModel.Tokens.Jwt;
using System.Security;
using System.Text.Json;
using Microsoft.IdentityModel.Tokens;
using PCL.Core.Minecraft.IdentityModel.Extensions.OpenId;

namespace PCL.Core.Minecraft.IdentityModel.Extensions.JsonWebToken;

/// <summary>
/// Json Web Token 类
/// </summary>
/// <param name="token">JWT 令牌字符串</param>
/// <param name="meta">OpenID 元数据</param>
public class JsonWebToken(string token, OpenIdMetadata meta)
{
public delegate SecurityToken? TokenValidateCallback(OpenIdMetadata metadata, string token, JsonWebKey? key, string? clientId);

/// <summary>
/// 安全令牌验证回调函数,默认验证签名、发行者、nbf 和 exp
/// </summary>
public TokenValidateCallback SecurityTokenValidateCallback { get; set; } = static (meta, token, key, clientId) =>
{
try
{
var handler = new JwtSecurityTokenHandler();

var parameter = new TokenValidationParameters
{
ValidateIssuer = true,
ValidIssuer = meta.Issuer,
ValidateAudience = !string.IsNullOrEmpty(clientId),
ValidAudience = clientId,
ValidateIssuerSigningKey = key != null,
IssuerSigningKey = key != null ? new JsonWebKeySet { Keys = { key } }.Keys[0] : null,
ValidateLifetime = true,
ClockSkew = TimeSpan.FromSeconds(60)
};

handler.ValidateToken(token, parameter, out var secToken);
return secToken;
}
catch (Exception ex)
{
throw new SecurityException($"令牌验证失败:{ex.Message}", ex);
}
};

private bool _verified;
private JwtSecurityToken? _parsedToken;
private readonly JwtSecurityTokenHandler _tokenHandler = new();

/// <summary>
/// 解析令牌(不验证签名)
/// </summary>
/// <returns>解析后的 JWT 令牌对象</returns>
/// <exception cref="SecurityException">令牌格式无效</exception>
private JwtSecurityToken _ParseToken()
{
if (_parsedToken != null)
return _parsedToken;

try
{
if (!_tokenHandler.CanReadToken(token))
throw new SecurityException("无法读取令牌:格式无效");

_parsedToken = _tokenHandler.ReadJwtToken(token);
return _parsedToken;
}
catch (Exception ex)
{
throw new SecurityException($"令牌解析失败:{ex.Message}", ex);
}
}

/// <summary>
/// 尝试读取 Token 中的字段
/// </summary>
/// <param name="allowUnverifyToken">是否允许在未验证的情况下读取字段,若为 false,当 Token 未验证时将抛出异常</param>
/// <typeparam name="T">声明值的目标类型</typeparam>
/// <returns>解析后的声明对象</returns>
/// <exception cref="SecurityException">未调用 VerifySignature() 且 allowUnverifyToken 为 false</exception>
/// <exception cref="InvalidOperationException">令牌中不存在 payload 数据</exception>
public T? ReadTokenPayload<T>(bool allowUnverifyToken = false)
{
if (!allowUnverifyToken && !_verified)
throw new SecurityException("不安全的令牌");

try
{
var jwtToken = _ParseToken();

if (jwtToken.Payload == null || jwtToken.Payload.Count == 0)
throw new InvalidOperationException("令牌 Payload 无效");

if (typeof(T).IsAssignableFrom(typeof(Dictionary<string, object>)))
return (T)(object)jwtToken.Payload;

if (typeof(T) == typeof(JwtPayload))
return (T)(object)jwtToken.Payload;

var payloadJson = JsonSerializer.Serialize(jwtToken.Payload);
var result = JsonSerializer.Deserialize<T>(payloadJson);

return result;
}
catch (SecurityException)
{
throw;
}
catch (Exception ex)
{
throw new SecurityException($"读取令牌 payload 失败:{ex.Message}", ex);
}
}

/// <summary>
/// 读取 Token 头
/// </summary>
/// <typeparam name="T">声明值的目标类型</typeparam>
/// <returns>解析后的头对象</returns>
/// <exception cref="InvalidOperationException">令牌中不存在 header 数据</exception>
public T? ReadTokenHeader<T>()
{
try
{
var jwtToken = _ParseToken();

if (jwtToken.Header == null || jwtToken.Header.Count == 0)
throw new InvalidOperationException("令牌中不存在 header 数据");

if (typeof(T).IsAssignableFrom(typeof(Dictionary<string, object>)))
return (T)(object)jwtToken.Header;

if (typeof(T) == typeof(JwtHeader))
return (T)(object)jwtToken.Header;

var headerJson = JsonSerializer.Serialize(jwtToken.Header);
var result = JsonSerializer.Deserialize<T>(headerJson);

return result;
}
catch (Exception ex)
{
throw new SecurityException($"读取令牌 header 失败:{ex.Message}", ex);
}
}

/// <summary>
/// 对 Token 进行签名验证 <br/>
/// 默认情况下仅对签名、iss、nbf、exp 进行验证,如果需要更细粒度验证,请设置 <see cref="SecurityTokenValidateCallback"/>
/// </summary>
/// <param name="key">用于验证签名的 JSON Web Key</param>
/// <param name="clientId">预期的受众(audience),可选</param>
/// <returns>验证成功返回 SecurityToken 对象,否则返回 null</returns>
public SecurityToken? VerifySignature(JsonWebKey key, string? clientId = null)
{
try
{
var result = SecurityTokenValidateCallback.Invoke(meta, token, key, clientId);
if (result != null)
_verified = true;
return result;
}
catch (Exception ex)
{
throw new SecurityException($"令牌签名验证失败:{ex.Message}", ex);
}
}

/// <summary>
/// 重载方法,用于无参调用验证(仅验证基本声明)
/// </summary>
/// <returns>验证成功返回 SecurityToken 对象,否则返回 null</returns>
public SecurityToken? VerifySignature()
{
try
{
var result = SecurityTokenValidateCallback.Invoke(meta, token, null, null);
if (result != null)
_verified = true;
return result;
}
catch (Exception ex)
{
throw new SecurityException($"令牌验证失败:{ex.Message}", ex);
}
}

/// <summary>
/// 获取令牌的过期时间
/// </summary>
/// <returns>过期时间,若不存在则返回 null</returns>
public DateTime? GetExpirationTime()
{
try
{
var jwtToken = _ParseToken();
return jwtToken.ValidTo != DateTime.MinValue ? jwtToken.ValidTo : null;
}
catch (Exception ex)
{
throw new SecurityException($"获取令牌过期时间失败:{ex.Message}", ex);
}
}

/// <summary>
/// 获取令牌的签发时间
/// </summary>
/// <returns>签发时间,若不存在则返回 null</returns>
public DateTime? GetIssuedAtTime()
{
try
{
var jwtToken = _ParseToken();
return jwtToken.ValidFrom != DateTime.MinValue ? jwtToken.ValidFrom : null;
}
catch (Exception ex)
{
throw new SecurityException($"获取令牌签发时间失败:{ex.Message}", ex);
}
}

/// <summary>
/// 检查令牌是否已过期
/// </summary>
/// <returns>若已过期返回 true,否则返回 false</returns>
public bool IsExpired()
{
try
{
var expTime = GetExpirationTime();
return expTime.HasValue && DateTime.UtcNow > expTime.Value;
}
catch
{
return true; // 如果无法解析,视为已过期
}
}

/// <summary>
/// 获取特定声明的值
/// </summary>
/// <param name="claimType">声明类型</param>
/// <param name="allowUnverifyToken">是否允许在未验证的情况下读取</param>
/// <returns>声明值,若不存在则返回 null</returns>
public string? GetClaimValue(string claimType, bool allowUnverifyToken = false)
{
try
{
var payload = ReadTokenPayload<Dictionary<string, object>>(allowUnverifyToken);
return payload?.TryGetValue(claimType, out var value) ?? false ? value.ToString() : null;
}
catch (Exception ex)
{
throw new SecurityException($"获取声明值失败({claimType}):{ex.Message}", ex);
}
}

/// <summary>
/// 获取原始令牌字符串
/// </summary>
/// <returns>JWT 令牌字符串</returns>
public string GetTokenString() => token;

/// <summary>
/// 检查令牌验证状态
/// </summary>
public bool IsVerified => _verified;
}
97 changes: 97 additions & 0 deletions PCL.Core/Minecraft/IdentityModel/Extensions/OpenId/OpenIdClient.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,97 @@
using System;
using System.Collections.Generic;
using System.Threading;
using System.Threading.Tasks;
using System.Windows.Documents;
using PCL.Core.Minecraft.IdentityModel.Extensions.Pkce;
using PCL.Core.Minecraft.IdentityModel.OAuth;
using PCL.Core.Utils.Exts;

namespace PCL.Core.Minecraft.IdentityModel.Extensions.OpenId;

public class OpenIdClient(OpenIdOptions options):IOAuthClient
{
private IOAuthClient? _client;
/// <summary>
/// 初始化并从网络加载 OpenId 配置
/// </summary>
/// <param name="token"></param>
/// <param name="checkAddress"></param>
/// <exception cref="InvalidOperationException">当要求检查地址并不存在任何授权端点时,将触发此错误</exception>
public async Task InitializeAsync(CancellationToken token,bool checkAddress = false)
{
var opt = await options.BuildOAuthOptionsAsync(token);
if (!checkAddress || opt.Meta.AuthorizeEndpoint.IsNullOrEmpty() || opt.Meta.DeviceEndpoint.IsNullOrEmpty())
{
_client = options.EnablePkceSupport ? new PkceClient(opt) : new SimpleOAuthClient(opt);
return;
}

throw new InvalidOperationException();
}
/// <summary>
/// 获取授权代码流地址
/// </summary>
/// <param name="scopes">权限列表</param>
/// <param name="state"></param>
/// <param name="extData">扩展数据</param>
/// <returns></returns>
/// <exception cref="InvalidOperationException">未调用 </exception>
public string GetAuthorizeUrl(string[] scopes, string state,Dictionary<string,string>? extData = null)
{
if (_client is null) throw new InvalidOperationException();
return _client.GetAuthorizeUrl(scopes, state, extData);
}
/// <summary>
/// 使用授权代码兑换 Token
/// </summary>
/// <param name="code"></param>
/// <param name="token"></param>
/// <param name="extData"></param>
/// <returns></returns>
/// <exception cref="InvalidOperationException"></exception>
public async Task<AuthorizeResult?> AuthorizeWithCodeAsync(string code, CancellationToken token, Dictionary<string, string>? extData = null)
{
if (_client is null) throw new InvalidOperationException();
return await _client.AuthorizeWithCodeAsync(code, token, extData);
}
/// <summary>
/// 获取设备代码流代码对
/// </summary>
/// <param name="scopes"></param>
/// <param name="token"></param>
/// <param name="extData"></param>
/// <returns></returns>
/// <exception cref="InvalidOperationException"></exception>
public async Task<DeviceCodeData?> GetCodePairAsync(string[] scopes, CancellationToken token, Dictionary<string, string>? extData = null)
{
if (_client is null) throw new InvalidOperationException();
return await _client.GetCodePairAsync(scopes, token, extData);
}
/// <summary>
/// 发起一次验证,以检查认证是否成功
/// </summary>
/// <param name="data"></param>
/// <param name="token"></param>
/// <param name="extData"></param>
/// <returns></returns>
/// <exception cref="InvalidOperationException"></exception>
public async Task<AuthorizeResult?> AuthorizeWithDeviceAsync(DeviceCodeData data, CancellationToken token, Dictionary<string, string>? extData = null)
{
if (_client is null) throw new InvalidOperationException();
return await _client.AuthorizeWithDeviceAsync(data, token, extData);
}
/// <summary>
/// 进行一次刷新调用
/// </summary>
/// <param name="data"></param>
/// <param name="token"></param>
/// <param name="extData"></param>
/// <returns></returns>
/// <exception cref="InvalidOperationException"></exception>
public async Task<AuthorizeResult?> AuthorizeWithSilentAsync(AuthorizeResult data, CancellationToken token, Dictionary<string, string>? extData = null)
{
if (_client is null) throw new InvalidOperationException();
return await _client.AuthorizeWithSilentAsync(data, token, extData);
}
}
Loading