Skip to content

社交认证

您可以使用 @adonisjs/ally 包在 AdonisJS 应用程序中实现社交认证。Ally 附带以下内置驱动程序,以及一个可扩展的 API 来注册自定义驱动程序

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

Ally 不会代您存储任何用户或访问令牌。它实现了 OAuth2 和 OAuth1 协议,通过社交服务验证用户身份,并提供用户详细信息。您可以将这些信息存储在数据库中,并使用 auth 包在应用程序中登录用户。

安装

使用以下命令安装和配置包:

sh
node ace add @adonisjs/ally

# 通过 CLI 标志定义提供商
node ace add @adonisjs/ally --providers=github --providers=google

:::disclosure

  1. 使用检测到的包管理器安装 @adonisjs/ally 包。

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

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

  4. 定义环境变量以存储所选 OAuth 提供商的 CLIENT_IDCLIENT_SECRET

:::

配置

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

另请参阅:配置存根

ts
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。

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

使用

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

ts
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() 方法执行重定向。

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

您可以传递回调函数以在重定向期间定义自定义作用域或查询字符串值。

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

处理回调响应

用户批准或拒绝认证请求后,将被重定向回您应用程序的 callbackUrl

在此路由中,您可以调用 .user() 方法获取已登录用户的详细信息和访问令牌。但是,您还必须检查响应中可能存在的错误状态。

ts
router.get('/github/callback', async ({ ally }) => {
  const gh = ally.use('github')

  /**
   * 用户通过取消登录流程拒绝了访问
   */
  if (gh.accessDenied()) {
    return '您已取消登录流程'
  }

  /**
   * OAuth 状态验证失败。当 CSRF cookie 过期时会发生这种情况。
   */
  if (gh.stateMisMatch()) {
    return '我们无法验证请求。请重试'
  }

  /**
   * GitHub 响应了某些错误
   */
  if (gh.hasError()) {
    return gh.getError()
  }

  /**
   * 访问用户信息
   */
  const user = await gh.user()
  return user
})

用户属性

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

ts
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 属性是底层访问令牌对象的引用。token 对象具有以下子属性。

ts
user.token.token
user.token.type
user.token.refreshToken
user.token.expiresAt
user.token.expiresIn
属性协议描述
tokenOAuth2 / OAuth1访问令牌的值。该值适用于 OAuth2OAuth1 协议。
secretOAuth1仅适用于 OAuth1 协议的令牌密钥。目前,Twitter 是唯一使用 OAuth1 的官方驱动程序。
typeOAuth2令牌类型。通常是 bearer token
refreshTokenOAuth2您可以使用刷新令牌创建新的访问令牌。如果 OAuth 提供商不支持刷新令牌,则该值将为 undefined
expiresAtOAuth2luxon DateTime 类的实例,表示访问令牌过期的绝对时间。
expiresInOAuth2令牌过期的秒数值。这是一个静态值,不会随着时间推移而改变。

original

OAuth 提供商原始响应的引用。如果规范化的用户属性集没有您需要的所有信息,您可能需要引用原始响应。

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

定义作用域

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

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

感谢 TypeScript,您将获得所有可用作用域的自动完成建议。

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

定义重定向查询参数

除了作用域,您还可以自定义重定向请求的查询参数。在以下示例中,我们定义了适用于 Google 提供商promptaccess_type 参数。

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

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

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

从访问令牌获取用户详细信息

有时,您可能想从存储在数据库中或通过另一个 OAuth 流程提供的访问令牌获取用户详细信息。例如,您通过移动应用使用了原生 OAuth 流程并收到了访问令牌。

您可以使用 .userFromToken() 方法获取用户详细信息。

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

对于 OAuth1 驱动程序,您可以使用 .userFromTokenAndSecret 方法获取用户详细信息。

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

无状态认证

许多 OAuth 提供商建议使用 CSRF 状态令牌来保护您的应用程序免受跨站请求伪造攻击。

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

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

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

完整配置参考

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

:::disclosure

ts
{
  github: services.github({
    clientId: '',
    clientSecret: '',
    callbackUrl: '',

    // GitHub 特定
    login: 'adonisjs',
    scopes: ['user', 'gist'],
    allowSignup: true,
  })
}

:::

:::disclosure

ts
{
  google: services.google({
    clientId: '',
    clientSecret: '',
    callbackUrl: '',

    // Google 特定
    prompt: 'select_account',
    accessType: 'offline',
    hostedDomain: 'adonisjs.com',
    display: 'page',
    scopes: ['userinfo.email', 'calendar.events'],
  })
}

:::

:::disclosure

ts
{
  twitter: services.twitter({
    clientId: '',
    clientSecret: '',
    callbackUrl: '',
  })
}

:::

:::disclosure

ts
{
  discord: services.discord({
    clientId: '',
    clientSecret: '',
    callbackUrl: '',

    // Discord 特定
    prompt: 'consent' | 'none',
    guildId: '',
    disableGuildSelect: false,
    permissions: 10,
    scopes: ['identify', 'email'],
  })
}

:::

:::disclosure

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

ts
{
  linkedin: services.linkedin({
    clientId: '',
    clientSecret: '',
    callbackUrl: '',

    // LinkedIn 特定
    scopes: ['r_emailaddress', 'r_liteprofile'],
  })
}

:::

:::disclosure

ts
{
  linkedin: services.linkedinOpenidConnect({
    clientId: '',
    clientSecret: '',
    callbackUrl: '',

    // LinkedIn 特定
    scopes: ['openid', 'profile', 'email'],
  })
}

:::

:::disclosure

ts
{
  facebook: services.facebook({
    clientId: '',
    clientSecret: '',
    callbackUrl: '',

    // Facebook 特定
    scopes: ['email', 'user_photos'],
    userFields: ['first_name', 'picture', 'email'],
    display: '',
    authType: '',
  })
}

:::

:::disclosure

ts
{
  spotify: services.spotify({
    clientId: '',
    clientSecret: '',
    callbackUrl: '',

    // Spotify 特定
    scopes: ['user-read-email', 'streaming'],
    showDialog: false
  })
}

:::

创建自定义社交驱动程序

我们创建了一个入门套件用于实现自定义社交驱动程序并发布到 npm。请查阅入门套件的 README 以获取进一步说明。