API 令牌
API 防护使用数据库支持的 opaque access token 来验证用户请求。在创建应由第三方客户端访问的 API 时,或为任何其他不支持 cookie 的系统创建 API 时,你可能希望使用 API 防护。
令牌存储#
API 令牌保护允许你将令牌存储在 SQL 数据库中或将它们存储在 Redis 中。两种存储选项都有自己的用例。
SQL 存储#
SQL 存储方法适用于 API 令牌不是主要身份验证模式的情况。例如:你可能希望允许应用程序的用户创建个人访问令牌 (就像 GitHub 所做的那样) 并使用它来验证 API 请求。
在此场景中,你不会批量生成太多令牌,而且大多数令牌将永久存在。
令牌的配置在保护配置对象下的 config/auth.ts
文件中进行管理。
{
api: {
driver: 'oat',
provider: {
driver: 'lucid',
identifierKey: 'id',
uids: ['email'],
model: () => import('App/Models/User'),
},
// highlight-start
tokenProvider: {
type: 'api',
driver: 'database',
table: 'api_tokens',
foreignKey: 'user_id',
},
// highlight-end
}
}
类型#
type 属性保存你正在生成的令牌的类型。当你使用多个 API 令牌保护时,请确保给它一个唯一的名称。
唯一的名称确保为同一用户生成令牌的两个守卫没有重叠或任何冲突。
驱动程序#
驱动程序的名称。将令牌存储在 SQL 表中时,它始终是 database
。
表#
用于存储令牌的数据库表。在初始设置过程中,AdonisJS 将为令牌表创建迁移文件。但是,你也可以手动创建迁移并从 stub file
外键#
建立用户和令牌之间关系的外键。稍后,你还可以列出给定用户的所有令牌。
Redis 存储#
当 API 令牌是主要的身份验证模式时,redis 存储是合适的。例如:你使用基于令牌的身份验证对来自移动应用程序的请求进行身份验证。
在这种情况下,你还希望令牌在给定的时间段后过期,并且 redis 可以自动从其存储中清除过期的令牌。
令牌的配置在保护配置对象下的 config/auth.ts
文件中进行管理。
{
api: {
driver: 'oat',
provider: {
driver: 'lucid',
identifierKey: 'id',
uids: ['email'],
model: () => import('App/Models/User'),
},
// highlight-start
tokenProvider: {
type: 'api',
driver: 'redis',
redisConnection: 'local',
foreignKey: 'user_id',
},
// highlight-end
}
}
类型#
type 属性保存你正在生成的令牌的类型。当你使用多个 API 令牌保护时,请确保给它一个唯一的名称。
唯一的名称确保为同一用户生成令牌的两个守卫没有重叠或任何冲突。
driver#
driver 的名称。在 redis 数据库中存储令牌时,始终是 redis
。
redisConnection#
对 config/redis.ts
文件中定义的连接的引用。请务必阅读 redis 指南 进行初始设置。
foreignKey#
建立用户和令牌之间关系的外键。
生成令牌#
你可以使用 auth.generate
或 auth.attempt
方法为用户生成 API 令牌。auth.attempt
方法从数据库中查找用户并验证他们的密码。
- 如果用户凭据正确,它将在内部调用
auth.generate
方法并返回令牌。 - 否则会引发 InvalidCredentialsException。
import Route from '@ioc:Adonis/Core/Route'
Route.post('login', async ({ auth, request, response }) => {
const email = request.input('email')
const password = request.input('password')
try {
const token = await auth.use('api').attempt(email, password)
return token
} catch {
return response.unauthorized('Invalid credentials')
}
})
你可以手动处理异常并返回响应,或者让异常自行处理并使用 [内容协商](github.com/adonisjs/auth/blob/deve.... ts#L87-L105)。
auth.generate#
如果 auth.attempt
查找策略不适合你的用例,那么你可以手动查找用户,验证他们的密码并调用 auth.generate
方法为他们生成令牌.
注意:
auth.login
方法是auth.generate
方法的别名。
import User from 'App/Models/User'
import Route from '@ioc:Adonis/Core/Route'
import Hash from '@ioc:Adonis/Core/Hash'
Route.post('login', async ({ auth, request, response }) => {
const email = request.input('email')
const password = request.input('password')
// 手动查找用户
const user = await User
.query()
.where('email', email)
.where('tenant_id', getTenantIdFromSomewhere)
.whereNull('is_deleted')
.firstOrFail()
// 验证密码
if (!(await Hash.verify(user.password, password))) {
return response.unauthorized('Invalid credentials')
}
// 生成令牌
const token = await auth.use('api').generate(user)
})
管理令牌到期#
你还可以在生成令牌时定义令牌的到期时间。
await auth.use('api').attempt(email, password, {
expiresIn: '7days'
})
await auth.use('api').generate(user, {
expiresIn: '30mins'
})
redis 驱动会自动删除过期的 token。但是,对于 SQL 存储,你可以编写一个自定义脚本并删除 expires_at
时间戳小于今天的令牌。
令牌属性#
以下是使用 auth.generate
方法生成的令牌对象的属性列表。
type#
令牌始终设置为 'bearer'
。
user#
为其生成令牌的用户。用户的值依赖于 guard 使用的底层用户辅助器。
The user for which the token was generated. The value of the user relies on the underlying user provider used by the guard.
expiresAt#
luxon Datetime 的一个实例,表示令牌到期的静态时间。仅在已明确定义令牌到期时才存在。
expiresIn#
令牌将过期的时间 (以秒为单位)。它是一个静态值,不会随着时间的推移而改变。
meta#
随令牌附加的任何元数据。你可以在生成令牌时在选项对象中定义元数据。
注意:
底层存储驱动程序会将元数据持久化到数据库中。如果是 SQL,请确保还创建了所需的列。
await auth.use('api').attempt(email, password, {
ip_address: '192.168.1.0'
})
name#
与令牌关联的名称。当你允许应用程序的用户生成个人访问令牌 (就像 GitHub 所做的那样) 并给他们一个好记的名字时,这通常很有帮助。
name 属性仅在在生成令牌时定义它时才存在。
await auth.use('api').attempt(email, password, {
name: 'For the CLI app'
})
token#
生成的令牌的值。你必须与客户端共享此值,并且客户端必须安全地存储它。
你以后无法访问此值,因为存储在数据库中的值是令牌的哈希值,无法转换为真实值。
tokenHash#
存储在数据库中的值。确保永远不要与客户端共享该值。
在 auth.authenticate
请求期间,我们会将客户端提供的值与 tokenHash 进行比较。
toJSON#
将令牌转换为你可以发回以响应请求的对象。toJSON
方法包含以下属性。
{
type: 'bearer',
token: 'the-token-value',
expires_at: '2021-04-28T17:43:37.235+05:30'
expires_in: 604800
}
验证后续请求#
客户端收到 API 令牌后,必须在 Authorization
标头下的每个 HTTP 请求中将其发回。标头的格式必须如下:
Authorization = Bearer TOKEN_VALUE
你可以使用 auth.authenticate
方法验证令牌是否有效。如果令牌无效或数据库中不存在用户,则会引发 AuthenticationException。
否则,你可以使用 auth.user
属性访问登录用户。
import Route from '@ioc:Adonis/Core/Route'
Route.get('dashboard', async ({ auth }) => {
await auth.use('api').authenticate()
console.log(auth.use('api').user!)
})
在每条路由中手动调用此方法是不切实际的,因此你可以使用存储在 ./app/Middleware/Auth.ts
文件中的 auth 中间件。
[了解更多关于 auth 中间件的信息 →](https://learnku.com/docs/adonisjs/5.x/auth-middleware/13113)
撤销令牌#
在注销阶段,你可以通过从数据库中删除令牌来撤销令牌。令牌必须在 Authorization
标头下再次发送。
auth.revoke
方法将从数据库中删除当前请求期间发送的令牌。
import Route from '@ioc:Adonis/Core/Route'
Route.post('/logout', async ({ auth, response }) => {
await auth.use('api').revoke()
return {
revoked: true
}
})
其他方法 / 属性#
以下是 api
守卫可用的方法 / 属性列表。
isLoggedIn#
查找用户是否已登录。在调用 auth.generate
方法之后或通过 auth.authenticate
检查时,该值为 true
。
await auth.use('api').authenticate()
auth.use('api').isLoggedIn // true
await auth.use('api').attempt(email, password)
auth.use('api').isLoggedIn // true
isGuest#
查找用户是否为访客 (表示未登录)。该值始终与 isLoggedIn
标志相反。
isAuthenticated#
查找当前请求是否通过了身份验证检查。此标志与 isLoggedIn
标志不同,并且在 auth.login
调用期间未设置为 true。
await auth.use('api').authenticate()
auth.use('api').isAuthenticated // true
await auth.use('api').attempt(email, password)
auth.use('api').isAuthenticated // false
isLoggedOut#
查找令牌是否在当前请求期间被撤销。在调用 auth.revoke
方法后,该值将是 true
。
await auth.use('api').revoke()
auth.use('api').isLoggedOut
authenticationAttempted#
查找是否已尝试对当前请求进行身份验证。当你调用 auth.authenticate
方法时,该值设置为 true
auth.use('api').authenticationAttempted // false
await auth.use('api').authenticate()
auth.use('api').authenticationAttempted // true
provider#
对守卫使用的底层用户提供程序的引用。
tokenProvider#
对守卫使用的底层令牌提供程序的引用。
verifyCredentials#
一种验证用户凭据的方法。auth.attempt
方法在后台使用此方法。当凭据无效时会引发 InvalidCredentialsException。
try {
await auth.use('api').verifyCredentials(email, password)
} catch (error) {
console.log(error)
}
check#
该方法与 auth.authenticate
方法相同。但是,当请求未经过身份验证时,它不会引发任何异常。将其视为检查令牌是否对当前请求有效的可选尝试。
await auth.use('api').check()
if (auth.use('api').isLoggedIn) {
}
本译文仅用于学习和交流目的,转载请务必注明文章译者、出处、和本文链接
我们的翻译工作遵照 CC 协议,如果我们的工作有侵犯到您的权益,请及时联系我们。