Django實戰【六】—權限管理系統rbac組件實現


一、權限管理rbac組件

1.權限管理組件的實現思路

表結構分析

rbac的意思之前我詳細提過,就是基於角色的訪問權限控制,其實說白了啊,就是針對不同的用戶角色, 給他們分配了訪問哪些url的權利,因為在實際工作場景中,不同分工的人之間的業務也應該是各自來展開的。

也就是說權限本質上是一個url訪問路徑,而在我們實現的rbac組件中,權限是分配到對應的角色下,然后角色和用戶之間又是一層多對多的關系。

有人會問,既然你是想要給用戶分配不同的權限,那么為什么不直接單獨給用戶來分配權限,而是要通過角色表來呢?其實這里涉及到的是一種編程思想。

  • 如果我們用戶數量很多,權限也很多,那么這么多用戶,每一個用戶都需要單獨分配不同的權限,每個用戶的權限也可能很多,這樣在權限分配的時候會很繁瑣。
  • 而通過角色表,將不同角色的權限進行統一的划分,即時你用戶再多,權限再多,角色數量總歸不會太多。這個時候我們只需要第一次指定好權限和角色之間的對應關系,以后只需要關注用戶和角色之間的關系,這樣就大大的減少了權限分配業務量,而且整體的關系也更加明朗。

實現思路

  • 訪問權限

由於我們是要對每個用戶的權限來限制,我們想一想什么地方能夠做到呢?如果你django基礎扎實,那么你很快就能想到中間件,中間件的process_request方法就是對所有過來的請求做一些全局的處理。

那么我們又該如何是每個用戶的權限有區分呢?這種因角色不同而不同的數據肯定不可能通過全局來存儲,肯定是將對應的權限信息存放在自己獨有的空間里,這樣的話我們就不難想到session,還記的session把?每個用戶用來保存標識客戶端的信息,其實session還可以存儲其他的信息,比如我們這里的用戶權限信息等等。

  • 菜單權限

權限驗證思路有了,現在的具體情況是如果某個用戶有某個權限,我們讓他順利訪問,如果他沒有某個權限,但是他有這個url權限的鏈接顯示,結果我們給人家展示了一個大黃頁,你沒有權限。想想都角色有點腦殘是吧!用戶體驗極差,卷鋪蓋回家把。

那么這里改怎么處理呢,其實在給用戶權限的時候,也應當分配好用戶所擁有的菜單權限,也就是是夠給用戶展示的權限。具體的實現就是根據用戶權限,來確定他對應擁有的菜單權限,從而在前端頁面展示出來。

  • 訪問權限和菜單權限的注入

訪問權限和菜單權限思路都有了,就涉及到什么時候注入了,我們知道權限是針對用戶來的,那么這些權限的注入當然也是應該在用戶登錄成功的一瞬間就注入到這次會話的session中,這樣大致的過程也就迎刃而解了。

2.前期頁面准備

 為了展示二級菜單效果,我們增加了一個私戶展示頁面,和公戶頁面同屬於客戶信息展示下的二級菜單。班級課程記錄展示和學員學習記錄展示屬於教學信息展示頁面下二級菜單。

同時為了顯示沒有子菜單的一級菜單效果,還增加了一個主頁url權限,主頁沒有子菜單,而且主頁所有人都可以訪問。

主頁url

我寫在項目urls中,也就是ObCRM/urls.py中

from customer.views import customer
urlpatterns = [
    # 主頁url
    url(r'^index/', customer.Index.as_view(),name="index"),
]

主頁視圖函數

主頁視圖我放在customer下views/customer.py中了。

# 主頁
class Index(views.View):
    @method_decorator(login_required)  # 裝飾器函數驗證是否登錄
    def dispatch(self, request, *args, **kwargs):
        res = super().dispatch(request, *args, **kwargs)
        return res
    def get(self,request):
        # 展示主頁
        return render(request,"index.html")
