访问令牌守卫
访问令牌在服务器无法在终端用户设备上持久化 cookies 的 API 上下文中认证 HTTP 请求,例如,第三方对 API 的访问或移动应用程序的认证。
访问令牌可以以任何格式生成;例如,符合 JWT 标准的令牌称为 JWT 访问令牌,而专有格式的令牌称为不透明访问令牌。
AdonisJS 使用不透明访问令牌,其结构和存储方式如下。
- 令牌由加密安全的随机值表示,后缀为 CRC32 校验和。
- 令牌值的哈希持久化在数据库中。此哈希用于在认证时验证令牌。
- 最终的令牌值是 base64 编码的,前缀为
oat_。前缀可以自定义。 - 前缀和 CRC32 校验和后缀帮助密钥扫描工具识别令牌并防止它们在代码库中泄露。
配置用户模型
在使用访问令牌守卫之前,你必须在 User 模型上设置令牌提供者。令牌提供者用于创建、列出和验证访问令牌。
auth 包附带一个数据库令牌提供者,它将令牌持久化在 SQL 数据库中。你可以按如下方式配置它。
import { BaseModel } from '@adonisjs/lucid/orm'
// highlight-start
import { DbAccessTokensProvider } from '@adonisjs/auth/access_tokens'
// highlight-end
export default class User extends BaseModel {
// ...模型的其余属性
// highlight-start
static accessTokens = DbAccessTokensProvider.forModel(User)
// highlight-end
}DbAccessTokensProvider.forModel 接受 User 模型作为第一个参数,选项对象作为第二个参数。
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_tokensimport { 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 模型生成它们。
如果你使用访问令牌作为用户登录/注销的主要方式,你可能更喜欢直接通过 auth 守卫创建/使令牌失效,请参阅登录和注销。
在以下示例中,我们通过 id 查找用户并使用 User.accessTokens.create 方法为他们发出访问令牌。当然,在实际应用程序中,你会让这个端点受到认证保护,但现在让我们保持简单。
.create 方法接受 User 模型的实例并返回 AccessToken 类的实例。
token.value 属性包含必须与用户共享的值(包装为 Secret)。该值仅在生成令牌时可用,用户将无法再次看到它。
import router from '@adonisjs/core/services/router'
import User from '#models/user'
router.post('users/:id/tokens', async ({ 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', async ({ params }) => {
const user = await User.findOrFail(params.id)
const token = await User.accessTokens.create(user)
// delete-start
return {
type: 'bearer',
value: token.value!.release(),
}
// delete-end
// insert-start
return token
// insert-end
})
/**
* response: {
* type: 'bearer',
* value: 'oat_MTA.aWFQUmo2WkQzd3M5cW0zeG5JeHdiaV9rOFQzUWM1aTZSR2xJaDZXYzM5MDE4MzA3NTU',
* expiresAt: null,
* }
*/定义能力
根据你正在构建的应用程序,你可能希望将访问令牌限制为仅执行特定任务。例如,发出一个允许读取和列出项目而不能创建或删除它们的令牌。
在以下示例中,我们将能力数组定义为第二个参数。能力被序列化为 JSON 字符串并持久化在数据库中。
对于 auth 包,能力没有真正的意义。你的应用程序需要在执行给定操作之前检查令牌能力。
await User.accessTokens.create(user, ['server:create', 'server:read'])令牌能力 vs. Bouncer 能力
你不应该将令牌能力与 bouncer 授权检查混淆。让我们通过一个实际的例子来理解区别。
假设你定义了一个允许管理员用户创建新项目的 bouncer 能力。
同一个管理员用户为自己创建了一个令牌,但为了防止令牌滥用,他们将令牌能力限制为读取项目。
现在,在你的应用程序中,你需要实现访问控制,允许管理员用户创建新项目,同时不允许令牌创建新项目。
你可以按如下方式为此用例编写 bouncer 能力。
TIP
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 对象下配置。
// title: config/auth.ts
import { defineConfig } from '@adonisjs/auth'
// highlight-start
import { tokensGuard, tokensUserProvider } from '@adonisjs/auth/access_tokens'
// highlight-end
const authConfig = defineConfig({
default: 'api',
guards: {
// highlight-start
api: tokensGuard({
provider: tokensUserProvider({
tokens: 'accessTokens',
model: () => import('#models/user'),
})
}),
// highlight-end
},
})
export default authConfigtokensGuard 方法创建 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 作为类型引用,你可能需要在模型本身上声明此属性。
:::caption
不要合并 currentAccessToken
:::
import { AccessToken } from '@adonisjs/auth/access_tokens'
Bouncer.ability((
user: User & { currentAccessToken?: AccessToken }
) => {
}):::caption
将其声明为模型上的属性
:::
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 方法也返回过期的令牌。你可能需要在渲染列表之前过滤它们或在令牌旁边显示 "令牌已过期" 消息。例如
@each(token in tokens)
<h2> {{ token.name }} </h2>
@if(token.isExpired())
<p> 已过期 </p>
@end
<p> 能力: {{ token.abilities.join(',') }} </p>
@end删除令牌
你可以使用 accessTokens.delete 方法删除令牌。该方法接受用户作为第一个参数,令牌 id 作为第二个参数。
await User.accessTokens.delete(user, token.identifier)事件
请查看事件参考指南以查看访问令牌守卫发出的可用事件列表。
登录和注销
访问令牌有时是用户登录和注销的首选方法——例如,当从移动应用程序进行认证时。
为了适应这些情况,访问令牌守卫提供了类似于 session 守卫的 login 和 logout 方法的 API。
登录:
const token = await auth.use('api').createToken(user)注销(当前认证的令牌):
await auth.use('api').invalidateToken()Session 控制器示例
假设访问令牌守卫 api 已经就位(例如,你已经设置了:用户模型、访问令牌和认证守卫),可以按以下方式实现 session 控制器:
// title: app/controllers/session_controller.ts
import User from '#models/user'
import { HttpContext } from '@adonisjs/core/http'
export default class SessionController {
async store({ request, auth, response }: HttpContext) {
const { email, password } = request.only(['email', 'password'])
const user = await User.verifyCredentials(email, password)
return await auth.use('api').createToken(user)
}
async destroy({ request, auth, response }: HttpContext) {
await auth.use('api').invalidateToken()
}
}// title: start/routes.ts
import router from '@adonisjs/core/services/router'
const SessionController = () => import('#controllers/session_controller')
router.post('session', [SessionController, 'store'])
router.delete('session', [SessionController, 'destroy'])
.use(middleware.auth({ guards: ['api'] }))WARNING
当 User.verifyCredentials 失败(并抛出 E_INVALID_CREDENTIALS)时,使用内容协商来获取适当的响应。
在上面示例的情况下,客户端应该在对 /session 的 post 请求中包含 Accept=application/json 头。这确保失败将导致 json 格式的响应而不是重定向。
TIP
如果你使用访问令牌从外部来源(如移动应用程序)登录,你可能需要禁用 CSRF 保护。 你可以全局禁用 CSRF 保护(如果你的应用程序仅用作 API),或为 API 路由(包括 /session 路由)添加例外。请参阅 shield 配置参考