透過 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 應用程式中使用此模組 ✨

文件

!注意 您可以查看遊樂場,以了解模組的實際運作情況。

設定

在使用模組並定義您的第一個能力之前,您需要提供 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 hook 內設定,並接收事件。您可以使用它從 session 或請求中檢索使用者。如果使用者未通過驗證,它應該會傳回使用者物件或 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 個彈跳器函數: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 元件提供更靈活和集中的方式,來處理單一元件內的可以和不可以情境。您可以使用 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 授權