基於Django的rbac權限驗證

模型:rbac 基於角色的權限訪問控制
什麼是權限?
一個包含正則表達式的url就是權限

第一個版本,實現簡單的權限驗證需求

表結構:

class UserInfo(models.Model):

    name=models.CharField(max_length=32)
    pwd=models.CharField(max_length=32)
    roles=models.ManyToManyField("Role")

    def __str__(self):
        return self.name

class Role(models.Model):
    title=models.CharField(max_length=32)
    permissions=models.ManyToManyField("Permission")

    def __str__(self):
        return self.title

class Permission(models.Model):
    title=models.CharField(verbose_name="權限名稱",max_length=32)
    url=models.CharField(max_length=32)

    def __str__(self):
        return self.title

URL

url(r'^login/', views.LoginView.as_view()),
url(r'^userinfo/$', views.UserView.as_view()),
url(r'^userinfo/add', views.adduser),
url(r'^userinfo/(\d+)/change/', views.change),

Login(註冊session):

views:

from rbac.models import UserInfo
from  django.views import View

class LoginView(View):

    def get(self,request):
        return render(request,"login.html")

    def post(self,request):

        user=request.POST.get("user")
        pwd=request.POST.get("pwd")
        #查詢是否有這個賬戶
        user=UserInfo.objects.filter(name=user,pwd=pwd).first()
        #如果用戶存在
        if user:

            # 將當前登錄用戶的權限列表註冊到session中
            request.session["user_id"]=user.pk

            permission_list = []
            #查詢這個用戶所有的權限(通過用戶-角色-權限),並去重
            permissions=user.roles.all().values("permissions__url").distinct()
            for per in permissions:
                #將這個用戶的權限添加到權限列表
                permission_list.append(per.get("permissions__url"))

            print("permission_list:  ",permission_list)

            #將權限列表寫入session中
            request.session["permission_list"]=permission_list

        return HttpResponse("OK")

註冊中間件

settings

"rbac.apps.RbacConfig"

中間件
rbac.py

from django.utils.deprecation import MiddlewareMixin
from django.shortcuts import HttpResponse,render,redirect

class PermissionValid(MiddlewareMixin):

    def process_request(self,request):
        #獲取當前訪問路徑
        current_path = request.path  # /userinfo/add/

        #設置白名單
        white_list=["/login/","/admin/*"]
        #如果訪問路徑在白名單中則放行
        for per in white_list:
             import re
             ret=re.search(per,current_path)
             if ret:
                return None

        #認證用戶是否登錄
        #從session中獲取用戶ID
        user_id=request.session.get("user_id")
        #如果用戶沒有登錄則跳轉到登錄頁面
        if not user_id:
            return redirect("/login/")

        #校驗權限(第一種方式)
        #從session中獲取用戶權限列表
        permission_list = request.session["permission_list"]
        import re
        flag = False
        for permission in permission_list:  # "/userinfo/"
            #確定校驗規則
            permission = "^%s$" % permission
            ret = re.search(permission, current_path)
            if ret:
                return None

        return HttpResponse("您沒有權限訪問!")

在使用正則驗證權限的時候要注意在確定校驗規則時別忘了在路徑前後加上^和$

校驗包含三個部分:
白名單校驗(正則)
登錄校驗
權限校驗

前端頁面的校驗

前端頁面驗證當前登錄用戶是否有登錄權限,如果有就顯示相應選項

{% if '/userinfo/add/' in permission_list %}
      <a href="/userinfo/add/" class="btn btn-warning addbtn">添加用戶</a>
    {% endif %}

    <table class="table table-bordered">
        {% for user in user_list %}
        <tr>
            <td>{{ user.name }}</td>

            {% if "/userinfo/(\d+)/delete/"  in permission_list %}
               <td><a href="/userinfo/{{ user.pk }}/delete/">刪除</a></td>
            {% endif %}

            {% if "/userinfo/(\d+)/change/"  in permission_list %}
               <td><a href="/userinfo/{{ user.pk }}/change/">編輯</a></td>
            {% endif %}

        </tr>
        {% endfor %}

    </table>

思考:上面的操作已經實現權限控制的需求,通過url來控制按鈕的顯示,但是判斷權限使用的條件是寫死的,例如/userinfo/add/,如果能讓代碼更通用更簡潔?


第二版

變更數據結構:
添加PermissionGroup表,用於存儲權限分組
權限表中添加一個action字段,裏面保存add,delete之類的描述行爲的信息
權限表中添加一個group字段,與PermissionGroup 表爲一對多關係,比如用戶表的增刪改查權限全規到用戶組,角色表的增刪改查全歸到角色組

