nuxt-authorization

管理應用程式和伺服器內部的許可權。

Nuxt 授權

npm versionnpm downloadsLicenseNuxtpkg.pr.new

在 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 元件提供了一種更靈活、更集中的方式,可以在單個元件中處理“可以”和“不可以”的場景。您無需使用單獨的 CanCannot 元件,而是可以利用 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 的啟發。它是一個編寫精良的軟體包,我認為每次都重新發明輪子是沒有必要的。

許可證

麻省理工學院許可證