承接 Future Social Authentication Improvements… 的讨论
目前,我们正将所有“关联账户”信息迁移到单一的数据库表中。这将有助于显著减少重复逻辑,并为未来的开发提速。例如,将我们核心的 Twitter 逻辑 迁移 到新系统后,代码行数从 136 行减少到了仅 24 行
。
本帖旨在提供一个概览,并在必要时指向相关的源代码,而不是作为添加新身份验证提供方的逐步操作手册。
实现身份验证器(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? 的身份验证器将退出这两个检查门控。
注: 为了兼容多站点(multisite),重要的是通过
setuplambda 向 omniauth 提供特定于站点的信息,而不是在定义时固定。请参阅所有核心身份验证器以获取此操作的示例。
将外部账户链接到 Discourse 账户的所有逻辑都由 Auth::ManagedAuthenticator 处理。这依赖于 omniauth 提供方按照其文档中定义的格式返回数据。如果需要对此数据进行任何操作,身份验证器可以重写 after_authenticate 方法,并按需操作 auth_token。例如,核心 Twitter 身份验证器会从 token 中移除所有 extra 信息:
数据存储在 user_associated_accounts 数据库表中。provider_uid、info、credentials 和 extra 均直接取自 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_rows 或 oauth2_user_infos。user_associated_accounts 行所需的最少数据是 provider_name、provider_uid 和 user_id。迁移示例请参见:
一旦 ManagedAuthenticator 系统在 v2.2.0 版本中发布到稳定分支,我们将开始迁移官方的身份验证插件。届时,这里将添加一个 plugin_store_row 迁移示例。
本文档受版本控制 - 请在 github 上建议修改。