View Code

主頁html文件

這個主頁我隨便拷貝的模板,沒有具體內容,繼承的也是base頁面,寫在customer應用下templates中。

{% extends 'BASE.html' %}
{% block head %}
    {{ block.super }}
{% endblock head %}

{% block title %}
    歡迎使用AliCRM系統
{% endblock title %}
{% block content %}
    這是主頁
{% endblock content %}

{% block js %}
    {{ block.super }}
{% endblock js %}
index.html

 私戶展示url

customer應用下urls中

# 私戶數據展示
url(r'^private/list/', customer.PrivateList.as_view(), name="private_list"),

私戶展示視圖

私戶展示我就簡單實現了搜索,添加,編輯,刪除等都是同樣的,所以這里就不再實現了。

# 私戶數據展示
class PrivateList(views.View):
    @method_decorator(login_required)
    def dispatch(self, request, *args, **kwargs):
        res = super().dispatch(request, *args, **kwargs)
        return res

    def get(self, request):
        condition = request.GET.get("condition", "")
        query = request.GET.get("q", "")
        condition = condition + "__contains"
        q = Q()  # Q實例化生成q對象,q對象可以幫我們拼接字符串為 condition__contians= xx的關鍵字參數傳到filter中。
        q.children.append((condition, query))

        if condition and query:  # 如果有查詢調參數,兩個參數都有,根據查詢參數查詢后找到數據
            all_customers = models.Customer.objects.filter(q, consultant=request.user).order_by("-pk")
        else:  # 判斷有沒有查詢參數,只有有一個沒有參數,就查詢所有公戶數據
            all_customers = models.Customer.objects.filter(consultant=request.user).order_by("-pk")

        # 開始分頁展示
        data_counts = all_customers.count()

        # 生成一個分頁對象
        paginator = Paginator(request, data_counts, 10)

        # 獲取當前頁展示數據的范圍
        try:  # 異常是否查到了數據,查到了才切片,不然會報錯
            all_customers = all_customers[paginator.start:paginator.end]
        except Exception:
            pass

        # 獲取分頁的標簽
        paginator_tag = paginator.paginate()  # 調用定義好的分頁方法

        # 獲取跳轉頁的標簽
        jump_tag = paginator.jump_page()  # 調用定義好的跳轉頁方法

        return render(request, "private_list.html",
                      {"all_customers": all_customers, "paginator_tag": paginator_tag, "jump_tag": jump_tag})
私戶展示視圖類

私戶展示html

私戶展示html,與公戶區別不大。customer應用下templates中

