社交身份认证

社交身份认证

你可以使用 @adonisjs/ally 软件包在你的 AdonisJS 应用中实现社交身份认证。 Ally 自带以下内置驱动,并提供了一个可扩展的 API 来注册自定义驱动

  • Twitter
  • Facebook
  • Spotify
  • Google
  • GitHub
  • Discord
  • LinkedIn

Ally 不会替你存储任何用户或访问令牌。它实现了 OAuth2 和 OAuth1 协议,使用社交服务对用户进行认证,并提供用户详情。你可以将这些信息存储在数据库中,并使用 auth 软件包在你的应用中登录该用户。

安装

使用以下命令安装并配置该软件包:

node ace add @adonisjs/ally
# 将 providers 定义为 CLI 标志
node ace add @adonisjs/ally --providers=github --providers=google
  1. 使用检测到的包管理器安装 @adonisjs/ally 软件包。

  2. adonisrc.ts 文件中注册以下服务提供者。

    {
    providers: [
    // ...其他 providers
    () => import('@adonisjs/ally/ally_provider')
    ]
    }
  3. 创建 config/ally.ts 文件。该文件包含所选 OAuth 提供器的配置设置。

  4. 定义环境变量,用于存储所选 OAuth 提供器的 CLIENT_IDCLIENT_SECRET

配置

@adonisjs/ally 软件包的配置存储在 config/ally.ts 文件中。你可以在单个配置文件中定义多个服务的配置。

另见:配置桩(stub)

import { defineConfig, services } from '@adonisjs/ally'
defineConfig({
github: services.github({
clientId: env.get('GITHUB_CLIENT_ID')!,
clientSecret: env.get('GITHUB_CLIENT_SECRET')!,
callbackUrl: '',
}),
twitter: services.twitter({
clientId: env.get('TWITTER_CLIENT_ID')!,
clientSecret: env.get('TWITTER_CLIENT_SECRET')!,
callbackUrl: '',
}),
})

配置回调 URL

OAuth 提供器要求你注册一个回调 URL,以处理用户授权登录请求后的重定向响应。

回调 URL 必须在 OAuth 服务提供器处注册。例如:如果你使用的是 GitHub,必须登录你的 GitHub 账户,创建一个新应用,并使用 GitHub 界面定义回调 URL。

此外,你必须在 config/ally.ts 文件中使用 callbackUrl 属性注册相同的回调 URL。

使用

配置好软件包后,你可以使用 ctx.ally 属性与 Ally API 交互。你可以使用 ally.use() 方法在已配置的认证提供器之间切换。例如:

router.get('/github/redirect', ({ ally }) => {
// GitHub 驱动实例
const gh = ally.use('github')
})
router.get('/twitter/redirect', ({ ally }) => {
// Twitter 驱动实例
const twitter = ally.use('twitter')
})
// 你也可以动态地获取驱动
router.get('/:provider/redirect', ({ ally, params }) => {
const driverInstance = ally.use(params.provider)
}).where('provider', /github|twitter/)

重定向用户以进行认证

社交身份认证的第一步是将用户重定向到某个 OAuth 服务,并等待其批准或拒绝认证请求。

你可以使用 .redirect() 方法执行重定向。

router.get('/github/redirect', ({ ally }) => {
return ally.use('github').redirect()
})

你可以传入一个回调函数,在重定向期间定义自定义的 scope(作用域)或查询字符串值。

router.get('/github/redirect', ({ ally }) => {
return ally
.use('github')
.redirect((request) => {
request.scopes(['user:email', 'repo:invite'])
request.param('allow_signup', false)
})
})

处理回调响应

在用户批准或拒绝认证请求后,他们将被重定向回你应用的 callbackUrl

在这个路由中,你可以调用 .user() 方法获取已登录用户的详情和访问令牌。不过,你也必须检查响应中是否存在可能的错误状态。

router.get('/github/callback', async ({ ally }) => {
const gh = ally.use('github')
/**
* 用户通过取消登录流程
* 拒绝了访问
*/
if (gh.accessDenied()) {
return 'You have cancelled the login process'
}
/**
* OAuth 状态验证失败。这发生在
* CSRF cookie 过期时。
*/
if (gh.stateMisMatch()) {
return 'We are unable to verify the request. Please try again'
}
/**
* GitHub 返回了某些错误
*/
if (gh.hasError()) {
return gh.getError()
}
/**
* 访问用户信息
*/
const user = await gh.user()
return user
})

用户属性

以下是从 .user() 方法调用的返回值中可以访问的属性列表。这些属性在所有底层驱动中都是一致的。

const user = await gh.user()
user.id
user.email
user.emailVerificationState
user.name
user.nickName
user.avatarUrl
user.token
user.original

id

OAuth 提供器返回的唯一 ID。

email

OAuth 提供器返回的电子邮件地址。如果 OAuth 请求没有要求用户的电子邮件地址,该值将为 null

emailVerificationState

许多 OAuth 提供器允许使用未验证邮箱的用户登录并对 OAuth 请求进行认证。你应该使用此标志来确保只有邮箱已验证的用户才能登录。

以下是可能的值列表。

  • verified:用户的电子邮件地址已在 OAuth 提供器处通过验证。
  • unverified:用户的电子邮件地址未经验证。
  • unsupported:OAuth 提供器不共享邮箱验证状态。

name

OAuth 提供器返回的用户名称。

nickName

用户对外可见的昵称。如果 OAuth 提供器没有昵称的概念,nickNamename 的值将相同。

avatarUrl

指向用户公开头像图片的 HTTP(s) URL。

token

token 属性是对底层访问令牌对象的引用。该令牌对象具有以下子属性。

