NextUI 用户身份与认证设计方案

一、问题分析

当前状态

  • IUserPreferences - 存储在浏览器 localStorage/Cookie,无用户概念
  • IUserState - 简单的用户状态,无认证机制
  • 所有数据都是客户端本地存储,无服务端持久化

目标场景

  1. 单应用认证 - 一个应用使用 OpenID Connect 登录
  2. 多应用 SSO - 多个 NextUI 应用共享同一用户身份
  3. 混合用户类型 - 内部员工 + 外部客户使用同一系统

核心决策

决策点 建议 理由
放在哪个包? NextUI.Identity (新包) 不与 UI 混合,可独立升级
做认证服务? 不做 复杂度高,已有成熟方案
做什么? 客户端集成层 对接 OIDC,管理用户状态

二、架构设计

整体架构

┌─────────────────────────────────────────────────────────────────┐
│                        NextUI 应用                               │
├─────────────────────────────────────────────────────────────────┤
│  NextUI.Blazor (UI)              │  NextUI.App (应用层)          │
│  ├── SxLoginButton               │  ├── App-specific services   │
│  ├── SxUserMenu                  │  └── ...                     │
│  └── SxAuthGuard                 │                              │
├──────────────────────────────────┼──────────────────────────────┤
│              NextUI.Identity (新包)                              │
│  ├── IIdentityService         用户身份服务                       │
│  ├── IUserProfileService      用户资料服务                       │
│  ├── IUserPreferencesSync     偏好同步服务                       │
│  ├── OpenIdConnectHandler     OIDC 处理器                       │
│  └── UserContext              用户上下文                         │
├─────────────────────────────────────────────────────────────────┤
│              NextUI.Core (核心接口)                              │
│  ├── ICurrentUser             当前用户抽象                       │
│  ├── IUserPreferences         用户偏好 (已有)                    │
│  └── IUserState               用户状态 (已有)                    │
├─────────────────────────────────────────────────────────────────┤
│                     外部认证服务                                  │
│  ┌──────────┐  ┌──────────┐  ┌──────────┐  ┌──────────┐        │
│  │ Keycloak │  │  Auth0   │  │ Azure AD │  │  自建    │        │
│  └──────────┘  └──────────┘  └──────────┘  └──────────┘        │
└─────────────────────────────────────────────────────────────────┘

包职责划分

NextUI.Core          - 接口定义,无外部依赖
NextUI.Identity      - 身份认证集成,OIDC 客户端  ← 新包
NextUI.Identity.EF   - Entity Framework 持久化   ← 可选包
NextUI.Blazor        - 身份相关 UI 组件

三、核心接口设计

3.1 用户身份抽象 (NextUI.Core)

namespace NextUI.Core;

/// <summary>
/// 当前用户抽象 - 所有应用统一使用此接口
/// </summary>
public interface ICurrentUser
{
    /// <summary>是否已认证</summary>
    bool IsAuthenticated { get; }

    /// <summary>用户唯一标识 (来自 IdP 的 sub claim)</summary>
    string? UserId { get; }

    /// <summary>显示名称</summary>
    string? DisplayName { get; }

    /// <summary>邮箱</summary>
    string? Email { get; }

    /// <summary>头像 URL</summary>
    string? AvatarUrl { get; }

    /// <summary>用户角色</summary>
    IReadOnlyList<string> Roles { get; }

    /// <summary>用户声明</summary>
    IReadOnlyDictionary<string, string> Claims { get; }

    /// <summary>检查是否有指定角色</summary>
    bool IsInRole(string role);

    /// <summary>检查是否有指定权限</summary>
    bool HasPermission(string permission);

    /// <summary>用户状态变更事件</summary>
    event Action? Changed;
}

/// <summary>
/// 用户资料 - 应用层面的扩展信息
/// </summary>
public interface IUserProfile
{
    string UserId { get; }
    string? Nickname { get; set; }
    string? Bio { get; set; }
    string? PhoneNumber { get; set; }
    string? Timezone { get; set; }
    string? Locale { get; set; }
    Dictionary<string, object?> Metadata { get; }
    DateTimeOffset CreatedAt { get; }
    DateTimeOffset UpdatedAt { get; }
}

3.2 身份服务 (NextUI.Identity)

namespace NextUI.Identity;

/// <summary>
/// 身份认证服务
/// </summary>
public interface IIdentityService
{
    /// <summary>当前用户</summary>
    ICurrentUser CurrentUser { get; }

    /// <summary>发起登录流程</summary>
    /// <param name="returnUrl">登录后返回地址</param>
    /// <param name="provider">指定认证提供者 (可选)</param>
    Task LoginAsync(string? returnUrl = null, string? provider = null);

    /// <summary>发起注销流程</summary>
    Task LogoutAsync(string? returnUrl = null);

    /// <summary>静默刷新 Token</summary>
    Task<bool> RefreshTokenAsync();

    /// <summary>获取访问令牌 (用于调用 API)</summary>
    Task<string?> GetAccessTokenAsync();

    /// <summary>用户登录事件</summary>
    event Action<ICurrentUser>? UserLoggedIn;

    /// <summary>用户注销事件</summary>
    event Action? UserLoggedOut;
}

/// <summary>
/// 用户资料服务
/// </summary>
public interface IUserProfileService
{
    /// <summary>获取用户资料</summary>
    Task<IUserProfile?> GetProfileAsync(string userId);

    /// <summary>获取当前用户资料</summary>
    Task<IUserProfile?> GetCurrentProfileAsync();

    /// <summary>更新用户资料</summary>
    Task UpdateProfileAsync(IUserProfile profile);

    /// <summary>用户资料变更事件</summary>
    event Action<IUserProfile>? ProfileChanged;
}

/// <summary>
/// 用户偏好同步服务 - 将本地偏好同步到服务端
/// </summary>
public interface IUserPreferencesSync
{
    /// <summary>同步本地偏好到服务端</summary>
    Task SyncToServerAsync();

    /// <summary>从服务端加载偏好到本地</summary>
    Task SyncFromServerAsync();

    /// <summary>启用自动同步</summary>
    bool AutoSyncEnabled { get; set; }
}

3.3 配置选项

namespace NextUI.Identity;

/// <summary>
/// 身份认证配置
/// </summary>
public class IdentityOptions
{
    /// <summary>认证模式</summary>
    public AuthenticationMode Mode { get; set; } = AuthenticationMode.OpenIdConnect;

    /// <summary>OIDC 配置</summary>
    public OpenIdConnectOptions? OpenIdConnect { get; set; }

    /// <summary>用户资料存储方式</summary>
    public ProfileStorageMode ProfileStorage { get; set; } = ProfileStorageMode.LocalOnly;

    /// <summary>用户资料 API 端点 (当 ProfileStorage = RemoteApi 时)</summary>
    public string? ProfileApiEndpoint { get; set; }

    /// <summary>偏好自动同步</summary>
    public bool AutoSyncPreferences { get; set; } = true;

    /// <summary>多租户配置</summary>
    public MultiTenantOptions? MultiTenant { get; set; }

    /// <summary>离线支持配置</summary>
    public OfflineOptions Offline { get; set; } = new();

    /// <summary>Token 存储配置</summary>
    public TokenStorageOptions TokenStorage { get; set; } = new();

    /// <summary>权限模型配置</summary>
    public AuthorizationOptions Authorization { get; set; } = new();
}

public class OpenIdConnectOptions
{
    /// <summary>Authority URL (如 https://login.example.com)</summary>
    public string Authority { get; set; } = "";

    /// <summary>Client ID</summary>
    public string ClientId { get; set; } = "";

    /// <summary>Client Secret (机密客户端时需要)</summary>
    public string? ClientSecret { get; set; }

    /// <summary>Scopes</summary>
    public List<string> Scopes { get; set; } = new() { "openid", "profile", "email" };

    /// <summary>Response Type</summary>
    public string ResponseType { get; set; } = "code";

    /// <summary>登录后回调路径</summary>
    public string CallbackPath { get; set; } = "/signin-oidc";

    /// <summary>注销后回调路径</summary>
    public string SignedOutCallbackPath { get; set; } = "/signout-callback-oidc";
}

public enum AuthenticationMode
{
    /// <summary>无认证 (匿名模式)</summary>
    None,

    /// <summary>OpenID Connect</summary>
    OpenIdConnect,

    /// <summary>自定义 (由应用实现)</summary>
    Custom
}

public enum ProfileStorageMode
{
    /// <summary>仅本地存储 (localStorage)</summary>
    LocalOnly,

    /// <summary>远程 API</summary>
    RemoteApi,

    /// <summary>本地 + 远程同步</summary>
    Hybrid
}

四、多应用 SSO 方案

场景分析

┌─────────────────────────────────────────────────────────────────┐
│                      Identity Provider                          │
│                   (Keycloak / Auth0 / Azure AD)                 │
│                                                                 │
│  Realm: my-company                                              │
│  ├── Client: cms-app        (内容管理系统)                       │
│  ├── Client: crm-app        (客户关系管理)                       │
│  ├── Client: chat-app       (即时通信)                          │
│  └── Client: portal-app     (用户门户)                          │
│                                                                 │
│  Users:                                                         │
│  ├── Internal Users (员工)  - Role: employee                    │
│  └── External Users (客户)  - Role: customer                    │
└─────────────────────────────────────────────────────────────────┘
                              │
           ┌──────────────────┼──────────────────┐
           │                  │                  │
           ▼                  ▼                  ▼
    ┌─────────────┐   ┌─────────────┐   ┌─────────────┐
    │   CMS App   │   │   CRM App   │   │  Chat App   │
    │  (NextUI)   │   │  (NextUI)   │   │  (NextUI)   │
    └─────────────┘   └─────────────┘   └─────────────┘
           │                  │                  │
           └──────────────────┼──────────────────┘
                              │
                              ▼
                    ┌─────────────────┐
                    │  Shared User    │
                    │  Profile Store  │
                    │   (Optional)    │
                    └─────────────────┘