修改後的數據結構

class User(models.Model):
    name=models.CharField(max_length=32)
    pwd=models.CharField(max_length=32)
    roles=models.ManyToManyField(to="Role")

    def __str__(self): return self.name

class Role(models.Model):
    title=models.CharField(max_length=32)
    permissions=models.ManyToManyField(to="Permission")

    def __str__(self): return self.title

class Permission(models.Model):
    title=models.CharField(max_length=32)
    url=models.CharField(max_length=32)

    action=models.CharField(max_length=32,default="")
    group=models.ForeignKey("PermissionGroup",default=1)
    def __str__(self):return self.title

class PermissionGroup(models.Model):
    title = models.CharField(max_length=32)

    def __str__(self): return self.title

url

urlpatterns = [
    url(r'^admin/', admin.site.urls),
    url(r'^users/$', views.users),
    url(r'^users/add', views.add_user),
    url(r'^users/delete/(\d+)', views.del_user),
    url(r'^roles/', views.roles),
    url(r'^login/', views.login),
]

註冊session

將註冊session功能單獨提取出來,與rbac模塊放到一起
perssions.py

def initial_session(user,request):

    #獲取權限url、權限組id、權限action,並去重
    permissions = user.roles.all().values("permissions__url","permissions__group_id","permissions__action").distinct()

    #將獲取到的permissions數據重新組合,構建一個字典,變爲我們需要的數據格式
    permission_dict={}
    for item in permissions:
        gid=item.get('permissions__group_id')

        if not gid in permission_dict:

            permission_dict[gid]={
                "urls":[item["permissions__url"],],
                "actions":[item["permissions__action"],]
            }
        else:
            permission_dict[gid]["urls"].append(item["permissions__url"])
            permission_dict[gid]["actions"].append(item["permissions__action"])

    #將轉換後的數據寫到session中
    request.session['permission_dict']=permission_dict

中間件(不要忘了註冊到settings.py中)

rbac.py

class ValidPermission(MiddlewareMixin):

    def process_request(self,request):

        #當前訪問路徑
        current_path = request.path_info

        #檢查是否屬於白名單
        valid_url_list=["/login/","/reg/","/admin/.*"]

        for valid_url in valid_url_list:
            ret=re.match(valid_url,current_path)
            if ret:
                return None

        #校驗是否登錄
        user_id=request.session.get("user_id")

        if not user_id:
            return redirect("/login/")

        #校驗權限2
        permission_dict=request.session.get("permission_dict")

        for item in permission_dict.values():
              urls=item['urls']
              for reg in urls:
                  reg="^%s$"%reg
                  ret=re.match(reg,current_path)
                  if ret:

                      #如果匹配成功,就給request添加一個actions
                      request.actions=item['actions']
                      return None

        return HttpResponse("沒有訪問權限!")

視圖views

from rbac.models import *
#封裝增刪改查四種權限的判斷語句,模板可以直接調用這個類下的屬性即可
class Per(object):
    def __init__(self,actions):
        self.actions=actions
    def add(self):
        return "add" in self.actions
    def delete(self):
        return "delete" in self.actions
    def edit(self):
        return "edit" in self.actions
    def list(self):
        return "list" in self.actions

def users(request):

    user_list=User.objects.all()

    #查詢當前登錄人得名字
    id=request.session.get("user_id")
    user=User.objects.filter(id=id).first()

    #調用上面的Per類,這樣在模板中if per.delete就等同於 if "delete" in self.actions,讓代碼變得更簡潔了
    per = Per(request.actions)

    return render(request,"users.html",locals())

def add_user(request):

    return HttpResponse("add user.....")

def del_user(request,id):

    return HttpResponse("del"+id)

def roles(request):

    role_list=Role.objects.all()
    per = Per(request.actions)
    return render(request,"roles.html",locals())

from rbac.service.perssions import initial_session

def login(request):

    if  request.method=="POST":

        user=request.POST.get("user")
        pwd=request.POST.get("pwd")

        user=User.objects.filter(name=user,pwd=pwd).first()
        if user:
            ############################### 在session中註冊用戶ID######################
            request.session["user_id"]=user.pk

            ###############################在session註冊權限列表##############################
            #查詢當前登錄用戶的所有權限,註冊到session中
            initial_session(user,request)

            return HttpResponse("登錄成功!")

    return render(request,"login.html")

模板

舉例:

{% if per.add %}
<a href="/users/add/" class="btn btn-primary">添加用戶</a>
{% endif %}

