Backstage 后端插件开发实战:从yarn new创建到服务注入与安全加固
【免费下载链接】backstageBackstage is an open framework for building developer portals项目地址: https://gitcode.com/GitHub_Trending/ba/backstage
本文以 Backstage 新后端系统(New Backend System)为主线,完整讲解后端插件(backend plugin)的创建、独立开发调试、接入应用、安全默认策略(secure by default)、依赖注入(deps / init)、数据库访问与用户身份获取的完整流程。读者在读完本文后,将能够从零创建一个可独立运行、可挂载到 Backstage 主后端、并具备认证控制与持久化能力的生产级后端插件。
本文主体基于 docs/plugins/backend-plugin.md,并结合仓库内yarn new的实际脚手架模板(packages/cli-module-new/templates/backend-plugin/)与核心服务定义源码(如 packages/backend-plugin-api/src/services/definitions/HttpRouterService.ts)进行源码级印证与扩充。
注意:该页面属于 legacy plugins 文档的一部分,虽然页面本身已按新后端系统模式书写,但 Backstage 官方当前推荐的后端插件权威指南位于 docs/backend-system/building-plugins-and-modules/01-index.md,本文内容与其相互印证、可对照阅读。
创建后端插件
在 Backstage 仓库根目录执行yarn new,在交互式提示中选择backend-plugin,即可生成一个最小可用的后端插件包:
yarn new创建过程会要求为插件提供一个名称。这个名称将成为 NPM 包名的一部分,因此建议使用简短、只含小写字母和连字符的标识符。例如,如果你要开发一个与名为 Carmen 的系统做集成的插件,可以命名为carmen;结合传给new命令的其他标志以及根目录package.json中针对new命令的配置,最终 NPM 包名形如@internal/plugin-carmen-backend。
创建插件会花费一些时间,因为脚手架会自动运行依赖安装与构建命令,使包直接处于可开发状态。生成后的代码位于仓库plugins目录下的新文件夹中,本例为plugins/carmen-backend。
从仓库模板 packages/cli-module-new/templates/backend-plugin/package.json.hbs 可以看出,一个全新的后端插件包具备以下特征:
"backstage": { "role": "backend-plugin", "pluginId": "..." }:声明该包是后端插件,并标注插件 ID;- 提供
start/build/lint/test/clean/prepack/postpack等脚本,统一基于backstage-cli package *命令; - 默认依赖
@backstage/backend-defaults、@backstage/backend-plugin-api、@backstage/catalog-client、@backstage/errors、express、express-promise-router、zod等;测试相关依赖包括@backstage/backend-test-utils、supertest。
独立开发模式(Standalone)
为方便开发,后端插件可以被独立启动,无需依赖整个 Backstage 应用。进入插件目录并启动:
cd plugins/carmen-backend yarn start启动一段时间后会输出Listening on :7007。在另一个终端窗口中执行:
curl localhost:7007/api/carmen/todos预期响应为:
{ "items": [] }当前模板的默认路由就是/api/<pluginId>/todos;如果希望为健康检查提供/health端点,需要自己在 router 中实现。
独立开发环境的实现在模板的 packages/cli-module-new/templates/backend-plugin/dev/index.ts.hbs 中。可以看到,它用createBackend()组装了一个最小后端:
import { createBackend } from '@backstage/backend-defaults'; import { mockServices } from '@backstage/backend-test-utils'; import { catalogServiceMock } from '@backstage/plugin-catalog-node/testUtils'; const backend = createBackend(); // 使用 mock 的 auth 与 httpAuth 服务,这样无需真实认证即可调用插件 API; // 如需真实认证,可改为: // backend.add(import('@backstage/plugin-auth-backend')); // backend.add(import('@backstage/plugin-auth-backend-module-guest-provider')); backend.add(mockServices.auth.factory()); backend.add(mockServices.httpAuth.factory()); // 使用带固定实体集合的 catalog mock,而非真实 catalog backend.add( catalogServiceMock.factory({ entities: [ { apiVersion: 'backstage.io/v1alpha1', kind: 'Component', metadata: { name: 'sample', title: 'Sample Component' }, spec: { type: 'service' }, }, ], }), ); backend.add(import('../src')); backend.start();模板自带的 dev 注释还提供了几组可直接试用的 curl 命令,可用于验证创建、列出 TODO 以及显式携带 mock 认证头的场景:
# 创建一条 TODO(独立创建或关联 sample 组件) curl http://localhost:7007/api/<pluginId>/todos -H 'Content-Type: application/json' -d '{"title": "My Todo"}' curl http://localhost:7007/api/<pluginId>/todos -H 'Content-Type: application/json' -d '{"title": "My Todo", "entityRef": "component:default/sample"}' # 列出 TODO curl http://localhost:7007/api/<pluginId>/todos # 显式使用未认证 / 服务认证的 token curl http://localhost:7007/api/<pluginId>/todos -H 'Authorization: Bearer mock-none-token' curl http://localhost:7007/api/<pluginId>/todos -H 'Authorization: Bearer mock-service-token'将插件接入主后端
一个新创建的后端插件在应用层面“什么都不做”:它只有一组基础依赖,并在src/service/router.ts中暴露一个 Express router。你需要把路由接入具体业务功能,同时在 Backstage 应用/后端中显式挂载它。
首先在 Backstage 根目录将插件包加入后端依赖(包名以插件实际package.json中的为准):
yarn --cwd packages/backend add @internal/plugin-carmen-backend@^0.1.0然后修改packages/backend/src/index:
const backend = createBackend(); // ... backend.add(import('@internal/plugin-carmen-backend')); // ... backend.start();从仓库根目录用yarn start-backend启动后端后,即可访问插件接口:
# 注意这里的 /api 前缀 curl localhost:7007/api/carmen/health预期返回{"status":"ok"},说明插件已成功挂载到主后端。
需要指出的是:模板中插件的入口文件 packages/cli-module-new/templates/backend-plugin/src/index.ts.hbs 只是把plugin.ts中的插件对象以默认导出再导出一次:
export { {{pluginVar}} as default } from './plugin';这正是backend.add(import('...'))所要求的默认导出形态,也是整个新后端系统“零配置装配”的基础。
Secure by Default:默认安全与认证策略
自 Backstage 1.25 起,插件体系开始转向 secure by default 模型:插件收到的网络请求默认不允许未认证用户访问。以文档中允许未认证访问的/health请求为例,其路由定义在plugins/carmen-backend/src/service/router.ts:
export async function createRouter( options: RouterOptions, ): Promise<express.Router> { // ... router.get('/health', (_, response) => { logger.info('PONG!'); response.json({ status: 'ok' }); }); // ... return router; }可以看到路由本身没有定义任何认证机制,只有路由名与响应数据——认证由插件定义(plugins/carmen-backend/src/plugin.ts)统一处理:
httpRouter.use( await createRouter({ logger, }), ); httpRouter.addAuthPolicy({ path: '/health', allow: 'unauthenticated', });addAuthPolicy声明了对插件/health端点的放行策略,允许该路径的请求以未认证身份通过;而其他未显式声明的路径则继续遵循 secure by default 的默认拒绝行为。
该 API 的底层契约定义在 packages/backend-plugin-api/src/services/definitions/HttpRouterService.ts:addAuthPolicy(policy: HttpRouterServiceAuthPolicy): void,其中HttpRouterServiceAuthPolicy携带path与allow(如'unauthenticated')字段;而 HttpAuthService 文档中同样指出,对未认证请求的放行应配合HttpRouterService.addAuthPolicy使用。从源码结构看,实际的请求凭据校验由后端默认实现中的createCredentialsBarrier等机制承载(参见 packages/backend-defaults/src/entrypoints/httpRouter/httpRouterServiceFactory.ts 及同目录的 createCredentialsBarrier.ts),它会拦截未携带有效凭据的请求,只有命中显式放行策略的路径才能以未认证身份通过。
使用依赖:deps 声明与 init 注入
新后端系统中,依赖在注册阶段以静态方式声明,在初始化阶段被“注入”。以插件定义 packages/cli-module-new/templates/backend-plugin/src/plugin.ts.hbs 为参照:
export const carmenPlugin = createBackendPlugin({ pluginId: 'carmen', register(env) { env.registerInit({ deps: { httpRouter: coreServices.httpRouter, logger: coreServices.logger, }, async init({ httpRouter, logger }) { // ... }, }); }, });deps中的每一项都会在init的参数对象中可用。要添加自定义依赖,只需在deps中新增具名条目:
deps: { myDependency: coreServices.rootConfig, },然后在init中通过解构访问:
async init({ myDependency }) { // .. }之后就可以自由地调用它,或将其传入 router 等业务代码中。新后端系统的一个关键特性是:plugin 作用域的服务(如logger、httpRouter)是“专属于当前插件”的实例——logger 可能会用插件 ID 标记日志,httpRouter 可能会为 API 路由自动加上插件 ID 前缀(具体行为取决于实现),这让多插件并行运行时代码更加隔离与整洁。
Backstage 内置了丰富的coreServices,完整列表见 docs/backend-system/core-services/01-index.md。
使用数据库
Backstage 后端内置了统一的 SQL 数据库访问能力。大多数有持久化需求的插件都应优先使用该设施,以便 Backstage 运维人员能够统一管理数据库需求。
使用方式是在插件定义中依赖coreServices.database,它会提供一个 Knex 连接对象:
deps: { // ... database: coreServices.database, }, async init({ database, }) { // 将 client 传入插件实际实现代码,例如: const model = new CarmenDatabaseModel(database); httpRouter.use( await createRouter({ model, logger, }), ); }所有插件数据库需求都配置在app-config.yaml的backend.database配置键下。框架还会在后台根据 Backstage 运维者设定的规则,在逻辑数据库不存在时自动创建它。
需要注意两点限制:
- 框架不负责数据库 schema 迁移:主仓库中的内置插件选择使用 Knex 库来管理 schema 迁移,你也可以采用任何你认为合适的方式;
- 迁移表命名冲突:模块若与目标插件共享同一逻辑数据库实例,应谨慎选择表名。
01-index.md中给出的建议命名模式是<package name>__<table name>,例如 scheduler 核心服务创建的backstage_backend_tasks__<table>表。若使用 Knex 默认迁移设施,还应为其内部记账用的迁移状态表指定带前缀的名字,避免与插件主迁移表冲突:
await knex.migrate.latest({ directory: migrationsDir, tableName: 'backstage_backend_tasks__knex_migrations', });Knex 迁移编写与 SQL 查询的具体示例,参见 Knex 官方文档。
使用用户身份
Backstage 后端提供coreServices.httpAuth与coreServices.userInfo两个核心服务来访问用户身份。先在插件定义中声明:
deps: { httpAuth: coreServices.httpAuth, userInfo: coreServices.userInfo, }, async init({ httpAuth, userInfo, }) { httpRouter.use( await createRouter({ httpAuth, userInfo, logger, }), ); }随后在 router 中从请求里提取身份信息:
export interface RouterOptions { logger: LoggerService; userInfo: UserInfoService; httpAuth: HttpAuthService; } export async function createRouter( options: RouterOptions, ): Promise<express.Router> { const { userInfo, httpAuth } = options; router.post('/me', async (req, res) => { const credentials = await httpAuth.credentials(req, { // 这会拒绝来自非用户的请求。仅当插件确实需要访问用户身份时才使用; // 大多数情况下只需调用 `httpAuth.credentials(req)` 即可。 allow: ['user'], }); const user = await userInfo.getUserInfo(credentials); res.json({ // 用户的 catalog 实体引用。 userEntityRef: user.userEntityRef, // 该用户或其所属团队所拥有的实体引用列表。 ownershipEntityRefs: user.ownershipEntityRefs, }); }); // ... }这一模式同样出现在模板 router(packages/cli-module-new/templates/backend-plugin/src/router.ts)中:创建 TODO 的接口通过httpAuth.credentials(req, { allow: ['user'] })校验用户身份,并把凭据传给业务服务。模板中的TodoListService(packages/cli-module-new/templates/backend-plugin/src/services/TodoListService.ts)进一步展示了两个进阶实践:
- 跨插件调用 catalog:通过
catalogServiceRef依赖 catalog 服务,用getEntityByRef根据实体引用拉取实体(跨插件通信使用服务到服务认证,AuthService可生成仅对目标插件有效的 token;若想以插件后端自身身份发起请求,可调用auth.getOwnServiceCredentials(),但要注意这绕过了用户权限检查); - 记录创建者:从
options.credentials.principal.userEntityRef中读取当前用户实体引用,写入 TODO 记录。
源码视角:模板中的完整数据流
综合模板各文件可以看出一个后端插件的完整分层结构:
- 入口层:src/index.ts.hbs 默认导出插件对象;
- 装配层:src/plugin.ts.hbs 声明
deps、在init中把服务注入 router; - 路由层:src/router.ts 使用
express-promise-router定义 REST 路由,用zod做请求体校验,并用httpAuth.credentials把关身份; - 业务层:src/services/TodoListService.ts 通过
createServiceRef/createServiceFactory定义可注入的服务; - 测试层:src/router.test.ts 直接针对
createRouter做单元测试——用mockServices.httpAuth()模拟认证、用jest.Mocked模拟 todoList 服务,并断言:携带 mock 用户凭据的 POST 返回 201;携带mockCredentials.none.header()(未认证)的 POST 返回 401。这个测试用例恰好从另一个角度印证了 secure by default 行为。
测试依赖统一来自@backstage/backend-test-utils(如mockServices、mockCredentials、mockErrorHandler),相关类型可参阅 packages/backend-test-utils 目录。
下一步
- 后端插件的权威指南与模块(module)开发、扩展点(extension point)机制,见 docs/backend-system/building-plugins-and-modules/01-index.md;
- 核心服务的完整清单与用法,见 docs/backend-system/core-services/01-index.md;
- 为新插件定义配置 schema 时,参考 docs/conf/defining.md 中关于插件自定义配置的说明;
- 生成脚手架本身的实现位于 packages/cli-module-new(
yarn new即由该 CLI 模块驱动,模板目录为 packages/cli-module-new/templates)。
【免费下载链接】backstageBackstage is an open framework for building developer portals项目地址: https://gitcode.com/GitHub_Trending/ba/backstage
创作声明:本文部分内容由AI辅助生成(AIGC),仅供参考