user.token.token
user.token.type
user.token.refreshToken
user.token.expiresAt
user.token.expiresIn
属性协议描述
tokenOAuth2 / OAuth1访问令牌的值。该值对 OAuth2OAuth1 协议都可用。
secretOAuth1令牌密钥,仅适用于 OAuth1 协议。目前,Twitter 是唯一使用 OAuth1 的官方驱动。
typeOAuth2令牌类型。通常是一个 bearer 令牌
refreshTokenOAuth2你可以使用刷新令牌来创建一个新的访问令牌。如果 OAuth 提供器不支持刷新令牌,该值将为 undefined
expiresAtOAuth2一个 luxon DateTime 类的实例,表示访问令牌过期的绝对时间。
expiresInOAuth2以秒为单位的值,在此之后令牌将过期。它是一个静态值,不随时间推移而改变。

original

对 OAuth 提供器原始响应的引用。如果规范化后的用户属性集合没有你所需的全部信息,你可能需要引用原始响应。

const user = await github.user()
console.log(user.original)

定义 scopes

scopes 指的是在用户批准认证请求后你想要访问的数据。scopes 的名称以及你可以访问的数据因 OAuth 提供器而异;因此,你必须阅读它们的文档。

scopes 可以在 config/ally.ts 文件中定义,也可以在重定向用户时定义。

得益于 TypeScript,你将获得所有可用 scopes 的自动补全建议。

config/ally.ts
github: {
driver: 'github',
clientId: env.get('GITHUB_CLIENT_ID')!,
clientSecret: env.get('GITHUB_CLIENT_SECRET')!,
callbackUrl: '',
scopes: ['read:user', 'repo:invite'],
}
在重定向期间
ally
.use('github')
.redirect((request) => {
request.scopes(['read:user', 'repo:invite'])
})

定义重定向查询参数

你可以与 scopes 一起自定义重定向请求的查询参数。在以下示例中,我们定义了适用于 Google 提供器promptaccess_type 参数。

router.get('/google/redirect', async ({ ally }) => {
return ally
.use('google')
.redirect((request) => {
request
.param('access_type', 'offline')
.param('prompt', 'select_account')
})
})

你可以使用请求上的 .clearParam() 方法清除任何现有参数。如果参数默认值已在配置中定义,而你需要为单独的自定义认证流程重新定义它们,这会很有用。

router.get('/google/redirect', async ({ ally }) => {
return ally
.use('google')
.redirect((request) => {
request
.clearParam('redirect_uri')
.param('redirect_uri', '')
})
})

从访问令牌获取用户详情

有时,你可能想从存储在数据库中的访问令牌或通过另一个 OAuth 流程提供的访问令牌中获取用户详情。例如,你通过原生 OAuth 流程(经由移动应用)获取到了一个访问令牌。

你可以使用 .userFromToken() 方法获取用户详情。

const user = await ally
.use('github')
.userFromToken(accessToken)

对于 OAuth1 驱动,你可以使用 .userFromTokenAndSecret 方法获取用户详情。

const user = await ally
.use('github')
.userFromTokenAndSecret(token, secret)

无状态认证(Stateless authentication)

许多 OAuth 提供器建议使用 CSRF 状态令牌 来防止你的应用遭受跨站请求伪造攻击。

Ally 会创建一个 CSRF 令牌并将其保存在加密的 cookie 中,稍后在用户批准认证请求后对其进行检查验证。

不过,如果你由于某种原因无法使用 cookie,你可以启用无状态模式,在该模式下不会进行任何状态验证,因此也不会生成 CSRF cookie。

重定向
ally.use('github').stateless().redirect()
处理回调响应
const gh = ally.use('github').stateless()
await gh.user()

完整配置参考

以下是所有驱动的完整配置参考。你可以直接将以下对象复制粘贴到 config/ally.ts 文件中。

{
github: services.github({
clientId: '',
clientSecret: '',
callbackUrl: '',
// GitHub 专属
login: 'adonisjs',
scopes: ['user', 'gist'],
allowSignup: true,
})
}
{
google: services.google({
clientId: '',
clientSecret: '',
callbackUrl: '',
// Google 专属
prompt: 'select_account',
accessType: 'offline',
hostedDomain: 'adonisjs.com',
display: 'page',
scopes: ['userinfo.email', 'calendar.events'],
})
}
{
twitter: services.twitter({
clientId: '',
clientSecret: '',
callbackUrl: '',
})
}
{
discord: services.discord({
clientId: '',
clientSecret: '',
callbackUrl: '',
// Discord 专属
prompt: 'consent' | 'none',
guildId: '',
disableGuildSelect: false,
permissions: 10,
scopes: ['identify', 'email'],
})
}

此配置已根据更新后的 LinkedIn OAuth 要求 被弃用。

{
linkedin: services.linkedin({
clientId: '',
clientSecret: '',
callbackUrl: '',
// LinkedIn 专属
scopes: ['r_emailaddress', 'r_liteprofile'],
})
}
{
linkedin: services.linkedinOpenidConnect({
clientId: '',
clientSecret: '',
callbackUrl: '',
// LinkedIn 专属
scopes: ['openid', 'profile', 'email'],
})
}
{
facebook: services.facebook({
clientId: '',
clientSecret: '',
callbackUrl: '',
// Facebook 专属
scopes: ['email', 'user_photos'],
userFields: ['first_name', 'picture', 'email'],
display: '',
authType: '',
})
}
{
spotify: services.spotify({
clientId: '',
clientSecret: '',
callbackUrl: '',
// Spotify 专属
scopes: ['user-read-email', 'streaming'],
showDialog: false
})
}

创建自定义社交驱动

我们创建了一个启动套件,用于在 npm 上实现并发布自定义的社交驱动。请阅读该启动套件的 README 以获取更多说明。