社交身份认证
你可以使用 @adonisjs/ally 软件包在你的 AdonisJS 应用中实现社交身份认证。
Ally 自带以下内置驱动,并提供了一个可扩展的 API 来注册自定义驱动。
- Spotify
- GitHub
- Discord
Ally 不会替你存储任何用户或访问令牌。它实现了 OAuth2 和 OAuth1 协议,使用社交服务对用户进行认证,并提供用户详情。你可以将这些信息存储在数据库中,并使用 auth 软件包在你的应用中登录该用户。
安装
使用以下命令安装并配置该软件包:
node ace add @adonisjs/ally
# 将 providers 定义为 CLI 标志
node ace add @adonisjs/ally --providers=github --providers=google
-
使用检测到的包管理器安装
@adonisjs/ally软件包。 -
在
adonisrc.ts文件中注册以下服务提供者。{providers: [// ...其他 providers() => import('@adonisjs/ally/ally_provider')]} -
创建
config/ally.ts文件。该文件包含所选 OAuth 提供器的配置设置。 -
定义环境变量,用于存储所选 OAuth 提供器的
CLIENT_ID和CLIENT_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。
OAuth 提供器返回的电子邮件地址。如果 OAuth 请求没有要求用户的电子邮件地址,该值将为 null。
emailVerificationState
许多 OAuth 提供器允许使用未验证邮箱的用户登录并对 OAuth 请求进行认证。你应该使用此标志来确保只有邮箱已验证的用户才能登录。
以下是可能的值列表。
verified:用户的电子邮件地址已在 OAuth 提供器处通过验证。unverified:用户的电子邮件地址未经验证。unsupported:OAuth 提供器不共享邮箱验证状态。
name
OAuth 提供器返回的用户名称。
nickName
用户对外可见的昵称。如果 OAuth 提供器没有昵称的概念,nickName 和 name 的值将相同。
avatarUrl
指向用户公开头像图片的 HTTP(s) URL。
token
token 属性是对底层访问令牌对象的引用。该令牌对象具有以下子属性。
user.token.token
user.token.type
user.token.refreshToken
user.token.expiresAt
user.token.expiresIn
| 属性 | 协议 | 描述 |
|---|---|---|
token | OAuth2 / OAuth1 | 访问令牌的值。该值对 OAuth2 和 OAuth1 协议都可用。 |
secret | OAuth1 | 令牌密钥,仅适用于 OAuth1 协议。目前,Twitter 是唯一使用 OAuth1 的官方驱动。 |
type | OAuth2 | 令牌类型。通常是一个 bearer 令牌。 |
refreshToken | OAuth2 | 你可以使用刷新令牌来创建一个新的访问令牌。如果 OAuth 提供器不支持刷新令牌,该值将为 undefined |
expiresAt | OAuth2 | 一个 luxon DateTime 类的实例,表示访问令牌过期的绝对时间。 |
expiresIn | OAuth2 | 以秒为单位的值,在此之后令牌将过期。它是一个静态值,不随时间推移而改变。 |
original
对 OAuth 提供器原始响应的引用。如果规范化后的用户属性集合没有你所需的全部信息,你可能需要引用原始响应。
const user = await github.user()
console.log(user.original)
定义 scopes
scopes 指的是在用户批准认证请求后你想要访问的数据。scopes 的名称以及你可以访问的数据因 OAuth 提供器而异;因此,你必须阅读它们的文档。
scopes 可以在 config/ally.ts 文件中定义,也可以在重定向用户时定义。
得益于 TypeScript,你将获得所有可用 scopes 的自动补全建议。
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 提供器 的 prompt 和 access_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 以获取更多说明。