SSO 配置方式

// 每个应用的 Program.cs
builder.Services.AddNextUIIdentity(options =>
{
    options.Mode = AuthenticationMode.OpenIdConnect;
    options.OpenIdConnect = new OpenIdConnectOptions
    {
        Authority = "https://auth.my-company.com/realms/my-company",
        ClientId = "cms-app",  // 每个应用不同的 Client ID
        Scopes = { "openid", "profile", "email", "roles" }
    };

    // 可选:共享用户资料服务
    options.ProfileStorage = ProfileStorageMode.RemoteApi;
    options.ProfileApiEndpoint = "https://api.my-company.com/user-profiles";
});

用户类型区分

// 通过 Claims/Roles 区分用户类型
public static class UserTypes
{
    public const string Internal = "internal";  // 内部员工
    public const string External = "external";  // 外部客户
    public const string Partner = "partner";    // 合作伙伴
}

// 使用示例
@inject ICurrentUser User

@if (User.IsInRole("employee"))
{
    <AdminMenu />
}
else if (User.IsInRole("customer"))
{
    <CustomerMenu />
}

五、实现路线图

Phase 1: 基础框架 (MVP)

任务 描述
1.1 NextUI.Core 定义 ICurrentUser 接口
1.2 NextUI.Identity 创建新包,实现 OIDC 客户端
1.3 NextUI.Identity 实现 IIdentityService
1.4 NextUI.Blazor 添加 <AuthorizeView> 集成

MVP 目标:用户可以通过 OIDC 登录/注销,获取基本信息。

Phase 2: 用户资料

任务 描述
2.1 NextUI.Identity 实现 IUserProfileService
2.2 NextUI.Identity 本地资料存储
2.3 NextUI.Identity 远程 API 集成
2.4 NextUI.Identity 偏好同步 (IUserPreferencesSync)

Phase 3: UI 组件

任务 描述
3.1 NextUI.Blazor SxLoginButton - 登录按钮
3.2 NextUI.Blazor SxUserAvatar - 用户头像
3.3 NextUI.Blazor SxUserMenu - 用户下拉菜单
3.4 NextUI.Blazor SxAuthGuard - 路由权限守卫

Phase 4: 多租户

任务 描述
4.1 NextUI.Identity ITenant, ITenantMembership 接口
4.2 NextUI.Identity ITenantContext - 多租户上下文
4.3 NextUI.Identity ITenantService - 租户管理服务
4.4 NextUI.Blazor SxTenantSwitcher - 租户切换组件

Phase 5: 管理组件

任务 描述
5.1 NextUI.Identity.Admin SxUserList, SxUserEditor
5.2 NextUI.Identity.Admin SxTenantList, SxTenantEditor
5.3 NextUI.Identity.Admin SxMemberList - 成员管理
5.4 NextUI.Identity.Admin 预组装管理页面
5.5 NextUI.Identity.Admin 自动路由注册

Phase 6: 持久化与高级功能 (可选)

任务 描述
6.1 NextUI.Identity.EF Entity Framework 持久化
6.2 NextUI.Identity 审计日志集成
6.3 NextUI.Identity 权限细粒度控制 (ABAC)

六、使用示例

6.1 基础用法 (开箱即用)

// Program.cs
builder.Services.AddNextUIIdentity(options =>
{
    options.OpenIdConnect = new()
    {
        Authority = "https://login.example.com",
        ClientId = "my-app"
    };
});

// 使用
builder.Services.AddNextDesignSystem();
@* App.razor *@
<Router AppAssembly="@typeof(App).Assembly">
    <Found Context="routeData">
        <AuthorizeRouteView RouteData="@routeData" DefaultLayout="@typeof(MainLayout)">
            <NotAuthorized>
                <RedirectToLogin />
            </NotAuthorized>
        </AuthorizeRouteView>
    </Found>
</Router>
@* MainLayout.razor *@
<SxAppShell>
    <NavBar>
        <SxNavBar>
            <UserMenu>
                <SxUserMenu />  @* 自动显示登录/用户信息 *@
            </UserMenu>
        </SxNavBar>
    </NavBar>
    <Body>
        @Body
    </Body>
</SxAppShell>

6.2 自定义用法

@inject IIdentityService Identity
@inject ICurrentUser User

<div>
    @if (User.IsAuthenticated)
    {
        <span>欢迎, @User.DisplayName!</span>
        <button @onclick="() => Identity.LogoutAsync()">注销</button>
    }
    else
    {
        <button @onclick="() => Identity.LoginAsync()">登录</button>
    }
</div>

6.3 权限控制

@* 基于角色 *@
<AuthorizeView Roles="admin,manager">
    <Authorized>
        <AdminPanel />
    </Authorized>
    <NotAuthorized>
        <p>您没有权限访问此内容</p>
    </NotAuthorized>
</AuthorizeView>

@* 基于策略 *@
<AuthorizeView Policy="CanEditContent">
    <EditButton />
</AuthorizeView>

七、与现有方案的兼容性

场景:已有 ASP.NET Core Identity

// 应用已经有自己的认证
builder.Services.AddAuthentication()
    .AddCookie()
    .AddOpenIdConnect();

// NextUI 可以直接使用,不冲突
builder.Services.AddNextUIIdentity(options =>
{
    options.Mode = AuthenticationMode.Custom;  // 不启用 NextUI 的 OIDC
});

// 只使用 NextUI 的用户资料服务
builder.Services.AddScoped<ICurrentUser, AspNetCoreCurrentUserAdapter>();

场景:纯前端应用 (无服务端)

builder.Services.AddNextUIIdentity(options =>
{
    options.Mode = AuthenticationMode.OpenIdConnect;
    options.ProfileStorage = ProfileStorageMode.LocalOnly;  // 纯本地存储
});

八、技术选型

方面 选择 理由
OIDC 库 Microsoft.AspNetCore.Authentication.OpenIdConnect 官方支持,稳定
Token 存储 ProtectedBrowserStorage 安全,Blazor 原生
状态管理 AuthenticationStateProvider Blazor 标准机制
API 客户端 HttpClient + IHttpClientFactory 标准,可配置

九、安全考虑

  1. Token 存储:使用 ProtectedBrowserStorage,不存明文
  2. CSRF 防护:OIDC 流程自带 state 参数
  3. XSS 防护:不在 localStorage 存储敏感 token
  4. Token 刷新:静默刷新,用户无感知
  5. Logout:同时清理 IdP session 和本地 session

十、职责边界

NextUI.Identity vs 身份提供者 (IdP)

明确划分职责,避免重复建设:

┌─────────────────────────────────────────────────────────────────────┐
│                    身份提供者 (IdP) 职责                              │
│  Keycloak / Auth0 / Azure AD / IdentityServer                       │
├─────────────────────────────────────────────────────────────────────┤
│  ✓ 用户注册/登录认证                                                  │
│  ✓ 密码管理、重置、MFA                                                │
│  ✓ 用户存储 (账号、密码、基本信息)                                     │
│  ✓ 角色/组管理                                                       │
│  ✓ Token 签发 (ID Token, Access Token, Refresh Token)               │
│  ✓ SSO 会话管理                                                      │
│  ✓ 社交登录 (Google, GitHub, etc.)                                  │
│  ✓ 用户联邦 (LDAP, Active Directory)                                │
│  ✓ 审计日志                                                          │
└─────────────────────────────────────────────────────────────────────┘
                                  │
                                  ▼
┌─────────────────────────────────────────────────────────────────────┐
│                    NextUI.Identity 职责                              │
├─────────────────────────────────────────────────────────────────────┤
│  ✓ OIDC 客户端集成 (登录/注销流程)                                    │
│  ✓ Token 管理 (存储、刷新、附加到 API 请求)                           │
│  ✓ 当前用户状态 (ICurrentUser)                                       │
│  ✓ Blazor AuthenticationStateProvider 集成                          │
│  ✓ 应用级用户资料扩展 (IUserProfileService)                          │
│  ✓ 用户偏好同步 (主题、语言、布局等)                                   │
│  ✓ 权限检查辅助 (IsInRole, HasPermission)                            │
│  ✓ 身份相关 UI 组件                                                  │
│  ✓ 多租户上下文管理                                                  │
└─────────────────────────────────────────────────────────────────────┘

我们不做什么

功能 说明 建议
用户注册 复杂度高,各 IdP 都有完善方案 使用 IdP 内置页面或 API
密码管理 安全敏感,不应重复实现 使用 IdP 内置功能
MFA 需要专业安全知识 使用 IdP 内置功能
用户存储 IdP 核心功能 使用 IdP 数据库
审计日志 IdP 有详细记录 查询 IdP 审计 API

我们扩展什么

功能 说明 存储位置
用户偏好 主题、语言、布局设置 应用数据库 / localStorage
应用资料 昵称、签名、个人主页设置 应用数据库
租户关联 用户所属组织、角色映射 应用数据库
通知设置 邮件/推送通知偏好 应用数据库
最近访问 最近打开的项目、文档 应用数据库

十一、多租户设计

概念模型

