Orchard Core OIDC 认证与授权方案总结
34

1. 概述

本文档详细总结了一个基于 Orchard Core 作为 OpenID Connect (OIDC) 认证服务器、ASP.NET Core Web API 作为受保护资源服务器 (MySecureWebApi),以及 Vue.js 前端作为 OIDC 客户端的认证与授权集成方案。内容涵盖了从最初的认证授权到 Access Token 刷新,以及在过程中遇到的问题和解决方案。

2. 项目角色与技术栈

  • 认证服务器 (Identity Server):

    • 项目: Orchard Core CMS 实例

    • 模块: OrchardCore.OpenId (OpenID Connect Provider/Server 功能), OrchardCore.Users (用户管理)

    • 作用: 负责用户身份验证、颁发 Access Token、ID Token 和 Refresh Token。

  • 资源服务器 (Resource Server):

    • 项目: MySecureWebApi (ASP.NET Core Web API)

    • 技术: Microsoft.AspNetCore.Authentication.JwtBearer (JWT Bearer 认证中间件)

    • 作用: 提供受保护的 API 接口 (/WeatherForecast),验证传入的 Access Token 并基于 Scope 进行授权。

  • 客户端 (Client Application):

    • 项目: Vue.js 前端应用

    • 技术: oidc-client.js

    • 作用: 引导用户登录、获取 Access Token、携带 Access Token 访问 API、以及在 Access Token 过期后静默刷新。

3. 认证与授权流程详解

