Nuxt 授權
在 Nuxt 和 Nitro 中輕鬆處理授權。
此模組不實作 ACL 或 RBAC。它提供底層的基礎元件,您可以利用它們來實作自己的授權邏輯。
!NOTE 未來,此模組可能會以 Nitro 模組和 Nuxt 模組的形式提供,但 Nitro 模組尚未準備就緒。
若要深入了解此模組及其解決的問題,請查看我的部落格文章:Nuxt 中的授權。
功能
- ⛰ 同時適用於客戶端 (Nuxt) 和伺服器端 (Nitro)
- 🌟 編寫一次權限能力,即可在任何地方使用
- 👨👩👧👦 與身份驗證層無關
- 🫸 使用組件有條件地顯示 UI 的一部分
- 💧 基礎元件可供存取以進行完全客製化
快速設定
透過一個指令將模組安裝到您的 Nuxt 應用程式
npx nuxi module add nuxt-authorization
完成!您現在可以在您的 Nuxt 應用程式中使用此模組 ✨
文件
!NOTE 您可以查看 playground 以查看模組的實際運作情況。
設定
在使用模組並定義您的第一個權限能力之前,您需要提供 2 個解析器 (resolvers)。這些函數在內部用於檢索使用者,但您必須實作它們。這使模組能夠與身份驗證層無關。
對於 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
。它可以是異步的 (async)。
對於 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
},
}
})
})
!NOTE 閱讀更多關於
event.context
的資訊
此解析器在 request
hook 中設定,並接收事件。您可以使用它從 session 或 request 中檢索使用者。如果使用者未通過身份驗證,它應返回使用者物件或 null
。它可以是異步的 (async)。
通常,您使用插件在應用程式啟動時獲取使用者,然後儲存它。解析器函數應僅返回儲存的使用者,而不是再次獲取它 (否則,您可能會遇到嚴重的效能問題)。
使用 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
},
}
})
})
簡單!
定義權限能力
!NOTE 透過 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 資料夾僅導出目錄的第一層。 因此,您必須在 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 情況。您可以利用 Bouncer 組件及其 具名插槽,而不是使用單獨的 Can
和 Cannot
組件,在一個統一的區塊中處理兩種狀態。
<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 的啟發。它是一個編寫精良的套件,我認為每次都重新發明輪子是不必要的。