Nuxt 授权
在 Nuxt 和 Nitro 中轻松处理授权。
此模块不实现 ACL 或 RBAC。它提供了你可以用来实现自己的授权逻辑的底层原语。
!注意 将来,此模块可以作为 Nitro 模块和 Nuxt 模块提供,但 Nitro 模块尚未准备就绪。
要了解有关此模块及其解决的问题的更多信息,请查看我的博客文章,关于 Nuxt 中的授权。
特性
- ⛰ 在客户端 (Nuxt) 和服务器端 (Nitro) 上均可工作
- 🌟 编写一次能力,在任何地方使用
- 👨👩👧👦 与身份验证层无关
- 🫸 使用组件有条件地显示 UI 的一部分
- 💧 可以访问原语以进行完全自定义
快速设置
使用一个命令将模块安装到您的 Nuxt 应用程序
npx nuxi module add nuxt-authorization
就这样!你现在可以在你的 Nuxt 应用程序中使用该模块了✨
文档
!注意 您可以查看 playground 来查看模块的实际效果。
设置
在使用模块和定义你的第一个能力之前,你需要提供 2 个解析器。这些函数在内部用于检索用户,但你必须实现它们。这使得该模块与身份验证层无关。
对于 Nuxt 应用程序,在 plugins/authorization-resolver.ts
中创建一个新插件
export default defineNuxtPlugin({
name: 'authorization-resolver',
parallel: true,
setup() {
return {
provide: {
authorization: {
resolveClientUser: () => {
// Your logic to retrieve the user from the client
},
},
},
}
},
})
每次在客户端检查授权时都会调用此函数。它应返回用户对象,如果用户未通过身份验证,则返回 null
。它可以是异步的。
对于 Nitro 服务器,在 server/plugins/authorization-resolver.ts
中创建一个新插件
export default defineNitroPlugin((nitroApp) => {
nitroApp.hooks.hook('request', async (event) => {
event.context.$authorization = {
resolveServerUser: () => {
// Your logic to retrieve the user from the server
},
}
})
})
!注意 了解更多关于
event.context
此解析器在钩子 request
中设置,并接收事件。你可以使用它从会话或请求中检索用户。它应返回用户对象,如果用户未通过身份验证,则返回 null
。它可以是异步的。
通常,你使用插件在应用程序启动时获取用户,然后存储它。解析器函数应仅返回存储的用户,而不再次获取它(否则,可能会出现严重的性能问题)。
使用 nuxt-auth-utils
的示例
模块 nuxt-auth-utils
为 Nuxt 提供了一个身份验证层。 如果你使用此模块,则可以使用以下解析器
Nuxt 插件
export default defineNuxtPlugin({
name: 'authorization-resolver',
parallel: true,
setup() {
return {
provide: {
authorization: {
resolveClientUser: () => useUserSession().user.value,
},
},
}
},
})
Nitro 插件
export default defineNitroPlugin((nitroApp) => {
nitroApp.hooks.hook('request', async (event) => {
event.context.$authorization = {
resolveServerUser: async () => {
const session = await getUserSession(event)
return session.user ?? null
},
}
})
})
简单!
定义能力
!注意 使用 Nuxt 4,将引入一个新的
shared
目录,以便轻松地在客户端和服务器之间共享代码。查看 Alexander Lichter 的视频。
现在解析器已设置好,你可以定义你的第一个能力。能力是一个至少接受用户并返回布尔值的函数,以指示用户是否可以执行该操作。它还可以接受其他参数。
我建议创建一个新文件 shared/utils/abilities.ts
来创建你的能力
export const listPosts = defineAbility(() => true) // Only authenticated users can list posts
export const editPost = defineAbility((user: User, post: Post) => {
return user.id === post.authorId
})
如果你有很多能力,你可能更喜欢创建一个目录 shared/utils/abilities/
并为每个能力创建一个文件。将能力放在 shared/utils
目录中允许自动导入在客户端中工作,同时在服务器中进行简单导入 ~~/shared/utils/abilities
。 请记住,共享文件夹仅导出目录的第一级。 所以你必须在 shared/utils/abilities/index.ts
文件中导出能力。
默认情况下,不允许访客执行任何操作,并且不会调用该能力。此行为可以按能力进行更改
export const listPosts = defineAbility({ allowGuest: true }, (user: User | null) => true)
现在,未经身份验证的用户可以列出帖子。
使用能力
要使用能力,你可以访问 3 个 bouncer 函数:allows
、denies
和 authorize
。它们都可以在客户端和服务器端使用。 *实现是不同的,但 API(几乎)相同,并且对开发人员来说完全是透明的。在服务器端,第一个参数是来自处理程序的 event
。*
allows
函数返回一个布尔值,表示用户是否可以执行该操作
if (await allows(listPosts)) {
// User can list posts
}
对于服务器
if (await allows(event, listPosts)) {
// User can list posts
}
denies
函数返回一个布尔值,表示用户是否无法执行该操作
if (await denies(editPost, post)) {
// User cannot edit the post
}
对于服务器
if (await denies(event, editPost, post)) {
// User cannot edit the post
}
authorize
函数在用户无法执行操作时引发错误
await authorize(editPost, post)
// User can edit the post
对于服务器
await authorize(event, editPost, post)
你可以自定义每个能力返回值的错误消息和状态代码。这对于返回 404 而不是 403 以便让用户不知道资源的存在非常有用。
export const editPost = defineAbility((user: User, post: Post) => {
if(user.id === post.authorId) {
return true // or allow()
}
return deny('This post does not exist', 404)
})
allow
和 deny
类似于返回 true
和 false
,但是 deny
允许为错误返回自定义消息和状态代码。
大多数情况下,你的 API 端点将使用 authorize
。如果不需要参数,或者在数据库查询后检查用户是否有权访问资源,这可以是端点的第一行。你无需捕获错误,因为它是一个 H3Error
,并且将被 Nitro 服务器捕获。
allows
和 denies
函数在客户端中对于执行条件渲染或逻辑非常有用。你也可以使用它们来精细控制你的授权逻辑。
使用组件
该模块提供了 2 个组件来帮助你条件性地显示 UI 的一部分。想象一下,你有一个用于编辑帖子的按钮,未经授权的用户不应看到该按钮。
<template>
<Can
:ability="editPost"
:args="[post]" // Optional if the ability does not take any arguments
>
<button>Edit</button>
</Can>
</template>
只有当用户可以编辑帖子时,Can
组件才会呈现该按钮。如果用户无法编辑帖子,则不会呈现该按钮。
作为对比,你可以使用 Cannot
组件仅在用户无法编辑帖子时呈现该按钮。
<template>
<Cannot
:ability="editPost"
:args="[post]" // Optional if the ability does not take any arguments
>
<p>You're not allowed to edit the post.</p>
</Cannot>
</template>
Bouncer
组件提供了一种更灵活和集中的方式来处理单个组件中的 can 和 cannot 两种情况。你无需使用单独的 Can
和 Cannot
组件,而是可以利用 Bouncer 组件及其命名插槽在统一块中处理两种状态。
<Bouncer
:ability="editPost"
:args="[post]" // Optional if the ability does not take any arguments
>
<template #can>
<button>Edit</button>
</template>
<template #cannot>
<p>You're not allowed to edit the post.</p>
</template>
</Bouncer>
所有这些组件都接受一个名为 as
的 prop 来定义要渲染的 HTML 标签。默认情况下,它是一个无渲染组件。
<Can
:ability="editPost"
:args="[post]"
as="div"
>
<button>Edit</button>
</Can>
这将渲染
<div>
<button>Edit</button>
</div>
而不是
<button>Edit</button>
多种能力
如果你拥有多种能力,则可以向组件提供一个能力数组。仅当所有能力匹配组件的指定要求时,该组件才会呈现。
<Can :ability="[editPost, deletePost]" :args="[[post], [post]]" />
<Cannot :ability="[editPost, deletePost]" :args="[[post], [post]]" />
<Bouncer :ability="[editPost, deletePost]" :args="[[post], [post]]">
<template #can>
<button>Edit</button>
<button>Delete</button>
</template>
<template #cannot>
<p>You're not allowed to edit or delete the post.</p>
</template>
</Bouncer>
贡献
本地开发
# Install dependencies
npm install
# Generate type stubs
npm run dev:prepare
# Develop with the playground
npm run dev
# Build the playground
npm run dev:build
# Run ESLint
npm run lint
# Run Vitest
npm run test
npm run test:watch
# Release new version
npm run release
鸣谢
该模块的代码和设计都深受Adonis Bouncer的启发。这是一个编写良好的软件包,我认为每次都重新发明轮子是不必要的。