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

承接 Future Social Authentication Improvements… 的讨论

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

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

实现身份验证器(Authenticator)

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

实现类必须重写 nameregister_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: 注: 为了兼容多站点(multisite),重要的是通过 setup lambda 向 omniauth 提供特定于站点的信息,而不是在定义时固定。请参阅所有核心身份验证器以获取此操作的示例。

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

数据存储在 user_associated_accounts 数据库表中。provider_uidinfocredentialsextra 均直接取自 omniauth 返回的数据。

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

auth_provider authenticator: OpenIDConnectAuthenticator.new()

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

来自 @fantasticfears 的关于 ManagedAuthenticator 的补充说明

ManagedAuthenticator 详解

你可能需要针对身份验证处理一些特殊逻辑。如果你想更多地了解 ManagedAuthenticator,基本上它包含若干操作和选项,并控制数据的使用方式。

Discourse 使用两个控制器来管理用户信息。Users::OmniauthCallbacksController 在 OAuth2 身份验证完成后管理负载(payload)。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 在此处被调用。

核心逻辑保留在 after_authenticate 中,使用 Auth::Result 数据类。我们在此遵循数据结构。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_rowsoauth2_user_infosuser_associated_accounts 行所需的最少数据是 provider_nameprovider_uiduser_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 是否包含用户名和电子邮件身份验证的提示,并响应渲染一个“向我发送登录链接”的表单?