本章导读:想象你正在设计一座城堡的入口系统。你需要确认每一位访客的身份(身份验证),然后决定他们可以进入哪些房间(授权)。在数字世界中,这两项任务构成了应用安全的第一道防线。现代应用不再自己存储用户密码——那就像在城堡门口放一串钥匙一样危险。相反,我们将身份验证委托给专业的"守门人":IdentityServer、Auth0、Azure AD 或其他身份提供者。本章将带你穿越 OAuth2 和 OIDC 的迷宫,在 Uno Platform 中构建坚不可摧的认证系统。
在深入技术细节之前,让我们先明确两个核心概念:身份验证(Authentication) 和 授权(Authorization)。这两个词看起来很像,但它们的含义截然不同。
身份验证回答的问题是"你是谁?"——验证用户的身份,确认他们就是声称的那个人。这就像在机场安检时出示护照,工作人员核对你的照片和姓名。
授权回答的问题是"你能做什么?"——确定已认证用户可以访问哪些资源、执行哪些操作。这就像护照盖上的签证,它决定了你可以进入哪些国家。
第一性原理:为什么要将身份验证委托给第三方服务?答案在于安全专业主义。身份管理是一个极其复杂的领域——密码哈希、防暴力破解、多因素认证、令牌刷新、安全审计——每一个环节都需要专业知识。与其自己构建一个可能有漏洞的系统,不如使用那些经过时间检验、由安全专家维护的服务。这就像你不会自己建造银行金库,而是把钱存在专业的银行里一样。
在传统的 Web 应用中,认证相对简单:用户提交用户名和密码,服务器验证后创建一个会话(Session),并在浏览器中设置一个 Cookie。但在移动应用和跨平台应用中,情况变得复杂得多。
首先,移动应用不能依赖 Cookie——它们需要使用令牌(Token)来维护认证状态。其次,用户可能希望在多个设备上登录,每个设备都需要独立的令牌管理。最后,出于安全考虑,我们不应该在应用中直接处理用户密码——密码应该只由用户输入到身份提供者的登录页面。
现代认证架构通常采用 OAuth 2.0 和 OpenID Connect (OIDC) 协议。这些协议定义了一套标准的流程,让应用能够安全地获取用户身份,而无需直接接触用户的凭据。
┌─────────────┐ ┌─────────────┐ ┌─────────────┐
│ 用户 │ │ 应用 │ │ 身份提供者 │
│ (Resource │ │ (Client) │ │ (IdP) │
│ Owner) │ │ │ │ │
└──────┬──────┘ └──────┬──────┘ └──────┬──────┘
│ │ │
│ 1. 点击"登录" │ │
│─────────────────────>│ │
│ │ │
│ 2. 打开浏览器登录页 │ │
│<─────────────────────│ │
│ │ │
│ 3. 输入用户名密码 │ │
│─────────────────────────────────────────────>│
│ │ │
│ 4. 授权确认 │ │
│─────────────────────────────────────────────>│
│ │ │
│ 5. 授权码回调 │ │
│<─────────────────────────────────────────────│
│ │ │
│ 6. 传递授权码 │ │
│─────────────────────>│ │
│ │ │
│ │ 7. 用授权码换取令牌 │
│ │─────────────────────>│
│ │ │
│ │ 8. 返回访问令牌 │
│ │<─────────────────────│
│ │ │
│ 9. 登录成功 │ │
│<─────────────────────│ │
在开始编写代码之前,我们需要深入理解 OAuth 2.0 和 OpenID Connect 的工作原理。这两个协议构成了现代身份验证的基础。
OAuth 2.0 是一个授权框架,它允许第三方应用在用户授权下,访问用户在某个服务上的资源,而无需用户提供密码。OAuth 2.0 定义了四种角色:
资源所有者(Resource Owner):通常是用户本人,拥有对受保护资源的访问权。
客户端(Client):你的应用,想要访问用户的资源。
授权服务器(Authorization Server):身份提供者的服务器,负责发放令牌。
资源服务器(Resource Server):存储用户资源的服务器,需要令牌才能访问。
OAuth 2.0 定义了几种授权流程(Grant Types),每种适用于不同的场景。对于移动应用和跨平台应用,授权码流程(Authorization Code Flow) 是最安全的选择。
授权码流程是目前最安全的 OAuth 2.0 流程,它通过一个两步验证过程来确保令牌的安全。
第一步:获取授权码。当用户点击"登录"时,应用会打开系统浏览器,导航到身份提供者的授权端点。URL 中包含以下参数:
https://auth.example.com/authorize?
response_type=code&
client_id=my-app-client-id&
redirect_uri=myapp://callback&
scope=openid profile email&
state=random_state_string&
code_challenge=hashed_code_verifier&
code_challenge_method=S256
技术术语:PKCE(Proof Key for Code Exchange) 是授权码流程的一个安全扩展。它通过第二步:用户认证和授权。用户在浏览器中输入用户名和密码,并授权应用访问其信息。身份提供者验证用户身份后,会将浏览器重定向回应用:code_verifier和code_challenge防止授权码被恶意应用截获。在移动应用中,PKCE 是强制要求的,因为自定义 URL Scheme 可能被其他应用注册。
myapp://callback?code=authorization_code&state=random_state_string
第三步:用授权码换取令牌。应用捕获这个回调,提取授权码,然后向身份提供者的令牌端点发送一个后端请求:
POST /token HTTP/1.1
Host: auth.example.com
Content-Type: application/x-www-form-urlencoded
grant_type=authorization_code&
code=authorization_code&
redirect_uri=myapp://callback&
client_id=my-app-client-id&
code_verifier=original_code_verifier
身份提供者验证请求后,返回访问令牌和刷新令牌:
{
"access_token": "eyJhbGciOiJSUzI1NiIsInR5cCI6IkpXVCJ9...",
"token_type": "Bearer",
"expires_in": 3600,
"refresh_token": "dGhpcyBpcyBhIHJlZnJlc2ggdG9rZW4...",
"id_token": "eyJhbGciOiJSUzI1NiIsInR5cCI6IkpXVCJ9...",
"scope": "openid profile email"
}
OAuth 2.0 只是一个授权框架,它不关心用户是谁。OpenID Connect (OIDC) 在 OAuth 2.0 之上添加了一个身份层,提供了用户身份信息。
OIDC 引入了 ID Token 的概念,这是一个 JWT(JSON Web Token),包含了用户的身份信息。通过解码 ID Token,应用可以获取用户的唯一标识符、邮箱、姓名等信息,而无需额外请求。
// ID Token 解码后的内容示例
{
"sub": "user-unique-id-12345",
"name": "张三",
"email": "zhangsan@example.com",
"email_verified": true,
"picture": "https://example.com/avatar.jpg",
"iat": 1634567890,
"exp": 1634571490
}
费曼技巧提问:为什么授权码流程需要两步(先获取授权码,再换取令牌),而不是一步到位? 想象你要从银行取钱。如果只经过柜台人员,你告诉他们你的密码,他们就直接给你现金——这样做的问题是,柜台人员知道了你的密码。更安全的方式是:你先在一个私密房间里验证身份,得到一张"取款凭证"(授权码),然后用这张凭证在柜台取钱(换取令牌)。这样,柜台人员永远不知道你的密码,只知道你有合法的取款凭证。授权码流程就是这样设计的:浏览器和用户可见的部分只传递授权码,真正的令牌只在后端通道中传递。
Uno Platform 提供了 Uno.Extensions.Authentication 库,它封装了复杂的 OAuth 2.0 / OIDC 流程,让你只需几行配置就能实现完整的认证系统。
首先,在你的 Shared 项目中添加 NuGet 包:
<ItemGroup>
<PackageReference Include="Uno.Extensions.Authentication" Version="*" />
<PackageReference Include="Uno.Extensions.Authentication.Oidc" Version="*" />
<PackageReference Include="Uno.Extensions.Hosting" Version="*" />
</ItemGroup>
在 App.xaml.cs 中配置认证服务。Uno.Extensions 使用了现代化的主机(Host)模式,让依赖注入和配置管理变得简单。
// 文件位置:App.xaml.cs
using Microsoft.UI.Xaml;
using Uno.Extensions.Hosting;
using Uno.Extensions.Authentication;
using Uno.Extensions.Navigation;
public sealed partial class App : Application
{
protected async override void OnLaunched(LaunchActivatedEventArgs args)
{
// 创建应用主机构建器
var builder = this.CreateBuilder(args);
// 配置服务
builder
// 添加导航支持
.UseNavigation<App>()
// 配置认证服务
.UseAuthentication(auth =>
{
// 配置 OIDC 认证提供者
auth.AddOidc(oidc =>
{
// 身份提供者的基础地址
// 这里使用 Duende Software 的演示服务器作为示例
// 在生产环境中,替换为你自己的身份提供者地址
oidc.Authority = "https://demo.duendesoftware.com";
// 客户端 ID
// 在身份提供者处注册应用时获得
oidc.ClientId = "interactive.confidential";
// 客户端密钥(对于机密客户端)
// 注意:在移动应用中,客户端密钥不能真正保密
// 应该使用 PKCE 来增强安全性
oidc.ClientSecret = "secret";
// 请求的权限范围
// openid: 获取用户身份
// profile: 获取用户基本信息
// email: 获取用户邮箱
// offline_access: 获取刷新令牌
oidc.Scopes = new[] { "openid", "profile", "email", "offline_access" };
// 重定向 URI
// 这是用户登录成功后返回应用的地址
// 必须与身份提供者处注册的 URI 完全匹配
oidc.RedirectUri = "myapp://callback";
// 登出后的重定向 URI
oidc.PostLogoutRedirectUri = "myapp://logout";
// 自动刷新令牌
// 当访问令牌过期时,自动使用刷新令牌获取新的访问令牌
oidc.AutoRefreshToken = true;
});
})
// 配置其他服务
.ConfigureServices(services =>
{
// 注册你的服务
services.AddSingleton<IUserService, UserService>();
});
// 构建主机并启动应用
var window = builder.Window;
var host = builder.Build();
// 激活窗口
window.Activate();
}
}
为了让应用能够接收认证回调,你需要配置平台特定的设置。
Android 平台:在 Platforms/Android/AndroidManifest.xml 中添加 Intent Filter:
<activity android:name="crc64a0e0a82d0db9a07d.MainActivity"
android:exported="true">
<intent-filter>
<action android:name="android.intent.action.VIEW" />
<category android:name="android.intent.category.DEFAULT" />
<category android:name="android.intent.category.BROWSABLE" />
<!-- 自定义 URL Scheme -->
<data android:scheme="myapp" android:host="callback" />
</intent-filter>
</activity>
iOS 平台:在 Platforms/iOS/Info.plist 中添加 URL Type:
<key>CFBundleURLTypes</key>
<array>
<dict>
<key>CFBundleURLSchemes</key>
<array>
<string>myapp</string>
</array>
</dict>
</array>
// 文件位置:ViewModels/LoginViewModel.cs
using CommunityToolkit.Mvvm.ComponentModel;
using CommunityToolkit.Mvvm.Input;
using Uno.Extensions.Authentication;
using Uno.Extensions.Navigation;
public partial class LoginViewModel : ObservableObject
{
private readonly IAuthenticationService _authService;
private readonly INavigator _navigator;
[ObservableProperty]
private bool _isBusy;
[ObservableProperty]
private string _errorMessage;
public LoginViewModel(
IAuthenticationService authService,
INavigator navigator)
{
_authService = authService;
_navigator = navigator;
}
/// <summary>
/// 登录命令
/// </summary>
[RelayCommand]
public async Task LoginAsync()
{
if (IsBusy) return;
IsBusy = true;
ErrorMessage = null;
try
{
// 调用认证服务进行登录
// LoginAsync 会自动处理:
// 1. 打开系统浏览器导航到登录页面
// 2. 等待用户完成登录
// 3. 拦截回调 URL 并提取授权码
// 4. 用授权码换取令牌
// 5. 安全存储令牌
var result = await _authService.LoginAsync();
if (result.Success)
{
// 登录成功,导航到主页
await _navigator.NavigateViewModelAsync<MainViewModel>();
}
else
{
// 用户取消了登录,或登录失败
ErrorMessage = result.Error?.Message ?? "登录已取消";
}
}
catch (Exception ex)
{
ErrorMessage = $"登录失败: {ex.Message}";
}
finally
{
IsBusy = false;
}
}
/// <summary>
/// 静默登录(尝试使用已存储的令牌)
/// </summary>
public async Task<bool> TrySilentLoginAsync()
{
// 检查是否有有效的令牌
var isAuthenticated = await _authService.IsAuthenticatedAsync();
if (isAuthenticated)
{
// 有有效的令牌,直接导航到主页
await _navigator.NavigateViewModelAsync<MainViewModel>();
return true;
}
// 尝试使用刷新令牌获取新的访问令牌
var refreshResult = await _authService.RefreshLoginAsync();
if (refreshResult.Success)
{
await _navigator.NavigateViewModelAsync<MainViewModel>();
return true;
}
return false; // 需要用户重新登录
}
}
// 文件位置:ViewModels/MainViewModel.cs
public partial class MainViewModel : ObservableObject
{
private readonly IAuthenticationService _authService;
private readonly INavigator _navigator;
[ObservableProperty]
private string _userName;
[ObservableProperty]
private string _userEmail;
public MainViewModel(
IAuthenticationService authService,
INavigator navigator,
IUserService userService)
{
_authService = authService;
_navigator = navigator;
// 获取当前用户信息
var user = userService.GetCurrentUser();
UserName = user?.Name ?? "未知用户";
UserEmail = user?.Email ?? "";
}
/// <summary>
/// 登出命令
/// </summary>
[RelayCommand]
public async Task LogoutAsync()
{
// 调用认证服务登出
// LogoutAsync 会:
// 1. 清除本地存储的令牌
// 2. (可选)通知身份提供者撤销令牌
// 3. 清除用户会话
await _authService.LogoutAsync();
// 导航回登录页面
await _navigator.NavigateViewModelAsync<LoginViewModel>();
}
}
如果你的应用需要集成 Microsoft 365、Azure AD 或个人 Microsoft 账户,MSAL(Microsoft Authentication Library)是官方推荐的选择。
<ItemGroup>
<PackageReference Include="Microsoft.Identity.Client" Version="4.*" />
</ItemGroup>
// 文件位置:Services/MsalAuthService.cs
using Microsoft.Identity.Client;
public class MsalAuthService : IAuthService
{
private readonly IPublicClientApplication _pca;
private readonly string[] _scopes = new[] { "User.Read" };
public MsalAuthService()
{
// 创建公共客户端应用
_pca = PublicClientApplicationBuilder
.Create("your-client-id") // 在 Azure AD 中注册应用时获得
.WithAuthority(AzureCloudInstance.AzurePublic, "common") // 支持所有账户类型
.WithRedirectUri("msal{your-client-id}://auth") // MSAL 标准重定向 URI
.Build();
}
/// <summary>
/// 交互式登录
/// </summary>
public async Task<AuthResult> LoginAsync()
{
try
{
// 尝试静默获取令牌(如果已有缓存的令牌)
var accounts = await _pca.GetAccountsAsync();
var firstAccount = accounts.FirstOrDefault();
if (firstAccount != null)
{
try
{
var silentResult = await _pca
.AcquireTokenSilent(_scopes, firstAccount)
.ExecuteAsync();
return new AuthResult
{
Success = true,
AccessToken = silentResult.AccessToken,
Account = silentResult.Account
};
}
catch (MsalUiRequiredException)
{
// 需要交互式登录
}
}
// 交互式登录
var interactiveResult = await _pca
.AcquireTokenInteractive(_scopes)
.WithUseEmbeddedWebView(false) // 使用系统浏览器
.ExecuteAsync();
return new AuthResult
{
Success = true,
AccessToken = interactiveResult.AccessToken,
Account = interactiveResult.Account
};
}
catch (MsalException ex)
{
return new AuthResult
{
Success = false,
Error = ex.Message
};
}
}
/// <summary>
/// 获取访问令牌
/// </summary>
public async Task<string> GetAccessTokenAsync()
{
try
{
var accounts = await _pca.GetAccountsAsync();
var firstAccount = accounts.FirstOrDefault();
if (firstAccount == null)
{
return null; // 用户未登录
}
var result = await _pca
.AcquireTokenSilent(_scopes, firstAccount)
.ExecuteAsync();
return result.AccessToken;
}
catch (MsalUiRequiredException)
{
// 令牌过期或需要重新认证
return null;
}
}
/// <summary>
/// 登出
/// </summary>
public async Task LogoutAsync()
{
var accounts = await _pca.GetAccountsAsync();
foreach (var account in accounts)
{
await _pca.RemoveAsync(account);
}
}
}
public class AuthResult
{
public bool Success { get; set; }
public string AccessToken { get; set; }
public IAccount Account { get; set; }
public string Error { get; set; }
}
为了提升用户体验,许多应用会在首次登录后提示用户开启生物识别。这样,用户不需要每次输入密码,只需验证指纹或面部即可快速登录。
Uno Platform 提供了对生物识别的跨平台抽象。以下是一个简化版的实现:
// 文件位置:Services/BiometricService.cs
using Windows.Security.Credentials.UI;
public class BiometricService
{
/// <summary>
/// 检查设备是否支持生物识别
/// </summary>
public async Task<bool> IsAvailableAsync()
{
try
{
// UserConsentVerifier 是 WinUI 的生物识别 API
// 在不同平台上,它会调用:
// - Windows: Windows Hello
// - iOS: Face ID / Touch ID
// - Android: Fingerprint / Face Unlock
var availability = await UserConsentVerifier.CheckAvailabilityAsync();
return availability == UserConsentVerifierAvailability.Available;
}
catch
{
return false;
}
}
/// <summary>
/// 请求生物识别验证
/// </summary>
/// <param name="message">提示消息</param>
public async Task<bool> VerifyAsync(string message = "请验证身份")
{
try
{
var result = await UserConsentVerifier.RequestVerificationAsync(message);
return result == UserConsentVerificationResult.Verified;
}
catch (Exception ex)
{
System.Diagnostics.Debug.WriteLine($"生物识别验证失败: {ex.Message}");
return false;
}
}
/// <summary>
/// 检查并请求生物识别权限(iOS 特有)
/// </summary>
public async Task<bool> RequestPermissionAsync()
{
// 在 iOS 上,需要在 Info.plist 中添加权限说明
// <key>NSFaceIDUsageDescription</key>
// <string>我们需要 Face ID 来验证您的身份</string>
var isAvailable = await IsAvailableAsync();
return isAvailable;
}
}
// 文件位置:Services/QuickLoginService.cs
public class QuickLoginService
{
private readonly BiometricService _biometric;
private readonly SecureStorageService _secureStorage;
private readonly IAuthenticationService _authService;
public QuickLoginService(
BiometricService biometric,
SecureStorageService secureStorage,
IAuthenticationService authService)
{
_biometric = biometric;
_secureStorage = secureStorage;
_authService = authService;
}
/// <summary>
/// 尝试快速登录(使用生物识别)
/// </summary>
public async Task<QuickLoginResult> TryQuickLoginAsync()
{
// 1. 检查用户是否已登录
var isAuthenticated = await _authService.IsAuthenticatedAsync();
if (!isAuthenticated)
{
return QuickLoginResult.NeedFullLogin;
}
// 2. 检查是否启用了生物识别
var biometricEnabled = _secureStorage.GetCredential(
"settings", "biometric_enabled")?.Password == "true";
if (!biometricEnabled)
{
return QuickLoginResult.NeedFullLogin;
}
// 3. 检查设备是否支持生物识别
var biometricAvailable = await _biometric.IsAvailableAsync();
if (!biometricAvailable)
{
return QuickLoginResult.NeedFullLogin;
}
// 4. 请求生物识别验证
var verified = await _biometric.VerifyAsync("验证身份以进入应用");
if (verified)
{
// 验证成功,使用存储的令牌刷新登录状态
var refreshResult = await _authService.RefreshLoginAsync();
if (refreshResult.Success)
{
return QuickLoginResult.Success;
}
}
return QuickLoginResult.NeedFullLogin;
}
/// <summary>
/// 启用生物识别快速登录
/// </summary>
public async Task EnableBiometricAsync()
{
var available = await _biometric.IsAvailableAsync();
if (available)
{
// 验证一次生物识别以确认用户身份
var verified = await _biometric.VerifyAsync("验证身份以启用快速登录");
if (verified)
{
_secureStorage.SaveCredential("settings", "biometric_enabled", "true");
}
}
}
}
public enum QuickLoginResult
{
Success,
NeedFullLogin,
Failed
}
Access Token 通常只有短期的有效期(如 1 小时)。当它过期时,应用需要使用 Refresh Token 获取新的 Access Token,而不需要用户重新登录。
// 文件位置:Services/TokenRefreshHandler.cs
using System.Net.Http.Headers;
public class TokenRefreshHandler : DelegatingHandler
{
private readonly IAuthenticationService _authService;
private readonly SemaphoreSlim _refreshLock = new SemaphoreSlim(1, 1);
public TokenRefreshHandler(IAuthenticationService authService)
{
_authService = authService;
}
protected async override Task<HttpResponseMessage> SendAsync(
HttpRequestMessage request,
CancellationToken cancellationToken)
{
// 获取当前访问令牌
var accessToken = await _authService.GetAccessTokenAsync();
if (!string.IsNullOrEmpty(accessToken))
{
request.Headers.Authorization = new AuthenticationHeaderValue(
"Bearer",
accessToken
);
}
// 发送请求
var response = await base.SendAsync(request, cancellationToken);
// 如果返回 401,尝试刷新令牌并重试
if (response.StatusCode == System.Net.HttpStatusCode.Unauthorized)
{
var refreshSuccess = await RefreshTokenAsync();
if (refreshSuccess)
{
// 使用新令牌重试请求
var newAccessToken = await _authService.GetAccessTokenAsync();
request.Headers.Authorization = new AuthenticationHeaderValue(
"Bearer",
newAccessToken
);
response = await base.SendAsync(request, cancellationToken);
}
else
{
// 刷新失败,触发登出
// 应用应该导航到登录页面
OnSessionExpired();
}
}
return response;
}
private async Task<bool> RefreshTokenAsync()
{
// 使用锁确保只刷新一次
await _refreshLock.WaitAsync();
try
{
var result = await _authService.RefreshLoginAsync();
return result.Success;
}
finally
{
_refreshLock.Release();
}
}
private void OnSessionExpired()
{
// 发布会话过期事件
// 在实际应用中,可以使用消息中心或事件聚合器
Messenger.Publish(new SessionExpiredMessage());
}
}
// 文件位置:Services/ApiService.cs
public class ApiService
{
private readonly HttpClient _httpClient;
public ApiService(IAuthenticationService authService, IHttpMessageHandlerFactory handlerFactory)
{
// 创建带有令牌刷新处理器的 HTTP 客户端
var handler = new TokenRefreshHandler(authService)
{
InnerHandler = new HttpClientHandler()
};
_httpClient = new HttpClient(handler)
{
BaseAddress = new Uri("https://api.example.com/")
};
}
public async Task<UserProfile> GetProfileAsync()
{
var response = await _httpClient.GetAsync("user/profile");
response.EnsureSuccessStatusCode();
var json = await response.Content.ReadAsStringAsync();
return JsonSerializer.Deserialize<UserProfile>(json);
}
}
身份验证和安全是企业级应用的基石。通过本章的学习,你已经掌握了在 Uno Platform 中构建安全认证系统的核心技能。
让我们回顾本章的关键要点:
第一,现代认证架构将身份验证委托给专业的身份提供者,通过 OAuth 2.0 和 OIDC 协议实现安全的令牌获取流程。授权码流程配合 PKCE 是移动应用的最佳实践。
第二,Uno.Extensions.Authentication 库极大地简化了认证实现。几行配置就能完成浏览器弹出、回调拦截、令牌交换和存储等复杂操作。
第三,对于 Microsoft 生态系统的应用,MSAL 提供了对 Azure AD 和 Microsoft 账户的原生支持,并能自动处理令牌缓存和刷新。
第四,生物识别认证提供了便捷的用户体验。通过 Windows Hello 风格的 API,你可以在所有平台上实现统一的生物识别验证。
第五,令牌刷新和会话管理是安全性的重要组成部分。自动刷新机制确保用户不会因为令牌过期而频繁重新登录,同时保持了安全性。
在下一章中,我们将进入性能优化的领域——学习如何让你的 Uno 应用运行得更快、更轻量。我们将探讨 AOT 编译、IL 裁剪、内存优化等技术。
动手实验:
- OAuth 演示应用:使用 Duende Software 的演示服务器(https://demo.duendesoftware.com)创建一个简单的登录演示应用。实现登录、获取用户信息、登出功能。观察令牌的生命周期和自动刷新行为。
- 生物识别开关:创建一个设置页面,允许用户启用或禁用生物识别快速登录。使用之前学习的 SecureStorage 保存用户的偏好设置。
- API 调用封装:实现一个完整的 API 服务类,包含自动令牌刷新、错误处理和重试逻辑。模拟一个需要认证的 API,测试令牌过期后的自动刷新行为。
还没有人回复