┌─────────────────────────────────────────────────────────────────────┐
│                         组织 (Tenant)                               │
│  ┌──────────────┐  ┌──────────────┐  ┌──────────────┐              │
│  │   Acme Inc   │  │  Tech Corp   │  │  Startup X   │              │
│  │  tenant_001  │  │  tenant_002  │  │  tenant_003  │              │
│  └──────┬───────┘  └──────┬───────┘  └──────┬───────┘              │
│         │                 │                 │                       │
│         ▼                 ▼                 ▼                       │
│  ┌────────────────────────────────────────────────────────────┐    │
│  │                   用户-租户关联表                            │    │
│  │  ┌─────────┬───────────┬──────────┬─────────────────────┐  │    │
│  │  │ user_id │ tenant_id │ role     │ permissions         │  │    │
│  │  ├─────────┼───────────┼──────────┼─────────────────────┤  │    │
│  │  │ alice   │ tenant_001│ owner    │ ["*"]               │  │    │
│  │  │ alice   │ tenant_002│ member   │ ["read", "write"]   │  │    │
│  │  │ bob     │ tenant_001│ admin    │ ["read","write","admin"]│ │    │
│  │  │ bob     │ tenant_003│ viewer   │ ["read"]            │  │    │
│  │  └─────────┴───────────┴──────────┴─────────────────────┘  │    │
│  └────────────────────────────────────────────────────────────┘    │
└─────────────────────────────────────────────────────────────────────┘

核心接口

namespace NextUI.Identity;

/// <summary>
/// 租户信息
/// </summary>
public interface ITenant
{
    /// <summary>租户唯一标识</summary>
    string TenantId { get; }

    /// <summary>租户名称</summary>
    string Name { get; }

    /// <summary>租户显示名称</summary>
    string? DisplayName { get; }

    /// <summary>租户 Logo</summary>
    string? LogoUrl { get; }

    /// <summary>租户元数据</summary>
    IReadOnlyDictionary<string, object?> Metadata { get; }
}

/// <summary>
/// 用户在租户中的成员身份
/// </summary>
public interface ITenantMembership
{
    /// <summary>租户信息</summary>
    ITenant Tenant { get; }

    /// <summary>用户在此租户中的角色</summary>
    IReadOnlyList<string> Roles { get; }

    /// <summary>用户在此租户中的权限</summary>
    IReadOnlyList<string> Permissions { get; }

    /// <summary>是否为此租户的所有者</summary>
    bool IsOwner { get; }

    /// <summary>加入时间</summary>
    DateTimeOffset JoinedAt { get; }
}

/// <summary>
/// 多租户上下文
/// </summary>
public interface ITenantContext
{
    /// <summary>当前租户 (用户选择的活动租户)</summary>
    ITenant? CurrentTenant { get; }

    /// <summary>用户所属的所有租户</summary>
    IReadOnlyList<ITenantMembership> Memberships { get; }

    /// <summary>切换当前租户</summary>
    Task SwitchTenantAsync(string tenantId);

    /// <summary>获取用户在当前租户的角色</summary>
    IReadOnlyList<string> GetCurrentRoles();

    /// <summary>检查当前租户权限</summary>
    bool HasPermission(string permission);

    /// <summary>租户切换事件</summary>
    event Action<ITenant?>? TenantChanged;
}

/// <summary>
/// 租户管理服务 (管理员使用)
/// </summary>
public interface ITenantService
{
    /// <summary>创建租户</summary>
    Task<ITenant> CreateTenantAsync(string name, string? displayName = null);

    /// <summary>获取租户</summary>
    Task<ITenant?> GetTenantAsync(string tenantId);

    /// <summary>更新租户</summary>
    Task UpdateTenantAsync(ITenant tenant);

    /// <summary>删除租户</summary>
    Task DeleteTenantAsync(string tenantId);

    /// <summary>添加用户到租户</summary>
    Task AddMemberAsync(string tenantId, string userId, IEnumerable<string> roles);

    /// <summary>移除用户</summary>
    Task RemoveMemberAsync(string tenantId, string userId);

    /// <summary>更新用户角色</summary>
    Task UpdateMemberRolesAsync(string tenantId, string userId, IEnumerable<string> roles);

    /// <summary>获取租户成员</summary>
    Task<IReadOnlyList<TenantMember>> GetMembersAsync(string tenantId);
}

public class TenantMember
{
    public string UserId { get; set; } = "";
    public string? DisplayName { get; set; }
    public string? Email { get; set; }
    public IReadOnlyList<string> Roles { get; set; } = [];
    public DateTimeOffset JoinedAt { get; set; }
}

使用示例

@inject ITenantContext TenantContext
@inject ICurrentUser User

@* 租户切换器 *@
<SxSelect @bind-Value="@_selectedTenant"
          TValue="string"
          Label="当前组织"
          OnChange="OnTenantChange">
    @foreach (var membership in TenantContext.Memberships)
    {
        <SxSelectItem Value="@membership.Tenant.TenantId">
            @membership.Tenant.DisplayName
            @if (membership.IsOwner)
            {
                <SxBadge>Owner</SxBadge>
            }
        </SxSelectItem>
    }
</SxSelect>

@* 基于租户角色的权限控制 *@
@if (TenantContext.HasPermission("manage_members"))
{
    <SxButton OnClick="OpenMemberManagement">管理成员</SxButton>
}

@code {
    private string? _selectedTenant;

    protected override void OnInitialized()
    {
        _selectedTenant = TenantContext.CurrentTenant?.TenantId;
    }

    private async Task OnTenantChange(string tenantId)
    {
        await TenantContext.SwitchTenantAsync(tenantId);
    }
}

配置选项

public class MultiTenantOptions
{
    /// <summary>是否启用多租户</summary>
    public bool Enabled { get; set; } = false;

    /// <summary>租户解析策略</summary>
    public TenantResolutionStrategy ResolutionStrategy { get; set; }
        = TenantResolutionStrategy.UserSelection;

    /// <summary>租户 API 端点</summary>
    public string? TenantApiEndpoint { get; set; }

    /// <summary>默认租户 (首次登录时)</summary>
    public string? DefaultTenantId { get; set; }

    /// <summary>租户切换后是否刷新页面</summary>
    public bool ReloadOnSwitch { get; set; } = false;
}

public enum TenantResolutionStrategy
{
    /// <summary>用户手动选择</summary>
    UserSelection,

    /// <summary>从 URL 子域名解析 (如 acme.app.com)</summary>
    Subdomain,

    /// <summary>从 URL 路径解析 (如 /tenant/acme/...)</summary>
    PathSegment,

    /// <summary>从 HTTP Header 解析</summary>
    Header,

    /// <summary>从 Cookie 解析</summary>
    Cookie
}

十二、后台管理策略

方案对比

方案 优点 缺点 适用场景
A: 组件库 灵活定制,可深度集成到应用 需要自己组装页面 有定制需求的应用
B: 独立模块 开箱即用,快速集成 定制性受限 快速开发场景
C: 混合方案 平衡灵活性和便利性 复杂度略高 推荐方案

推荐:混合方案 (C)

┌─────────────────────────────────────────────────────────────────────┐
│                      NextUI.Identity.Admin                          │
├─────────────────────────────────────────────────────────────────────┤
│  Layer 1: 管理组件 (可独立使用)                                       │
│  ┌────────────────┐ ┌────────────────┐ ┌────────────────┐          │
│  │ SxUserList     │ │ SxUserEditor   │ │ SxRoleEditor   │          │
│  │ 用户列表组件    │ │ 用户编辑表单    │ │ 角色编辑表单    │          │
│  └────────────────┘ └────────────────┘ └────────────────┘          │
│  ┌────────────────┐ ┌────────────────┐ ┌────────────────┐          │
│  │ SxTenantList   │ │ SxTenantEditor │ │ SxMemberList   │          │
│  │ 租户列表组件    │ │ 租户编辑表单    │ │ 成员管理组件    │          │
│  └────────────────┘ └────────────────┘ └────────────────┘          │
├─────────────────────────────────────────────────────────────────────┤
│  Layer 2: 预组装页面 (可选使用)                                       │
│  ┌────────────────────────────────────────────────────────────┐    │
│  │ AdminUserManagementPage   - 完整的用户管理页面               │    │
│  │ AdminTenantManagementPage - 完整的租户管理页面               │    │
│  │ AdminRoleManagementPage   - 完整的角色管理页面               │    │
│  │ AdminAuditLogPage         - 审计日志查看页面                 │    │
│  └────────────────────────────────────────────────────────────┘    │
├─────────────────────────────────────────────────────────────────────┤
│  Layer 3: 路由注册 (可选)                                            │
│  ┌────────────────────────────────────────────────────────────┐    │
│  │ builder.Services.AddNextUIAdminRoutes("/admin");           │    │
│  │                                                            │    │
│  │ 自动注册:                                                   │    │
│  │   /admin/users      → AdminUserManagementPage              │    │
│  │   /admin/tenants    → AdminTenantManagementPage            │    │
│  │   /admin/roles      → AdminRoleManagementPage              │    │
│  │   /admin/audit-logs → AdminAuditLogPage                    │    │
│  └────────────────────────────────────────────────────────────┘    │
└─────────────────────────────────────────────────────────────────────┘

使用方式

方式 1: 只用组件 (最大灵活性)

@* 自定义页面,使用管理组件 *@
@page "/my-admin/users"
@attribute [Authorize(Roles = "admin")]

<h1>我的用户管理</h1>

<SxUserList @bind-SelectedUser="@_selectedUser" />

@if (_selectedUser != null)
{
    <SxUserEditor User="@_selectedUser"
                  OnSave="HandleSave"
                  ShowRoleEditor="true"
                  ShowTenantSelector="true" />
}

方式 2: 使用预组装页面

@* 直接使用预组装页面,传入配置 *@
@page "/admin/users"
@attribute [Authorize(Roles = "admin")]

<AdminUserManagementPage AllowCreate="true"
                         AllowDelete="true"
                         ShowTenantColumn="true" />