{% extends 'BASE.html' %}
{% load static %}
{% block head %}
    {{ block.super }}
{% endblock head %}
{% block title %}
        私戶信息展示
{% endblock title %}
{% block content %}
    <div class="row">
        <div class="col-xs-12">
            <div class="box">
                <div class="box-header">
                    <h3 class="box-title"></h3>
                    <form action="" method="get" class="navbar-form navbar-left">
                        <div class="input-group">
                            <div class="input-group-btn btn-info">
                                <select name="condition" id="search" class="btn input-group-sm btn-info"
                                        style="border: 0">
                                    <option value="" readonly>條件</option>
                                    <option value="qq_name">昵稱</option>
                                    <option value="qq">QQ號</option>
                                </select>
                            </div>
                            <input type="text" name="q" class="form-control" placeholder="Search...">
                            <span class="input-group-btn">
                                <button type="submit" id="search-btn" class="btn btn-flat">
                                    <i class="fa fa-search"></i>
                                </button>
                            </span>
                        </div>
                    </form>
                </div>
                <div class="box-body">
                    <div class="row">
                        <div class="col-sm-6">
                        </div>
                    </div>
                    <form action="" method="post">
                        {% csrf_token %}
                        <div class="input-group" style="width: 220px;margin-bottom: 5px;margin-left: 15px">
                            <select name="operate" id="operate" class="form-control btn-default">
                                <option value="">選擇批量操作</option>
                                <option value="batch_delete">批量刪除</option>
                                <option value="batch_update">批量更改客戶狀態</option>
                                {% if flag %}
                                    <option value="batch_c2p">批量公轉私</option>
                                {% else %}
                                    <option value="batch_c2p">批量公轉私</option>
                                {% endif %}
                            </select>
                            <span class="input-group-btn">
                                <button type="submit" class="btn btn-warning btn-flat">Go!</button>
                            </span>
                        </div>
                        {% if name_str %}
                            <div class="btn text-danger" id="choose_error">顧客:{{ name_str }}已經被選走了</div>
                        {% endif %}


                        <table id="example2" class="table table-bordered table-hover text-center">
                            <thead>
                            <tr>
                                <th style="width: 6%">
                                    <span>
                                        <i class="fa fa-check-square-o"></i>
                                        <input type="checkbox" name="batch_choose">
                                    </span>
                                </th>
                                <th style="width: 5%">序號</th>
                                <th>qq</th>
                                <th>姓名</th>
                                <th>電話</th>
                                <th>來源</th>
                                <th>咨詢課程</th>
                                <th>客戶狀態</th>
                                <th>銷售老師</th>
                                <th>操作</th>

                            </tr>
                            </thead>
                            <tbody>
                            {% for customer in all_customers %}
                                <tr>
                                    <td><input type="checkbox" name="choose" value="{{ customer.pk }}"></td>
                                    <td>{{ forloop.counter }}</td>
                                    <td>{{ customer.qq }}</td>
                                    <td>{{ customer.qq_name }}</td>
                                    <td>
                                        {{ customer.phone|default:"暫無" }}
                                    </td>
                                    <td>{{ customer.get_source_display|default:'暫無' }}</td>
                                    <td>{{ customer.get_course_display|default:"暫無" }}</td>
                                    <td>{{ customer.get_status_display }}</td>
                                    <td>{{ customer.consultant.username|default:'暫無' }}</td>
                                    <td>
                                        <a style="color: #00c3cc;" href="{% url 'common_edit' customer.pk %}">
                                            <i class="fa fa-edit" aria-hidden="true"></i>
                                        </a>
                                        |
                                        <a style="color: #d9534f;" href="{% url 'common_del' customer.pk %}">
                                            <i class="fa fa-trash-o"></i>
                                        </a>
                                    </td>
                                </tr>
                            {% endfor %}

                            </tbody>
                            <tfoot>
                            </tfoot>
                        </table>
                        {% if not all_customers %}
                            <h3 class="text-center">沒有相關記錄!</h3>
                        {% endif %}
                    </form>
                    <div class="pull-right" style="display:inline-block; width: 120px;margin: 22px 10px">
                        {{ jump_tag|safe }}
                    </div>

                    <div class="pull-right">
                        {{ paginator_tag|safe }}
                    </div>
                </div>
                <!-- /.box-body -->
            </div>
            <!-- /.box -->
        </div>
        <!-- /.col -->
    </div>
{% endblock content %}
{% block js %}
    {{ block.super }}
{% endblock js %}

{% block customjs %}
    <script>
        $("[name=batch_choose]").click(function () {
            var status = $(this).prop("checked");
            $("[name=choose]").prop('checked', status)
        });
        $("#choose_error").click(function () {
            $("#choose_error").css("display", "none");
        })
    </script>
    {{ jump_js|safe }}
{% endblock customjs %}
private_list.html

 沒有分配權限時頁面結構如下:

3.權限和菜單代碼實現

 rbac表結構在看一下

from django.db import models

# Create your models here.

from django.db import models
from django.contrib.auth.models import AbstractUser

# Create your models here.

