限管理實戰(zhàn):模型設(shè)計、校驗實現(xiàn)與性能優(yōu)化)
做后端的人都繞不過權(quán)限管理這關(guān)。早幾年我在某公司負責一個后臺管理系統(tǒng)業(yè)務(wù)功能加了一堆用戶角色越來越亂每次梳理權(quán)限都在加班補窟窿。那段時間我光是整理權(quán)限清單就要花小半天更別提上線之后各種“這個功能明明能用換個賬號就報403”的反饋。后來花了兩個晚上把整個項目的權(quán)限模塊推倒重來用RBAC模型重新建模才徹底把這塊理順了。如果你也在用Django做后臺系統(tǒng)或者正準備給項目加權(quán)限控制這篇就把我的方案、踩過的坑、以及排查思路完整講一遍。Django自帶的auth應(yīng)用里其實已經(jīng)有Group用戶組和Permission權(quán)限兩張表也能做到基于角色的粗粒度控制但真正落到業(yè)務(wù)里你會發(fā)現(xiàn)原生的東西離“夠用”還差得遠。比如“菜單級權(quán)限”“按鈕級權(quán)限”“不同部門的相同角色擁有不同數(shù)據(jù)范圍”這些需求光靠Django原生那套很難優(yōu)雅實現(xiàn)。所以我基于RBAC的核心思想在Django上做了一套更完整的權(quán)限體系包含用戶、角色、權(quán)限、菜單四個維度下面從設(shè)計思路開始逐個拆解。1. RBAC權(quán)限模型的設(shè)計思路1.1 RBAC到底解決了什么問題在RBACRole-Based Access Control基于角色的訪問控制之前很多小項目用的是最原始的方式在用戶表里直接存“是不是管理員”“能不能導(dǎo)出”“能不能刪數(shù)據(jù)”之類的布爾字段。用戶少的時候沒什么用戶一多、功能一多就亂了——今天A要加一個導(dǎo)出權(quán)限明天B也要加后天發(fā)現(xiàn)C不該有但代碼里已經(jīng)寫死了判斷邏輯只能頻繁改代碼、改數(shù)據(jù)庫最后誰都不敢動權(quán)限相關(guān)的東西。RBAC的核心思路是引入“角色”這個中間層。用戶不直接綁定權(quán)限而是綁定角色角色再擁有權(quán)限。這樣一來權(quán)限的分配變成角色與權(quán)限的配置而不再需要頻繁修改用戶代碼或表結(jié)構(gòu)。類比一下一家公司的門禁卡系統(tǒng)員工不是直接擁有“進財務(wù)室”的權(quán)利而是因為“財務(wù)部員工”這個崗位角色天然就被授權(quán)進入了財務(wù)室。換崗的時候只需要換角色不需要挨個改門禁記錄。加上“角色”這層抽象之后權(quán)限管理從“點對點”變成了“面到面”數(shù)據(jù)集小、邏輯清晰、可追溯。這就是RBAC能成為主流權(quán)限方案的根本原因。1.2 為什么最終選了RBAC而不是ACL或ABAC做權(quán)限選型的時候市面上主要有三種模型ACLAccess Control List、RBACRole-Based Access Control、ABACAttribute-Based Access Control。ACL是最樸素的思路——直接把某個用戶和某種操作權(quán)限掛上鉤。適合小型工單系統(tǒng)、個人博客后臺但一旦用戶量上百、功能上百ACL表的膨脹速度非常驚人而且無法自然表達“一類人”的共性權(quán)限。ABAC則更靈活用屬性規(guī)則來判斷比如說“銷售部門的員工只能看本部門客戶的工單”但要維護規(guī)則引擎、表達式解析復(fù)雜度直接上一個臺階中小型項目一般是hold不住的。所以在絕大多數(shù)管理后臺、中后臺系統(tǒng)的場景里RBAC是性價比最高的方案。它管住了“誰能做什么”又不像ABAC那樣引入巨大的規(guī)則復(fù)雜度。而如果后面又出現(xiàn)“不同角色只能看不同部門數(shù)據(jù)”這種行級數(shù)據(jù)權(quán)限需求也完全可以在RBAC的基礎(chǔ)上增加一個數(shù)據(jù)范圍字段來配合實現(xiàn)不需要推翻整體設(shè)計。這也是我在實操中最終選擇RBAC并保留擴展空間的原因。1.3 Django自帶的auth權(quán)限體系夠不夠用凡是開過Django項目的人都知道django.contrib.auth里已經(jīng)提供了User、Group、Permission還自帶has_perm()方法和permission_required裝飾器。那為什么我還要自己再擼一套RBAC因為原生的設(shè)計目標是“給Django管理后臺admin提供權(quán)限控制”而不是直接給業(yè)務(wù)系統(tǒng)做權(quán)限中臺。它的問題主要集中在幾點第一權(quán)限粒度是按模型來的一個Permission對應(yīng)“添加/修改/刪除/查看某個模型”但你做業(yè)務(wù)系統(tǒng)時會有“導(dǎo)出報表”“審批工單”“分配任務(wù)”這類跟模型CRUD不對應(yīng)的操作權(quán)限原生比較難表達。第二菜單和接口權(quán)限沒有天然建模。你做的是帶側(cè)邊欄的后臺側(cè)邊欄菜單誰可見誰不可見原生完全不管需要自己去查詢、渲染。第三Group雖然可以作為角色表用但它只能關(guān)聯(lián)權(quán)限不能掛級聯(lián)的數(shù)據(jù)范圍、角色層級、排序等擴展字段使用起來限制很大。所以實操中我保留了Django原生的Permission來承接原子操作權(quán)限再用自定義的Role、Menu等模型來承載業(yè)務(wù)權(quán)限結(jié)構(gòu)。原生框架的理念值得沿用但必須做擴展才能真正支撐起一個靈活的后臺權(quán)限系統(tǒng)。2. 數(shù)據(jù)模型與表結(jié)構(gòu)設(shè)計2.1 五張核心表的職責劃分在給后臺系統(tǒng)設(shè)計RBAC表結(jié)構(gòu)時我最終拆出了五張核心表外加兩張關(guān)聯(lián)中間表。五張核心表的職責非常清晰User用戶表直接用Django的AbstractUser擴展增加外鍵關(guān)聯(lián)到角色也可保留原生的多對多關(guān)系。Role角色表存儲角色名稱、角色標識符、排序、狀態(tài)、數(shù)據(jù)范圍等。Permission權(quán)限表存儲權(quán)限編碼、名稱可以直接沿用Django自帶的Permission也可以自定義一張權(quán)限表看項目規(guī)模。Menu菜單表存儲系統(tǒng)菜單、按鈕包括菜單類型目錄/菜單/按鈕、路由地址、權(quán)限標識等。MenuRole、RoleUser關(guān)聯(lián)表由于角色和菜單、角色和用戶都是多對多關(guān)系所以需要中間表來維護。這里有個關(guān)鍵取舍我是直接用Django的Permission還是另建一張Permission表如果你的項目是在Django Admin體系內(nèi)做管理直接復(fù)用即可但如果你要寫一個獨立的后臺管理系統(tǒng)我更建議自定義Permission表或菜單表自帶權(quán)限標識因為你后期會有大量“查詢權(quán)限是否存在”“校驗?zāi)硞€請求URL是否匹配某個權(quán)限標識”的場景自己的表更靈活可控不用跟Django的ContentType混在一起。下面給出我實際使用的一套模型定義。2.2 核心模型代碼實例先擴展用戶模型。用AbstractUser加一個roles多對多字段兼容Django原本的groups和user_permissions。from django.contrib.auth.models import AbstractUser, Permission from django.db import models class User(AbstractUser): 擴展用戶表增加與角色的多對多關(guān)系 roles models.ManyToManyField( Role, throughUserRole, related_nameusers, blankTrue, verbose_name角色列表 ) # 如果業(yè)務(wù)需要可以增加部門、崗位等字段 department models.CharField(max_length64, blankTrue, verbose_name部門) class Meta(AbstractUser.Meta): verbose_name 用戶 verbose_name_plural verbose_name然后是Role模型給角色增加數(shù)據(jù)范圍和狀態(tài)等擴展字段。class Role(models.Model): 角色表 DATA_SCOPE_CHOICES [ (1, 全部數(shù)據(jù)), (2, 本部門及以下), (3, 僅本部門), (4, 僅本人), ] name models.CharField(max_length64, uniqueTrue, verbose_name角色名稱) code models.CharField(max_length64, uniqueTrue, verbose_name角色標識) data_scope models.IntegerField(choicesDATA_SCOPE_CHOICES, default3, verbose_name數(shù)據(jù)范圍) status models.BooleanField(defaultTrue, verbose_name狀態(tài)) sort models.IntegerField(default0, verbose_name排序) permissions models.ManyToManyField(Permission, blankTrue, verbose_name權(quán)限列表) desc models.CharField(max_length256, blankTrue, verbose_name角色描述) created_at models.DateTimeField(auto_now_addTrue) def __str__(self): return self.name class Meta: db_table system_role verbose_name 角色 verbose_name_plural verbose_name這里要注意permissions字段與Django原生的Permission建立多對多關(guān)系這樣一方面可以兼容has_perm相關(guān)的邏輯另一方面也給權(quán)限的批量管理留好了口子。接下來是Menu表。它承載了兩個功能一是后臺側(cè)邊欄菜單的渲染二是按鈕級權(quán)限的判斷。因為很多權(quán)限校驗最終都落到“某個按鈕標識是否在用戶的權(quán)限集合里”所以菜單表需要一個唯一的perms字段來標識。class Menu(models.Model): 菜單與按鈕權(quán)限表 MENU_TYPE_CHOICES [ (1, 目錄), (2, 菜單), (3, 按鈕), ] parent models.ForeignKey( self, on_deletemodels.CASCADE, nullTrue, blankTrue, related_namechildren, verbose_name父級菜單 ) name models.CharField(max_length64, verbose_name菜單名稱) path models.CharField(max_length255, blankTrue, verbose_name路由地址) component models.CharField(max_length255, blankTrue, verbose_name前端組件路徑) perms models.CharField(max_length255, blankTrue, verbose_name權(quán)限標識) type models.IntegerField(choicesMENU_TYPE_CHOICES, default2, verbose_name菜單類型) icon models.CharField(max_length128, blankTrue, verbose_name圖標) sort models.IntegerField(default0, verbose_name排序) visible models.BooleanField(defaultTrue, verbose_name是否顯示) status models.BooleanField(defaultTrue, verbose_name狀態(tài)) roles models.ManyToManyField(Role, throughMenuRole, related_namemenus, blankTrue, verbose_name角色列表) class Meta: db_table system_menu verbose_name 菜單 verbose_name_plural verbose_name def __str__(self): return self.name最后是兩個中間表。雖然Django會自動生成但我需要給中間表增加自定義字段所以顯式寫出來。class UserRole(models.Model): 用戶-角色中間表 user models.ForeignKey(User, on_deletemodels.CASCADE, related_nameuser_roles) role models.ForeignKey(Role, on_deletemodels.CASCADE, related_nameuser_roles) class MenuRole(models.Model): 角色-菜單中間表 role models.ForeignKey(Role, on_deletemodels.CASCADE, related_namemenu_roles) menu models.ForeignKey(Menu, on_deletemodels.CASCADE, related_namemenu_roles) class Meta: unique_together (role, menu)從實操角度看這樣的模型設(shè)計已經(jīng)能覆蓋95%以上后臺系統(tǒng)的權(quán)限需求。如果還要再升級可以將User和Role改成多對多扁平結(jié)構(gòu)允許一個用戶擁有多個角色數(shù)據(jù)權(quán)限則可以借助data_scope字段在查詢中動態(tài)拼接SQL或ORM過濾條件。2.3 表關(guān)系與預(yù)置數(shù)據(jù)規(guī)劃表關(guān)系上整體是典型的“星型結(jié)構(gòu)”用戶表在中心左邊連著角色右邊連著權(quán)限菜單表作為前端渲染和權(quán)限標識的錨點和角色建立關(guān)聯(lián)。為了讓查詢性能更好我在Role里直接持有permissions多對多關(guān)系在Menu里直接持有roles多對多關(guān)系這樣查詢用戶時可以先拿到角色再由角色快速關(guān)聯(lián)到菜單和權(quán)限。初始化數(shù)據(jù)時建議做一個基礎(chǔ)數(shù)據(jù)腳本用Django的management command來跑不要手動往庫里插否則環(huán)境差異很容易造成數(shù)據(jù)不一致。一個典型的初始化流程是創(chuàng)建超級管理員角色并綁定所有權(quán)限標識。創(chuàng)建通用角色如普通用戶、運維只讀角色綁定對應(yīng)的菜單和權(quán)限。創(chuàng)建系統(tǒng)的菜單結(jié)構(gòu)目錄、菜單、按鈕三個層級分開錄入。把默認的超級管理員的用戶ID與角色綁定。我自己實際跑起來的時候發(fā)現(xiàn)最費時間的不是寫模型而是整理菜單和權(quán)限標識的清單。這個清單直接決定后續(xù)權(quán)限管理的易用性。權(quán)限標識的命名我建議統(tǒng)一用冒號分層例如system:user:add、system:user:edit、system:user:delete。這樣正則匹配容易前端按鈕和后端接口校驗也好統(tǒng)一對。3. 實操落地權(quán)限校驗完整實現(xiàn)3.1 項目初始化與配置第一步自然是搭一個可運行的Django項目。我習慣先用獨立的目錄把項目建好再逐步合入權(quán)限模塊。基礎(chǔ)配置里需要把自定義用戶模型設(shè)置好在settings.py里加AUTH_USER_MODEL然后注冊到INSTALLED_APPS。# settings.py INSTALLED_APPS [ django.contrib.admin, django.contrib.auth, django.contrib.contenttypes, django.contrib.sessions, django.contrib.messages, django.contrib.staticfiles, rest_framework, # 如果走API型后臺建議集成DRF system, # 權(quán)限所在的業(yè)務(wù)模塊 ] AUTH_USER_MODEL system.User我用Django REST Framework的原因是當前后臺管理基本都是前后端分離的后端只提供JSON接口權(quán)限校驗放在視圖層和中間件層。如果不打算前后端分離也可以忽略DRF直接用View和模板渲染菜單。隨后執(zhí)行遷移命令確認模型建表沒問題。python manage.py makemigrations python manage.py migrate這個時候有個非常容易踩的坑如果你是在已有項目上引入AUTH_USER_MODEL數(shù)據(jù)庫里可能已經(jīng)有了其他表的外鍵關(guān)聯(lián)到原生的auth.User遷移會直接報錯。解決方法是先確保被關(guān)聯(lián)的表比如日志表、工單表要么用settings.AUTH_USER_MODEL做外鍵要么在引入之前處理好懸空外鍵的問題。3.2 權(quán)限校驗裝飾器與中間件有了模型之后最核心的部分就是把“權(quán)限標識”和“接口請求”關(guān)聯(lián)起來。我封了一個裝飾器permission_check用于視圖函數(shù)或DRF視圖類。from django.http import JsonResponse from functools import wraps def permission_check(perm_code): 簡單的權(quán)限校驗裝飾器支持DRF視圖 def decorator(view_func): wraps(view_func) def wrapped_view(request, *args, **kwargs): if not request.user.is_authenticated: return JsonResponse({code: 401, msg: 未登錄}, status401) if request.user.is_superuser: return view_func(request, *args, **kwargs) # 從request中取出用戶擁有的權(quán)限集合 perm_set request.user.get_all_permissions() if perm_code not in perm_set: return JsonResponse({code: 403, msg: 無權(quán)限訪問}, status403) return view_func(request, *args, **kwargs) return wrapped_view return decorator permission_check(system:user:add) def add_user(request): # 新增用戶邏輯 pass這里的關(guān)鍵點在于request.user.get_all_permissions()為了讓它每次都返回最新權(quán)限我在User模型上自定義了一個方法。這個方法的核心邏輯是從當前用戶關(guān)聯(lián)的角色找到這些角色綁定的權(quán)限收集所有權(quán)限編碼。from django.contrib.auth.models import AbstractUser from django.db import models class User(AbstractUser): roles models.ManyToManyField(Role, throughUserRole, related_nameusers, blankTrue) def get_all_permissions(self): if self.is_superuser: # 超級管理員直接返回一個始終包含通過的標記 return {*,} # 通過角色收集權(quán)限編碼 perms set() role_ids self.roles.filter(statusTrue).values_list(id, flatTrue) for role_id in role_ids: # 從Role關(guān)聯(lián)到Permission permissions Role.objects.get(pkrole_id).permissions.all() for p in permissions: perms.add(p.codename) # 如果還涉及菜單表權(quán)限標識 menu_perm Menu.objects.filter(roles__id__inrole_ids, perms__isnullFalse).values_list(perms, flatTrue) perms.update(menu_perm) return perms這里有一個容易混淆的點Django原生Permission自帶codename這個codename是像add_userchange_user這樣的短名稱但為了方便統(tǒng)一控制我更建議直接把權(quán)限標識寫入codename或新建一個字段比如system:user:add。實際項目中為了讓菜單權(quán)限和后端權(quán)限共用一個權(quán)限集合下面這段邏輯是主流做法。不過要注意get_all_permissions這個方法是DjangoBaseUser里就存在的鉤子我在覆寫時保留一致的返回類型后面很多內(nèi)置功能都不會出問題。如果不想在每一個視圖函數(shù)上加裝飾器也可以在中間件層做統(tǒng)一攔截。以下是我常用的一個輕量中間件它把“白名單 登錄態(tài)”自動處理了。class RbacMiddleware: def __init__(self, get_response): self.get_response get_response def __call__(self, request): # 對不需要權(quán)限校驗的路由放行 path request.path allow_list [ /api/login/, /api/captcha/, /static/, ] if any(path.startswith(prefix) for prefix in allow_list): return self.get_response(request) if not request.user.is_authenticated: return JsonResponse({code: 401, msg: 未登錄}, status401) # 判斷當前請求的URL是否需要“資源權(quán)限”校驗 # 通過一個服務(wù)函數(shù)獲取 URL - 權(quán)限標識 的映射關(guān)系 perm_code matcher.get_permission_by_path(path) if perm_code and not request.user.is_superuser: if perm_code not in request.user.get_all_permissions(): return JsonResponse({code: 403, msg: 無權(quán)限訪問}, status403) return self.get_response(request)用中間件的好處是接口不用逐個加裝飾器新增接口只要在路由配置的時候做好權(quán)限標識和路徑的映射就行。但它也有一個劣勢——每個請求都會跑一遍權(quán)限檢查對性能有一定影響所以通常會和緩存方案配合使用。后面第4章我會專門講性能優(yōu)化。3.3 前端菜單與按鈕級權(quán)限控制做后臺管理系統(tǒng)前端核心訴求就兩個菜單按角色動態(tài)渲染按鈕按權(quán)限動態(tài)顯示。前端拿到當前用戶后調(diào)用一個獲取用戶信息的接口接口返回該用戶可見的菜單樹和權(quán)限標識集合。在Django視圖里獲取菜單樹的邏輯大概是這樣的def get_user_menu_tree(request): user request.user if user.is_superuser: menus Menu.objects.filter(statusTrue, type__in[1, 2]).order_by(sort) else: role_ids user.roles.filter(statusTrue).values_list(id, flatTrue) menus Menu.objects.filter(roles__id__inrole_ids, statusTrue, type__in[1, 2]).order_by(sort) tree build_menu_tree(menus) return JsonResponse({code: 200, data: tree}) def build_menu_tree(menus): 將扁平的菜單列表轉(zhuǎn)換為樹形結(jié)構(gòu) menu_list list(menus.values(id, parent_id, name, path, component, icon, sort, type)) tree [] # 轉(zhuǎn)化為dict便于快速查找 menu_map {} for item in menu_list: parent_id item.pop(parent_id) item[children] [] menu_map[item[id]] item # 組裝樹 for item in menu_list: item.pop(parent_id, None) return menu_list上面的代碼為了簡化示例我忽略了遞歸組樹的過程實際需要遞歸根據(jù)parent_id掛到對應(yīng)的children里。前端拿到這棵菜單樹之后直接遍歷渲染側(cè)邊欄即可。按鈕級權(quán)限同理接口返回一個扁平的權(quán)限標識列表前端在按鈕上判斷const hasPerm (perm) { return store.state.permissions.includes(perm); }這樣就能實現(xiàn)“角色變了按鈕自動隱藏”的效果。需要注意的是前端權(quán)限控制只是體驗優(yōu)化真正的安全防線永遠在后端。接口和數(shù)據(jù)的權(quán)限校驗不能因為前端隱藏了入口就省略一定要在視圖層再校驗一次防止有人直接請求接口地址。4. 性能優(yōu)化與緩存方案4.1 權(quán)限查詢的N1問題RBAC模型在實現(xiàn)之后最容易暴露的問題就是查詢性能。原因很簡單用戶 - 角色 - 權(quán)限 - 菜單是多層關(guān)聯(lián)如果不做優(yōu)化一次權(quán)限查詢會實時發(fā)起大量SQL。舉個例子如果用戶有3個角色每個角色有20個權(quán)限標識那么獲取用戶全部權(quán)限時如果用遍歷角色的方式逐個查詢權(quán)限SQL次數(shù)就是“1次查角色 N次查權(quán)限”。在用戶登錄、用戶訪問接口、獲取菜單樹這些關(guān)鍵節(jié)點都會被放大多倍。解決方案有兩個層面。第一個層面是使用prefetch_related預(yù)取關(guān)聯(lián)數(shù)據(jù)在查詢角色列表時就把每個角色的權(quán)限集合一次性取出來例如roles Role.objects.filter(statusTrue).prefetch_related(permissions)第二個層面更徹底——把權(quán)限集合做緩存不用每次請求都去數(shù)據(jù)庫里查。4.2 緩存策略與Redis集成考慮到權(quán)限數(shù)據(jù)的特點是“讀多寫少”非常適合用緩存。我在項目里把用戶權(quán)限集合、菜單樹都放到了Redis里緩存前綴按用戶ID區(qū)分例如rbac:perms:user:{user_id}緩存時間設(shè)為30分鐘。集成Redis之后get_all_permissions()的改造邏輯是這樣的from django.core.cache import cache def get_all_permissions(self): cache_key frbac:perms:user:{self.id} perms cache.get(cache_key) if perms is not None: return perms # 未命中則從數(shù)據(jù)庫查詢 if self.is_superuser: perms {*,} else: perms set() role_list self.roles.filter(statusTrue).prefetch_related(permissions) for role in role_list: for p in role.permissions.all(): perms.add(p.codename) # 補充菜單權(quán)限標識 menu_perms Menu.objects.filter( roles__id__inself.roles.filter(statusTrue), perms__isnullFalse ).values_list(perms, flatTrue) perms.update(menu_perms) # 寫入緩存 cache.set(cache_key, perms, timeout1800) return perms這里有個細節(jié)perms是無序集合在緩存時要注意cache.set能否接受set類型。大部分Redis緩存后端會自動把set序列化成list取回來就變成list了所以最好在寫入前統(tǒng)一list(perms)讀取時再轉(zhuǎn)成集合避免類型不一致導(dǎo)致后續(xù)in判斷出錯。另外我強烈建議把緩存的鍵設(shè)計成可控制的版本號比如rbac:perms:user:{user_id}:v2。這樣當權(quán)限邏輯升級時只需要改版本號讓舊緩存自然失效避免老用戶因為舊緩存一直看到舊的權(quán)限數(shù)據(jù)。4.3 權(quán)限變更即時生效緩存能提升性能但也帶來一個常見問題管理員修改了角色權(quán)限用戶的緩存卻沒有及時刷新導(dǎo)致新權(quán)限遲遲不生效。解決辦法是在權(quán)限變更的業(yè)務(wù)邏輯里主動清除對應(yīng)用戶的緩存。實際處理時我封裝了一個服務(wù)類專門負責角色權(quán)限的修改與緩存清理class RbacService: staticmethod def assign_permissions_to_role(role, perm_codes): 給角色分配權(quán)限 # 處理權(quán)限編碼的更新 role.permissions.clear() ... # 清理所有擁有該角色的用戶的緩存 user_ids role.users.values_list(user_id, flatTrue) for user_id in user_ids: cache_key frbac:perms:user:{user_id} cache.delete(cache_key)有同學可能會問如果沒有Redis緩存而是本地內(nèi)存緩存這個失效怎么處理我建議不要把本地緩存作為核心方案因為多進程部署下本地緩存無法聯(lián)動失效最終導(dǎo)致權(quán)限數(shù)據(jù)不一致。生產(chǎn)環(huán)境直接用Redis省事也可靠。5. 常見問題與排查技巧實錄5.1 權(quán)限明明加了卻總校驗失敗這是我遇到最多的問題場景通常是“給角色勾選了某個權(quán)限前端菜單也顯示了但調(diào)接口還是403”。排查路徑一般按下面幾步走確認當前登錄的用戶其角色是否真的已經(jīng)關(guān)聯(lián)到該接口對應(yīng)的權(quán)限標識。很多情況是權(quán)限標識寫錯了比如接口校驗的是system:user:add菜單按鈕綁定的是system:User:add大小寫不一致直接匹配失敗。確認是否有緩存。如果有Redis緩存先手動清一下該用戶的緩存鍵再重新調(diào)用接口。確認是否走的是超級管理員邏輯。is_superuserTrue的用戶在部分實現(xiàn)里會被直接放行如果業(yè)務(wù)希望超級管理員也需要綁定角色要注意代碼分支。一個比較穩(wěn)妥的習慣是在權(quán)限校驗代碼里臨時打印當前用戶的權(quán)限集合再打印接口需要的權(quán)限標識一眼就能看出問題在哪。logger.debug(fcurrent user perms: {request.user.get_all_permissions()}) logger.debug(frequire perm: {perm_code})5.2 超級用戶權(quán)限處理誤區(qū)Django的is_superuser是一個“一票通過”的開關(guān)默認情況下他擁有所有權(quán)限。但在RBAC落地時存在兩個容易出問題的地方。第一個是“頁面和菜單渲染”。如果前端拿到is_superuser就直接返回全部菜單那角色配置功能就完全用不上了可如果超級管理員連菜單都要跟著角色走又容易漏配導(dǎo)致他自己也看不到某些管理入口。我通常的取舍是超級管理員直接返回全部菜單和全部權(quán)限但前端在“用戶管理”和“角色管理”頁面仍然可以按角色查詢普通用戶實現(xiàn)視角分離。第二個是“數(shù)據(jù)權(quán)限”。超級管理員的數(shù)據(jù)權(quán)限一般默認為“全部數(shù)據(jù)”但普通角色即使有權(quán)限標識數(shù)據(jù)范圍也可能限制為“僅本人”。所以在查詢列表數(shù)據(jù)時除了權(quán)限校驗還要額外根據(jù)角色data_scope進行數(shù)據(jù)范圍過濾這一步超級管理員是直接全部放行的。我把這兩個邏輯串起來講是想強調(diào)權(quán)限校驗和數(shù)據(jù)范圍過濾是兩個完全不同的層級千萬不要混在同一個函數(shù)里判斷否則很容易出現(xiàn)“有權(quán)限看見按鈕卻看不到按鈕背后的任何數(shù)據(jù)”這種怪異現(xiàn)象。5.3 多角色權(quán)限合并與優(yōu)先級沖突實際業(yè)務(wù)里一個用戶往往有多種身份既是項目成員又是部門經(jīng)理還可能兼任某個專員。這時權(quán)限集合不是“取最新一個角色”而是“所有角色權(quán)限的并集”。我常用的模型是“一用戶多角色”在權(quán)限集合采用set進行合并天然去重。但如果不同角色的同一個權(quán)限標識對應(yīng)的數(shù)據(jù)范圍不同例如“項目成員”只能看個人數(shù)據(jù)“部門經(jīng)理”能看本部門數(shù)據(jù)這時簡單的并集無法表達。我采用的方案是數(shù)據(jù)范圍取所有角色中的“最大范圍”比如其中一個角色是“全部數(shù)據(jù)”那這個用戶的數(shù)據(jù)范圍就是“全部數(shù)據(jù)”如果一個是“本部門”一個是“僅本人”那就取“本部門”。這個策略在大多數(shù)業(yè)務(wù)場景下都能說得通而且實現(xiàn)起來也簡單。如果你遇到的要求是“不同數(shù)據(jù)范圍都要能單獨限制到某個菜單”那RBAC基礎(chǔ)模型就有點不夠用了建議引入“數(shù)據(jù)權(quán)限規(guī)則表”在RBAC上層增加一個策略引擎。但這個復(fù)雜度一般項目不會用到我在這里只提一句。5.4 排查命令速查最后把我在排查Django RBAC問題時常用的一些命令和SQL整理成速查表方便你直接抄。場景排查命令確認用戶角色python manage.py shell里執(zhí)行user.roles.all()確認角色權(quán)限r(nóng)ole.permissions.values_list(codename, flatTrue)確認菜單權(quán)限標識Menu.objects.filter(perms__isnullFalse).values_list(perms, flatTrue)清空某個用戶的權(quán)限緩存cache.delete(rbac:perms:user:12)強制遷移構(gòu)建表python manage.py makemigrations system python manage.py migrate查看接口路由對應(yīng)視圖python manage.py show_urls或查項目的urls.py這些操作基本覆蓋了日常權(quán)限模塊的調(diào)試場景。每次遇到問題先做數(shù)據(jù)層面的核對再考慮代碼邏輯最后懷疑緩存基本能解決90%的問題。最后再分享一個小技巧權(quán)限標識的命名和菜單路由最好在項目啟動前就規(guī)劃好而不是邊寫邊取。你可以先畫一張“系統(tǒng)功能權(quán)限清單”表格列出功能模塊、菜單路徑、按鈕名稱、權(quán)限標識、對應(yīng)接口評審?fù)ㄟ^后再落到代碼里。很多權(quán)限混亂的爛攤子根源不在代碼而在沒有一個清晰的權(quán)限臺賬。我后來維護這套RBAC模塊時總是一邊查代碼一邊對著這臺賬看自己都沒想到能省這么多時間。