方式 3: 自动路由注册 (最快集成)

// Program.cs
builder.Services.AddNextUIIdentity(options => { ... });
builder.Services.AddNextUIAdminRoutes("/admin", options =>
{
    options.RequireRole = "admin";
    options.EnableUserManagement = true;
    options.EnableTenantManagement = true;
    options.EnableAuditLog = true;
});

管理后台部署方式

管理组件支持多种部署方式,根据你的需求选择:

┌─────────────────────────────────────────────────────────────────────┐
│                    部署方式对比                                       │
├─────────────────────────────────────────────────────────────────────┤
│                                                                     │
│  方式 A: 集成到业务应用中                                             │
│  ────────────────────────                                           │
│  ┌─────────────────────────────────────────────────────────────┐   │
│  │                      你的业务应用                             │   │
│  │  ┌─────────────────┐  ┌─────────────────┐                   │   │
│  │  │   业务页面       │  │   管理页面       │                   │   │
│  │  │  /products      │  │  /admin/users   │ ← 管理组件集成      │   │
│  │  │  /orders        │  │  /admin/tenants │                   │   │
│  │  │  /customers     │  │  /admin/roles   │                   │   │
│  │  └─────────────────┘  └─────────────────┘                   │   │
│  │                                                              │   │
│  │  适用: 单应用、小团队、管理员就是业务用户                       │   │
│  └─────────────────────────────────────────────────────────────┘   │
│                                                                     │
│  方式 B: 独立管理服务                                                │
│  ────────────────────                                               │
│  ┌─────────────┐  ┌─────────────┐  ┌─────────────┐                 │
│  │   CMS App   │  │   CRM App   │  │  Chat App   │                 │
│  │  (业务应用)  │  │  (业务应用)  │  │  (业务应用)  │                 │
│  └──────┬──────┘  └──────┬──────┘  └──────┬──────┘                 │
│         │                │                │                         │
│         └────────────────┼────────────────┘                         │
│                          │ 共享用户/租户                              │
│                          ▼                                          │
│  ┌─────────────────────────────────────────────────────────────┐   │
│  │              Admin Portal (独立部署)                         │   │
│  │                   admin.example.com                          │   │
│  │                                                              │   │
│  │  一个独立的 Blazor 应用,使用管理组件构建                      │   │
│  │  可以扩展自定义页面                                           │   │
│  │                                                              │   │
│  │  适用: 多应用、集中管理、专职管理员                            │   │
│  └─────────────────────────────────────────────────────────────┘   │
│                                                                     │
│  方式 C: 混合模式                                                    │
│  ─────────────                                                      │
│  ┌─────────────────────────────────────────────────────────────┐   │
│  │                      你的业务应用                             │   │
│  │  ┌─────────────────┐  ┌─────────────────┐                   │   │
│  │  │   业务页面       │  │   简化管理页面    │ ← 只放常用功能     │   │
│  │  │  /products      │  │  /admin/users   │   (用户列表、      │   │
│  │  │  /orders        │  │                 │    快速添加)       │   │
│  │  └─────────────────┘  └─────────────────┘                   │   │
│  └─────────────────────────────────────────────────────────────┘   │
│                          │                                          │
│                          │ 高级管理功能                              │
│                          ▼                                          │
│  ┌─────────────────────────────────────────────────────────────┐   │
│  │              Admin Portal (独立部署)                         │   │
│  │                                                              │   │
│  │  完整的管理功能: 审计日志、权限配置、租户管理等                  │   │
│  │  适用: 日常在应用内管理,高级功能去专门后台                     │   │
│  └─────────────────────────────────────────────────────────────┘   │
│                                                                     │
└─────────────────────────────────────────────────────────────────────┘

独立管理服务的搭建

如果选择部署独立的管理服务,我们会提供:

1. 项目模板

# 使用 CLI 创建管理后台项目
nx new admin-portal --template identity-admin

# 生成的项目结构:
admin-portal/
├── Program.cs              # 服务配置
├── appsettings.json        # 配置文件
├── Pages/
│   ├── Users/              # 可以扩展或覆盖默认页面
│   ├── Tenants/
│   └── Custom/             # 你的自定义页面
└── Components/             # 自定义组件

2. 最小配置启动

// Program.cs - 最简配置
var builder = WebApplication.CreateBuilder(args);

// 添加管理后台所需服务
builder.Services.AddNextUIIdentity(options =>
{
    options.OpenIdConnect = new()
    {
        Authority = "https://auth.example.com",
        ClientId = "admin-portal"
    };
    options.ProfileApiEndpoint = "https://api.example.com/profiles";
    options.MultiTenant = new() { Enabled = true };
});

// 添加管理页面路由
builder.Services.AddNextUIAdminRoutes("/", options =>
{
    options.RequireRole = "admin";
    options.EnableAllFeatures();  // 启用所有管理功能
});

var app = builder.Build();

app.UseAuthentication();
app.UseAuthorization();
app.MapBlazorHub();
app.MapFallbackToPage("/_Host");

app.Run();

3. 扩展自定义功能

@* 添加自定义仪表板 *@
@page "/dashboard"
@attribute [Authorize(Roles = "admin")]

<h1>管理仪表板</h1>

<div class="grid grid-cols-3 gap-4">
    <SxCard>
        <SxCardHeader>用户统计</SxCardHeader>
        <SxCardBody>
            <UserStatsWidget />  @* 自定义组件 *@
        </SxCardBody>
    </SxCard>

    <SxCard>
        <SxCardHeader>租户统计</SxCardHeader>
        <SxCardBody>
            <TenantStatsWidget />
        </SxCardBody>
    </SxCard>

    <SxCard>
        <SxCardHeader>最近活动</SxCardHeader>
        <SxCardBody>
            <RecentActivityWidget />
        </SxCardBody>
    </SxCard>
</div>

@* 下面仍然可以使用预置的管理组件 *@
<h2>待审核用户</h2>
<SxUserList Filter="@(u => u.Status == "pending")"
            OnApprove="HandleApprove" />

跨应用共享管理后台

┌─────────────────────────────────────────────────────────────────────┐
│                    共享管理后台架构                                   │
├─────────────────────────────────────────────────────────────────────┤
│                                                                     │
│   ┌─────────────┐   ┌─────────────┐   ┌─────────────┐              │
│   │   CMS App   │   │   CRM App   │   │  Chat App   │              │
│   └──────┬──────┘   └──────┬──────┘   └──────┬──────┘              │
│          │                 │                 │                      │
│          │      不需要各自实现管理后台          │                      │
│          │                 │                 │                      │
│          └────────────────┬┴─────────────────┘                      │
│                           │                                         │
│                           ▼                                         │
│   ┌───────────────────────────────────────────────────────────┐    │
│   │              Admin Portal (独立应用)                       │    │
│   │                                                           │    │
│   │  使用 NextUI.Identity.Admin 组件构建                       │    │
│   │  连接同一个 IdP 和用户资料 API                              │    │
│   │                                                           │    │
│   │  /users      - 统一用户管理                                │    │
│   │  /tenants    - 统一租户管理                                │    │
│   │  /apps       - 应用管理 (可选)                             │    │
│   │  /audit-logs - 统一审计日志                                │    │
│   └───────────────────────────────────────────────────────────┘    │
│                           │                                         │
│           ┌───────────────┼───────────────┐                         │
│           ▼               ▼               ▼                         │
│   ┌─────────────┐ ┌─────────────┐ ┌─────────────────┐              │
│   │     IdP     │ │ Profile API │ │  Tenant API     │              │
│   │ (Keycloak)  │ │  (共享)      │ │  (共享)          │              │
│   └─────────────┘ └─────────────┘ └─────────────────┘              │
│                                                                     │
└─────────────────────────────────────────────────────────────────────┘

十三、配置指南

配置方式

提供多种配置方式,从简单到复杂:

1. 代码配置 (推荐)

builder.Services.AddNextUIIdentity(options =>
{
    // 基础 OIDC 配置
    options.OpenIdConnect = new OpenIdConnectOptions
    {
        Authority = "https://auth.example.com",
        ClientId = "my-app",
        Scopes = { "openid", "profile", "email" }
    };

    // 用户资料
    options.ProfileStorage = ProfileStorageMode.RemoteApi;
    options.ProfileApiEndpoint = "https://api.example.com/profiles";

    // 多租户
    options.MultiTenant = new MultiTenantOptions
    {
        Enabled = true,
        TenantApiEndpoint = "https://api.example.com/tenants"
    };
});

2. 配置文件 (appsettings.json)

{
  "NextUI": {
    "Identity": {
      "OpenIdConnect": {
        "Authority": "https://auth.example.com",
        "ClientId": "my-app",
        "Scopes": ["openid", "profile", "email"]
      },
      "ProfileStorage": "RemoteApi",
      "ProfileApiEndpoint": "https://api.example.com/profiles",
      "MultiTenant": {
        "Enabled": true,
        "TenantApiEndpoint": "https://api.example.com/tenants"
      }
    }
  }
}
// Program.cs
builder.Services.AddNextUIIdentity(
    builder.Configuration.GetSection("NextUI:Identity"));

3. 环境变量 (适合容器/CI)

NEXTUI__IDENTITY__OPENIDCONNECT__AUTHORITY=https://auth.example.com
NEXTUI__IDENTITY__OPENIDCONNECT__CLIENTID=my-app
NEXTUI__IDENTITY__PROFILEAPIENDPOINT=https://api.example.com/profiles

配置向导工具

提供 CLI 工具帮助生成配置:

# 交互式配置向导
nx identity init