# 身份分類
role_choices = (
    ("1", "董事"),
    ("2", "CEO"),
    ("3", "銷售"),
    ("4", "網咨"),
    ("5", "老師"),
    ("6", "班主任"),
)

# 擴展的用戶表
class UserInfo(AbstractUser):
    """用戶信息表:老師,助教,銷售,班主任"""
    id = models.AutoField(primary_key=True)
    gender_type = (("male", ""), ("female", ""))
    gender = models.CharField(choices=gender_type, null=True, max_length=12)
    phone = models.CharField(max_length=11, null=True, unique=True)
    role = models.ManyToManyField("Role")

    def __str__(self):
        return self.username


# 身份表
class Role(models.Model):
    title = models.CharField("職位", choices=role_choices, max_length=32)
    permission = models.ManyToManyField("Permission")

    def __str__(self):
        return self.title


# 權限表
class Permission(models.Model):
    name = models.CharField(max_length=32, verbose_name=u'權限名')
    url = models.CharField(
        max_length=300,
        verbose_name=u'權限url地址',
        null=True,
        blank=True,
        help_text=u'是否給菜單設置一個url地址'
    )
    icon = models.CharField(
        max_length=32,
        verbose_name='權限圖標',
        null=True,
        blank=True
    )
    # 指定屬於哪個父級權限
    parent = models.ForeignKey(
        'self',
        verbose_name=u'父級權限',
        null=True,
        blank=True,
        help_text=u'如果添加的是子權限,請選擇父權限'
    )

    # 指定屬於哪個menu
    menu = models.ForeignKey(to="Menu",verbose_name=u'對應菜單',blank=True,null=True)

    def __str__(self):
        return "{parent}{name}".format(name=self.name, parent="%s-->" % self.parent.name if self.parent else '')

    class Meta:
        verbose_name = u"權限表"
        verbose_name_plural = u"權限表"
        ordering = ["id"]

# 菜單表
class Menu(models.Model):
    title = models.CharField(max_length=32, verbose_name=u'菜單名')
    # 菜單顯示圖標
    icon = models.CharField(
        max_length=32,
        verbose_name='菜單圖標',
        null=True,
        blank=True
    )
    # 指定屬於哪個父級菜單
    parent = models.ForeignKey(
        'self',
        verbose_name=u'父級菜單',
        null=True,
        blank=True,
        help_text=u'如果添加的是子菜單,請選擇父菜單'
    )

    priority = models.IntegerField(
        verbose_name=u'顯示優先級',
        null=True,
        blank=True,
        help_text=u'菜單的顯示順序,優先級越小顯示越靠前'
    )

    def __str__(self):
        return "{parent}{title}".format(title=self.title, parent="%s-->" % self.parent.title if self.parent else '')

    class Meta:
        verbose_name = u"菜單表"
        verbose_name_plural = u"菜單表"
        ordering = ["priority","id"]  # 根據優先級和id來排序
rbac表結構

首先我們插入一些模擬的數據,數據如下:

其實用戶角色,角色權限之間的關系結構如下

中間件驗證權限

權限認證我們之前想出的解決方法,是通過中間件對所有請求進行權限驗證,並設置白名單,放行某一些通用的權限。

白名單配置

在項目下settings文件中配置白名單

# 配置白名單
WHITE_URL_LIST = [
    r'^/admin/.*',  # 放行admin應用url
    r'^/rbac/login/',
    r'^/rbac/register/',
    r'^/rbac/get_auth_img/',
]

 

中間件寫法

在rbac應用下新建middlewares文件下的permission.py中自定義我們的中間件,用來驗證用戶登錄和用戶權限,之后設計的面包屑也是在這里面設置,這個之后再說。

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

