社交身份验证
本指南介绍如何使用 Ally 包在 AdonisJS 中实现社交身份验证。你将学习:
- 如何安装并配置带有 OAuth 提供者的 Ally
- 如何将用户重定向到提供者并处理回调
- 如何从提供者获取用户信息
- 如何创建或查找用户并登录
- 如何为 SPA 和移动应用使用无状态身份验证
- 如何创建自定义的社交驱动
概述
社交身份验证允许用户使用他们在 GitHub、Google、X(原 Twitter)或 Discord 等服务的现有账户登录。用户无需创建新的用户名和密码,而是授权你的应用从提供者处访问他们的个人资料信息。
AdonisJS 通过 @adonisjs/ally 包提供社交身份验证。Ally 处理 OAuth 流程(将用户重定向、用授权码换取令牌、获取用户数据),并提供跨不同提供者的统一 API。它支持 OAuth 1.0(Twitter)和 OAuth 2.0(X、GitHub、Google 以及大多数其他提供者)。
Ally 不会在你的数据库中存储用户或令牌。它处理 OAuth 流程并返回用户信息,然后你可以使用这些信息在你的数据库中创建或查找用户,并通过一个 auth 守卫 让他们登录。
安装
使用 add 命令安装并配置该包:
node ace add @adonisjs/ally
# 在安装期间指定提供者
node ace add @adonisjs/ally --providers=github --providers=google
查看 add 命令执行的步骤
-
使用检测到的包管理器安装
@adonisjs/ally包。 -
在
adonisrc.ts文件内注册以下服务提供者。
{
providers: [
// ...其他提供者
() => import('@adonisjs/ally/ally_provider')
]
}
-
创建
config/ally.ts,包含所选提供者的配置。 -
为每个提供者定义
CLIENT_ID和CLIENT_SECRET的环境变量。
配置
在 config/ally.ts 中配置你的 OAuth 提供者。每个提供者都需要一个客户端 ID、客户端密钥和回调 URL:
import env from '#start/env'
import { defineConfig, services } from '@adonisjs/ally'
export default defineConfig({
github: services.github({
clientId: env.get('GITHUB_CLIENT_ID'),
clientSecret: env.get('GITHUB_CLIENT_SECRET'),
callbackUrl: 'http://localhost:3333/github/callback',
}),
google: services.google({
clientId: env.get('GOOGLE_CLIENT_ID'),
clientSecret: env.get('GOOGLE_CLIENT_SECRET'),
callbackUrl: 'http://localhost:3333/google/callback',
}),
})
在提供者处注册回调 URL
OAuth 提供者要求你在他们的开发者控制台中注册你的回调 URL。例如,要使用 GitHub 身份验证:
- 前往 GitHub Developer Settings
- 创建一个新的 OAuth App
- 将 Authorization callback URL 设置为与你的配置中的
callbackUrl相匹配
你配置中的回调 URL 必须与你在该提供者处注册的完全一致。
将用户重定向到提供者
创建一个将用户重定向到 OAuth 提供者的路由。使用 ally.use() 获取驱动实例,并调用 redirect()。
在重定向之前,将 redirect.previousUrl 存储到 session 中,这样错误处理程序和 back() 调用会将用户重定向回你的应用中的正确页面,而不是 OAuth 提供者的域名。
import router from '@adonisjs/core/services/router'
router.get('/github/redirect', ({ ally, session }) => {
session.put('redirect.previousUrl', '/login')
return ally.use('github').redirect()
})
请求 scopes
scopes 定义了你的应用可以访问哪些数据。不同的提供者拥有不同的可用 scopes。你可以在 config/ally.ts 中配置它们,或在重定向时指定:
github: services.github({
clientId: env.get('GITHUB_CLIENT_ID'),
clientSecret: env.get('GITHUB_CLIENT_SECRET'),
callbackUrl: 'http://localhost:3333/github/callback',
scopes: ['user:email', 'read:user'],
}),
router.get('/github/redirect', ({ ally }) => {
return ally.use('github').redirect((request) => {
request.scopes(['user:email', 'read:user'])
})
})
添加查询参数
某些提供者接受附加参数。例如,Google 的 prompt 参数控制同意屏幕的行为:
router.get('/google/redirect', ({ ally }) => {
return ally.use('google').redirect((request) => {
request.param('prompt', 'select_account')
request.param('access_type', 'offline')
})
})
要移除在配置中设置的参数,使用 clearParam:
router.get('/google/redirect', ({ ally }) => {
return ally.use('google').redirect((request) => {
request.clearParam('prompt')
})
})
处理回调
在用户授权(或拒绝)访问后,提供者会将他们重定向到你的回调 URL。在驱动上调用 user() 以用授权码换取访问令牌并获取用户的个人资料。
import router from '@adonisjs/core/services/router'
router.get('/github/callback', async ({ ally }) => {
const github = ally.use('github')
const githubUser = await github.user()
return githubUser
})
如果在回调过程中出现问题(用户拒绝访问、state 令牌不匹配,或缺少授权码),user() 方法会抛出一个自行处理的异常。该异常会自动对响应进行内容协商:
- 带有 session 的 HTML 请求:错误消息会被闪现(flash)到 session 中,用户被重定向回去。如果你在 OAuth 重定向之前在 session 中存储了
redirect.previousUrl,用户会返回到该页面。 - JSON 请求:返回一个带有适当状态码的 JSON 错误响应。
- JSONAPI 请求:返回一个 JSONAPI 格式的错误响应。
如果你需要自己处理特定的错误情况,仍然可以在调用 user() 之前对其进行检查。
router.get('/github/callback', async ({ ally, response }) => {
const github = ally.use('github')
if (github.accessDenied()) {
return response.redirect().toPath('/login?error=access_denied')
}
const githubUser = await github.user()
return githubUser
})
用户属性
user() 方法返回一个规范化的用户对象,在不同提供者之间具有一致的属性:
| Property | Description |
|---|---|
id | 来自提供者的唯一标识符 |
email | 用户的邮箱地址(如果未请求或不可用,可能为 null) |
emailVerificationState | 为 verified、unverified 或 unsupported 之一 |
name | 用户的显示名称 |
nickName | 用户名或手柄(如果提供者不支持昵称,则与 name 相同) |
avatarUrl | 用户头像图片的 URL |
token | 用于发起 API 调用的访问令牌对象 |
original | 来自提供者的原始响应 |
邮箱验证状态
不同的提供者处理邮箱验证的方式不同。在信任邮箱之前,请检查 emailVerificationState:
verified:提供者已验证该邮箱地址unverified:邮箱存在但未被验证unsupported:提供者不共享验证状态
访问令牌
token 属性包含用于向提供者发起额外 API 调用的 OAuth 令牌:
| Property | Protocol | Description |
|---|---|---|
token | OAuth 1 & 2 | 访问令牌字符串 |
secret | 仅 OAuth 1 | 令牌密钥(Twitter 使用) |
type | OAuth 2 | 令牌类型(通常为 bearer) |
refreshToken | OAuth 2 | 用于获取新访问令牌的令牌 |
expiresAt | OAuth 2 | 令牌过期的 DateTime |
expiresIn | OAuth 2 | 距离过期的秒数 |
原始响应
通过 original 属性访问提供者特定的数据:
const githubUser = await github.user()
console.log(githubUser.original)
创建用户并登录
Ally 提供用户信息,但不会创建用户或 session。在从提供者处获取用户数据后,你需要:
- 在你的数据库中查找或创建用户
- 使用一个 auth 守卫让他们登录
使用 session 守卫
对于服务端渲染的应用,在社交身份验证之后创建一个 session。回调期间的错误(拒绝访问、state 不匹配)会被自行处理,并将用户带着闪现消息重定向回去。
import User from '#models/user'
import router from '@adonisjs/core/services/router'
router.get('/github/callback', async ({ ally, auth, response }) => {
const githubUser = await ally.use('github').user()
/**
* 查找现有用户或创建一个新用户
*/
const user = await User.firstOrCreate(
{ email: githubUser.email },
{
email: githubUser.email,
fullName: githubUser.name,
/**
* 生成一个随机密码,因为社交用户
* 不会使用基于密码的登录
*/
password: crypto.randomUUID(),
}
)
/**
* 为用户创建一个 session
*/
await auth.use('web').login(user)
/**
* 重定向到用户试图访问的页面,
* 如果没有存储目标 URL,则重定向到 /dashboard
*/
return response.redirect().toIntended('/dashboard')
})
使用 access tokens 守卫
对于 API 和移动应用,在社交身份验证之后签发一个 access token。
import User from '#models/user'
import router from '@adonisjs/core/services/router'
router.get('/github/callback', async ({ ally }) => {
const github = ally.use('github')
if (github.accessDenied()) {
return { error: 'access_denied' }
}
if (github.stateMisMatch()) {
return { error: 'state_mismatch' }
}
if (github.hasError()) {
return { error: github.getError() }
}
const githubUser = await github.user()
const user = await User.firstOrCreate(
{ email: githubUser.email },
{
email: githubUser.email,
fullName: githubUser.name,
password: crypto.randomUUID(),
}
)
/**
* 为用户创建一个 access token
*/
const token = await User.accessTokens.create(user)
return {
type: 'bearer',
value: token.value!.release(),
}
})
禁止某个提供者的注册
当一个提供者只应用于登录或账户关联,而不用于创建新的本地账户时,使用 disallowLocalSignup 选项。当你曾在历史上允许使用某个提供者进行注册,但现在希望限制它只能用于登录时,这会很有帮助。
export default defineConfig({
github: services.github({
clientId: env.get('GITHUB_CLIENT_ID'),
clientSecret: env.get('GITHUB_CLIENT_SECRET'),
callbackUrl: 'http://localhost:3333/github/callback',
disallowLocalSignup: true,
}),
})
启用 disallowLocalSignup 后,调用 ally.use(provider, { intent: 'signup' }) 会抛出一个状态码为 403 的异常。
router.get('/github/signup/redirect', ({ ally }) => {
return ally.use('github', { intent: 'signup' }).redirect()
})
无状态身份验证
默认情况下,Ally 使用一个存储在 cookie 中的 CSRF 令牌来防止跨站请求伪造。如果你的应用无法使用 cookie(例如,使用 webview 的移动应用),请启用无状态模式:
router.get('/github/redirect', ({ ally }) => {
return ally.use('github').stateless().redirect()
})
router.get('/github/callback', async ({ ally }) => {
const github = ally.use('github').stateless()
// 处理回调...
const user = await github.user()
})
重定向和回调都必须使用无状态模式。在没有 CSRF 检查的情况下,请确保你的应用有其他保护机制,防止未经授权的 OAuth 流程。
从已有令牌获取用户
如果你已经拥有一个访问令牌(例如,来自移动应用的原生 OAuth 流程),可以直接获取用户信息:
router.post('/auth/github', async ({ request, ally }) => {
const { accessToken } = request.only(['accessToken'])
const user = await ally.use('github').userFromToken(accessToken)
return user
})
对于 OAuth 1 提供者(Twitter),使用 userFromTokenAndSecret:
const user = await ally.use('twitter').userFromTokenAndSecret(token, secret)
动态选择提供者
使用路由参数通过一个单一路由处理多个提供者:
router
.get('/:provider/redirect', ({ ally, params }) => {
if (!ally.has(params.provider)) {
return 'Unknown provider'
}
return ally.use(params.provider).redirect()
})
.where('provider', /github|google|twitter/)
router
.get('/:provider/callback', async ({ ally, params }) => {
if (!ally.has(params.provider)) {
return 'Unknown provider'
}
const driver = ally.use(params.provider)
// 处理回调...
})
.where('provider', /github|google|twitter/)
你还可以在运行时检查已配置的提供者:
router.get('/login', ({ ally }) => {
return {
providers: ally.configuredProviderNames(),
signupProviders: ally.signupProviderNames(),
}
})
提供者配置参考
每个提供者接受特定的配置选项。以下示例展示了每个内置提供者的所有可用选项。
GitHub
github: services.github({
clientId: '',
clientSecret: '',
callbackUrl: '',
disallowLocalSignup: false,
scopes: ['user', 'gist'],
login: 'adonisjs',
allowSignup: true,
})
google: services.google({
clientId: '',
clientSecret: '',
callbackUrl: '',
disallowLocalSignup: false,
scopes: ['userinfo.email', 'calendar.events'],
prompt: 'select_account',
accessType: 'offline',
hostedDomain: 'adonisjs.com',
display: 'page',
})
twitter: services.twitter({
clientId: '',
clientSecret: '',
callbackUrl: '',
disallowLocalSignup: false,
})
X
twitterX: services.twitterX({
clientId: '',
clientSecret: '',
callbackUrl: '',
disallowLocalSignup: false,
scopes: ['tweet.read', 'users.read', 'users.email'],
})
Discord
discord: services.discord({
clientId: '',
clientSecret: '',
callbackUrl: '',
disallowLocalSignup: false,
scopes: ['identify', 'email'],
prompt: 'consent',
guildId: '',
disableGuildSelect: false,
permissions: 10,
})
LinkedIn (OpenID Connect)
linkedinOpenidConnect: services.linkedinOpenidConnect({
clientId: '',
clientSecret: '',
callbackUrl: '',
disallowLocalSignup: false,
scopes: ['openid', 'profile', 'email'],
})
facebook: services.facebook({
clientId: '',
clientSecret: '',
callbackUrl: '',
disallowLocalSignup: false,
scopes: ['email', 'user_photos'],
userFields: ['first_name', 'picture', 'email'],
display: '',
authType: '',
})
Spotify
spotify: services.spotify({
clientId: '',
clientSecret: '',
callbackUrl: '',
disallowLocalSignup: false,
scopes: ['user-read-email', 'streaming'],
showDialog: false,
})
创建自定义驱动
如果你需要集成 Ally 未包含的提供者,可以创建一个自定义驱动。AdonisJS 提供了一个用于构建和发布自定义驱动的 starter kit 。有关实现的详细信息,请参阅该 starter kit 的 README。