# 输出:
# NextUI Identity 配置向导
# ========================
#
# ? 选择身份提供者:
#   > Keycloak
#     Auth0
#     Azure AD
#     自定义 OIDC
#
# ? Authority URL: https://auth.example.com
# ? Client ID: my-app
# ? 启用多租户? (y/N): y
# ? 租户 API 端点: https://api.example.com/tenants
#
# 配置已生成到 appsettings.Identity.json

常见 IdP 配置模板

Keycloak

builder.Services.AddNextUIIdentity(options =>
{
    options.UseKeycloak(
        authority: "https://keycloak.example.com/realms/my-realm",
        clientId: "my-blazor-app"
    );
});

Auth0

builder.Services.AddNextUIIdentity(options =>
{
    options.UseAuth0(
        domain: "my-tenant.auth0.com",
        clientId: "xxxxxxxxxxxxxxxxxx"
    );
});

Azure AD

builder.Services.AddNextUIIdentity(options =>
{
    options.UseAzureAD(
        tenantId: "xxxxxxxx-xxxx-xxxx-xxxx-xxxxxxxxxxxx",
        clientId: "xxxxxxxx-xxxx-xxxx-xxxx-xxxxxxxxxxxx"
    );
});

十四、技术方案详解

14.1 离线支持

问题:用户网络断开时,已登录用户应该如何处理?

方案对比

方案 描述 优点 缺点
A: 严格在线 离线立即失效 最安全 用户体验差
B: 宽限期 离线后一段时间内仍可用 平衡安全与体验 需要本地存储状态
C: 完全离线 长期离线也能用 体验最好 安全风险高

推荐方案:B - 宽限期模式

public class OfflineOptions
{
    /// <summary>是否启用离线支持</summary>
    public bool Enabled { get; set; } = true;

    /// <summary>离线宽限期 (默认 24 小时)</summary>
    public TimeSpan GracePeriod { get; set; } = TimeSpan.FromHours(24);

    /// <summary>离线时允许的功能级别</summary>
    public OfflineAccessLevel AccessLevel { get; set; } = OfflineAccessLevel.ReadOnly;

    /// <summary>敏感操作是否需要在线验证</summary>
    public bool RequireOnlineForSensitiveOps { get; set; } = true;
}

public enum OfflineAccessLevel
{
    /// <summary>完全禁用 - 离线时强制登出</summary>
    None,

    /// <summary>只读 - 可查看已缓存数据,不能修改</summary>
    ReadOnly,

    /// <summary>完全访问 - 离线时也可操作,联网后同步</summary>
    Full
}

工作流程

┌─────────────────────────────────────────────────────────────────────┐
│                      离线支持工作流程                                 │
├─────────────────────────────────────────────────────────────────────┤
│                                                                     │
│   用户登录成功                                                       │
│        │                                                            │
│        ▼                                                            │
│   ┌─────────────────────────────────────────────────────────────┐  │
│   │ 缓存到 ProtectedBrowserStorage:                              │  │
│   │   - 用户基本信息 (ID, 名称, 头像)                             │  │
│   │   - 角色和权限                                                │  │
│   │   - 最后在线时间戳                                            │  │
│   │   - 加密的 Refresh Token (可选)                               │  │
│   └─────────────────────────────────────────────────────────────┘  │
│        │                                                            │
│        ▼                                                            │
│   网络断开                                                          │
│        │                                                            │
│        ▼                                                            │
│   ┌─────────────────────────────────────────────────────────────┐  │
│   │ 检查本地缓存:                                                 │  │
│   │   - 有缓存且未过期 → 使用缓存的用户状态                        │  │
│   │   - 有缓存但已过期 → 提示需要联网重新登录                      │  │
│   │   - 无缓存 → 视为未登录                                       │  │
│   └─────────────────────────────────────────────────────────────┘  │
│        │                                                            │
│        ▼                                                            │
│   网络恢复                                                          │
│        │                                                            │
│        ▼                                                            │
│   ┌─────────────────────────────────────────────────────────────┐  │
│   │ 静默验证:                                                     │  │
│   │   - 尝试刷新 Token                                            │  │
│   │   - 成功 → 继续使用                                           │  │
│   │   - 失败 → 提示重新登录                                       │  │
│   └─────────────────────────────────────────────────────────────┘  │
│                                                                     │
└─────────────────────────────────────────────────────────────────────┘

14.2 Token 缓存与刷新

问题:Access Token 通常很短命(5-60分钟),如何管理?

Token 类型说明

Token 类型 用途 有效期 存储位置
ID Token 证明用户身份 短 (5-60分钟) 内存
Access Token 调用 API 短 (5-60分钟) 内存或安全存储
Refresh Token 获取新 Token 长 (天/周/月) 安全存储

存储方案对比

存储位置 安全性 持久性 推荐
内存 最高 页面刷新丢失 Access Token
sessionStorage 关闭标签页丢失 不推荐
localStorage 永久 不推荐 (XSS 风险)
ProtectedBrowserStorage 可配置 Refresh Token
HttpOnly Cookie 最高 可配置 服务端渲染时

推荐方案

public class TokenStorageOptions
{
    /// <summary>Access Token 存储位置</summary>
    public TokenStorage AccessTokenStorage { get; set; } = TokenStorage.Memory;

    /// <summary>Refresh Token 存储位置</summary>
    public TokenStorage RefreshTokenStorage { get; set; } = TokenStorage.ProtectedBrowserStorage;

    /// <summary>Token 刷新策略</summary>
    public TokenRefreshStrategy RefreshStrategy { get; set; } = TokenRefreshStrategy.Proactive;

    /// <summary>提前多久刷新 (默认过期前 5 分钟)</summary>
    public TimeSpan RefreshBeforeExpiry { get; set; } = TimeSpan.FromMinutes(5);
}

public enum TokenStorage
{
    Memory,                    // 仅内存,最安全
    ProtectedBrowserStorage,   // Blazor 加密存储
    HttpOnlyCookie            // 服务端管理
}

public enum TokenRefreshStrategy
{
    /// <summary>按需刷新 - 收到 401 时才刷新</summary>
    OnDemand,

    /// <summary>主动刷新 - 过期前自动刷新 (推荐)</summary>
    Proactive,

    /// <summary>后台刷新 - 定时检查并刷新</summary>
    Background
}

刷新流程

┌─────────────────────────────────────────────────────────────────────┐
│                    Token 刷新流程 (主动刷新)                          │
├─────────────────────────────────────────────────────────────────────┤
│                                                                     │
│   API 调用前                                                        │
│        │                                                            │
│        ▼                                                            │
│   ┌─────────────────────────────────────────────────────────────┐  │
│   │ 检查 Access Token:                                           │  │
│   │   - 有效且未接近过期 → 直接使用                                │  │
│   │   - 接近过期 (< 5分钟) → 触发静默刷新                          │  │
│   │   - 已过期 → 必须刷新后才能用                                  │  │
│   └─────────────────────────────────────────────────────────────┘  │
│        │                                                            │
│        ▼ (需要刷新)                                                 │
│   ┌─────────────────────────────────────────────────────────────┐  │
│   │ 使用 Refresh Token 请求新 Token:                             │  │
│   │   POST /oauth/token                                          │  │
│   │   { grant_type: "refresh_token", refresh_token: "xxx" }      │  │
│   └─────────────────────────────────────────────────────────────┘  │
│        │                                                            │
│        ├─── 成功 ──► 更新内存中的 Access Token,继续 API 调用       │
│        │                                                            │
│        └─── 失败 ──► Refresh Token 也过期,需要重新登录             │
│                                                                     │
└─────────────────────────────────────────────────────────────────────┘

14.3 权限模型

三种主流权限模型解释

RBAC (Role-Based Access Control) - 基于角色

最简单、最常用的模型

用户 ──拥有──► 角色 ──包含──► 权限

示例:
  用户 "张三"
    └── 角色 "编辑"
          ├── 权限: 创建文章
          ├── 权限: 编辑文章
          └── 权限: 删除自己的文章

代码:
  if (user.IsInRole("editor")) { ... }
  if (user.HasPermission("article:create")) { ... }

ABAC (Attribute-Based Access Control) - 基于属性

更灵活,基于多个属性动态判断

判断依据:
  - 用户属性 (部门、职级、地区)
  - 资源属性 (所有者、分类、敏感级别)
  - 环境属性 (时间、IP、设备)
  - 操作属性 (读、写、删除)

示例:
  规则: "用户可以编辑自己部门的、非机密的文档"

  检查:
    user.Department == document.Department
    AND document.Classification != "机密"
    AND action == "edit"

代码:
  if (authz.CanAccess(user, document, "edit")) { ... }

PBAC (Policy-Based Access Control) - 基于策略

ABAC 的正式化版本,使用策略语言定义规则

策略示例 (类似 AWS IAM):
{
  "Effect": "Allow",
  "Action": ["document:read", "document:edit"],
  "Resource": "documents/*",
  "Condition": {
    "StringEquals": { "document:department": "${user:department}" },
    "StringNotEquals": { "document:classification": "机密" }
  }
}

对比总结

特性 RBAC ABAC PBAC
复杂度
灵活性 最高
维护成本
适用场景 大多数应用 复杂业务规则 企业级/合规要求
学习成本

推荐方案:RBAC + 可选 ABAC 扩展

// 基础: RBAC (开箱即用)
public interface ICurrentUser
{
    bool IsInRole(string role);
    bool HasPermission(string permission);
}

// 扩展: ABAC (按需启用)
public interface IAuthorizationService
{
    /// <summary>
    /// 基于属性的访问控制检查
    /// </summary>
    Task<bool> AuthorizeAsync<TResource>(
        ICurrentUser user,
        TResource resource,
        string action,
        IDictionary<string, object>? context = null);
}

