为 Discourse 添加新的“托管”身份验证方法

接续 Future Social Authentication Improvements… 的内容

我们目前正在将所有“关联账户”信息迁移到单个数据库表中。这将有助于显著减少重复逻辑,并加快未来的开发速度。例如,将核心 Twitter 逻辑迁移到新系统后,代码行数从 136 行减少到了仅 24 行 :tada:。

本帖旨在提供一个概览,并在必要时指向相关的源代码,而不是作为添加新身份验证提供程序的逐步操作手册。

实现身份验证器 (Authenticator)

每个身份验证器必须实现 Auth::Authenticator 的子类。若要使用新的共享逻辑,身份验证器可以改为扩展 Auth::ManagedAuthenticator。可以在核心 Facebook 身份验证器中找到一个基础实现的示例:

实现类必须重写 name 和 register_middleware,以及 enable_setting —— 管理员用于开启该提供程序的布尔型站点设置。

身份验证器应当还声明 required_settings:在身份验证成功之前必须具有值的站点设置。基类使用它们来确定 configured?,而 enabled? 是 enable_setting && configured?,因此,如果凭据缺失,该提供程序不会在登录页面上显示,其 /auth/<name> 路由将保持关闭 —— 否则,点击按钮会将用户困在提供程序自己的错误页面上,且无法返回。声明 required_settings 还可以让 AuthProviderCredentialsValidator 拒绝启用该提供程序;在启用设置上使用 validator: "AuthProviderCredentialsValidator" 将其配置好。

def enable_setting
  :enable_google_oauth2_logins
end

def required_settings
  %i[google_oauth2_client_id google_oauth2_client_secret]
end

直接重写 enabled? 的身份验证器将退出这两个门控机制。

:information_source: 旁注: 为了多站点兼容性,重要的是,任何特定于站点的信息都应在 setup lambda 中提供给 omniauth,而不是在定义时固定。请参见所有核心身份验证器以了解此示例。

将外部账户链接到 Discourse 账户的所有逻辑均由 Auth::ManagedAuthenticator 处理。这依赖于 omniauth 提供程序返回符合其文档中定义格式的数据。如果需要对此数据进行任何操作,身份验证器可以重写 after_authenticate 方法,并按需操作 auth_token。例如,核心 Twitter 身份验证器从令牌中删除所有 extra 信息:

数据存储在 user_associated_accounts 数据库表中。provider_uid、info、credentials 和 extra 均直接取自 omniauth 返回的数据。

提供程序的图像在身份验证时从 info.image 下载,并保留在每个账户的 avatar_upload_id 中。它们与用户上传的照片和可选预设保持独立。头像选择器在现有的头像权限下提供来自已启用提供程序的缓存图像。

UserAvatar 负责头像的导入、选择、刷新和清理。身份验证通过 UserAvatar.retrieve_for_associated_account 调度提供程序的获取;下载作业和导入器使用 UserAvatar.import_url_for_user,并为提供程序图像传递 associated_account_id。用户便捷方法和账户/上传生命周期回调将头像更改委托给 UserAvatar;权限保留在 Guardian 中。

user_avatars.selected_user_associated_account_id 记录明确选择的提供程序。只要该提供程序保持选中状态,后续的下载就会更新显示的头像。新账户在未分配头像时初始选择其提供程序;auth_overrides_avatar 继续强制提供程序选择。现有头像保留其外观,并且不会自动分配提供程序。现有链接账户将在下次登录时填充其提供程序选择。

选择另一个头像会清除提供程序选择。断开账户连接会将其当前显示的图像保留为本地快照,并保留任何单独上传的照片。失败的下载会保留之前的图像。如果账户已断开连接、移动到另一个用户或现在提供不同的图像 URL,则排队的下载将被丢弃。

Gravatar 选择仍使用上传 ID 匹配。因此,与缓存的 Gravatar 相同的自定义或预设照片可能会跟随后续的 Gravatar 更新。

一旦定义了 Authenticator 类,就需要对其进行注册。这必须在应用程序生命周期的早期进行,并且不能在插件的 after_initialize 方法内进行。最小注册可以仅包含对身份验证器的引用。在插件中,可以使用 auth_provider 函数进行注册。例如:

auth_provider authenticator: OpenIDConnectAuthenticator.new()

在核心中,注册发生在 discourse.rb 中。可能的 AuthProvider 选项完整列表可以在这里找到。文本内容可以使用这些选项定义,但最好按照标准键在 client.en.yml 中提供可本地化的字符串。例如:

@fantasticfears 关于 ManagedAuthenticator 的附加说明

ManagedAuthenticator 详解

您可能需要为身份验证处理一些特殊功能。您希望更了解 ManagedAuthenticator。基本上,它具有多种操作、选项,并控制数据的使用方式。

Discourse 使用两个控制器管理用户信息。Users::OmniauthCallbacksController 在 OAuth2 身份验证完成后管理有效载荷。after_authenticate 在此处被调用。can_connect_existing_user? 也在此处使用。
有一些私有方法您可以阅读,以了解不同数据字段的工作方式。

if authenticator.can_connect_existing_user? && current_user
  @auth_result = authenticator.after_authenticate(auth, existing_account: current_user)
else
  @auth_result = authenticator.after_authenticate(auth)
end

UsersController 有一个 revoke_account,它使用 can_revoke? 和 revoke。但要让 revoke 方法远程工作,您需要构建自己的实现。

UserAuthenticator 是一个帮助身份验证(验证电子邮件确认或 OAuth2 路径)用户的服务类。after_create_account 在此处被调用。

核心逻辑仍然位于带有 Auth::Result 数据类的 after_authenticate 中。我们在此遵循数据结构。extra_data 将传递给 after_create_account 以创建相关记录。

result.extra_data = {
  provider: auth_token[:provider],
  uid: auth_token[:uid],
  info: auth_token[:info],
  extra: auth_token[:extra],
  credentials: auth_token[:credentials]
}

它将尝试匹配并连接到现有账户。

您可能想知道为什么自动创建账户是可能的,但没有 User.create。这是在 UsersController#create 中完成的。

authentication = UserAuthenticator.new(user, session)

用户是一个新实例,将由身份验证提供程序准备好的会话数据填充。相信我,这只是魔法。


迁移到新系统

为了提供无缝切换到新系统,数据应从旧存储位置迁移。对于核心身份验证提供程序,这可能是专用表。对于插件,这可能是 plugin_store_rows 或 oauth2_user_infos。user_associated_accounts 行所需的最小数据是 provider_name、provider_uid 和 user_id。迁移示例请参见:

一旦 ManagedAuthenticator 系统在 v2.2.0 中发布到稳定分支,我们将开始迁移官方身份验证插件。届时,这里将添加 plugin_store_row 迁移示例。


本文档受版本控制 - 建议在 github 上提出更改。

23 个赞

@david 这里完成的所有工作都太棒了。非常感谢。我也有机会玩了 https://github.com/discourse/discourse-development-auth,它非常方便。

只是有一个注意事项,该功能在本地与 ember cli 不能很好地协同工作(不会弹出注册弹窗)。我在编写一个用于添加身份验证提供程序的插件时挠头不已,突然想到使用 NO_EMBER_CLI=1,然后一切都开始正常工作了。

7 个赞

我想了解一下,实现一个身份验证器是否是正确的路径,可以参考:

我的理解对吗?所有已注册的身份验证器都会在应用程序早期被调用,因此我可以在其中测试 URL 是否包含用户名和电子邮件身份验证的提示,并响应渲染一个“向我发送登录链接”的表单?