【用户管理与权限 - 篇六】前端动态展现:基于权限的菜单与按钮控制
前言
在前面的系列文章中,我们已经成功构建了用户、角色和权限的后端模型与API,并实现了前端的用户管理、角色管理以及为角色分配权限的界面。但是,仅仅在后端做权限关联还不够,前端的展现也需要根据用户的权限进行调整。
理想的用户体验应该是:
- 用户登录后,侧边栏菜单只显示他们有权限访问的模块
- 在各个页面中,只有具备相应操作权限的用户才能看到对应的操作按钮
- 无权限的用户不会看到他们无法操作的功能,避免点击后被拒绝的尴尬体验
本文将带你完整实现这套前端权限控制系统,让权限配置真正发挥作用。
实现思路
整体架构
- 后端增强:登录接口返回用户权限编码列表
- 前端存储:Pinia store 管理用户权限状态
- 动态菜单:根据权限动态生成侧边栏菜单
- 按钮控制:页面中的操作按钮根据权限显示/隐藏
- 路由守卫:在路由层面也进行权限验证
技术方案
- 使用 JWT Token 携带基础用户信息
- 登录响应中附加完整的权限编码列表
- 前端使用
v-if
指令进行条件渲染 - 创建全局权限检查方法
hasPermission()
第一部分:后端权限接口增强
1. 自定义 TokenObtainPairSerializer
首先修改后端登录接口,使其返回用户的权限信息。
在 api/serializers.py
中添加自定义序列化器:
# api/serializers.py
from rest_framework_simplejwt.serializers import TokenObtainPairSerializer
from typing import Set
class MyTokenObtainPairSerializer(TokenObtainPairSerializer):
@classmethod
def get_token(cls, user):
token = super().get_token(user) # 调用父类方法获取基础 token
# 添加自定义声明到 token 的 payload 中
token['username'] = user.username
# 如果用户是超级用户,则在token中标记
if user.is_superuser:
token['is_superuser'] = True
return token
def validate(self, attrs):
data = super().validate(attrs) # 调用父类 validate 获取 access 和 refresh token
# 添加用户权限编码列表到返回数据中
user_permissions: Set[str] = set()
# self.user 是通过父类 validate 认证成功后的用户对象
if hasattr(self.user, 'profile') and self.user.profile is not None:
for role in self.user.profile.roles.prefetch_related('permissions').all():
for perm in role.permissions.all():
user_permissions.add(perm.code)
if self.user.is_superuser:
data['is_superuser'] = True # 标记超级用户
# 超管拥有所有权限
from .models import Permission
all_permissions = Permission.objects.values_list('code', flat=True)
data['permissions'] = list(all_permissions)
else:
data['is_superuser'] = False
data['permissions'] = list(user_permissions)
data['username'] = self.user.username
data['user_id'] = self.user.id
return data
关键实现点:
- 继承
TokenObtainPairSerializer
并重写validate
方法 - 通过用户的
profile -> roles -> permissions
关系链获取所有权限编码 - 使用
set()
去重,确保权限编码唯一 - 超级管理员自动获得所有权限
- 使用
prefetch_related('permissions')
优化数据库查询
2. 更新 URL 配置
修改 backend/urls.py
,使用自定义的登录视图:
# backend/urls.py
from django.contrib import admin
from django.urls import path, include
from rest_framework_simplejwt.views import (
TokenRefreshView,
TokenVerifyView,
)
from rest_framework_simplejwt.views import TokenObtainPairView as BaseTokenObtainPairView
from api.serializers import MyTokenObtainPairSerializer
# 创建一个使用自定义 Serializer 的视图
class MyTokenObtainPairView(BaseTokenObtainPairView):
serializer_class = MyTokenObtainPairSerializer
urlpatterns = [
path('admin/', admin.site.urls),
path('api/', include('api.urls')),
# 使用自定义的 TokenObtainPairView
path('api/token/', MyTokenObtainPairView.as_view(), name='token_obtain_pair'),
path('api/token/refresh/', TokenRefreshView.as_view(), name='token_refresh'),
path('api/token/verify/', TokenVerifyView.as_view(), name='token_verify'),
]
现在登录接口的响应格式如下:
第二部分:前端用户状态管理
1. 更新用户 Store
修改 frontend/src/stores/user.ts
,增加权限管理功能:
// frontend/src/stores/user.ts
import {
defineStore } from 'pinia'
import {
ElMessage } from 'element-plus'
import router from '@/router'
import request from '@/utils/request'
import type {
AxiosError } from 'axios'
// 更新后端登录接口的响应类型
interface LoginResponse {
access: string;
refresh: string;
username: string;
user_id: number;
permissions: string[]; // 权限编码列表
is_superuser: boolean; // 是否为超级用户
}
// 更新用户信息的接口类型
interface UserState {
accessToken: string | null;
refreshToken: string | null;
username: string | null;
userId: number | null;
permissions: string[]; // 存储用户权限编码
isSuperuser: boolean; // 存储是否为超级用户
}
export const useUserStore = defineStore('user', {
state: (): UserState => ({
accessToken: localStorage.getItem('access-token') || null,
refreshToken: localStorage.getItem('refresh-token') || null,
username: localStorage.getItem('username') || null,
userId: localStorage.getItem('user-id') ? Number(localStorage.getItem<