// 使用示例
// 简单场景 - RBAC
if (user.IsInRole("admin")) { /* 允许 */ }

// 复杂场景 - ABAC
var canEdit = await authz.AuthorizeAsync(user, document, "edit", new {
    RequestTime = DateTime.Now,
    ClientIP = httpContext.Connection.RemoteIpAddress
});

配置

public class AuthorizationOptions
{
    /// <summary>权限模型</summary>
    public PermissionModel Model { get; set; } = PermissionModel.RBAC;

    /// <summary>ABAC 规则提供者 (当 Model = ABAC 时)</summary>
    public IAbacPolicyProvider? PolicyProvider { get; set; }
}

public enum PermissionModel
{
    /// <summary>基于角色 (推荐大多数场景)</summary>
    RBAC,

    /// <summary>基于属性 (复杂业务场景)</summary>
    ABAC,

    /// <summary>混合模式 - RBAC 为主,特定场景用 ABAC</summary>
    Hybrid
}

14.4 租户数据隔离

问题:多租户系统中,不同租户的数据如何隔离?

三种隔离级别

┌─────────────────────────────────────────────────────────────────────┐
│                    数据隔离级别对比                                   │
├─────────────────────────────────────────────────────────────────────┤
│                                                                     │
│  Level 1: 逻辑隔离 (推荐)                                            │
│  ┌─────────────────────────────────────────────────────────────┐   │
│  │                    共享数据库                                │   │
│  │  ┌─────────────────────────────────────────────────────┐   │   │
│  │  │ documents 表                                         │   │   │
│  │  │ ┌──────────┬───────────┬─────────────────────────┐  │   │   │
│  │  │ │ id       │ tenant_id │ content                 │  │   │   │
│  │  │ ├──────────┼───────────┼─────────────────────────┤  │   │   │
│  │  │ │ 1        │ acme      │ Acme 的文档...           │  │   │   │
│  │  │ │ 2        │ techcorp  │ TechCorp 的文档...       │  │   │   │
│  │  │ └──────────┴───────────┴─────────────────────────┘  │   │   │
│  │  └─────────────────────────────────────────────────────┘   │   │
│  │  优点: 简单、成本低、易维护                                  │   │
│  │  缺点: 需要每个查询都带 tenant_id                            │   │
│  └─────────────────────────────────────────────────────────────┘   │
│                                                                     │
│  Level 2: Schema 隔离                                               │
│  ┌─────────────────────────────────────────────────────────────┐   │
│  │                    共享数据库                                │   │
│  │  ┌───────────────┐  ┌───────────────┐  ┌───────────────┐   │   │
│  │  │ schema: acme  │  │schema:techcorp│  │ schema: ...   │   │   │
│  │  │ - documents   │  │ - documents   │  │ - documents   │   │   │
│  │  │ - users       │  │ - users       │  │ - users       │   │   │
│  │  └───────────────┘  └───────────────┘  └───────────────┘   │   │
│  │  优点: 隔离性更好、查询不需要 tenant_id                       │   │
│  │  缺点: 管理复杂、迁移困难                                     │   │
│  └─────────────────────────────────────────────────────────────┘   │
│                                                                     │
│  Level 3: 数据库隔离                                                │
│  ┌───────────────┐  ┌───────────────┐  ┌───────────────┐          │
│  │  DB: acme     │  │ DB: techcorp  │  │  DB: ...      │          │
│  │  - documents  │  │ - documents   │  │  - documents  │          │
│  │  - users      │  │ - users       │  │  - users      │          │
│  └───────────────┘  └───────────────┘  └───────────────┘          │
│  优点: 最强隔离、可独立备份/恢复、满足合规要求                        │
│  缺点: 成本高、运维复杂、跨租户查询困难                              │
│                                                                     │
└─────────────────────────────────────────────────────────────────────┘

对比

特性 逻辑隔离 Schema 隔离 数据库隔离
成本 最低 最高
隔离性 软隔离 最强
运维复杂度
扩展性 需要规划
合规性 一般 较好 最好
适用场景 大多数 SaaS 中型企业 金融/医疗

推荐方案:逻辑隔离 + 可选升级

public class TenantIsolationOptions
{
    /// <summary>隔离级别</summary>
    public TenantIsolationLevel Level { get; set; } = TenantIsolationLevel.Logical;

    /// <summary>是否自动应用租户过滤器 (EF Core)</summary>
    public bool AutoApplyFilter { get; set; } = true;

    /// <summary>允许跨租户查询的角色</summary>
    public List<string> CrossTenantRoles { get; set; } = new() { "super_admin" };
}

public enum TenantIsolationLevel
{
    /// <summary>逻辑隔离 - 共享表,tenant_id 列 (推荐)</summary>
    Logical,

    /// <summary>Schema 隔离 - 每租户独立 schema</summary>
    Schema,

    /// <summary>数据库隔离 - 每租户独立数据库</summary>
    Database
}

EF Core 自动租户过滤

// DbContext 配置
public class AppDbContext : DbContext
{
    private readonly ITenantContext _tenantContext;

    protected override void OnModelCreating(ModelBuilder modelBuilder)
    {
        // 自动为所有实现 ITenantEntity 的实体添加过滤器
        foreach (var entityType in modelBuilder.Model.GetEntityTypes())
        {
            if (typeof(ITenantEntity).IsAssignableFrom(entityType.ClrType))
            {
                modelBuilder.Entity(entityType.ClrType)
                    .HasQueryFilter(e => ((ITenantEntity)e).TenantId == _tenantContext.CurrentTenant!.TenantId);
            }
        }
    }
}

// 实体基类
public interface ITenantEntity
{
    string TenantId { get; set; }
}

// 使用 - 查询自动过滤,开发者无需手动加 where
var myDocuments = await db.Documents.ToListAsync();
// 自动变成: SELECT * FROM Documents WHERE TenantId = 'current_tenant'

十五、设计决策总结

根据讨论,以下是关键设计决策:

问题 决策 理由
离线支持 宽限期模式 (24h) 平衡安全与用户体验
Token 存储 Access Token 内存,Refresh Token 加密存储 安全性最优
Token 刷新 主动刷新 (过期前 5 分钟) 用户无感知
权限模型 RBAC 为主 + 可选 ABAC 简单场景开箱即用,复杂场景可扩展
租户隔离 逻辑隔离 (tenant_id 列) 成本低、灵活、适合大多数场景
管理后台 三层组件 (可集成或独立部署) 灵活性 + 开箱即用

15.1 Token 内容策略

Token 应保持精简,只包含认证和授权所需的核心数据。

数据类型 存放位置 理由
用户基本信息 (sub, name, email) Token OIDC 标准 claims,认证必需
全局角色 (Realm Roles) Token 频繁使用,避免每次请求 API
应用角色 (Client Roles) Token Keycloak 自动只含当前应用角色,大小可控
当前组织 ID Token 每次请求都需要,放 Token 避免额外调用
组织列表 API 获取 可能较多,组织切换时再获取
组织详情 (名称、Logo 等) API 获取 不频繁使用,按需获取
用户偏好 API 获取 数据量可能很大,分层存储

原则: Token 小而快,API 获取大而全。Token 解析在每个请求都发生,保持精简才能高效。

15.2 多组织 (Multi-Org) 架构

用户可以属于多个组织,但一次只能在一个组织上下文中操作。

┌─────────────────────────────────────────────────────────────────────┐
│                       多组织切换流程                                    │
├─────────────────────────────────────────────────────────────────────┤
│                                                                     │
│  1. 用户登录                                                         │
│     └── Token 中包含默认组织 ID (org_id claim)                        │
│                                                                     │
│  2. 用户通过 SxTenantSwitcher 切换组织                                │
│     └── UI 调用 ITenantContext.SwitchTenantAsync(orgId)             │
│                                                                     │
│  3. 前端发起 API 请求                                                 │
│     └── 自动附加 Header: X-Organization-Id: {selected_org_id}        │
│                                                                     │
│  4. 后端验证                                                         │
│     ├── 检查 Token 有效性                                            │
│     ├── 检查用户是否属于该组织 (从 Token claims 或数据库)               │
│     └── 执行业务逻辑,数据自动按组织隔离                               │
│                                                                     │
└─────────────────────────────────────────────────────────────────────┘

为什么不用 Token Exchange?

Token Exchange (RFC 8693) 虽然更"标准",但实际开发中:

  • 配置复杂,需要 Keycloak 开启相应功能
  • 每次切换组织都要和 IdP 交互,延迟高
  • 对于"用户属于哪些组织"这种应用级数据,Header 方案更简单实用

15.3 多应用角色 (Multi-App Roles)

使用 Keycloak Client Roles 原生解决不同应用有不同角色的问题。

Keycloak Realm: my-company
├── Realm Roles (全局角色)
│   ├── employee    → 所有员工
│   └── admin       → 超级管理员
│
├── Client: cms-app (内容管理系统)
│   └── Client Roles
│       ├── editor      → 可编辑内容
│       ├── publisher   → 可发布内容
│       └── reviewer    → 可审核内容
│
├── Client: crm-app (客户管理系统)
│   └── Client Roles
│       ├── sales       → 销售人员
│       ├── support     → 客服人员
│       └── manager     → 销售经理
│
└── Client: analytics-app (数据分析)
    └── Client Roles
        ├── viewer      → 只读查看
        └── analyst     → 完整分析权限

工作原理:

  1. 用户 Alice 被分配: cms-app/editor, crm-app/sales
  2. Alice 登录 CMS 应用时,Token 只包含: ["employee", "editor"]
  3. Alice 登录 CRM 应用时,Token 只包含: ["employee", "sales"]
  4. 无需额外配置,Keycloak 自动处理

