Backstage 接入 Google OAuth 登录:Provider 配置、Sign-in Resolver 与源码级实现解析

Backstage 接入 Google OAuth 登录:Provider 配置、Sign-in Resolver 与源码级实现解析

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

本文基于 Backstage 仓库中的 Google 认证 Provider 文档,完整讲解如何为 Backstage 接入 Google OAuth 登录:从 Google Cloud Console 创建 OAuth 凭据,到在 app-config.yaml 中配置 provider、选择 sign-in resolver,再到后端模块安装与前端登录页接入。文章同时结合 @backstage/plugin-auth-backend-module-google-provider 插件的真实源码(authenticator.tsresolvers.tsmodule.ts)与单元测试(module.test.ts),帮助你理解 Google 认证在 Backstage 中的底层工作原理,并能在自己的开发环境中一步不落地跑通。

概述:Google Provider 在 Backstage 中的位置

Backstage 的 core-plugin-api 包自带 Google 认证 Provider,可让用户通过 Google OAuth 完成登录认证。在仓库中,它的实现位于 @backstage/plugin-auth-backend-module-google-provider 插件目录下(plugins/auth-backend-module-google-provider),该模块面向 Backstage 新后端系统(New Backend System),通过 createBackendModuleauth 插件注册名为 google 的 provider。

从源码结构看,Google Provider 的实现由三部分构成(见 index.ts):

  • googleAuthenticator:基于 passport-google-oauth20 策略的 OAuth 认证器,负责与 Google 交互;
  • googleSignInResolvers:Google 专属的 sign-in resolver 工厂;
  • authModuleGoogleProvider(默认导出):后端模块,把上述两者装配到 auth 插件。

整个认证流程遵循 Backstage 通用的 OAuth 离线访问流程(详见 OAuth 与 OIDC 文档):前端弹窗指向 auth 后端 /start 端点,跳转到 Google 的 OAuth 授权页面,用户同意后回调 /handler/frame 端点完成令牌交换。该流程基于服务端离线访问实现,不依赖第三方 Cookie,在严格隐私设置的浏览器上也能稳定工作。

第一步:在 Google Cloud Console 创建 OAuth 凭据

