您正在查看静态缓存页面 · 查看完整动态版本 · 登录 参与讨论

第十三章:企业级应用:身份验证与安全

✨步子哥 (steper) 2026年02月17日 05:28 0 次浏览

第十三章:企业级应用:身份验证与安全

本章导读:想象你正在设计一座城堡的入口系统。你需要确认每一位访客的身份(身份验证),然后决定他们可以进入哪些房间(授权)。在数字世界中,这两项任务构成了应用安全的第一道防线。现代应用不再自己存储用户密码——那就像在城堡门口放一串钥匙一样危险。相反,我们将身份验证委托给专业的"守门人":IdentityServer、Auth0、Azure AD 或其他身份提供者。本章将带你穿越 OAuth2 和 OIDC 的迷宫,在 Uno Platform 中构建坚不可摧的认证系统。

🔐 13.1 安全性:企业级应用的基石

在深入技术细节之前,让我们先明确两个核心概念:身份验证(Authentication)授权(Authorization)。这两个词看起来很像,但它们的含义截然不同。

身份验证回答的问题是"你是谁?"——验证用户的身份,确认他们就是声称的那个人。这就像在机场安检时出示护照,工作人员核对你的照片和姓名。

授权回答的问题是"你能做什么?"——确定已认证用户可以访问哪些资源、执行哪些操作。这就像护照盖上的签证,它决定了你可以进入哪些国家。

第一性原理:为什么要将身份验证委托给第三方服务?答案在于安全专业主义。身份管理是一个极其复杂的领域——密码哈希、防暴力破解、多因素认证、令牌刷新、安全审计——每一个环节都需要专业知识。与其自己构建一个可能有漏洞的系统,不如使用那些经过时间检验、由安全专家维护的服务。这就像你不会自己建造银行金库,而是把钱存在专业的银行里一样。

🌐 13.1.1 现代认证架构概览

在传统的 Web 应用中,认证相对简单:用户提交用户名和密码,服务器验证后创建一个会话(Session),并在浏览器中设置一个 Cookie。但在移动应用和跨平台应用中,情况变得复杂得多。

首先,移动应用不能依赖 Cookie——它们需要使用令牌(Token)来维护认证状态。其次,用户可能希望在多个设备上登录,每个设备都需要独立的令牌管理。最后,出于安全考虑,我们不应该在应用中直接处理用户密码——密码应该只由用户输入到身份提供者的登录页面。

现代认证架构通常采用 OAuth 2.0OpenID Connect (OIDC) 协议。这些协议定义了一套标准的流程,让应用能够安全地获取用户身份,而无需直接接触用户的凭据。

┌─────────────┐        ┌─────────────┐        ┌─────────────┐
│   用户      │        │   应用      │        │  身份提供者  │
│  (Resource  │        │   (Client)  │        │    (IdP)    │
│   Owner)    │        │             │        │             │
└──────┬──────┘        └──────┬──────┘        └──────┬──────┘
       │                      │                      │
       │  1. 点击"登录"        │                      │
       │─────────────────────>│                      │
       │                      │                      │
       │  2. 打开浏览器登录页   │                      │
       │<─────────────────────│                      │
       │                      │                      │
       │  3. 输入用户名密码     │                      │
       │─────────────────────────────────────────────>│
       │                      │                      │
       │  4. 授权确认          │                      │
       │─────────────────────────────────────────────>│
       │                      │                      │
       │  5. 授权码回调         │                      │
       │<─────────────────────────────────────────────│
       │                      │                      │
       │  6. 传递授权码         │                      │
       │─────────────────────>│                      │
       │                      │                      │
       │                      │  7. 用授权码换取令牌   │
       │                      │─────────────────────>│
       │                      │                      │
       │                      │  8. 返回访问令牌      │
       │                      │<─────────────────────│
       │                      │                      │
       │  9. 登录成功          │                      │
       │<─────────────────────│                      │

🔑 13.2 现代认证协议:OIDC 与 OAuth2 深入解析

在开始编写代码之前,我们需要深入理解 OAuth 2.0 和 OpenID Connect 的工作原理。这两个协议构成了现代身份验证的基础。

📖 13.2.1 OAuth 2.0:授权框架

OAuth 2.0 是一个授权框架,它允许第三方应用在用户授权下,访问用户在某个服务上的资源,而无需用户提供密码。OAuth 2.0 定义了四种角色:

资源所有者(Resource Owner):通常是用户本人,拥有对受保护资源的访问权。

客户端(Client):你的应用,想要访问用户的资源。

授权服务器(Authorization Server):身份提供者的服务器,负责发放令牌。

资源服务器(Resource Server):存储用户资源的服务器,需要令牌才能访问。

OAuth 2.0 定义了几种授权流程(Grant Types),每种适用于不同的场景。对于移动应用和跨平台应用,授权码流程(Authorization Code Flow) 是最安全的选择。

🔄 13.2.2 授权码流程详解

授权码流程是目前最安全的 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_verifiercode_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"
}

🆔 13.2.3 OpenID Connect:身份层

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
}
费曼技巧提问:为什么授权码流程需要两步(先获取授权码,再换取令牌),而不是一步到位? 想象你要从银行取钱。如果只经过柜台人员,你告诉他们你的密码,他们就直接给你现金——这样做的问题是,柜台人员知道了你的密码。更安全的方式是:你先在一个私密房间里验证身份,得到一张"取款凭证"(授权码),然后用这张凭证在柜台取钱(换取令牌)。这样,柜台人员永远不知道你的密码,只知道你有合法的取款凭证。授权码流程就是这样设计的:浏览器和用户可见的部分只传递授权码,真正的令牌只在后端通道中传递。

🛠️ 13.3 使用 Uno.Extensions.Authentication 简化认证

Uno Platform 提供了 Uno.Extensions.Authentication 库,它封装了复杂的 OAuth 2.0 / OIDC 流程,让你只需几行配置就能实现完整的认证系统。

⚙️ 13.3.1 安装依赖

首先,在你的 Shared 项目中添加 NuGet 包:

<ItemGroup>
    <PackageReference Include="Uno.Extensions.Authentication" Version="*" />
    <PackageReference Include="Uno.Extensions.Authentication.Oidc" Version="*" />
    <PackageReference Include="Uno.Extensions.Hosting" Version="*" />
</ItemGroup>

🔧 13.3.2 配置认证服务

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();
    }
}

📱 13.3.3 处理认证回调

为了让应用能够接收认证回调,你需要配置平台特定的设置。

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>

📝 13.3.4 在 ViewModel 中实现登录逻辑

// 文件位置: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; // 需要用户重新登录
    }
}

🚪 13.3.5 登出实现

// 文件位置: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>();
    }
}

🏢 13.4 集成 Microsoft 认证(MSAL)

如果你的应用需要集成 Microsoft 365、Azure AD 或个人 Microsoft 账户,MSAL(Microsoft Authentication Library)是官方推荐的选择。

📦 13.4.1 安装 MSAL 包

<ItemGroup>
    <PackageReference Include="Microsoft.Identity.Client" Version="4.*" />
</ItemGroup>

🔧 13.4.2 配置 MSAL 应用

// 文件位置: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; }
}

👆 13.5 生物识别认证:Face ID 与指纹

为了提升用户体验,许多应用会在首次登录后提示用户开启生物识别。这样,用户不需要每次输入密码,只需验证指纹或面部即可快速登录。

🔬 13.5.1 使用 Windows Hello 风格 API

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;
    }
}

🔄 13.5.2 生物识别 + 令牌存储的登录流程

// 文件位置: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
}

⏰ 13.6 处理会话过期与令牌刷新

Access Token 通常只有短期的有效期(如 1 小时)。当它过期时,应用需要使用 Refresh Token 获取新的 Access Token,而不需要用户重新登录。

🔄 13.6.1 自动令牌刷新

// 文件位置: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());
    }
}

📡 13.6.2 在 HTTP 客户端中使用

// 文件位置: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 裁剪、内存优化等技术。


动手实验
  1. OAuth 演示应用:使用 Duende Software 的演示服务器(https://demo.duendesoftware.com)创建一个简单的登录演示应用。实现登录、获取用户信息、登出功能。观察令牌的生命周期和自动刷新行为。
  2. 生物识别开关:创建一个设置页面,允许用户启用或禁用生物识别快速登录。使用之前学习的 SecureStorage 保存用户的偏好设置。
  3. API 调用封装:实现一个完整的 API 服务类,包含自动令牌刷新、错误处理和重试逻辑。模拟一个需要认证的 API,测试令牌过期后的自动刷新行为。

讨论回复

0 条回复

还没有人回复