Backstage User Info Service:获取用户凭据与所有权实体引用的核心服务详解
Backstage User Info Service获取用户凭据与所有权实体引用的核心服务详解【免费下载链接】backstageBackstage is an open framework for building developer portals项目地址: https://gitcode.com/GitHub_Trending/ba/backstage在 Backstage 后端插件开发中很多场景需要知道当前请求是谁发起的以及这个人拥有owns哪些实体。User Info Service 正是为此而生的核心服务core service它接收一组用户凭据credentials返回该用户的主实体引用userEntityRef及其所有权实体引用列表ownershipEntityRefs。阅读本文后你将掌握coreServices.userInfo的标准注入与调用方式、它与auth/httpAuth服务的协作关系以及默认实现中 JWT 声明提取与 HTTP 回退两条数据路径的底层原理。说明本文所有示例均假设你已经了解凭据credentials处理的基础概念建议先阅读 Auth Service 与 HTTP Auth Service 的文档它们是本文示例的前提。服务定位与核心概念User Info Service 的职责是从一组用户凭据中提取更多关于该用户的信息最典型的用途就是提取用户 principal 的 ownership entity refs所有权实体引用。需要注意两个边界只处理用户凭据不处理服务凭据——该服务只接受包含 user principal 的 credentials对 service principal 请求会直接抛错数据来自登录时刻——返回的用户信息是用户 sign-in登录过程中提取的数据并非每次调用时重新从外部身份源查询。服务的公共接口定义在 UserInfoService.ts 中export interface BackstageUserInfo { userEntityRef: string; ownershipEntityRefs: string[]; } export interface UserInfoService { /** * Retrieve user information based on the provided credentials. */ getUserInfo(credentials: BackstageCredentials): PromiseBackstageUserInfo; }可以看到返回值结构非常精简一个userEntityRef如user:default/alice加一个ownershipEntityRefs字符串数组如[group:default/my-team]。在服务注册表中声明coreServices.userInfo该服务作为 core service 之一在 coreServices.ts 中注册其服务引用 id 为core.userInfoexport const userInfo createServiceRef import(./UserInfoService).UserInfoService ({ id: core.userInfo, });这意味着任何后端插件backend plugin都可以通过coreServices.userInfo以依赖注入的方式声明对该服务的依赖无需关心它由哪个包实现。在插件中注入与调用 User Info Service以下代码示例中userInfo、auth、httpAuth等变量均为依赖注入得到的服务实例分别来自coreServices.userInfo与coreServices.httpAuth。对一个后端插件来说标准的注册写法如下export default createBackendPlugin({ pluginId: my-plugin, register(env) { env.registerInit({ deps: { auth: coreServices.auth, httpAuth: coreServices.httpAuth, httpRouter: coreServices.httpRouter, userInfo: coreServices.userInfo, }, async init({ auth, httpAuth, httpRouter, userInfo }) { // Your code goes here }, }); }, });获取用户信息下面的示例从请求中提取用户凭据再获取该 principal 的额外信息router.get(/some-request, async (req, res) { const credentials await httpAuth.credentials(req, { allow: [user] }); const info await userInfo.getUserInfo(credentials); });这里httpAuth.credentials(req, { allow: [user] })在源头就把凭据限定为用户凭据因此可以直接交给userInfo使用。同时允许用户与服务凭据时的处理如果某个端点同时允许用户凭据和服务凭据即httpAuth.credentials(req)未加allow限制就需要在调用getUserInfo之前先做 principal 类型检查否则服务凭据会触发异常router.get(/some-request, async (req, res) { const credentials await httpAuth.credentials(req); if (auth.isPrincipal(credentials, user)) { const info await userInfo.getUserInfo(credentials); // ... } });默认实现原理两条数据获取路径Backstage 为该服务提供了开箱即用的默认实现。服务工厂位于 userInfoServiceFactory.ts它依赖coreServices.discovery将DefaultUserInfoService包在一层CachedUserInfoService缓存中并把缓存条目 Map 作为根上下文共享给同一进程内创建的所有实例export const userInfoServiceFactory createServiceFactory({ service: coreServices.userInfo, deps: { discovery: coreServices.discovery, }, createRootContext() { return new Mapstring, UserInfoCacheEntry(); }, async factory({ discovery }, entries) { return new CachedUserInfoService( new DefaultUserInfoService({ discovery }), { entries }, ); }, });DefaultUserInfoServiceJWT 优先HTTP 回退核心逻辑在 DefaultUserInfoService.ts。getUserInfo方法按顺序执行以下步骤限定 principal 类型通过toInternalBackstageCredentials拿到内部凭据若不是 user 类型直接抛出Only user credentials are supported这与前文只处理用户凭据的约束一致若用户凭据缺失 token则抛User credentials is unexpectedly missing token。解码 JWT 提取声明使用jose库的decodeJwt从 token 中读取sub与ent两个声明sub必须是字符串映射为userEntityRefententity refs 声明即为所有权实体引用列表。无ent声明时回退到 HTTP 接口若 token 中不含ent服务会通过DiscoveryService找到auth插件的 base URL携带Authorization: Bearer token请求{auth}/v1/userinfo并从响应的claims.ent中取出所有权引用if (!ownershipEntityRefs) { const userInfoResp await fetch( ${await this.discovery.getBaseUrl(auth)}/v1/userinfo, { headers: { Authorization: Bearer ${internalCredentials.token}, }, }, ); if (!userInfoResp.ok) { throw await ResponseError.fromResponse(userInfoResp); } const { claims: { ent }, } await userInfoResp.json(); ownershipEntityRefs ent; }结果校验最终ownershipEntityRefs必须是非空的字符串数组否则抛出Ownership entity refs can not be determined或Ownership entity refs must be an array of strings。从源码结构看这种JWT 优先、HTTP 回退的设计意味着只要 auth 插件在签发 token 时就把ent写入 JWT例如通过身份解析器在登录时提取插件侧的getUserInfo调用就是零网络开销的纯本地解码只有当 token 中未携带ent时才会触发一次到 auth 插件的内部 HTTP 调用。行为验证测试用例印证两条路径DefaultUserInfoService.test.ts 用 MSW 模拟请求精确验证了上述行为token 无ent时发起预期的 HTTP 调用mock 掉https://example.com/api/auth/v1/userinfo端点断言请求头携带Bearer token返回claims: { ent: [group:default/my-team] }最终解析结果为{ userEntityRef: user:default/alice, ownershipEntityRefs: [group:default/my-team] }token 含ent时直接使用JWT payload 中带ent: [group:default/my-team]时直接得到相同结果不触发任何 HTTP 调用服务端错误透传当/v1/userinfo返回 404 时错误以ResponseError形式向上抛出[ResponseError: Request failed with 404 Not Found]。性能层CachedUserInfoService 的进程内缓存默认工厂不会裸用DefaultUserInfoService而是外面包了一层 CachedUserInfoService.ts。从源码看它的策略是以 token 字符串为缓存键每个用户凭据的 token 对应一条缓存项值为已发起的解析 Promise 过期时间戳。缓存 Promise 而非结果天然实现了同一 token 并发请求时的去重请求合并TTL 5 秒惰性清扫默认过期时间DEFAULT_TTL_MS 5_000且每 30 秒SWEEP_INTERVAL_MS 30_000才在请求触发时惰性清扫一次过期项失败不缓存若解析 Promise 失败会检查并删除该 token 对应的缓存项避免把失败结果固定下来无 token 时直通凭据没有 token 时直接委托给内层服务不进缓存。由于 TTL 很短5 秒登录时提取语义与调用方拿到近实时信息之间的平衡点就在这里——你可以理解为该服务在短窗口内复用同一 token 的解析结果而不改变信息源自 sign-in这一本质。小结与适用前提User Info Service 是 Backstage 后端 core service 之一引用 idcore.userInfo通过coreServices.userInfo声明依赖即可在插件中注入使用它只接受 user 凭据若端点同时放行 service 凭据务必先用auth.isPrincipal(credentials, user)做类型判断返回内容userEntityRefownershipEntityRefs来自用户 sign-in 期间提取的数据默认实现优先从 JWT 的sub/ent声明本地解码缺失ent时回退请求 auth 插件的/v1/userinfo端点并带有 5 秒 TTL 的进程内缓存依赖前提你的应用已正确配置 auth 与 httpAuth 服务且用户 token 为 Backstage 标准签发的 JWT。【免费下载链接】backstageBackstage is an open framework for building developer portals项目地址: https://gitcode.com/GitHub_Trending/ba/backstage创作声明:本文部分内容由AI辅助生成(AIGC),仅供参考