附錄:註冊session時的數據轉換

在上面initial_session中permissions獲取到的是一個QuerySet,其格式爲:

<QuerySet [
{
'permissions__url': '/stark/crm/customer/', 
'permissions__group_id': 1, 
'permissions__action': 'list'}, 
{
'permissions__url': '/stark/crm/customer/add/', 
'permissions__group_id': 1, 
'permissions__action': 'add'}, 
{
'permissions__url': '/stark/crm/customer/(\\d+)/change/', 
'permissions__group_id': 1, 
'permissions__action': 'edit'}, 
{
'permissions__url': '/stark/crm/customer/public/', 
'permissions__group_id': 1, 
'permissions__action': 'list'},
]

我們需要把它轉換爲我們想要的格式,爲此我們需要構建一個字典:

{1: 
  {
  'urls': 
   ['/stark/crm/customer/', 
    '/stark/crm/customer/add/', 
    '/stark/crm/customer/(\\d+)/change/', 
    '/stark/crm/customer/public/', 
    '/stark/crm/customer/mycustomer/'
   ], 
  'actions': 
   ['list', 
    'add', 
    'edit', 
    'list', 
    'list']
   }

 2: 
  {
 'urls': ['/roles/'],
 'actions': ['list']}
  }
}

方法1:

#將獲取到的permissions重新組合,變爲我們需要的數據格式
permission_dict={}
for item in permissions:
    gid=item.get('permissions__group_id')

    if not gid in permission_dict:

        permission_dict[gid]={
            "urls":[item["permissions__url"],],
            "actions":[item["permissions__action"],]
        }
    else:
        permission_dict[gid]["urls"].append(item["permissions__url"])
        permission_dict[gid]["actions"].append(item["permissions__action"])

擴展

側邊菜單的權限控制

基於Django的rbac權限驗證

菜單欄的顯示

舉例菜單欄在頁面左側,當前登錄用戶有幾個權限就應該顯示幾行內容,例如當前用戶有查看學生信息的權限,那麼菜單中就應該出現學生信息的鏈接,點擊鏈接可以跳轉到相應頁面,因此是否顯示某一個鏈接只需要判斷當前用戶對這個頁面是否有list權限即可,那麼如何做呢?

在上面的註冊權限信息到session階段,我們構建了一個名爲permission_dict的字典,但permission_dict並不方便用於菜單欄顯示的判斷,因此我們在initial_session函數中再構建一個menu_permission_list列表或元祖來爲我們左側菜單欄的權限判斷做服務

代碼:
同樣在perssions.py的initial_session函數裏面

#註冊菜單權限
permissions = user.roles.all().values("permissions__url", "permissions__action", "permissions__title").distinct()

menu_permission_list = []
for item in permissions:
    if item["permissions__action"] == "list":
        menu_permission_list.append((item["permissions__url"], item["permissions__title"]))

print("menu_permission_list==>", menu_permission_list)
request.session["menu_permission_list"] = menu_permission_list

獲取"permissions__url", "permissions__action", "permissions__title"三個數據
permissions__action用於判斷是否是list
如果是的話將對應的permissions__url和permissions__title存入session
其中permissions__url用於構建a標籤中的href,permissions__title用於構建a標籤中的文本

menu_permission_list註冊的內容如下:

[
('/stark/crm/customer/', '客戶管理組'), 
('/stark/crm/customer/public/', '客戶管理組'), 
('/stark/crm/customer/mycustomer/', '客戶管理組')
]

模板代碼

<div>
     {% for item in menu_permission_list %}
          <p class="menu_btn"><a href="{{ item.0 }}">{{ item.1 }}</a></p>
     {% endfor %}
</div>

rbac組件的解耦

爲了方便使用rbac組件,同時有利於解耦,可以在rbac目錄下新建一個templates目錄,將menu.html等相關模板放入到這個templates目錄下,這樣就把每個app自己的模板都放到了同一個app目錄下。
基於Django的rbac權限驗證

找的到嗎?

在view中return一個模板通常會到主目錄的templates中取尋找相應模板,但如果沒找到則會到每個app目錄下的名爲templates的目錄中去尋找這個html模板

多個app的templates有同名模板怎麼辦?

會按照app註冊優先順序查找,先找到哪個就返回哪個,其它app下同名文件也相同
爲了避免這個問題,可以在app的templates目錄下再新建一個目錄,將模板放入這個目錄,例如rbac\templates\rbac\menu.html
在視圖中return一個模板時就要寫“rbac/users.html”


提示:側邊欄可以使用inclusion_tag來實現

發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章