15.4 内外部用户区分

内部员工和外部客户可以使用同一个 Keycloak Realm,通过 Groups 和 Client Access 控制区分。

Keycloak Realm: my-company
├── Groups
│   ├── internal-users/        → 内部员工组
│   │   ├── engineering
│   │   ├── marketing
│   │   └── sales
│   │
│   └── external-users/        → 外部用户组
│       ├── customers
│       └── partners
│
├── Client: internal-portal
│   └── Access: 只允许 internal-users 组成员
│
├── Client: customer-portal
│   └── Access: 只允许 external-users 组成员
│
└── Client: public-docs
    └── Access: 所有用户可访问

配置方式 (Keycloak):

  1. 创建 Groups 层级结构
  2. 每个 Client 的 Authorization 设置中配置 "Client Access" 策略
  3. 基于 Group 或 Role 限制访问

优势:

  • 同一 Realm,SSO 统一管理
  • Groups 提供灵活的组织结构
  • Client 级别访问控制精确限制
  • 无需维护多个 Realm

15.5 层级用户偏好 (Hierarchical Preferences)

用户偏好支持三级继承,从全局到应用逐级覆盖。

偏好作用域层级:
/                       (Global - 所有应用的默认值)
└── /devtools           (Group - devtools 类应用共享)
    └── /devtools/ide   (App - IDE 应用专属)

继承规则:
├── 子级未设置的值,继承父级
├── 子级设置的值,覆盖父级
└── Clear 操作只清除当前级别,恢复继承

示例:
  /           → Theme: system, Language: zh-CN
  /devtools   → Theme: dark (Language 继承 zh-CN)
  /devtools/ide → Language: en-US (Theme 继承 dark)

解析 /devtools/ide 结果:
  Theme: dark (from /devtools)
  Language: en-US (from /devtools/ide)

接口: IUserPreferencesService

  • GetPreferencesAsync(scopePath) - 获取解析后的偏好
  • GetRawPreferencesAsync(scopePath) - 获取指定作用域的原始偏好
  • UpdatePreferencesAsync(update, scopePath) - 更新指定作用域的偏好
  • ClearPreferencesAsync(scopePath) - 清除指定作用域的偏好

默认配置 (开箱即用)

// 所有默认值都是推荐配置,大多数场景无需修改
builder.Services.AddNextUIIdentity(options =>
{
    // 只需配置 OIDC
    options.OpenIdConnect = new()
    {
        Authority = "https://auth.example.com",
        ClientId = "my-app"
    };

    // 以下都是默认值,列出来是为了说明:
    // options.Offline.Enabled = true;
    // options.Offline.GracePeriod = TimeSpan.FromHours(24);
    // options.TokenStorage.RefreshStrategy = TokenRefreshStrategy.Proactive;
    // options.Authorization.Model = PermissionModel.RBAC;
});

十六、总结

设计原则

原则 说明
专注客户端 不做认证服务,只做 OIDC 客户端集成
不重复造轮子 用户管理、密码、MFA 等交给 IdP
分层架构 Core (接口) → Identity (实现) → Admin (管理组件)
灵活 + 开箱即用 组件可单独使用,也可一键集成完整方案
多租户优先 原生支持用户属于多个组织的场景

包结构

NextUI.Core            - ICurrentUser 等接口定义
NextUI.Identity        - OIDC 集成、用户资料、多租户核心
NextUI.Identity.EF     - Entity Framework 持久化 (可选)
NextUI.Identity.Admin  - 管理组件和预组装页面 (可选)
NextUI.Blazor          - 身份相关 UI 组件 (SxUserMenu 等)

核心价值

价值 描述
开箱即用 配置 IdP 参数即可,无需写代码
灵活扩展 每层都可替换或扩展
多应用 SSO 天然支持,依赖 IdP
多租户 原生支持用户多组织关系
管理后台 组件 + 预组装页面 + 自动路由,按需选用

十七、共享配置文件 (Shared Profile) 详解

17.1 概述

共享配置文件功能允许用户的通用数据(头像、语言、主题等)在多个应用之间同步,提供一致的用户体验。

17.2 架构

┌─────────────────────────────────────────────────────────────────────┐
│                      共享配置文件架构                                   │
├─────────────────────────────────────────────────────────────────────┤
│                                                                     │
│   ┌─────────────┐   ┌─────────────┐   ┌─────────────┐              │
│   │   CMS App   │   │   CRM App   │   │  Chat App   │              │
│   │  (NextUI)   │   │  (NextUI)   │   │  (NextUI)   │              │
│   └──────┬──────┘   └──────┬──────┘   └──────┬──────┘              │
│          │                 │                 │                       │
│          │    ISharedProfileService          │                       │
│          │                 │                 │                       │
│          └────────────────┬┴─────────────────┘                      │
│                           │                                          │
│                           ▼                                          │
│   ┌───────────────────────────────────────────────────────────┐     │
│   │              Shared Profile API                            │     │
│   │                                                           │     │
│   │  GET    /api/shared-profiles/{userId}  获取配置           │     │
│   │  PATCH  /api/shared-profiles/{userId}  部分更新           │     │
│   │  PUT    /api/shared-profiles/{userId}  完整更新           │     │
│   └───────────────────────────────────────────────────────────┘     │
│                           │                                          │
│                           ▼                                          │
│   ┌───────────────────────────────────────────────────────────┐     │
│   │              Shared Profile Store                          │     │
│   │            (Database / Cache / etc.)                       │     │
│   └───────────────────────────────────────────────────────────┘     │
│                                                                     │
└─────────────────────────────────────────────────────────────────────┘

17.3 配置

builder.Services.AddNextUIIdentity(options =>
{
    options.OpenIdConnect = new OpenIdConnectOptions
    {
        Authority = "https://auth.example.com",
        ClientId = "my-app"
    };

    // 启用共享配置文件
    options.SharedProfile = new SharedProfileOptions
    {
        Enabled = true,
        ApiEndpoint = "https://api.example.com/shared-profiles",

        // 选择要同步的字段
        SyncFields = new List<string>
        {
            SharedProfileFields.Avatar,
            SharedProfileFields.DisplayName,
            SharedProfileFields.PreferredLanguage,
            SharedProfileFields.ThemePreference,
            SharedProfileFields.Timezone
        },

        // 登录时自动同步
        SyncOnLogin = true,

        // 更改时立即同步
        ImmediateSync = true,

        // 冲突解决策略
        ConflictResolution = ConflictResolution.LastWriteWins
    };
});

17.4 使用示例

读取共享配置文件

@inject ISharedProfileService SharedProfile

@code {
    private SharedProfile? _profile;

    protected override async Task OnInitializedAsync()
    {
        _profile = await SharedProfile.GetSharedProfileAsync();
    }
}

<div>
    @if (_profile != null)
    {
        <img src="@_profile.AvatarUrl" alt="Avatar" />
        <p>语言: @_profile.PreferredLanguage</p>
        <p>主题: @_profile.ThemePreference</p>
    }
</div>

更新共享配置文件

@inject ISharedProfileService SharedProfile

// 更新单个字段
await SharedProfile.UpdateFieldAsync(SharedProfileFields.ThemePreference, "dark");

// 批量更新
await SharedProfile.UpdateSharedProfileAsync(new Dictionary<string, object?>
{
    { SharedProfileFields.PreferredLanguage, "zh-CN" },
    { SharedProfileFields.Timezone, "Asia/Shanghai" }
});

监听配置变更

@inject ISharedProfileService SharedProfile

protected override void OnInitialized()
{
    SharedProfile.ProfileChanged += OnProfileChanged;
}

private void OnProfileChanged(SharedProfile profile)
{
    // 配置已更新,刷新 UI
    StateHasChanged();
}

public void Dispose()
{
    SharedProfile.ProfileChanged -= OnProfileChanged;
}

17.5 冲突解决

当多个应用同时修改同一字段时,需要冲突解决策略:

策略 说明 适用场景
LastWriteWins 最后的写入覆盖之前的 大多数场景(默认)
ServerWins 服务端值始终保留 需要集中控制
ClientWins 本地值始终保留 离线优先应用

17.6 自定义字段

除了预定义字段,还可以添加自定义字段:

// 配置自定义字段
options.SharedProfile.SyncFields.Add("customField1");
options.SharedProfile.SyncFields.Add("customField2");

// 使用自定义字段
await SharedProfile.UpdateSharedProfileAsync(new Dictionary<string, object?>
{
    { "customField1", "value1" },
    { "customField2", 123 }
});

// 读取自定义字段
var value = SharedProfile.GetField<string>("customField1");

十八、MAUI 平台注意事项

18.1 平台特定实现

NextUI.Identity 在 MAUI 平台需要特定的实现,因为移动平台的认证机制与 Web 不同。

平台差异

功能 Web (Blazor WebAssembly) MAUI
认证流程 OIDC redirect flow WebAuthenticator
Token 存储 ProtectedBrowserStorage SecureStorage
会话管理 Cookie/LocalStorage 平台 KeyChain/KeyStore
深度链接 URL callback App scheme callback

18.2 MAUI 配置

// MauiProgram.cs
public static MauiApp CreateMauiApp()
{
    var builder = MauiApp.CreateBuilder();
    builder
        .UseMauiApp<App>()
        .ConfigureFonts(fonts => { ... });

    // 注册 MAUI 特定的 Identity 服务
    builder.Services.AddScoped<IUserPreferences, MauiUserPreferences>();
    builder.Services.AddScoped<IIdentityStorage, MauiIdentityStorage>();
    builder.Services.AddScoped<ICurrentUser, MauiCurrentUser>();
    builder.Services.AddScoped<IIdentityService, MauiIdentityService>();

    return builder.Build();
}

