news 2026/9/11 6:48:35

Backstage 自定义认证 Provider 模块开发指南:基于 auth-backend 扩展新认证方式

作者头像

张小明

前端开发工程师

1.2k 24
文章封面图
Backstage 自定义认证 Provider 模块开发指南:基于 auth-backend 扩展新认证方式

Backstage 自定义认证 Provider 模块开发指南:基于 auth-backend 扩展新认证方式

【免费下载链接】backstageBackstage is an open framework for building developer portals项目地址: https://gitcode.com/GitHub_Trending/ba/backstage

本文是一份面向 Backstage 贡献者与平台开发者的技术指南,讲解如何为auth-backend添加全新的外部认证 Provider(如自建 OAuth 服务、企业内部 SSO 或反向代理认证)。文章以官方文档 docs/auth/add-auth-provider.md 为核心骨架,结合当前仓库中auth-backendplugin-auth-node与各内置认证模块的真实源码,深入剖析认证流程、核心接口与完整落地步骤。读完本文,你将掌握AuthProviderRouteHandlers接口模型、OAuthEnvironmentHandler多环境机制、Passport 策略封装方式,以及从yarn new创建模块到接入后端、调试验证的全流程实战能力。

认证(Authentication)是如何工作的

Backstage 应用本身不直接持有用户凭证,而是通过接入各种外部认证提供者来完成身份验证。在auth-backend中,每个外部 Provider 都被包装成一个实现了AuthProviderRouteHandlers接口的对象。该接口(定义于 plugins/auth-node/src/types.ts)由四个方法构成,每个方法默认挂载在/api/auth/[provider]/method形式的端点上:

/auth/[provider]/start -> 从网页发起一次登录 /auth/[provider]/handler/frame -> 处理一次已完成的外部认证操作(回调) /auth/[provider]/refresh -> 刷新一次登录的有效性 /auth/[provider]/logout -> 登出已登录的用户

其中refreshlogout是可选方法(接口中声明为refresh?logout?),只有 Provider 支持时才被挂载。整个登录流程如下:

  1. 用户尝试登录;
  2. 前端打开一个弹出窗口(popup),指向auth端点。该端点先完成一些初始准备(如写入 nonce cookie、拼接 state 参数),然后在弹窗内将用户重定向到外部认证方;
  3. 外部认证方验证用户身份,并把验证结果(成功或失败)返回给包装器的handler/frame端点;
  4. handler/frame渲染出的网页向打开弹窗的父页面发出适当的响应,随后弹窗关闭;
  5. 用户点击界面上的登出入口,网页向logout端点发出请求完成登出。

这段流程在源码中有着完整的落点:bindProviderRouters函数(见 plugins/auth-backend/src/providers/router.ts)会为每个已注册 Provider 逐一创建子路由并绑定方法:

r.get('/start', provider.start.bind(provider)); r.get('/handler/frame', provider.frameHandler.bind(provider)); r.post('/handler/frame', provider.frameHandler.bind(provider)); if (provider.logout) { r.post('/logout', provider.logout.bind(provider)); } if (provider.refresh) { r.get('/refresh', provider.refresh.bind(provider)); r.post('/refresh', provider.refresh.bind(provider)); } targetRouter.use(`/${providerId}`, r);

可以确认:start只暴露 GET,handler/frame同时支持 GET 与 POST,logout为 POST,refresh在支持时同时暴露 GET 与 POST。若配置缺失,bindProviderRouters还会注册一个抛出NotFoundError的兜底路由,提示auth.providers.<providerId>配置缺失或环境变量未定义(见 plugins/auth-backend/src/providers/router.ts)。

核心接口:AuthProviderRouteHandlers

任何认证包装器都必须实现AuthProviderRouteHandlers接口(plugins/auth-node/src/types.ts)。接口对四个方法做了明确约定:

  • start(req, res):处理start路由,发起签名请求。请求可携带可选scopes;响应为重定向到外部认证方,同时写入 nonce cookie 并把 nonce 作为state查询参数带在重定向 URL 中;
  • frameHandler(req, res):外部认证方完成登录或授权后重定向到callbackURL,由该方法处理。请求需携带 nonce cookie 与state参数;响应通过postMessage向父窗口发送包含accessTokenexpiresInSecondsidTokenscope等信息的载荷,若 Provider 支持刷新令牌还会设置 refresh token cookie;
  • refresh?(req, res):可选。用于在持有 refresh token cookie 时换取新的访问令牌,也可被代理类 Provider 用于按需创建新会话;
  • logout?(req, res):可选。处理登出请求,移除 refresh token cookie。

登录发起与回调:start 与 frameHandler

发起登录时,前端会打开一个弹窗,将登录请求发往由start方法处理的/start端点。start将用户重定向到外部认证方,认证方验证后把请求重定向回/handler/frame端点,由frameHandler方法接手。

