Skip to main content

登录身份和解析器

默认情况下,每个 Backstage 验证提供程序都只针对访问授权的用例进行配置,这使得 Backstage 可以代表用户从外部系统请求资源和操作,例如在 CI 中重新触发构建。

如果要使用认证提供商登录用户,则需要明确配置该提供商启用登录功能,并告诉它如何将外部身份映射到 Backstage 中的用户身份。

快速启动

请参阅 providers > 获取 auth 提供商及其内置登录解析器的完整列表。

用以下工具创建的Backstage项目npx @backstage/create-app该解析器使所有用户共享一个 "访客 "身份,仅作为快速启动和运行的最低要求。 您可以替换github如果您有需要,也可以为任何其他供应商提供服务。

此解析器不应在生产环境中使用,因为它使用的是单一共享身份,对谁能登录没有限制。 一旦需要为生产环境安装解析器,请务必通读本页其余部分,以了解Backstage身份系统。

访客解析器也可用于测试目的,它看起来像这样:

signIn: {
resolver(_, ctx) {
const userRef = 'user:default/guest'
return ctx.issueToken({
claims: {
sub: userRef,
ent: [userRef],
},
}),
},
},

用户身份Backstage

Backstage 中的用户身份是由两个信息组成的,一个是用户名,另一个是密码。实体参考用户登录时,Backstage令牌会根据这两条信息生成,然后用来在Backstage生态系统中识别用户。

用户实体引用应能在Backstage唯一标识已登录的用户。 我们鼓励软件目录中也存在匹配的用户实体,但这不是必须的。 如果目录中存在用户实体,则可用于存储用户的其他数据。 有些插件甚至需要这样才能运行。

所有权引用也是实体引用,同样鼓励这些实体存在于目录中,但这并不是必须的。 所有权引用用于确定用户拥有的内容,是用户声称拥有所有权的引用集。 例如,用户 Jane (user:default/jane) 可能有所有权引用user:default/jane,group:default/team-agroup:default/admins鉴于这些所有权要求,任何实体只要被标记为由以下任何一方拥有user:jane,team-aadmins将被视为简所有。

所有权要求通常包含用户实体引用本身,但这并不是必需的。 值得注意的是,所有权要求也可用于解决与所有权类似的其他关系,例如对一个maintaineroperator状态

封装用户身份的Backstage令牌是一个 JWT。 用户实体引用存储在sub而所有权引用则存储在一个自定义的ent用户和所有权引用都应始终是完整的实体引用,而不是仅有janeuser:jane.

登录解决程序

将用户登录到 Backstage 需要将用户身份从第三方认证提供商映射到 Backstage 用户身份。 这种映射在不同的组织和认证提供商之间可能会有很大不同,因此没有默认的用户身份解析方式。 要用于登录的认证提供商必须配置登录解析器,这是一个负责创建用户身份映射的函数。

登录解析器函数的输入是使用给定的认证提供程序成功登录的结果,以及一个上下文对象,该对象包含用于查找用户和签发令牌的各种助手。 还有一些内置的登录解析器可以使用,下面将详细介绍。

请注意,虽然可以配置多个授权提供商用于登录,但在这样做时要小心谨慎,最好确保不同的授权提供商不会有任何用户重叠,或者任何能够使用多个提供商登录的用户最终都使用相同的Backstage身份。

定制解析器示例

让我们来看一个为 Google 验证提供程序自定义登录解析器的示例。 这一切通常发生在您的packages/backend/src/plugins/auth.ts文件,该文件负责设置和配置 auth 后端插件。

在创建新的验证提供程序工厂时,您需要在传递的选项中提供解析器,这意味着您需要用自己创建的解析器替换默认的 Google 提供程序。 请确保同时包含现有的defaultAuthProviderFactories如果您想保留已安装的所有内置验证提供程序。

现在让我们看看这个例子,其余的注释将在代码注释中进行:

// File: packages/backend/src/plugins/auth.ts
import {
createRouter,
providers,
defaultAuthProviderFactories,
} from '@backstage/plugin-auth-backend';
import { Router } from 'express';
import { PluginEnvironment } from '../types';

export default async function createPlugin(
env: PluginEnvironment,
): Promise<Router> {
return await createRouter({
...env,
providerFactories: {
...defaultAuthProviderFactories,
google: providers.google.create({
signIn: {
resolver: async (info, ctx) => {
const {
profile: { email },
} = info;
// Profiles are not always guaranteed to to have an email address.
// You can also find more provider-specific information in `info.result`.
// It typically contains a `fullProfile` object as well as ID and/or access
// tokens that you can use for additional lookups.
if (!email) {
throw new Error('User profile contained no email');
}

// You can add your own custom validation logic here.
// Logins can be prevented by throwing an error like the one above.
myEmailValidator(email);

// This example resolver simply uses the local part of the email as the name.
const [name] = email.split('@');

// This helper function handles sign-in by looking up a user in the catalog.
// The lookup can be done either by reference, annotations, or custom filters.
//
// The helper also issues a token for the user, using the standard group
// membership logic to determine the ownership references of the user.
return ctx.signInWithCatalogUser({
entityRef: { name },
});
},
},
}),
},
});
}

内置分解器