要在 Backstage 中启用 Google 认证,首先需要创建 OAuth 客户端凭据。请按以下步骤操作:

  1. 登录 Google Cloud Console(https://console.cloud.google.com);
  2. 从顶栏下拉菜单中选择或新建一个项目;
  3. 导航到 APIs & Services > Credentials(API 与服务 > 凭据);
  4. 点击 Create Credentials(创建凭据),选择 OAuth client ID(OAuth 客户端 ID);
  5. 如有需要,先配置 OAuth consent screen(OAuth 同意屏幕):
    • 本地开发环境下,不需要填写任何 Authorized domain(已获授权的域名);
    • Scopes(范围)选择 openidauth/userinfo.emailauth/userinfo.profile。这与源码中 authenticator.ts 声明的 required scopes 完全一致:
      scopes: {
        required: [
          'openid',
          `https://www.googleapis.com/auth/userinfo.email`,
          `https://www.googleapis.com/auth/userinfo.profile`,
        ],
      },
      
    • 如果用户类型选择 External(外部),需要把自己添加为测试用户(Test user),否则 OAuth 授权将被拒绝;
  6. Application Type(应用类型)设为 Web Application,并填写以下设置:
    • Name(名称):Backstage(或你自己的应用名);
    • Authorized JavaScript origins(已获授权的 JavaScript 来源):http://localhost:3000
    • Authorized Redirect URIs(已获授权的重定向 URI):http://localhost:7007/api/auth/google/handler/frame
  7. 点击 Create(创建),生成 clientIdclientSecret

这里重定向 URI 的路径 /api/auth/google/handler/frame 正是 module.test.ts 中断言的回调地址格式:测试验证启动后的授权跳转 redirect_urihttp://localhost:${server.port()}/api/auth/google/handler/frame。该地址由 auth 后端依据 app.baseUrl 动态拼接,因此生产环境部署时需把这里的两个 localhost 地址替换为实际域名。

第二步:在 app-config.yaml 中配置 Google Provider

创建好凭据后,在 app-config.yaml 的根级 auth 配置下添加 provider 配置:

auth:
  environment: development
  providers:
    google:
      development:
        clientId: ${AUTH_GOOGLE_CLIENT_ID}
        clientSecret: ${AUTH_GOOGLE_CLIENT_SECRET}
        ## uncomment to set lifespan of user session
        # sessionDuration: { hours: 24 } # supports `ms` library format (e.g. '24h', '2 days'), ISO duration, "human duration" as used in code
        signIn:
          resolvers:
            # See https://backstage.io/docs/auth/google/provider#resolvers for more resolvers
            - resolver: emailMatchingUserEntityAnnotation

auth.providers.google 是典型的「环境键 -> provider 配置」结构,这里的 development 键对应 auth.environment 的值。每个 provider 配置包含两个核心字段:

  • clientId:上一步生成的客户端 ID,形如 10023341500512-beui241gjwwkrdkr2eh7dprewj2pp1q.apps.googleusercontent.com
  • clientSecret:与该客户端 ID 关联的客户端密钥。

可选配置项

  • sessionDuration:用户会话的生命周期。支持 ms 库的时间格式(如 '24h''2 days')、ISO 8601 时长格式以及 { hours: 24 } 这类人类可读的 HumanDuration 对象。

从仓库中的配置类型定义 config.d.ts 可以看到,该 provider 实际支持的配置键还包括:

配置键类型说明
clientIdstringOAuth 客户端 ID(前端可见,@visibility frontend
clientSecretstring客户端密钥(@visibility secret,禁止暴露到前端)
callbackUrlstring(可选)自定义回调地址,缺省时由 auth 后端根据 baseUrl 推导
additionalScopesstring \| string[](可选)在必需 scope 之外追加的 OAuth scope
signIn.resolvers数组sign-in resolver 列表,详见下一节
sessionDurationHumanDuration \| string(可选)会话生命周期

其中 clientSecret 标记为 @visibility secret,意味着 Backstage 的配置系统会阻止它被下发到前端,这对安全至关重要。如需额外的 Google API 权限,可通过 additionalScopes 追加,例如 https://www.googleapis.com/auth/cloud-platform

第三步:理解并选择 Sign-in Resolvers

Backstage 的 auth provider 默认只用于「访问委托」(access delegation),即代表用户向后端外部系统请求资源。要让 Google 成为真正的登录方式,必须显式配置 signIn.resolvers,告诉 Backstage 如何把 Google 身份映射为 Backstage 用户身份。相关背景可参阅 Sign-in Identities and Resolvers

Google Provider 内置的三个 Resolver

Google Provider 开箱即用地提供以下 resolver:

  • emailMatchingUserEntityProfileEmail:将 Google 账户的 email 与 Catalog 中 spec.profile.email 匹配的 User 实体关联。找不到匹配项时抛出 NotFoundError
  • emailLocalPartMatchingUserEntityName:将 Google 账户 email 的本地部分(local part,即 @ 之前的部分)name 匹配的 User 实体关联。找不到匹配项时抛出 NotFoundError
  • emailMatchingUserEntityAnnotation:将 Google 账户 email 与 google.com/email 注解值匹配的 User 实体关联。找不到匹配项时抛出 NotFoundError

注意:resolvers 会按配置顺序依次尝试,但只有抛出 NotFoundError 时才会被跳过(继续尝试下一个),其他错误会直接中断登录。

仓库中 resolvers.ts 的源码展示了 Google 专属 resolver 的实现方式,例如 emailMatchingUserEntityAnnotation

export const emailMatchingUserEntityAnnotation = createSignInResolverFactory({
  optionsSchema: z
    .object({
      dangerouslyAllowSignInWithoutUserInCatalog: z.boolean().optional(),
    })
    .optional(),
  create(options = {}) {
    return async (info, ctx) => {
      const { profile } = info;
      if (!profile.email) {
        throw new Error('Google profile contained no email');
      }
      return ctx.signInWithCatalogUser(
        {
          annotations: {
            'google.com/email': profile.email,
          },
        },
        {
          dangerousEntityRefFallback:
            options?.dangerouslyAllowSignInWithoutUserInCatalog
              ? { entityRef: { name: profile.email } }
              : undefined,
        },
      );
    };
  },
});

这里有两个关键实现事实:

  • 如果 Google 返回的 profile 没有 email,resolver 会直接抛错拒绝登录;
  • resolver 支持 dangerouslyAllowSignInWithoutUserInCatalog 选项。开启后,当 Catalog 中找不到对应的 User 实体时,仍会基于 profile.email 生成一个后备身份并签发令牌,而不抛出 NotFoundError

module.ts 中,注册 provider 时会把 googleSignInResolvers 与所有 provider 通用的 commonSignInResolvers 合并注册:

providers.registerProvider({
  providerId: 'google',
  factory: createOAuthProviderFactory({
    authenticator: googleAuthenticator,
    signInResolverFactories: {
      ...googleSignInResolvers,
      ...commonSignInResolvers,
    },
  }),
});

这也印证了原文档:emailMatchingUserEntityProfileEmailemailLocalPartMatchingUserEntityName 是所有 provider 通用的 resolver,而 emailMatchingUserEntityAnnotation 是 Google 专属。配置类型 config.d.ts 中仅允许这三个 resolver 名称,其中 emailLocalPartMatchingUserEntityName 额外支持 allowedDomains(允许登录的域名白名单)。

Resolver 使用建议

  • emailLocalPartMatchingUserEntityName 由于只匹配 email 的本地部分,强烈建议同时配置 allowedDomains 限制可登录的域名,例如:
    signIn:
      resolvers:
        - resolver: emailLocalPartMatchingUserEntityName
          allowedDomains:
            - acme.org
    
  • 为降低账户劫持风险,通常只为其中一个 auth provider 配置 sign-in resolver,避免同一用户通过多套身份进入系统后映射到不同身份。

使用自定义 Resolver

如果内置 resolver 不满足需求,可以编写完全自定义的 sign-in resolver。完整方案见 Sign-in Identities and Resolvers,核心步骤是:

  1. app-config.yaml 的 provider 配置中移除 signIn.resolvers(否则配置优先);
  2. packages/backend/src/index.ts 中,用 createBackendModule 构造自定义 provider 模块,通过 createOAuthProviderFactory 传入 googleAuthenticator 和自定义的 signInResolver 回调,替换原来的 backend.add(import('@backstage/plugin-auth-backend-module-google-provider'))

例如,在 resolver 回调中可以用 ctx.signInWithCatalogUser({ entityRef: { name } }) 查找 Catalog 用户并签发令牌;也可以直接调用 ctx.issueToken 跳过 Catalog 校验(此时务必自行做域名白名单等校验,否则存在安全风险)。Google 的 googleAuthenticator 正是 createOAuthProviderFactory 期望的 OAuth 认证器形态,二者可无缝组合。

第四步:后端安装 Google Provider 模块

Google Provider 以独立包形式发布。在 Backstage 根目录执行:

# 在你的 Backstage 根目录执行
yarn --cwd packages/backend add @backstage/plugin-auth-backend-module-google-provider

然后在后端入口文件 packages/backend/src/index.ts 中注册该模块:

// packages/backend/src/index.ts
backend.add(import('@backstage/plugin-auth-backend'));
/* highlight-add-start */
backend.add(import('@backstage/plugin-auth-backend-module-google-provider'));
/* highlight-add-end */

对照仓库中 packages/backend/src/index.ts 的真实写法,auth 相关模块正是以这种 backend.add(...) 的形式逐条注册的(如 @backstage/plugin-auth-backend、GitHub provider、guest provider、openshift provider 等)。@backstage/plugin-auth-backend-module-google-provider 的默认导出即 module.ts 中创建的 authModuleGoogleProvider 后端模块,注册后 auth 插件会自动识别 auth.providers.google 配置并启用该 provider。

仓库中的测试 module.test.ts 给出了该模块的完整验证流程,可以作为集成验证的参考:它用 startTestBackend 启动一个包含 auth 后端与 Google provider 的测试实例,然后请求 /api/auth/google/start?env=development,断言:

  • 返回 302 重定向到 https://accounts.google.com/o/oauth2/v2/auth
  • 授权 URL 携带 access_type=offlineprompt=consentinclude_granted_scopes=trueresponse_type=code 等参数(这些参数由 authenticator.tsstart 方法注入);
  • scope 为 openid https://www.googleapis.com/auth/userinfo.email https://www.googleapis.com/auth/userinfo.profile
  • 设置了 google-nonce Cookie,回调地址为 /api/auth/google/handler/frame

这意味着:只要你的 app-config.yamlclientId/clientSecret 配置正确,auth 后端启动后访问 /api/auth/google/start 即可触发完整的 Google 授权跳转。

第五步:将 Provider 添加到 Backstage 前端登录页

后端配置完成后,还需要在前端接入登录页。使用 @backstage/core-plugin-api 提供的 googleAuthApiRef 引用和 @backstage/core-components 提供的 SignInPage 组件即可,具体做法参见 登录配置说明

在新前端系统(New Frontend System)下,通过 SignInPageBlueprint 创建自定义登录页并注册为 app 插件的扩展,示例(文档原文):

// packages/app/src/App.tsx
import { createApp } from '@backstage/frontend-defaults';
import { googleAuthApiRef } from '@backstage/core-plugin-api';
import { SignInPageBlueprint } from '@backstage/plugin-app-react';
import { SignInPage } from '@backstage/core-components';
import { createFrontendModule } from '@backstage/frontend-plugin-api';

const signInPage = SignInPageBlueprint.make({
  params: {
    loader: async () => props =>
      (
        <SignInPage
          {...props}
          provider={{
            id: 'google-auth-provider',
            title: 'Google',
            message: 'Sign in using Google',
            apiRef: googleAuthApiRef,
          }}
        />
      ),
  },
});

export default createApp({
  features: [
    /* ...其他插件... */
    createFrontendModule({
      pluginId: 'app',
      extensions: [signInPage],
    }),
  ],
});

SignInPage 会在应用其他路由渲染之前呈现,负责提供当前用户身份;用户完成 Google 授权后,通过 onSignInSuccess 回调获得有效的 Backstage 用户身份,其余应用界面才会渲染。

几点补充:

  • 如果希望同时提供多种登录方式(例如开发环境允许 guest 登录),可将 provider 换成 providers 数组(多 Provider 示例);
  • 可以通过读取 auth.environment 配置,在开发环境渲染 Google 登录、生产环境使用代理登录页(如 ProxiedSignInPage provider="gcpiap"),实现按环境条件渲染登录方式(条件渲染示例);
  • 如果希望采用「重定向」而非「弹窗」的登录流程,可在 app-config.yaml 根级开启 enableExperimentalRedirectFlow: true文档说明)。

googleAuthApiRef 属于 Backstage 的 Utility API(OAuthApi & ProfileInfoApi & SessionApi 形态),前端插件可通过它按需请求 Google OAuth Access Token,用于调用 Google 外部服务,这也是 Backstage user-to-server OAuth 设计的一部分(相关背景)。

常见问题排查

错误一:Provider 未配置为支持登录

界面提示 "The 'Google' provider is not configured to support sign-in"。可能原因与解决方法:

  • signIn.resolvers 未添加到 Google Provider 配置中——补上即可;
  • app-config.yaml 存在语法错误——运行 yarn backstage-cli config:check --strict 可帮助定位。

错误二:无法解析用户身份

界面提示 "Failed to sign-in, unable to resolve user identity"。原因通常是所配置的 resolver 无法在 Catalog 中找到匹配的 User 实体。解决办法是从组织的可信数据源导入 User / Group 数据,例如通过现有的组织数据 Provider(如 Microsoft Entra ID(Azure AD/MS Graph)GitHub OrgGitLab 等),或编写自定义 Entity Provider 注入用户数据。

安全提醒:请谨慎配置 sign-in resolver,它直接决定了谁能登录你的 Backstage 实例、以什么身份登录。除有意支持多种登录方式外,请只为其中一个 auth provider 配置 sign-in resolver;开启 dangerouslyAllowSignInWithoutUserInCatalog 会绕过 Catalog 校验并可能把权限授予未纳入管理的用户,生产环境务必慎用。

小结

接入 Google OAuth 登录需要依次完成四件事:在 Google Cloud Console 创建 OAuth 客户端(回调指向 /api/auth/google/handler/frame)、在 app-config.yaml 配置 auth.providers.google 并选择 sign-in resolver、安装并注册 @backstage/plugin-auth-backend-module-google-provider 后端模块、在前端通过 googleAuthApiRefSignInPage 接入登录页。仓库源码(authenticator.tsresolvers.tsmodule.tsconfig.d.ts)与测试(module.test.ts)完整印证了文档中的每一步配置,可作为你集成与排障时的第一手依据。若需自定义登录身份映射,可进一步参考 Sign-in Identities and ResolversAuth 总览

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

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

实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值