frameHandler返回一个 HTML 响应,其中包含一段脚本,通过postMessage把请求结果发送给前端窗口。这条消息的类型就是WebMessageResponse(定义于 plugins/auth-node/src/flow/sendWebMessageResponse.ts):

export type WebMessageResponse = | { type: 'authorization_response'; response: ClientAuthResponse<unknown>; } | { type: 'authorization_response'; error: Error; };

注意:官方文档中提到的postMessageResponse工具函数,在当前仓库中的实际实现名为sendWebMessageResponse。它封装了生成postMessage响应的全部逻辑,负责正确处理 CORS,接收express.ResponseWebMessageResponse以及前端地址(appOrigin),最终返回内嵌脚本与消息的 HTML 页面。实现中有两个值得关注的细节(见 plugins/auth-node/src/flow/sendWebMessageResponse.ts):

  • 数据会经过safelyEncodeURIComponent编码(除常规编码外还会把'替换为%27),防止注入恶意脚本;
  • 由于postMessage在 targetOrigin 被拒绝时会静默失败,脚本会先以'*'为 targetOrigin 发送一条config_info类型消息告知目标 origin,再以appOrigin为 targetOrigin 发送真正的授权响应。若父窗口收到第一条消息却始终等不到第二条,即可判定 targetOrigin 被拒绝,属于配置问题。

最终响应会带上X-Frame-Options: sameorigin与基于 SHA-256 哈希的 CSPscript-src指令,以缓解跨站风险。

认证环境(env)隔离

env概念是 auth-backend 工作方式的核心。它通过env查询参数标识应用运行的环境(developmentstagingproduction等),同一运行时可以同时服务多个环境,并根据请求中的env参数分派到对应的处理器。

OAuthEnvironmentHandler(plugins/auth-node/src/oauth/OAuthEnvironmentHandler.ts)是OAuthHandlers的实用包装器:它实现AuthProviderRouteHandlers接口,同时支持多个env。从源码看,getEnvFromRequest会先从req.query.env读取环境,取不到时再尝试从state参数解码出的 OAuth 状态中提取(decodeOAuthState),最后在getProviderForEnv中按环境查找处理器,环境缺失或未配置会分别抛出InputErrorNotFoundError

要实例化同一 Provider 在不同环境下的多个实例,请使用OAuthEnvironmentHandler.mapConfig。它遍历"环境名 → 配置"的配置对象,把每个环境的配置块分别交给工厂函数。给定如下配置:

development: clientId: abc clientSecret: secret production: clientId: xyz clientSecret: supersecret

OAuthEnvironmentHandler.mapConfig(config, envConfig => ...)会按顶层developmentproduction键拆分配置,把每一块作为envConfig传入回调。源码实现(plugins/auth-node/src/oauth/OAuthEnvironmentHandler.ts)即遍历config.keys()并为每个环境调用一次factoryFunc

AuthProviderFactory则是需要实现的工厂函数,为给定 Provider 生成AuthProviderRouteHandlers。当前仓库中所有受支持的 Provider 都提供了一个返回OAuthEnvironmentHandlerAuthProviderFactory,从而能同时处理多个环境的认证。

为什么选择 Passport

Backstage 选用了 Passport 作为认证平台,原因是它拥有覆盖面极广的认证策略(strategy)生态。在实现自定义 Provider 时,可以直接复用 Passport 社区现有的策略包,再通过PassportOAuthAuthenticatorHelper将其无缝接入 Backstage 的认证框架,大幅降低实现成本。

如何添加一个新的策略 Provider

快速指南

  1. 根据需求,新建一个认证 Provider 模块(OAuth 类型),或创建一个基于代理(proxy)认证的 Provider;
  2. 把新模块接入后端(packages/backend/src/index.ts)。

创建新的认证 Provider 模块

以虚构的服务foobar为例。使用yarn new创建新模块:选择backend-module模板,插件 ID 填auth-backend,模块 ID 填foobar-provider。确保模块把对应的 passport provider 声明为依赖:

cd plugins/auth-backend-backend-module-foobar-provider yarn add passport-provider-a yarn add @types/passport-provider-a

实现 OAuth 类型的 Provider

定义后端模块

新模块通过authProvidersExtensionPoint扩展 auth-backend。扩展点接口定义于 plugins/auth-node/src/extensions/AuthProvidersExtensionPoint.ts,其registerProvider接收{ providerId, factory };而auth-backend侧(plugins/auth-backend/src/authPlugin.ts)注册该扩展点时会对重复的providerId抛出错误,保证 Provider 标识唯一。

import { createBackendModule } from '@backstage/backend-plugin-api'; import { authProvidersExtensionPoint, commonSignInResolvers, createOAuthProviderFactory, } from '@backstage/plugin-auth-node'; import { providerAuthenticator } from './authenticator'; /** @public */ export const authModuleFoobarProvider = createBackendModule({ pluginId: 'auth', moduleId: 'foobar', register(reg) { reg.registerInit({ deps: { providers: authProvidersExtensionPoint, }, async init({ providers }) { providers.registerProvider({ providerId: 'foobar', factory: createOAuthProviderFactory({ authenticator: providerAuthenticator, signInResolverFactories: { ...commonSignInResolvers, }, }), }); }, }); }, });

createOAuthProviderFactory会基于 authenticator 生成一个标准的AuthProviderFactory,并自动完成多环境包装;commonSignInResolvers提供了通用的登录解析器(如emailMatchingUserEntityAnnotationemailLocalPartMatchingUserEntityName等,见 plugins/auth-node/src/sign-in/commonSignInResolvers.ts),用于把外部身份映射为 Backstage 目录中的用户。

实现 authenticator

authenticator 负责基于 Passport 策略创建策略实例,并利用配置文件中的密钥(clientIdclientSecret等)驱动认证流程。它通过createOAuthAuthenticator创建,并借助PassportOAuthAuthenticatorHelper复用通用的 start / authenticate / refresh 逻辑(该 Helper 的fromdefaultProfileTransformstart等实现见 plugins/auth-node/src/oauth/PassportOAuthAuthenticatorHelper.ts)。

import { Strategy as ProviderStrategy } from 'passport-provider-a'; import { createOAuthAuthenticator, PassportOAuthAuthenticatorHelper, PassportOAuthDoneCallback, PassportProfile, } from '@backstage/plugin-auth-node'; /** @public */ export const providerAuthenticator = createOAuthAuthenticator({ defaultProfileTransform: PassportOAuthAuthenticatorHelper.defaultProfileTransform, scopes: { // Scopes required by the provider required: ['openid', 'email', 'profile', 'offline_access'], }, initialize({ callbackUrl, config }) { const clientId = config.getString('clientId'); const clientSecret = config.getString('clientSecret'); return PassportOAuthAuthenticatorHelper.from( new ProviderStrategy( { clientID: clientId, clientSecret: clientSecret, // ... other options }, ( accessToken: string, refreshToken: string, params: any, fullProfile: PassportProfile, done: PassportOAuthDoneCallback, ) => { done( undefined, { fullProfile, params, accessToken }, { refreshToken }, ); }, ), ); }, async start(input, helper) { return helper.start(input); }, async authenticate(input, helper) { return helper.authenticate(input); }, async refresh(input, helper) { return helper.refresh(input); }, });

scopes.required声明了该 Provider 必需的权限范围,PassportOAuthAuthenticatorHelper.defaultProfileTransform负责把 Passport 的全量 profile 转换为前端展示用的精简ProfileInfo

仓库中的真实实现示例

当前仓库中已有多个同类实现可直接对照参考:

  • Google:plugins/auth-backend-module-google-provider/src/authenticator.ts
  • GitHub:plugins/auth-backend-module-github-provider/src/authenticator.ts,其模块注册代码见plugins/auth-backend-module-github-provider/src/module.ts
  • Okta:plugins/auth-backend-module-okta-provider/src/authenticator.ts

以 GitHub 为例,其 authenticator(plugins/auth-backend-module-github-provider/src/authenticator.ts)除了读取clientId/clientSecret,还会读取可选的enterpriseInstanceUrl配置,并据此动态拼接authorizationUrltokenUrluserProfileUrl——这展示了一个真实 Provider 如何在initialize中读取更多配置项、扩展策略选项。其模块(plugins/auth-backend-module-github-provider/src/module.ts)在注册时还额外合并了githubSignInResolverscommonSignInResolvers

创建基于代理(Proxy)认证的 Provider

代理认证 Provider 是指借用另一个外部认证方完成身份验证的 Provider,例如 Google IAP(Identity-Aware Proxy)或 AWS ALB。仓库中已内置支持这两者(plugins/auth-backend-module-gcp-iap-providerplugins/auth-backend-module-aws-alb-provider)。

其实现方式与 OAuth Provider 大体一致,区别在于authenticator函数不同:代理 Provider 不再走"重定向到外部 OAuth 端点"的流程,而是由反向代理或网关注入身份头信息,authenticator 负责从请求中解析并校验这些信息。需要新实现时,可直接参照上述两个内置模块。

Verify Callback

策略需要一个所谓的 verify callback。verify callback 的目的是找出持有某组凭证的用户。当 Passport 认证一个请求时,它会解析请求中包含的凭证,然后以这些凭证为参数调用 verify callback……如果凭证有效,verify callback 调用done向 Passport 提供完成认证的用户。

如果凭证无效(例如密码错误),应调用done并传入false而非用户对象,以表示认证失败。

——引自 Passport 官方配置文档

把新 Provider 接入后端

添加新模块的方式与任何其他模块或后端插件相同。在packages/backend/src/index.ts中:

若该 Provider 仅用于内部安装,则用内部导入路径:

backend.add(import('@internal/plugin-auth-backend-module-foobar-provider'));

若模块已直接贡献给 Backstage,则用官方命名空间:

backend.add(import('@backstage/plugin-auth-backend-module-foobar-provider'));

完成注册后,auth-backend会自动挂载以下端点(对应 plugins/auth-backend/src/providers/router.ts 中的绑定逻辑):

router.get('/auth/providerA/start'); router.get('/auth/providerA/handler/frame'); router.post('/auth/providerA/handler/frame'); router.post('/auth/providerA/logout'); router.get('/auth/providerA/refresh'); // if supported router.post('/auth/providerA/refresh'); // if supported

可以看到每个端点都以/auth和 Provider 名称为前缀。

配置示例

app-config.yamlauth.providers下按环境提供配置即可启用(参考仓库根目录 app-config.yaml 中的内置 Provider 写法):

auth: environment: development providers: foobar: development: clientId: ${AUTH_FOOBAR_CLIENT_ID} clientSecret: ${AUTH_FOOBAR_CLIENT_SECRET}

结合前文OAuthEnvironmentHandler.mapConfig的机制,auth.providers.foobar下的每个环境键(developmentproduction等)都会生成一个独立的处理器,而auth.environment决定当前运行时默认使用哪个环境。

测试新 Provider

模块接入后,可以用 curl 发起一次登录来验证流程是否正常:

curl -i localhost:7007/api/auth/providerA/start

预期会返回302重定向,并带有Location头。把该头中的 URL 粘贴到浏览器中打开,即可触发完整的授权流程——浏览器中会看到跳转到外部认证方、授权后回调并关闭弹窗的完整链路。若返回的是 404 或配置错误提示,可检查auth.providers.<providerId>配置与环境变量是否就绪,并查看后端启动日志中bindProviderRouters打印的Configuring auth provider: <providerId>记录。

小结

为 Backstage 添加新的认证 Provider,本质上是三件事:一是实现AuthProviderRouteHandlers接口(或更常用地,通过createOAuthProviderFactory+createOAuthAuthenticator组合出工厂);二是借助OAuthEnvironmentHandler.mapConfig让同一 Provider 优雅地支持多环境;三是通过authProvidersExtensionPoint把模块注册进auth-backend。无论是接入 Passport 社区已有的 OAuth 策略,还是实现基于 IAP / ALB 的代理认证,上述路径都完全一致,仓库中 Google、GitHub、Okta、GCP IAP、AWS ALB 等内置模块即是可复刻的最佳范本。

【免费下载链接】backstageBackstage is an open framework for building developer portals项目地址: https://gitcode.com/GitHub_Trending/ba/backstage

创作声明:本文部分内容由AI辅助生成(AIGC),仅供参考

版权声明: 本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若内容造成侵权/违法违规/事实不符,请联系邮箱:809451989@qq.com进行投诉反馈,一经查实,立即删除!
网站建设 2026/9/11 6:46:45

互联网医院平台横向对比:量化评测方法、采样设计与实操指南

/* MD / 富文本中的 .toc(含博客园搬家等嵌套结构);.toc-box 在侧栏,不受影响 */#content_views .toc,/* 编辑器常在目录前后插入空 p(:empty 仍占 20px),一并去掉避免顶空隙 */#content_views.markdown_views > p:empty:has(+ .toc),#content_views.markdown_views …

作者头像 李华
网站建设 2026/9/11 6:43:20

论文写作避坑指南:从格式地狱到高效产出的进阶之路

引言&#xff1a;论文写作&#xff0c;一场与时间的拉锯战 在撰写论文的过程中&#xff0c;我发现有很多环节容易耗费大量时间&#xff0c;比如参考文献的格式、文本的修改、以及中英文混排的问题。尤其是在任务交接的时候&#xff0c;手动核对一遍又一遍&#xff0c;真的是让…

作者头像 李华
网站建设 2026/9/11 6:41:53

AI Agent用户记忆系统:跨会话持久化实战架构

1. 这不是“记住名字”&#xff0c;而是让AI真正理解“你”是谁 “走进AI Agent第三篇&#xff1a;让 Agent 记住你”——这个标题里藏着一个被严重低估的工程真相&#xff1a; 用户记忆从来不是加个变量、存个JSON就完事的技术动作&#xff0c;而是一场在状态、语义、时效与安…

作者头像 李华