通过 100+ 技巧合集学习 Nuxt!

nuxt-authorization

管理应用和服务器内部的权限。

Nuxt 授权

npm versionnpm downloadsLicenseNuxt

在 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 函数:allowsdeniesauthorize。它们都可以在客户端和服务器中使用。实现方式不同,但 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)
})

allowdeny 类似于返回 truefalse,但 deny 允许为错误返回自定义消息和状态代码。

大多数情况下,您的 API 端点将使用 authorize。如果不需要参数,它可以是端点的第一行,或者在数据库查询之后检查用户是否可以访问该资源。您不需要捕获错误,因为它是一个 H3Error,并且将被 Nitro 服务器捕获。

allowsdenies 函数在客户端中用于执行条件渲染或逻辑非常有用。您也可以使用它们来对您的授权逻辑进行细粒度控制。

使用组件

该模块提供了 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 场景。 您可以使用 Bouncer 组件及其 命名插槽 来处理统一块中的两种状态,而不是使用单独的 CanCannot 组件。

<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 的启发。 它是一个编写良好的软件包,我认为每次都重新发明轮子是不必要的。

许可证

MIT 许可证