class PermissionMiddleware(MiddlewareMixin):
    """自定義權限分配中間件"""
    def process_request(self,request):
        # 對權限進行校驗
        # 1. 當前訪問的URL在不在白名單
        for i in settings.WHITE_URL_LIST:
            ret = re.search(i,request.path)
            if ret:
                return None

        # 獲取當前用戶的所有權限
        user = request.user
        if not user:
            return redirect("login")

        # 獲取用戶權限列表
        permissions_list = request.session.get("permissions_list")
        if permissions_list:
            for permission in permissions_list:  # 遍歷權限列表,匹配當前路徑,匹配上放行
                url = permission['url']
                if re.search(f"^{url}",request.path):
                    # 請求子權限路徑,父級權限和父級菜單激活樣式設置
                    request.show_id = permission["parent_id"]

                    return None

        # 沒有匹配上,提示沒有權限
        return HttpResponse("沒有權限")    

 

在settings中注冊中間件,中間件才會生效。

MIDDLEWARE = [
    'rbac.middlewares.permission.PermissionMiddleware'  # 配置驗證用戶和用戶權限的中間件
]

用戶權限和菜單權限注入

既然配置了中間件,那么必然需要給用戶注入他有的權限,權限注入在用戶登錄成功后一瞬間注入。我們在rbac下新建一個severce文件夾,文件夾下新建init_permission.py,定義權限注入函數。

from rbac import models


def init_permission(request, user):
    """
    在登錄函數驗證通過后,在session中注入用戶權限和用戶菜單權限。
    :param request: 用戶登錄請求時的wsgi請求對象
    :param user: 用戶登錄驗證通過后的用戶賬號名
    :return: none,
    """
    permissions = models.Permission.objects.filter(role__userinfo__username=user).values("pk", "name", "url", "icon","parent_id", "menu__pk","menu__title", "menu__icon","menu__priority","menu__parent__id").order_by("menu__priority").distinct()
    # print(permissions)

    permissions_list = []  # 定義權限列表
    menus_dict = {}  # 定義菜單列表

    print("當前用戶權限>>>")
    for permission in permissions:  # 遍歷權限列表

        # 獲取用戶權限的數據結構,列表套字典,一個字典代表一個權限
        permissions_list.append({
            "pk": permission["pk"],
            "name": permission["name"],
            "url": permission["url"],
            "parent_id": permission["parent_id"],
        })
        print(permission["name"].center(8, " "), ":", permission["url"])

        # 獲取菜單權限的數據結構,字典套字典,一個字典代表一個菜單
        if permission["menu__pk"]:  # 如果父級菜單已存在,在兒子列表中添加
            if permission["menu__pk"] in menus_dict:  # ruguo
                menus_dict[permission["menu__pk"]]["children"].append({
                    "pk": permission["pk"],
                    "name": permission["name"],
                    "url": permission["url"],
                    "icon": permission["icon"],
                    "parent_id": permission["parent_id"],
                })
            else:  # 父級菜單不存在,則添加一個父級菜單。
                menus_dict[permission["menu__pk"]] = {
                    "pk": permission["menu__pk"],
                    "title": permission["menu__title"],
                    "icon": permission["menu__icon"],
                    "parent_id": permission["menu__parent__id"],
                    "priority": permission["menu__priority"],
                    "children": [{
                        "pk": permission["pk"],
                        "name": permission["name"],
                        "url": permission["url"],
                        "icon": permission["icon"],
                        "parent_id": permission["parent_id"],
                    }]  # 定義一個父級菜單包含所有兒子菜單的空列表
                }

    # print("權限列表",permissions_list)
    # print("菜單權限",menus_dict)

    # session中注入權限數據
    request.session["permissions_list"] = permissions_list

    # session中注入菜單數據
    request.session["menus_dict"] = menus_dict
init_perimission函數

權限注入函數在登錄視圖驗證成功后調用

from rbac.service import init_permission