您不必总是编写自己的自定义解析器。 auth 后端插件为许多常见的登录模式提供了内置解析器。 您可以通过resolvers例如,Google 提供商有一个内置的解析器,其工作原理与我们上面定义的一样:

// File: packages/backend/src/plugins/auth.ts
export default async function createPlugin(
// ...
return await createRouter({
// ...
providerFactories: {
// ...
google: providers.google.create({
signIn: {
resolver: providers.google.resolvers.emailLocalPartMatchingUserEntityName(),
},
});
}
})
)

还有其他选项,比如这个选项通过匹配google.com/email目录中用户实体的注释:

providers.google.create({
signIn: {
resolver: providers.google.resolvers.emailMatchingUserEntityAnnotation(),
},
});

Custom Ownership Resolution

如果您想对会员身份解析和登录过程中发生的令牌生成进行更多控制,您可以替换为ctx.signInWithCatalogUser和一组低级调用:

// File: packages/backend/src/plugins/auth.ts
import { getDefaultOwnershipEntityRefs } from '@backstage/plugin-auth-backend';

export default async function createPlugin(
// ...
return await createRouter({
// ...
providerFactories: {
// ...
google: async ({ profile: { email } }, ctx) => {
if (!email) {
throw new Error('User profile contained no email');
}

// This step calls the catalog to look up a user entity. You could for example
// replace it with a call to a different external system.
const { entity } = await ctx.findCatalogUser({
annotations: {
'acme.org/email': email,
},
});

// In this step we extract the ownership references from the user entity using
// the standard logic. It uses a reference to the entity itself, as well as the
// target of each `memberOf` relation where the target is of the kind `Group`.
//
// If you replace the catalog lookup with something that does not return
// an entity you will need to replace this step as well.
//
// You might also replace it if you for example want to filter out certain groups.
//
// Note that `getDefaultOwnershipEntityRefs` only includes groups to which the
// user has a direct MEMBER_OF relationship. It's perfectly fine to include
// groups that the user is transitively part of in the claims array, but the
// catalog doesn't currently provide a direct way of accessing this list of
// groups.
const ownershipRefs = getDefaultOwnershipEntityRefs(entity);

// The last step is to issue the token, where we might provide more options in the future.
return ctx.issueToken({
claims: {
sub: stringifyEntityRef(entity),
ent: ownershipRefs,
},
});
};
}
})
)

在目录中没有用户的情况下登录

虽然在目录中填充组织数据可以为浏览软件生态系统提供更强大的方式,但它可能并不总是一个可行或优先的选择。 不过,即使您没有在目录中填充用户实体,您仍然可以登录用户。 由于目前没有针对这种情况的内置登录解析器,您需要自己实施。

要登录一个不存在于目录中的用户,就像跳过上例中的目录查找步骤一样简单。 我们不需要查找用户,而是立即使用任何可用信息发出一个令牌。 需要注意的一点是,确定所有权引用可能比较麻烦,尽管可以通过查找外部服务等方式来实现。 通常情况下,我们希望至少使用用户本身作为唯一的所有权引用。

由于我们不再使用目录作为用户的允许列表,因此限制允许登录的用户通常很重要。 这可以是简单的电子邮件域检查,就像下面的示例,也可以是使用所提供结果对象中的用户访问令牌查找用户所属的 GitHub 组织。

// File: packages/backend/src/plugins/auth.ts
import { createRouter, providers } from '@backstage/plugin-auth-backend';
import { Router } from 'express';
import { PluginEnvironment } from '../types';
import {
stringifyEntityRef,
DEFAULT_NAMESPACE,
} from '@backstage/catalog-model';

export default async function createPlugin(
env: PluginEnvironment,
): Promise<Router> {
return await createRouter({
...env,
providerFactories: {
google: providers.google.create({
signIn: {
resolver: async ({ profile }, ctx) => {
if (!profile.email) {
throw new Error(
'Login failed, user profile does not contain an email',
);
}
// Split the email into the local part and the domain.
const [localPart, domain] = profile.email.split('@');

// Next we verify the email domain. It is recommended to include this
// kind of check if you don't look up the user in an external service.
if (domain !== 'acme.org') {
throw new Error(
`Login failed, this email ${profile.email} does not belong to the expected domain`,
);
}

// By using `stringifyEntityRef` we ensure that the reference is formatted correctly
const userEntity = stringifyEntityRef({
kind: 'User',
name: localPart,
namespace: DEFAULT_NAMESPACE,
});
return ctx.issueToken({
claims: {
sub: userEntity,
ent: [userEntity],
},
});
},
},
}),
},
});
}

AuthHandler

与自定义登录解析器类似,您也可以编写一个自定义认证处理程序函数,用于验证认证响应并将其转换为显示给用户的配置文件。 在这里您可以自定义显示名称和配置文件图片等内容。

在这里还可以对用户进行授权和验证,如果用户不允许访问Backstage,就会出错。

// File: packages/backend/src/plugins/auth.ts
export default async function createPlugin(
env: PluginEnvironment,
): Promise<Router> {
return await createRouter({
...
providerFactories: {
google: providers.google.create({
authHandler: async ({
fullProfile // Type: passport.Profile,
idToken // Type: (Optional) string,
}) => {
// Custom validation code goes here
return {
profile: {
email,
picture,
displayName,
}
};
}
})
}
})
}