18.3 MauiIdentityStorage 实现

using Microsoft.Maui.Storage;

public class MauiIdentityStorage : IIdentityStorage
{
    public async Task<T?> GetAsync<T>(string key)
    {
        var json = await SecureStorage.Default.GetAsync(key);
        if (string.IsNullOrEmpty(json))
            return default;

        return JsonSerializer.Deserialize<T>(json);
    }

    public async Task SetAsync<T>(string key, T value)
    {
        var json = JsonSerializer.Serialize(value);
        await SecureStorage.Default.SetAsync(key, json);
    }

    public Task RemoveAsync(string key)
    {
        SecureStorage.Default.Remove(key);
        return Task.CompletedTask;
    }
}

18.4 MauiIdentityService 实现

using Microsoft.Maui.Authentication;

public class MauiIdentityService : IIdentityService
{
    private readonly IdentityOptions _options;
    private readonly MauiCurrentUser _currentUser;
    private readonly MauiIdentityStorage _storage;

    public ICurrentUser CurrentUser => _currentUser;

    public event Action<ICurrentUser>? UserLoggedIn;
    public event Action? UserLoggedOut;

    public async Task LoginAsync(string? returnUrl = null, string? provider = null)
    {
        var oidc = _options.OpenIdConnect!;

        var authResult = await WebAuthenticator.Default.AuthenticateAsync(
            new Uri($"{oidc.Authority}/authorize?" +
                   $"client_id={oidc.ClientId}&" +
                   $"redirect_uri={GetCallbackUrl()}&" +
                   $"response_type=code&" +
                   $"scope={string.Join(" ", oidc.Scopes)}"),
            new Uri(GetCallbackUrl()));

        if (authResult != null)
        {
            // Exchange code for tokens
            await ExchangeCodeForTokensAsync(authResult.Properties["code"]);
        }
    }

    public async Task LogoutAsync(string? returnUrl = null)
    {
        await _storage.RemoveAsync("access_token");
        await _storage.RemoveAsync("refresh_token");
        _currentUser.Clear();
        UserLoggedOut?.Invoke();
    }

    private string GetCallbackUrl()
    {
        // 使用 app scheme
        return "myapp://callback";
    }
}

18.5 平台特定配置

Android (AndroidManifest.xml)

<activity android:name="Microsoft.Maui.Authentication.WebAuthenticatorCallbackActivity"
          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" />
        <data android:scheme="myapp" android:host="callback" />
    </intent-filter>
</activity>

iOS (Info.plist)

<key>CFBundleURLTypes</key>
<array>
    <dict>
        <key>CFBundleURLName</key>
        <string>My App</string>
        <key>CFBundleURLSchemes</key>
        <array>
            <string>myapp</string>
        </array>
    </dict>
</array>

十九、快速入门指南

19.1 安装

# 使用 NuGet
dotnet add package NextUI.Identity

# 或使用项目引用
<ProjectReference Include="../NextUI.Identity/NextUI.Identity.csproj" />

19.2 基础配置 (5 分钟上手)

1. 配置 Program.cs

using NextUI.Identity;

var builder = WebAssemblyHostBuilder.CreateDefault(args);

// 添加 NextUI Identity
builder.Services.AddNextUIIdentity(options =>
{
    options.UseKeycloak(
        authority: "https://auth.example.com/realms/my-realm",
        clientId: "my-blazor-app"
    );
});

// 其他服务...
builder.Services.AddNextDesignSystem();

await builder.Build().RunAsync();

2. 更新 App.razor

<CascadingAuthenticationState>
    <Router AppAssembly="@typeof(App).Assembly">
        <Found Context="routeData">
            <AuthorizeRouteView RouteData="@routeData"
                               DefaultLayout="@typeof(MainLayout)">
                <NotAuthorized>
                    <p>请先登录</p>
                    <SxLoginButton />
                </NotAuthorized>
            </AuthorizeRouteView>
        </Found>
        <NotFound>
            <p>页面未找到</p>
        </NotFound>
    </Router>
</CascadingAuthenticationState>

3. 添加登录按钮

@* MainLayout.razor *@
<SxAppShell>
    <NavBar>
        <SxNavBar>
            <UserMenu>
                <SxIdentityUserBar />
            </UserMenu>
        </SxNavBar>
    </NavBar>
    <Body>
        @Body
    </Body>
</SxAppShell>

19.3 常见场景

场景 1: 保护特定页面

@page "/admin"
@attribute [Authorize(Roles = "admin")]

<h1>管理页面</h1>
<p>只有管理员可以看到此页面</p>

场景 2: 条件渲染

@inject ICurrentUser User

@if (User.IsAuthenticated)
{
    <p>欢迎, @User.DisplayName!</p>
    @if (User.IsInRole("admin"))
    {
        <SxButton OnClick="OpenAdminPanel">管理面板</SxButton>
    }
}
else
{
    <SxLoginButton Text="登录" />
}

场景 3: 调用受保护 API

@inject IIdentityService Identity
@inject HttpClient Http

private async Task CallApiAsync()
{
    var token = await Identity.GetAccessTokenAsync();
    if (token != null)
    {
        Http.DefaultRequestHeaders.Authorization =
            new AuthenticationHeaderValue("Bearer", token);

        var response = await Http.GetAsync("/api/protected");
        // ...
    }
}

场景 4: 多租户切换

@inject ITenantContext TenantContext

<SxSelect @bind-Value="_selectedTenant" Label="选择组织">
    @foreach (var membership in TenantContext.Memberships)
    {
        <SxSelectItem Value="@membership.Tenant.TenantId">
            @membership.Tenant.DisplayName
        </SxSelectItem>
    }
</SxSelect>

@code {
    private string? _selectedTenant;

    private async Task OnTenantChange(string tenantId)
    {
        await TenantContext.SwitchTenantAsync(tenantId);
    }
}

19.4 IdP 配置示例

Keycloak

builder.Services.AddNextUIIdentity(options =>
{
    options.UseKeycloak(
        authority: "https://keycloak.example.com/realms/my-realm",
        clientId: "my-blazor-app",
        additionalScopes: new[] { "roles", "profile" }
    );
});

Auth0

builder.Services.AddNextUIIdentity(options =>
{
    options.UseAuth0(
        domain: "my-tenant.auth0.com",
        clientId: "your-client-id",
        audience: "https://api.example.com"
    );
});

Azure AD

builder.Services.AddNextUIIdentity(options =>
{
    options.UseAzureAD(
        tenantId: "your-tenant-id",
        clientId: "your-client-id"
    );
});

二十、API 参考

20.1 ICurrentUser

public interface ICurrentUser
{
    bool IsAuthenticated { get; }
    string? UserId { get; }
    string? DisplayName { get; }
    string? Email { get; }
    string? AvatarUrl { get; }
    IReadOnlyList<string> Roles { get; }
    IReadOnlyDictionary<string, string> Claims { get; }

    bool IsInRole(string role);
    bool HasPermission(string permission);

    event Action? Changed;
}

20.2 IIdentityService

public interface IIdentityService
{
    ICurrentUser CurrentUser { get; }

    Task LoginAsync(string? returnUrl = null, string? provider = null);
    Task LogoutAsync(string? returnUrl = null);
    Task<bool> RefreshTokenAsync();
    Task<string?> GetAccessTokenAsync();

    event Action<ICurrentUser>? UserLoggedIn;
    event Action? UserLoggedOut;
}

20.3 ISharedProfileService

public interface ISharedProfileService
{
    bool IsEnabled { get; }

    Task<SharedProfile?> GetSharedProfileAsync();
    Task UpdateSharedProfileAsync(IDictionary<string, object?> updates);
    Task UpdateFieldAsync(string field, object? value);
    Task SyncFromServerAsync();
    T? GetField<T>(string field, T? defaultValue = default);

    event Action<SharedProfile>? ProfileChanged;
}

20.4 ITenantContext

public interface ITenantContext
{
    ITenant? CurrentTenant { get; }
    IReadOnlyList<ITenantMembership> Memberships { get; }

    Task SwitchTenantAsync(string tenantId);
    IReadOnlyList<string> GetCurrentRoles();
    bool HasPermission(string permission);

    event Action<ITenant?>? TenantChanged;
}

二十一、故障排除

常见问题

Q: 登录后跳转不回应用

检查 OIDC 配置中的 CallbackPath 是否与 IdP 配置匹配:

options.OpenIdConnect = new OpenIdConnectOptions
{
    // ...
    CallbackPath = "/signin-oidc",  // 确保与 IdP 中配置的回调 URL 一致
};

Q: Token 刷新失败

检查 Refresh Token 是否启用:

options.OpenIdConnect.Scopes.Add("offline_access");

Q: 角色检查不生效

确认角色 Claim 名称配置正确:

options.Authorization.RolesClaimName = "roles";  // Keycloak 默认
// 或
options.Authorization.RolesClaimName = "http://schemas.microsoft.com/ws/2008/06/identity/claims/role";  // Azure AD

Q: 共享配置文件不同步

  1. 检查 SharedProfile.Enabled = true
  2. 确认 ApiEndpoint 可访问
  3. 检查用户是否已登录

二十二、下一步

  1. ✅ 确认此设计方案符合需求
  2. ✅ Phase 1-3 基础实现完成
  3. 接下来的工作:
    • 创建单元测试 (Phase 2)
    • 创建 Mock Provider (Phase 3)
    • Workbench 集成 (Phase 4)
    • Samples 集成 (Phase 5)
    • 部署脚本 (Phase 6-7)
    • CLI 支持 (Phase 8)