Next-js 應用實現權限管理
前言
在前面的文章中《使用 NextAuth.js 給 Next.js 應用添加鑑權與認證》[1],我們使用了 Github OAuth 和郵箱認證登錄,我們的視頻網站就有了用戶系統,和用戶系統離不開的,便是權限系統,今天我們就聊一聊權限系統的設計與實現,要在網站中實現複雜的權限管理對應新手來說,這可能會是比較困難的,但權限系統是軟件中不可或缺的部分,我們只要掌握一個套路,就會變得非常簡單,一起來看看吧!
權限區分
因爲有了權限,我們可以在一個系統中實現各種各樣的功能,系統也會變得龐大而複雜。一般可以將權限分爲 “功能權限”、“數據權限” 和“字段權限”。
功能權限:用戶具有哪些權利,例如特定數據的增、刪、改、查等;比如在一個視頻網站中,超級管理員擁有對所有視頻的審覈權限,而普通用戶只能擁有對着自己視頻的編輯和刪除權限。功能權限需要前後端共同實現;
數據權限:用戶可以看到哪些範圍的主數據。比如視頻網站中,VIP 用戶可以看到 VIP 視頻,而非 VIP 用戶只能看普通視頻。數據權限主要是後端實現;
字段權限:在特定的數據表中,可以看到哪些字段;比如普通用戶能夠看到其他用戶的基本信息,但是看不到其它人的賬戶信息。字段權限也主要是由後端實現;
權限系統設計
我們可以將網站中的功能按角色劃分,根據不同的角色來指定不同的權限,這便是大部分網站的實現方式。 比如在視頻網站中我們可以將角色劃分爲:
前臺角色
-
普通用戶
-
VIP 用戶
我們可以在數據庫中加入 2 個字段區分,User 表加入 isVip
, Video 表加入 vip
,prisma Schema 定義如下:
model User {
id String @id @default(cuid())
name String?
email String? @unique
isVip Boolean
emailVerified DateTime?
Video Video[]
}
model Video {
id Int @id @default(autoincrement())
title String @unique
vip Boolean
desc String?
}
那麼我們通過接口就可以查出 Vip 視頻。
import { authOptions } from "@/pages/api/auth/[...nextauth]";
import { unstable_getServerSession } from "next-auth/next";
export default async function handler( req: NextApiRequest, res: NextApiResponse ) {
const session = await unstable_getServerSession(req, res, authOptions);
if (!session) {
res.status(401).json({ message: "You must be logged in." });
return;
}
const user = await prisma.user.findFirst({
where: {
id: session.id as string,
},
});
if (!user.isVip) {
res.status(401).json({ message: "You are not vip user" });
return;
}
const videos = await prisma.video.findMany({
where: {
vip: true,
},
});
}
上面代碼中,先通過 session 獲得用戶 id,再查詢用戶是否爲 vip,若爲 vip 則查詢出 vip 視頻,不是則返回 401。
後臺角色
-
視頻管理員:用於視頻審覈,對不合法的視頻進行下架。
-
超級管理員:擁有所有權限,用戶管理、視頻管理等
備註:由於系統比較簡單,我們將前臺後臺的用戶系統使用同一個,只要在數據庫中設置一個字段
isAdmin
,然後在頁面上根據這個字段顯示後臺管理入口,就可以實現管理視頻啦。
一般情況下,我們需要給網站添加 2 張表:一張是角色表(Rule)、一張是權限表(Permission),角色和權限的關係是多對多的關係,一個角色可以有多個權限,一個權限也可以賦給多個角色, 因此需要加入第三章表關聯表,在 Prisma[2] 中,關聯表一般使用 TablesOnTables
的形式設計,使用 @relation
關聯表中的附鍵,下面代碼就是 prisma Schema 代碼
model User {
id String @id @default(cuid())
name String?
email String? @unique
roleId String?
role Role? @relation(fields: [roleId], references: [id])
}
model Role {
id String @id @default(cuid())
name String
PermissionsOnRoles PermissionsOnRoles[]
User User[]
}
model Permission {
id String @id @default(cuid())
pid String?
name String
code String
PermissionsOnRoles PermissionsOnRoles[]
}
model PermissionsOnRoles {
role Role @relation(fields: [roleId], references: [id])
roleId String
permission Permission @relation(fields: [permissionId], references: [id])
permissionId String
assignedAt DateTime @default(now())
@@id([roleId, permissionId])
}
在 prisma/schema.prisma
文件中修改 schema, 修改完 prisma schema 後,執行以下命令,我們便可以往數據庫遷移,生成真實的表。
npx prisma migrate dev
執行後,會在 prisma/migrations/*/migration.sql
文件下生成 Sql 語句,效果如下。
系統中,一般權限數據都數據庫內置的,因此需要新建一個seed.ts
文件,可以方便我們往默認數據庫插入數據
import { PrismaClient } from "@prisma/client";
// initialize Prisma Client
const prisma = new PrismaClient();
async function main() {
await prisma.permission.createMany({
data: [
{
name: "用戶管理",
code: "user_management",
},
{
name: "視頻管理",
code: "video_management",
},
],
});
await prisma.role.create({
data: {
name: "超級管理員",
permissions: {
create: [
{
assignedAt: new Date(),
permission: {
connect: {
code: "video_management",
},
},
},
{
assignedAt: new Date(),
permission: {
connect: {
code: "user_management",
},
},
},
],
},
},
});
await prisma.role.create({
data: {
name: "視頻管理員",
permissions: {
create: [
{
assignedAt: new Date(),
permission: {
connect: {
code: "video_management",
},
},
},
],
},
},
});
}
main()
.catch((e) => {
console.error(e);
process.exit(1);
})
.finally(async () => {
// close Prisma Client at the end
await prisma.$disconnect();
});
執行npx ts-node prisma/seed.ts
,就可以初始化角色和權限數據了,上述代碼中我們設置了 2 個角色,分別爲超級管理員和視頻管理員,添加了 2 個權限分別爲視頻管理和用戶管理並且設置了唯一鍵 code,code 可以擁有前端權限的判斷。
後端接口設計
有了數據庫和數據我們便可以實現一個接口,“用戶信息接口”,我們將它定義爲/api/user/me
,用於返回權限信息
新建一個pages/api/me.ts
文件, 代碼如下
import { authOptions } from "@/pages/api/auth/[...nextauth]";
import { unstable_getServerSession } from "next-auth/next";
import prisma from "@/lib/prisma";
import { makeSerializable } from "@/lib/util";
import { NextApiRequest, NextApiResponse } from "next";
export default async function handler(
req: NextApiRequest,
res: NextApiResponse
) {
const session = await unstable_getServerSession(req, res, authOptions);
if (!session) {
res.status(401).json({ message: "You must be logged in." });
return;
}
const user = await prisma.user.findFirst({
where: {
id: session.id as string,
},
});
const permissionsOnRoles = await prisma.permissionsOnRoles.findMany({
where: {
roleId: user.roleId,
},
});
const ids = permissionsOnRoles.map((item) => item.permissionId);
const permissions = await prisma.permission.findMany({
where: {
id: {
in: ids,
},
},
});
return res.json({
user,
permissions,
});
}
上面的代碼中,查詢步驟爲:
-
先通過 session 獲得用戶 id
-
通過用戶 id 查詢用戶信息,獲得角色 id
-
通過角色 id 查詢關聯表,獲得權限 id
-
再通過權限 id 查詢權限信息。
以上代碼便是多對多查詢過程,訪問接口,就可以獲得當前用戶的權限信息了。
那麼前端就可以通過該接口來判斷功能權限了,至此後端權限部分就完成了。
React 中實現權限管理
在 React 中實現狀態管理,我們可以在整個 App 組件(跟組件)渲染前選請求me
接口,然後通過 React Context
將 permission 信息進行全局狀態管理,這樣我們就可以在任意組件獲取權限信息,進行權限判斷了。
import React, { useContext, useEffect } from 'react';
import axios from 'axios';
import {
createBrowserRouter,
RouterProvider,
Route,
Outlet,
} from 'react-router-dom';
const PermissionContext = React.createContext([]);
const usePermission = function () {
return useContext(PermissionContext);
};
function Permission({ code, children }) {
const permissions = usePermission();
if (permissions.includes(code)) {
return children;
}
return null;
}
export default function App() {
const [permissions, setPermissions] = React.useState([]);
useEffect(() => {
axios.get('/api/user/me').then((res)=>{
const permissions=res.data.map(item=>item.code)
setPermissions(permissions);
})
}, []);
return (
<div>
<PermissionContext.Provider value={permissions}>
<RouterProvider router={router} />
</PermissionContext.Provider>
</div>
);
}
上面代碼中,我們創建了一個我們定義了一個Permission
組件,那麼在項目中,只要在要判斷權限的地方使用該組件,若沒有權限,則會不顯示。我們還定義了一個usePermission
自定義 hooks,這樣在後續開發中,若要使用到權限,我們就可以直接使用這個 Hooks,界面 UI 也可以重新定義了。
比如在 React-router 路由(V6)中,直接嵌套一個Permission
組件便可以實現權限控制了。
const router = createBrowserRouter([
{
path: '/',
element: <div>首頁</div>,
},
{
path: '/login',
element: <div>登錄</div>,
},
{
path: '/admin',
element: (
<div>
後臺管理 <Outlet />
</div>
),
children: [
{
path: 'user',
element: (
<Permission code="user_management">
<div>用戶管理</div>
</Permission>
),
},
{
path: 'video',
element: (
<Permission code="user_management">
<div>視頻管理</div>
</Permission>
),
},
],
},
]);
Next.js 實現權限管理
在 Next.js 中,我們可以同 React 一樣的方式來實現權限控制。若是服務端渲染的頁面,我們也可以在服務端控制。
import { authOptions } from "@/pages/api/auth/[...nextauth]";
import { unstable_getServerSession } from "next-auth/next";
import prisma from "@/lib/prisma";
import { makeSerializable } from "@/lib/util";
export async function getServerSideProps(context) {
const session = await unstable_getServerSession( context.req, context.res, authOptions );
if (!session) {
return {
redirect: {
destination: "/403",
permanent: false,
},
};
}
//const permissions = prisma query
if(!permissions.includes('video_management')){
return {
props: {
errorCode:403,
},
};
}
const data = await prisma.video.findMany({
include: { author: true },
});
return {
props: {
session,
data: makeSerializable(data),
},
};
}
同接口的方式一致,我們也可以在 getServerSideProps
通過獲得 session
,然後獲得用戶權限,再通過權限判斷,是否讓頁面顯示 403。
那如果有多個頁面有需要權限判斷,該怎麼辦呢?我們可以在根目錄下建立一個middleware.js
, 中間件會在每個請求的時候執行。
import { getToken } from "next-auth/jwt"
import { NextResponse } from "next/server"
export async function middleware(req) {
// 如果url不應該受到保護,請儘早返回
if (!req.url.includes("/protected-url")) {
return NextResponse.next()
}
const session = await getToken({ req, secret: process.env.SECRET })
if (!session) return NextResponse.redirect("/api/auth/signin")
...
// 如果授權通過則繼續。
return NextResponse.next()
}
在中間件中,我們也可以獲得session
,那麼與 api 接口一樣,就可以在中間件中判斷權限信息了,有一點需要注意的是,Url 不需要權限判斷,我們應該儘早返回,這樣可以避免多餘的查詢。
小結
本文以視頻網站爲例,講解了權限系統的設計與實現,主要涉及到的知識點有:
-
後端基於角色表和權限表,多對多表結構設計
-
Prisma 中實現多對多關係查詢
-
前端使用 React Context 和 自定義 hooks 實現全局狀態管理
-
利用 Next.js 的 middleware[3] 也可以獲得 session,並且用於權限判斷。
好了,以上就是本文的全部內容,你學會了嗎?接下來我將繼續分享 Next.js 相關的實戰文章,歡迎各位關注我的《Next.js 全棧開發實戰》 專欄,感謝您的閱讀。
[1] 使用 NextAuth.js 給 Next.js 應用添加鑑權與認證: https://juejin.cn/post/7155514465591984136
[2]NodeJS ORM: https://www.prisma.io/
[3]NextAuth.js Auth Middleware for Next.js 12: https://gist.github.com/balazsorban44/30e2267fe1105529f217acbe3763b468
本文由 Readfog 進行 AMP 轉碼,版權歸原作者所有。
來源:https://mp.weixin.qq.com/s/DRc8YO7-SfNxs2SgkNO8gA