# 用戶登錄視圖類
class Login(views.View):
    def get(self, request):
        # get請求返回登錄頁面
        return render(request, "login.html")

    def post(self, request):
        data = request.POST
        # 獲取用戶登錄信息
        authcode = data.get("authcode")
        username = data.get("username")
        password = data.get("password")
        # 驗證碼不正確
        if request.session.get("authcode").upper() != authcode.upper():
            return JsonResponse({"status": "1"})
        else:
            print(username)
            # 使用django的auth模塊進行用戶名密碼驗證
            user = auth.authenticate(username=username, password=password)
            if user:
                # 將用戶名存入session中
                request.session["user"] = username
                auth.login(request, user)  # 將用戶對象存入request對象的屬性中
                init_permission.init_permission(request, user)  # 調用權限注入函數,注入用戶權限
                return JsonResponse({"status": "2"})
            else:
                return JsonResponse({"status": "3"})
登錄視圖類Login

自定義標簽來生成菜單

通過權限注入和菜單注入后,我們可以在session中獲取用戶的菜單權限數據,還記得我們的inclusion_tag吧!這里簡單回顧一下。

inclusion_tag用來裝飾一個函數成為自定義標簽,把函數的返回值放在字典里面,調用了render方法渲染到指定的html文件中,並把這個html文件在前端調用inclusion_tag的位置當成組件使用。

說完那就看代碼吧!

  • 在rbac應用下templates中新建menu.html
<!DOCTYPE html>
<html lang="en">
<head>
    <meta charset="UTF-8">
    <title>Title</title>
</head>
<body>

{% for menu in menus_dict.values %}
    {% if menu.children|length == 1 %}
        <li class="{{ menu.class }}">
            <a href="{{ menu.children.0.url }}">
                <i class="fa {{ menu.icon }}"></i>
                <span>{{ menu.title }}</span>
                <span class="pull-right-container">
                </span>
            </a>
        </li>
    {% else %}
        <li class="treeview {{ menu.class }}">
            <a href="">
                <i class="fa {{ menu.icon }}"></i>
                <span>{{ menu.title }}</span>
                <span class="pull-right-container">
                    <span class="fa fa-angle-left pull-right"></span>
                </span>
            </a>
            <ul class="treeview-menu">
                {% for child_menu in menu.children %}
                    <li class="{{ child_menu.class }}">
                        <a href="{{ child_menu.url }}">
                            <i class="fa fa-circle-o"></i> {{ child_menu.name }}
                        </a>
                    </li>
                {% endfor %}
            </ul>
        </li>
    {% endif %}
{% endfor %}



<script></script>
</body>
</html>
menu.html
  • rbac應用下新建templatetags文件夾(名字固定不可變)中新建rbac.py,代碼寫法如下
import re
from django import template

register = template.Library()


@register.inclusion_tag("menu.html")
def get_menu(request):
    menus_dict = request.session.get("menus_dict")
    for menu in menus_dict.values():  # 遍歷菜單
        for child in menu.get("children"):  # 遍歷子菜單的children列表
            if re.match(child["url"],request.path) or request.show_id == child["pk"]:  # 對當前路徑進行匹配,匹配上了給二級菜單加上激活樣式,同時給父級菜單也加上激活樣式,或者當前路徑是某個二級菜單權限的子權限
                menu["class"] = "active"
                child["class"] = "active"
    return {"menus_dict": menus_dict}

 

這里我們做了一個樣式激活處理,就是判斷當前來的請求和菜單中的url去匹配,匹配上為這個二級菜單以及父級菜單加上激活樣式"active"。

  • 前端base頁面使用生成菜單標簽
<ul class="sidebar-menu" data-widget="tree">
     <li class="header">操作菜單</li>
        {% load rbac %}
        {% get_menu request %}
</ul>

 動態菜單實現后效果演示

 


免責聲明!

本站轉載的文章為個人學習借鑒使用,本站對版權不負任何法律責任。如果侵犯了您的隱私權益,請聯系本站郵箱yoyou2525@163.com刪除。



 
粵ICP備18138465號   © 2018-2025 CODEPRJ.COM