访问令牌守卫
在 API 上下文中,当服务器无法在最终用户设备上持久保存 cookie 时,可用访问令牌来验证 HTTP 请求,例如,第三方访问 API 或移动应用的身份验证。
可以生成为任何格式的访问令牌;例如,符合 JWT 标准的令牌称为 JWT 访问令牌,而专有格式的令牌称为不透明访问令牌。
AdonisJS 使用不透明访问令牌,结构化和存储方式如下。
- 令牌由一个加密安全的随机值表示,后缀为 CRC32 校验码。
- 令牌值的哈希存储在数据库中。此哈希用于在身份验证时验证令牌。
- 最终的令牌值经过 base64 编码,并以
oat_
为前缀。前缀可以自定义。 - 前缀和 CRC32 校验码后缀有助于秘密扫描工具识别令牌,并防止它们在代码库中泄漏。
配置用户模型
在使用访问令牌守卫之前,你必须为用户模型设置令牌提供者。令牌提供者用于创建、列出和验证访问令牌。
auth 包附带了一个数据库令牌提供者,该提供者将令牌持久化到 SQL 数据库中。你可以按如下方式配置它。
import { BaseModel } from '@adonisjs/lucid/orm'
import { DbAccessTokensProvider } from '@adonisjs/auth/access_tokens'
export default class User extends BaseModel {
// ...模型属性的其余部分
static accessTokens = DbAccessTokensProvider.forModel(User)
}
DbAccessTokensProvider.forModel
接受用户模型作为第一个参数,接受一个选项对象作为第二个参数。
export default class User extends BaseModel {
// ...模型属性的其余部分
static accessTokens = DbAccessTokensProvider.forModel(User, {
expiresIn: '30 days',
prefix: 'oat_',
table: 'auth_access_tokens',
type: 'auth_token',
tokenSecretLength: 40,
})
}
-
expiresIn
-
令牌过期的时间长度。你可以传递以秒为单位的数值或作为字符串的时间表达式。
默认情况下,令牌是长期有效的,不会过期。此外,你可以在生成令牌时指定其过期时间。
-
prefix
-
公开共享的令牌值的前缀。定义前缀有助于秘密扫描工具识别令牌,并防止其在代码库中泄漏。
在发出令牌后更改前缀将使它们无效。因此,请谨慎选择前缀,并且不要频繁更改。
默认为
oat_
。 -
table
-
用于存储访问令牌的数据库表名。默认为
auth_access_tokens
。 -
type
-
用于标识一组令牌的唯一类型。如果你在单个应用程序中发出多种类型的令牌,则必须为它们全部定义唯一的类型。
默认为
auth_token
。 -
tokenSecretLength
-
随机令牌值的长度(以字符为单位)。默认为
40
。
一旦配置了令牌提供者,你就可以代表用户开始发放令牌。发放令牌不需要设置身份验证守卫。守卫用于验证令牌。
创建访问令牌数据库表
在初始设置阶段,我们为 auth_access_tokens
表创建迁移文件。迁移文件存储在 database/migrations
目录中。
你可以通过执行 migration:run
命令来创建数据库表。
node ace migration:run
但是,如果你出于某种原因手动配置 auth 包,则可以手动创建迁移文件,并将以下代码片段复制粘贴到其中。
node ace make:migration auth_access_tokens
import { BaseSchema } from '@adonisjs/lucid/schema'
export default class extends BaseSchema {
protected tableName = 'auth_access_tokens'
async up() {
this.schema.createTable(this.tableName, (table) => {
table.increments('id')
table
.integer('tokenable_id')
.notNullable()
.unsigned()
.references('id')
.inTable('users')
.onDelete('CASCADE')
table.string('type').notNullable()
table.string('name').nullable()
table.string('hash').notNullable()
table.text('abilities').notNullable()
table.timestamp('created_at')
table.timestamp('updated_at')
table.timestamp('last_used_at').nullable()
table.timestamp('expires_at').nullable()
})
}
async down() {
this.schema.dropTable(this.tableName)
}
}
发放令牌
根据你的应用程序,你可能会在登录时或登录后从应用程序仪表板发放令牌。在这两种情况下,发放令牌都需要一个用户对象(为其生成令牌),并且你可以直接使用 User
模型生成它们。
在以下示例中,我们使用 User.accessTokens.create
方法通过ID查找用户并为其发出访问令牌。当然,在实际应用程序中,此端点将受身份验证保护,但我们现在保持简单。
.create
方法接受一个 User
模型实例,并返回一个 AccessToken 类实例。
token.value
属性包含值(包装为必须与用户共享的 Secret)。该值仅在生成令牌时可用,用户之后将无法再次查看它。
import router from '@adonisjs/core/services/router'
import User from '#models/user'
router.post('users/:id/tokens', ({ params }) => {
const user = await User.findOrFail(params.id)
const token = await User.accessTokens.create(user)
return {
type: 'bearer',
value: token.value!.release(),
}
})
你还可以直接在响应中返回 token
,它将被序列化为以下 JSON 对象。
router.post('users/:id/tokens', ({ params }) => {
const user = await User.findOrFail(params.id)
const token = await User.accessTokens.create(user)
return {
type: 'bearer',
value: token.value!.release(),
}
return token
})
/**
* response: {
* type: 'bearer',
* value: 'oat_MTA.aWFQUmo2WkQzd3M5cW0zeG5JeHdiaV9rOFQzUWM1aTZSR2xJaDZXYzM5MDE4MzA3NTU',
* expiresAt: null,
* }
*/
定义权限
根据你正在构建的应用程序,你可能希望限制访问令牌仅执行特定任务。例如,发出一个令牌,允许读取和列出项目,但不允许创建或删除它们。
在以下示例中,我们将权限数组作为第二个参数定义。权限被序列化为 JSON 字符串并持久化到数据库中。
对于 auth 包,权限没有实际意义。在执行给定操作之前,检查令牌权限是由你的应用程序负责的。
await User.accessTokens.create(user, ['server:create', 'server:read'])
令牌能力与 Bouncer 能力
不应将令牌能力(token abilities)与 bouncer 授权检查 混淆。让我们通过一个实际例子来理解两者的区别。
-
假设你定义了一个 允许管理员用户创建新项目的 bouncer 能力。
-
同一个管理员用户为自己创建了一个令牌,但为了防止令牌滥用,他们将令牌能力限制为 读取项目。
-
现在,在你的应用程序中,你需要实现访问控制,允许管理员用户创建新项目,同时禁止令牌创建新项目。
你可以为此用例编写如下的 bouncer 能力。
user.currentAccessToken
指的是当前 HTTP 请求中用于身份验证的访问令牌。你可以在 请求身份验证 部分了解更多信息。
import { AccessToken } from '@adonisjs/auth/access_tokens'
import { Bouncer } from '@adonisjs/bouncer'
export const createProject = Bouncer.ability(
(user: User & { currentAccessToken?: AccessToken }) => {
/**
* 如果没有 "currentAccessToken" 属性,则意味着
* 用户在没有访问令牌的情况下进行了身份验证
*/
if (!user.currentAccessToken) {
return user.isAdmin
}
/**
* 否则,检查用户是否为管理员以及他们用于
* 身份验证的令牌是否允许 "project:create" 能力。
*/
return user.isAdmin && user.currentAccessToken.allows('project:create')
}
)
令牌过期
默认情况下,令牌是长期有效的,且永不过期。但是,你可以在 配置令牌提供者 时或生成令牌时定义过期时间。
过期时间可以定义为表示秒数的数值或基于字符串的时间表达式。
await User.accessTokens.create(
user, // 为用户
['*'], // 具有所有能力
{
expiresIn: '30 days' // 30 天后过期
}
)
命名令牌
默认情况下,令牌没有名称。但是,你可以在生成令牌时为其分配一个名称。例如,如果你的应用程序允许用户自行生成令牌,你可以要求他们指定一个可识别的名称。
await User.accessTokens.create(
user,
['*'],
{
name: request.input('token_name'),
expiresIn: '30 days'
}
)
配置守卫
现在我们可以发放令牌了,接下来让我们配置一个身份验证守卫来验证请求并认证用户。守卫必须在 config/auth.ts
文件中的 guards
对象下进行配置。
import { defineConfig } from '@adonisjs/auth'
import { tokensGuard, tokensUserProvider } from '@adonisjs/auth/access_tokens'
const authConfig = defineConfig({
default: 'api',
guards: {
api: tokensGuard({
provider: tokensUserProvider({
tokens: 'accessTokens',
model: () => import('#models/user'),
})
}),
},
})
export default authConfig
tokensGuard
方法创建 AccessTokensGuard 类的实例。它接受一个用户提供者,用于验证令牌和查找用户。
tokensUserProvider
方法接受以下选项,并返回 AccessTokensLucidUserProvider 类的实例。
model
:用于查找用户的 Lucid 模型。tokens
:模型中引用令牌提供者的静态属性名称。
请求身份验证
配置好守卫后,你可以开始使用 auth
中间件或手动调用 auth.authenticate
方法来验证请求。
auth.authenticate
方法返回已认证用户的 User 模型实例,或者在无法认证请求时抛出 E_UNAUTHORIZED_ACCESS 异常。
import router from '@adonisjs/core/services/router'
router.post('projects', async ({ auth }) => {
// 使用默认守卫进行身份验证
const user = await auth.authenticate()
// 使用命名守卫进行身份验证
const user = await auth.authenticateUsing(['api'])
})
使用 auth 中间件
你可以使用 auth
中间件来验证请求或抛出异常,而无需手动调用 authenticate
方法。
auth 中间件接受一个守卫数组,用于验证请求。其中任一守卫完成请求验证后,身份验证过程就会停止。
import router from '@adonisjs/core/services/router'
import { middleware } from '#start/kernel'
router
.post('projects', async ({ auth }) => {
console.log(auth.user) // User
console.log(auth.authenticatedViaGuard) // 'api'
console.log(auth.user!.currentAccessToken) // AccessToken
})
.use(middleware.auth({
guards: ['api']
}))
检查请求是否已认证
你可以使用 auth.isAuthenticated
标志来检查请求是否已认证。对于已认证的请求,auth.user
的值总是已定义。
import { HttpContext } from '@adonisjs/core/http'
class PostsController {
async store({ auth }: HttpContext) {
if (auth.isAuthenticated) {
await auth.user!.related('posts').create(postData)
}
}
}
获取已认证用户或失败
如果你不喜欢在 auth.user
属性上使用 非空断言操作符,你可以使用 auth.getUserOrFail
方法。此方法将返回用户对象或抛出 E_UNAUTHORIZED_ACCESS 异常。
import { HttpContext } from '@adonisjs/core/http'
class PostsController {
async store({ auth }: HttpContext) {
const user = auth.getUserOrFail()
await user.related('posts').create(postData)
}
}
当前访问令牌
访问令牌守卫在成功验证请求后,会在用户对象上定义 currentAccessToken
属性。currentAccessToken
属性是 AccessToken 类的实例。
你可以使用 currentAccessToken
对象来获取令牌的能力或检查令牌的过期时间。此外,在身份验证期间,守卫会更新 last_used_at
列以反映当前时间戳。
如果你在代码库的其他部分将 User 模型与 currentAccessToken
作为类型引用,你可能希望在模型本身上声明此属性。
import { AccessToken } from '@adonisjs/auth/access_tokens'
Bouncer.ability((
user: User & { currentAccessToken?: AccessToken }
) => {
})
import { AccessToken } from '@adonisjs/auth/access_tokens'
export default class User extends BaseModel {
currentAccessToken?: AccessToken
}
Bouncer.ability((user: User) => {
})
列出所有令牌
你可以使用令牌提供者通过 accessTokens.all
方法获取所有令牌的列表。返回值将是 AccessToken
类实例的数组。
router
.get('/tokens', async ({ auth }) => {
return User.accessTokens.all(auth.user!)
})
.use(
middleware.auth({
guards: ['api'],
})
)
all
方法还会返回已过期的令牌。在呈现列表之前,你可能希望对其进行过滤,或者在令牌旁边显示 “Token expired”(令牌已过期)消息。例如:
@each(token in tokens)
<h2> {{ token.name }} </h2>
@if(token.isExpired())
<p> Expired </p>
@end
<p> Abilities: {{ token.abilities.join(',') }} </p>
@end
删除令牌
你可以使用 accessTokens.delete
方法删除令牌。该方法接受用户作为第一个参数,令牌 ID 作为第二个参数。
await User.accessTokens.delete(user, token.identifier)
事件
请查阅事件参考指南,以查看访问令牌守卫发出的可用事件列表。