3.1 初始认证流程 (Authorization Code Flow with PKCE)

  1. Vue 前端发起登录请求:

    • 用户在 Vue 应用中点击登录按钮。

    • oidc-client.js 构建一个包含以下参数的认证请求,并重定向用户浏览器到 Orchard Core 的授权端点 (/connect/authorize):

      • client_id: vue-frontend-app

      • redirect_uri: http://localhost:8080/callback

      • response_type: code (授权码流)

      • scope: openid profile api1 offline_access (关键:offline_access 用于请求刷新令牌)

      • code_challenge: PKCE 挑战码,由 code_verifier 生成

      • code_challenge_method: S256 (通常)

      • prompt: consent (可选,用于强制弹出授权页面)

    • 相关配置: Vue 前端 oidc-client.js settings 中的 scope, automaticSilentRenew: true, silent_redirect_uri, extraQueryParams: { prompt: 'consent' } (可选,调试用)。

  2. Orchard Core 用户认证与授权:

    • Orchard Core 接收到认证请求。

    • 如果用户未登录,Orchard Core 会显示登录界面,用户输入凭据进行验证。

    • 如果需要,Orchard Core 会显示授权页面 (Consent Screen),列出客户端请求的 Scope (openid, profile, api1, offline_access),请求用户授权。

    • 用户同意授权。

  3. Orchard Core 颁发授权码:

    • Orchard Core 将授权码 (code) 和 state 参数通过重定向回 redirect_uri (http://localhost:8080/callback)。

  4. Vue 前端交换授权码为令牌:

    • oidc-client.js 在回调页面接收到授权码。

    • 它使用授权码、redirect_uriclient_idgrant_type=authorization_code 以及之前生成的 code_verifier,向 Orchard Core 的令牌端点 (/connect/token) 发送 POST 请求。

  5. Orchard Core 颁发令牌:

    • Orchard Core 验证授权码和 PKCE code_verifier

    • 成功后,颁发:

      • Access Token (JWT): 用于访问受保护 API。包含 scope (如 openid profile api1 offline_access)、aud (audience,如 api1)、exp (过期时间) 等 Claim。

      • ID Token (JWT): 包含用户身份信息。

      • Refresh Token: 用于在 Access Token 过期后获取新的 Access Token。

  6. Vue 前端存储令牌并更新 UI:

    • oidc-client.js 将 Access Token、ID Token 和 Refresh Token 存储在 localStoragesessionStorage 中。

    • 更新 Vue 前端 UI,显示用户已登录状态和 Access Token 过期倒计时。

3.2 访问受保护 API (MySecureWebApi)

  1. Vue 前端发起 API 请求:

    • Vue 应用从 oidc-client.js 获取当前有效的 Access Token。

    • 在向 MySecureWebApi 发送的 HTTP 请求的 Authorization 头中携带 Access Token (例如: Authorization: Bearer <AccessToken>)。

  2. MySecureWebApi 验证 Access Token:

    • MySecureWebApi 收到请求,JWT Bearer 认证中间件开始验证 Access Token。

    • Program.cs 配置:

      • options.Authority: 指向 Orchard Core 地址 (http://localhost:5233/)。

      • options.Audience: api1

      • options.MapInboundClaims = false;: (强烈推荐) 禁用默认 Claim 类型映射,确保后端看到的 Claim 类型与 JWT 原始类型一致 (例如 scope 而不是 http://schemas.microsoft.com/ws/...)。

      • options.TokenValidationParameters.ValidateLifetime = true;: 确保验证令牌生命周期。

      • 临时测试配置 (LifetimeValidator): 为快速测试刷新令牌,MySecureWebApi 额外配置 LifetimeValidator,强制令牌在签发后 N 秒 (例如 10 秒) 内过期,无论原始 exp 值是多少。

    • 相关问题: 之前出现 403 Forbidden 错误,是因为授权策略 policy.RequireClaim("scope", claim => claim.Contains("api1")) 无法正确匹配 scope Claim 的字符串值。

    • 解决方案: 将授权策略修改为使用 RequireAssertion,通过 context.User.FindAll("scope").Any(c => c.Value.Contains("api1")) 来判断 scope Claim 的值是否包含 "api1"

  3. MySecureWebApi 执行授权策略:

    • 如果 Access Token 验证成功,MySecureWebApi 根据控制器或 Action 上的 [Authorize(Policy = "ApiScope")] 注解,执行名为 "ApiScope" 的授权策略。

    • "ApiScope" 策略要求用户已认证 (RequireAuthenticatedUser) 并且其 Access Token 中包含 api1 Scope。

    • 如果策略通过,API 请求被允许,MySecureWebApi 返回数据 (WeatherForecast)。否则,返回 403 Forbidden。

3.3 Access Token 刷新流程

  1. Access Token 即将过期 (前端检测):

    • oidc-client.js 通过 automaticSilentRenew: true 配置,会监控 Access Token 的过期时间 (exp Claim)。

    • 当 Access Token 的剩余有效期达到 accessTokenExpiringNotificationTime (默认 60 秒,测试时设置为 3500 秒,以提前触发) 时,oidc-client.js 会触发静默刷新流程。

    • 相关问题: 即使后端 LifetimeValidator 强制 10 秒过期,前端界面仍显示 3600 秒有效期,是因为前端的倒计时是基于令牌原始 exp Claim。

  2. 静默刷新请求:

    • oidc-client.js 在一个隐藏的 iframe 中加载 silent_redirect_uri (http://localhost:8080/silent-renew.html)。

    • silent-renew.html 中的 JavaScript (new Oidc.UserManager().signinSilentCallback();) 会使用存储的 Refresh Token 向 Orchard Core 的令牌端点 (/connect/token) 发送 POST 请求,grant_typerefresh_token

  3. Orchard Core 颁发新的 Access Token:

    • Orchard Core 验证 Refresh Token 的有效性。

    • 成功验证后,颁发新的 Access Token (通常也伴随一个新的 Refresh Token,如果配置了刷新令牌轮换)。

  4. Vue 前端更新令牌:

    • oidc-client.js 接收到新的令牌,更新其内部存储,并替换掉旧的 Access Token。

    • Vue 应用继续使用新的 Access Token 访问 API,用户感知不到重新登录。

4. 各项目总结与关键配置

4.1 Orchard Core (认证服务器 - Identity Server) 总结

作用: 作为 OpenID Connect 提供者,它是整个认证授权方案的中心。负责用户身份验证、管理客户端与资源、以及 Access Token、ID Token、Refresh Token 的签发与管理。

关键配置与注意事项:

  1. 特性启用:

    • OpenID Connect: 核心认证功能,必须启用。

    • OpenID Connect Client Integration (OrchardCore.OpenId.Client): 如果不希望登录页面显示由其他 OIDC 客户端特性自动注册的外部认证按钮(如 vue-frontend-app),应禁用此特性。

    • OpenID Connect Management: 必须启用,用于在后台管理 OIDC 客户端 (Applications) 和 Scope。

  2. 管理 -> 应用程序 (Applications):

    • 注册客户端 (vue-frontend-app):

      • Client ID: vue-frontend-app (与前端配置一致)。

      • Type: Public (因为前端是单页应用,无法安全存储 Client Secret)。

      • Redirect URIs: http://localhost:8080/callback, http://localhost:8080/silent-renew.html

      • Post Logout Redirect URIs: http://localhost:8080

      • Flows: 必须允许 Authorization Code Flow (这是 Vue 前端使用的流程)。

      • Scope: 确保前端客户端请求的所有 Scope 都已在此处允许,包括 openid, profile, api1, 和 offline_access

        • offline_access 的重要性: 只有当此处的客户端配置允许,并且前端在请求时包含 offline_access Scope,Orchard Core 才会签发 Refresh Token。在 Orchard Core 后台的 API (资源服务器,如 MyWebApi) 上配置 offline_access 与否,不影响客户端获取 Refresh Token

    • 注册资源 (MyWebApi):

      • Client ID: api1 (与 MySecureWebApiAudience 配置一致)。

      • Type: 即使是 API 资源,在 Orchard Core 中也会以 Application 形式出现。其主要目的是定义和暴露该 API 所能提供的 Scope。

      • Scope: 定义 api1 这个 Scope,表示此资源提供该 Scope 保护下的接口。

  3. 设置:

    • Authorization server:

      • Endpoints: 确保 Token Endpoint, Authorization Endpoint, Logout Endpoint, User Info Endpoint 等都已启用。

      • Flows: 确保 Allow Authorization Code FlowAllow Refresh Token Flow 已启用。

      • Proof Key for Code Exchange (PKCE): 通常建议 Require Proof Key for Code Exchange 启用,尤其对于 Public 客户端。

      • 令牌生命周期 (Access Token Lifetime): 已知问题点。在您的 Orchard Core 版本和配置中,该字段似乎未在 UI 中暴露。这意味着 Access Token 的默认生命周期可能为 3600 秒 (或 Orchard Core 内部设定的值)。若要测试刷新令牌,需依赖后端 API 强制令牌过期。

      • Refresh Token 行为: Disable Rolling Refresh Tokens(默认是滚动刷新,即每次刷新都会生成新的刷新令牌并使旧的失效)。

  4. Scope 定义:

    • Security -> OpenID Connect -> Management -> Scopes 中,确保 openid, profile, api1, offline_access 等自定义和标准 Scope 已被定义。这是告诉 Orchard Core 有哪些 Scope 可供使用。

  5. 登录界面样式修改:

    • 修改 Orchard Core 认证服务器的登录界面样式(如您截图中所示的登录页),需要创建或修改自定义的 Orchard Core 主题

    • 通过在自定义主题中覆盖 OrchardCore.Users 模块提供的视图文件 (如 Login.cshtml 或其对应的 Liquid 文件) 来实现。需要将主题设置为当前站点主题。

4.2 MySecureWebApi (资源服务器) 总结

作用: 提供受保护的 Web API 接口 (/WeatherForecast),并使用 JWT Bearer 认证中间件验证客户端传入的 Access Token。同时,执行基于 Scope 的授权策略。

关键配置与注意事项:

  1. JWT Bearer 认证配置 (Program.cs):

    C#

    builder.Services.AddAuthentication(JwtBearerDefaults.AuthenticationScheme)
        .AddJwtBearer(options =>
        {
            options.Authority = "http://localhost:5233/"; // 指向 Orchard Core 地址
            options.RequireHttpsMetadata = false; // 仅用于开发环境,生产环境应为 true
            options.Audience = "api1"; // 与 Orchard Core 中 API 的 Client ID (Resource Name) 匹配
            options.MapInboundClaims = false; // 强烈建议禁用,确保后端看到的 Claim 类型与 JWT 原始类型一致 (例如 'scope' 而不是冗长的 URI)
            options.TokenValidationParameters = new TokenValidationParameters
            {
                ValidateAudience = true,
                ValidAudience = "api1",
                ValidateIssuer = true,
                ValidIssuer = "http://localhost:5233/",
                ValidateLifetime = true, // 确保验证令牌生命周期
                ClockSkew = TimeSpan.Zero, // 严格校验令牌过期时间,无容忍度
                // 临时测试刷新令牌:强制令牌在 10 秒后过期,无论原始 exp 值
                LifetimeValidator = (notBefore, expires, securityToken, validationParameters) =>
                {
                    var shortLifetime = TimeSpan.FromSeconds(10); // 调整此值以控制测试过期时间
                    if (expires.HasValue && notBefore.HasValue && (expires.Value - notBefore.Value) > shortLifetime)
                    {
                        // 如果令牌的原始生命周期比我们定义的短生命周期长,则强制按短生命周期计算过期
                        return DateTime.UtcNow <= notBefore.Value.Add(shortLifetime);
                    }
                    // 否则,使用原始的过期时间进行验证
                    return expires.HasValue && expires.Value > DateTime.UtcNow;
                }
            };
            // OnTokenValidated 事件仅用于日志记录和调试,生产环境中通常精简或移除调试日志。
            // 确保移除了任何导致响应被处理或请求被提前中止的调试代码 (如 context.HandleResponse())。
            options.Events = new JwtBearerEvents
            {
                OnTokenValidated = context => { /* 仅日志记录 */ return Task.CompletedTask; },
                OnAuthenticationFailed = context => { /* 仅日志记录 */ return Task.CompletedTask; },
                OnForbidden = context => { /* 仅日志记录 */ return Task.CompletedTask; }
            };
        });
    
  2. 授权策略配置 (Program.cs):

    C#

    builder.Services.AddAuthorization(options =>
    {
        options.AddPolicy("ApiScope", policy =>
        {
            policy.RequireAuthenticatedUser();
            // 解决 403 Forbidden 问题:使用 RequireAssertion 来检查 'scope' Claim 是否包含 'api1'
            policy.RequireAssertion(context =>
            {
                var scopeClaims = context.User.FindAll("scope");
                return scopeClaims.Any(c => c.Value.Contains("api1"));
            });
        });
    });
    
    • 在控制器或 Action 上使用 [Authorize(Policy = "ApiScope")] 保护 API 接口。

4.3 Vue.js 前端应用 (客户端) 总结

作用: 作为用户界面,引导用户进行认证,管理 Access Token 的生命周期,并在 Access Token 过期后通过刷新令牌机制无缝获取新的令牌以访问受保护的 API。

关键配置与注意事项:

  1. oidc-client.js 配置 (settings 对象):

    JavaScript

    import { UserManager } from 'oidc-client';
    
    const settings = {
      authority: 'http://localhost:5233/', // Orchard Core 认证服务器的地址
      client_id: 'vue-frontend-app', // 对应 Orchard Core 中注册的客户端 ID
      redirect_uri: window.location.origin + '/callback', // 认证回调地址
      response_type: 'code', // 使用授权码流
      scope: 'openid profile api1 offline_access', // 必须包含 'offline_access' 才能从服务器获取刷新令牌
      post_logout_redirect_uri: window.location.origin, // 登出后重定向地址
    
      // ****** 刷新令牌关键配置 ******
      automaticSilentRenew: true, // 启用自动静默刷新
      silent_redirect_uri: window.location.origin + '/silent-renew.html', // 静默刷新回调页面
      // 当 Access Token 剩余有效期达到此值时,oidc-client 将尝试刷新。
      // 对于 Orchard Core 默认 3600 秒的令牌,将其设置为 3500 秒,可以在令牌实际签发约 100 秒后触发刷新。
      accessTokenExpiringNotificationTime: 3500,
      // ******************************
    
      // 调试时强制弹出授权页面 (使用后建议移除)
      // extraQueryParams: { prompt: 'consent' },
    
      filterProtocolClaims: true, // 过滤掉 OIDC 协议中不需要的 Claim
      loadUserInfo: true, // 登录后从 UserInfo 端点加载用户声明
      revokeAccessTokenOnSignout: true, // 登出时撤销 Access Token
    };
    
    const userManager = new UserManager(settings);
    
    // 重要的事件监听 (示例)
    userManager.events.addUserLoaded(user => {
        console.log("用户已加载", user);
        // 更新 UI 显示 Access Token 有效期等信息
    });
    userManager.events.addAccessTokenExpiring(() => {
        console.log("Access Token 即将过期,尝试静默刷新...");
    });
    userManager.events.addAccessTokenExpired(() => {
        console.log("Access Token 已过期。");
        // 可能需要通知用户或强制登出,如果静默刷新失败
    });
    userManager.events.addSilentRenewError(error => {
        console.error("静默刷新失败", error);
        // 静默刷新失败通常意味着 Refresh Token 已失效,需要用户重新登录
    });
    // ... 其他 userManager 事件和管理方法
    
  2. public/silent-renew.html 文件:

    • 该文件必须存在于 Vue 应用的 public 目录下,并能通过 silent_redirect_uri 访问。

    • 它的唯一作用是加载 oidc-client.min.js 并调用 signinSilentCallback() 来处理静默刷新流程的回调。

    HTML

    <!DOCTYPE html>
    <html>
    <head>
        <meta charset="utf-8" />
        <title>Silent Renew</title>
    </head>
    <body>
        <script src="https://cdn.jsdelivr.net/npm/oidc-client@latest/dist/oidc-client.min.js"></script>
        <script>
            // 此行代码会处理静默刷新回调
            new Oidc.UserManager().signinSilentCallback();
        </script>
    </body>
    </html>
    
  3. API 请求:

    • 使用 userManager.getUser() 获取当前用户对象,其中包含 Access Token。

    • 在向 MySecureWebApi 发送的 Axios 或 Fetch 请求头中携带 Authorization: Bearer <AccessToken>

5. 测试方法

  1. API 授权测试:

    • 确保 MySecureWebApi 正在运行。

    • 登录 Vue 前端,点击调用 API 按钮。

    • 验证 API 返回 200 OK,并且数据正确。

  2. 登录页面按钮移除测试:

    • 清除浏览器缓存和 Cookie。

    • 访问 Orchard Core 登录页,验证 "vue-frontend-app" 按钮是否消失。

  3. 刷新令牌测试:

    • 前提: MySecureWebApiLifetimeValidator 设置为短时间 (例如 10 秒)。

    • 清除所有浏览器缓存和数据 (Cookie, Local Storage, Session Storage)。

    • 登录 Vue 前端。

    • 打开浏览器开发者工具 (F12) 的 ConsoleNetwork 标签页。

    • 等待 MySecureWebApiLifetimeValidator 强制过期时间 (例如 10-15 秒)。

    • 再次点击 API 调用按钮。

    • 观察:

      • 第一次 API 调用可能会失败 (因为后端认为令牌已过期,例如 401 Unauthorized 或 403 Forbidden)。

      • Console (控制台): 出现 oidc-client.jsAccess token expiringAccess token renewed successfully. 日志。

      • Network (网络): 出现针对 Orchard Core /connect/tokenPOST 请求,其中 grant_type=refresh_token

      • 刷新成功后,再次调用 API 应该成功。

6.具体步骤

https://zdbxll.cn/console/posts/editor?name=9f68cda8-46c0-4fc3-a454-ae3d91280022

https://zdbxll.cn/console/posts/editor?name=a22240ea-77fc-4f29-9ee0-0d7b1c7a27c3

Orchard Core OIDC 认证与授权方案总结
https://zdbxll.cn/archives/1750148395963
作者
Administrator
发布于
更新于
许可