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)
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' }
(可选,调试用)。
Orchard Core 用户认证与授权:
Orchard Core 接收到认证请求。
如果用户未登录,Orchard Core 会显示登录界面,用户输入凭据进行验证。
如果需要,Orchard Core 会显示授权页面 (Consent Screen),列出客户端请求的 Scope (
openid
,profile
,api1
,offline_access
),请求用户授权。用户同意授权。
Orchard Core 颁发授权码:
Orchard Core 将授权码 (
code
) 和state
参数通过重定向回redirect_uri
(http://localhost:8080/callback
)。
Vue 前端交换授权码为令牌:
oidc-client.js
在回调页面接收到授权码。它使用授权码、
redirect_uri
、client_id
、grant_type=authorization_code
以及之前生成的code_verifier
,向 Orchard Core 的令牌端点 (/connect/token
) 发送POST
请求。
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。
Vue 前端存储令牌并更新 UI:
oidc-client.js
将 Access Token、ID Token 和 Refresh Token 存储在localStorage
或sessionStorage
中。更新 Vue 前端 UI,显示用户已登录状态和 Access Token 过期倒计时。
3.2 访问受保护 API (MySecureWebApi
)
Vue 前端发起 API 请求:
Vue 应用从
oidc-client.js
获取当前有效的 Access Token。在向
MySecureWebApi
发送的 HTTP 请求的Authorization
头中携带 Access Token (例如:Authorization: Bearer <AccessToken>
)。
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"
。
MySecureWebApi 执行授权策略:
如果 Access Token 验证成功,
MySecureWebApi
根据控制器或 Action 上的[Authorize(Policy = "ApiScope")]
注解,执行名为 "ApiScope" 的授权策略。"ApiScope" 策略要求用户已认证 (
RequireAuthenticatedUser
) 并且其 Access Token 中包含api1
Scope。如果策略通过,API 请求被允许,
MySecureWebApi
返回数据 (WeatherForecast
)。否则,返回 403 Forbidden。
3.3 Access Token 刷新流程
Access Token 即将过期 (前端检测):
oidc-client.js
通过automaticSilentRenew: true
配置,会监控 Access Token 的过期时间 (exp
Claim)。当 Access Token 的剩余有效期达到
accessTokenExpiringNotificationTime
(默认 60 秒,测试时设置为 3500 秒,以提前触发) 时,oidc-client.js
会触发静默刷新流程。相关问题: 即使后端
LifetimeValidator
强制 10 秒过期,前端界面仍显示 3600 秒有效期,是因为前端的倒计时是基于令牌原始exp
Claim。
静默刷新请求:
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_type
为refresh_token
。
Orchard Core 颁发新的 Access Token:
Orchard Core 验证 Refresh Token 的有效性。
成功验证后,颁发新的 Access Token (通常也伴随一个新的 Refresh Token,如果配置了刷新令牌轮换)。
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 的签发与管理。
关键配置与注意事项:
特性启用:
OpenID Connect: 核心认证功能,必须启用。
OpenID Connect Client Integration (OrchardCore.OpenId.Client): 如果不希望登录页面显示由其他 OIDC 客户端特性自动注册的外部认证按钮(如
vue-frontend-app
),应禁用此特性。OpenID Connect Management: 必须启用,用于在后台管理 OIDC 客户端 (Applications) 和 Scope。
管理 -> 应用程序 (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
(与MySecureWebApi
的Audience
配置一致)。Type: 即使是 API 资源,在 Orchard Core 中也会以 Application 形式出现。其主要目的是定义和暴露该 API 所能提供的 Scope。
Scope: 定义
api1
这个 Scope,表示此资源提供该 Scope 保护下的接口。
设置:
Authorization server:
Endpoints: 确保
Token Endpoint
,Authorization Endpoint
,Logout Endpoint
,User Info Endpoint
等都已启用。Flows: 确保
Allow Authorization Code Flow
和Allow 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
(默认是滚动刷新,即每次刷新都会生成新的刷新令牌并使旧的失效)。
Scope 定义:
在
Security
->OpenID Connect
->Management
->Scopes
中,确保openid
,profile
,api1
,offline_access
等自定义和标准 Scope 已被定义。这是告诉 Orchard Core 有哪些 Scope 可供使用。
登录界面样式修改:
修改 Orchard Core 认证服务器的登录界面样式(如您截图中所示的登录页),需要创建或修改自定义的 Orchard Core 主题。
通过在自定义主题中覆盖
OrchardCore.Users
模块提供的视图文件 (如Login.cshtml
或其对应的 Liquid 文件) 来实现。需要将主题设置为当前站点主题。
4.2 MySecureWebApi (资源服务器) 总结
作用: 提供受保护的 Web API 接口 (/WeatherForecast
),并使用 JWT Bearer 认证中间件验证客户端传入的 Access Token。同时,执行基于 Scope 的授权策略。
关键配置与注意事项:
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; } }; });
授权策略配置 (
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。
关键配置与注意事项:
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 事件和管理方法
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>
API 请求:
使用
userManager.getUser()
获取当前用户对象,其中包含 Access Token。在向
MySecureWebApi
发送的 Axios 或 Fetch 请求头中携带Authorization: Bearer <AccessToken>
。
5. 测试方法
API 授权测试:
确保
MySecureWebApi
正在运行。登录 Vue 前端,点击调用 API 按钮。
验证 API 返回 200 OK,并且数据正确。
登录页面按钮移除测试:
清除浏览器缓存和 Cookie。
访问 Orchard Core 登录页,验证 "vue-frontend-app" 按钮是否消失。
刷新令牌测试:
前提:
MySecureWebApi
的LifetimeValidator
设置为短时间 (例如 10 秒)。清除所有浏览器缓存和数据 (Cookie, Local Storage, Session Storage)。
登录 Vue 前端。
打开浏览器开发者工具 (F12) 的 Console 和 Network 标签页。
等待
MySecureWebApi
的LifetimeValidator
强制过期时间 (例如 10-15 秒)。再次点击 API 调用按钮。
观察:
第一次 API 调用可能会失败 (因为后端认为令牌已过期,例如 401 Unauthorized 或 403 Forbidden)。
Console (控制台): 出现
oidc-client.js
的Access token expiring
和Access token renewed successfully.
日志。Network (网络): 出现针对 Orchard Core
/connect/token
的POST
请求,其中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