权限组件之权限控制

1. 问:为什么程序需要权限控制?

   答:生活中的权限限制,① 看灾难片电影《2012》中富人和权贵有权登上诺亚方舟,穷苦老百姓只有等着灾难的来临;② 屌丝们,有没有想过为什么那些长得漂亮身材好的姑娘在你身边不存在呢?因为有钱人和漂亮姑娘都是珍贵稀有的,稀有的人在一起玩耍和解锁各种姿势。而你,无权拥有他们,只能自己玩自己了。
程序开发时的权限控制,对于不同用户使用系统时候就应该有不同的功能,如:

SRE实战 互联网时代守护先锋,助力企业售后服务体系运筹帷幄!一键直达领取阿里云限量特价优惠。
  • 普通员工
  • 部门主管
  • 总监
  • 总裁

所以,只要有不同角色的人员来使用系统,那么就肯定需要权限系统。

2. 问:为什么要开发权限组件?

   答:假设你今年25岁,从今天开始写代码到80岁,每年写5个项目,那么你的一生就会写275个项目,保守估计其中应该有150+个都需要用到权限控制,为了以后不再重复的写代码,所以就开发一个权限组件以便之后55年的岁月中使用。 亲,不要太较真哦,你觉得程序员能到80岁么,哈哈哈哈哈哈哈 
偷偷告诉你:老程序员开发速度快,其中一个原因是经验丰富,另外一个就是他自己保留了很多组件,新系统开发时,只需把组件拼凑起来基本就可以完成。

3. 问:web开发中权限指的是什么?

   答:web程序是通过 url 的切换来查看不同的页面(功能),所以权限指的其实就是URL,对url控制就是对权限的控制。

结论:一个人有多少个权限就取决于他有多少个URL的访问权限。

权限表结构设计:第一版

问答环节中已得出权限就是URL的结论,那么就可以开始设计表结构了。

  • 一个用户可以有多个权限。
  • 一个权限可以分配给多个用户。

你设计的表结构大概会是这个样子:

权限的管理,权限组件之权限控制 随笔 第1张

权限的管理,权限组件之权限控制 随笔 第2张

权限的管理,权限组件之权限控制 随笔 第3张

现在,此时此刻是不是觉得自己设计出的表结构棒棒哒!!!

But,无论是是否承认,你还是too young too native,因为老汉腚眼一看就有问题....

问题:假设 “老男孩”和“Alex” 这俩货都是老板,老板的权限一定是非常多。那么试想,如果给这俩货分配权限时需要在【用户权限关系表中】添加好多条数据。假设再次需要对老板的权限进行修改时,又需要在【用户权限关系表】中找到这俩人所有的数据进行更新,太他妈烦了吧!!! 类似的,如果给其他相同角色的人来分配权限时,必然会非常繁琐。

权限表结构设计:第二版

聪明机智的一定在上述的表述中看出了写门道,如果对用户进行角色的划分,然后对角色进行权限的分配,这不就迎刃而解了么。

  • 一个人可以有多个角色。
  • 一个角色可以有多个人。
  • 一个角色可以有多个权限。
  • 一个权限可以分配给多个角色。

表结构设计:

权限的管理,权限组件之权限控制 随笔 第4张

权限的管理,权限组件之权限控制 随笔 第5张

权限的管理,权限组件之权限控制 随笔 第6张

权限的管理,权限组件之权限控制 随笔 第7张

权限的管理,权限组件之权限控制 随笔 第8张

 这次调整之后,由原来的【基于用户的权限控制】转换成【基于角色的权限控制】,以后再进行分配权限时只需要给指定角色分配一次权限,给众多用户再次分配指定角色即可。

权限的管理,权限组件之权限控制 随笔 第9张
from django.db import models


class Permission(models.Model):
    """
    权限表
    """
    title = models.CharField(verbose_name='标题', max_length=32)
    url = models.CharField(verbose_name='含正则的URL', max_length=128)

    def __str__(self):
        return self.title


class Role(models.Model):
    """
    角色
    """
    title = models.CharField(verbose_name='角色名称', max_length=32)
    permissions = models.ManyToManyField(verbose_name='拥有的所有权限', to='Permission', blank=True)

    def __str__(self):
        return self.title


class UserInfo(models.Model):
    """
    用户表
    """
    name = models.CharField(verbose_name='用户名', max_length=32)
    password = models.CharField(verbose_name='密码', max_length=64)
    email = models.CharField(verbose_name='邮箱', max_length=32)
    roles = models.ManyToManyField(verbose_name='拥有的所有角色', to='Role', blank=True)

    def __str__(self):
        return self.name
models.py 示例

小伙子,告诉你一个事实,不经意间,你居然设计出了一个经典的权限访问控制系统:rbac(Role-Based Access Control)基于角色的权限访问控制。你这么优秀,为什么不来老男孩IT教育?路飞学城也行呀! 哈哈哈哈。

注意:现在的设计还不是最终版,但之后的设计都是在此版本基础上扩增的,为了让大家能够更好的理解,我们暂且再此基础上继续开发,直到遇到无法满足的情况,再进行整改。

源码示例猛击下载

客户管理之权限控制

学习知识最好的方式就是试错,坑踩多了那么学到的知识自然而然就多了,所以接下里下来我们用《客户管理》系统为示例,提出功能并实现,并且随着功能越来越多,一点点来找出问题,并解决问题。

目录结构:

luffy_permission/
├── db.sqlite3
├── luffy_permission
│   ├── __init__.py
│   ├── settings.py
│   ├── urls.py
│   └── wsgi.py
├── manage.py
├── rbac            # 权限组件,便于以后应用到其他系统
│   ├── __init__.py
│   ├── admin.py
│   ├── apps.py
│   ├── models.py
│   ├── tests.py
│   └── views.py
├── templates
└── web            # 客户管理业务
    ├── __init__.py
    ├── admin.py
    ├── apps.py
    ├── models.py
    ├── tests.py
    └── views.py
权限的管理,权限组件之权限控制 随笔 第11张
from django.db import models


class Permission(models.Model):
    """
    权限表
    """
    title = models.CharField(verbose_name='标题', max_length=32)
    url = models.CharField(verbose_name='含正则的URL', max_length=128)

    def __str__(self):
        return self.title


class Role(models.Model):
    """
    角色
    """
    title = models.CharField(verbose_name='角色名称', max_length=32)
    permissions = models.ManyToManyField(verbose_name='拥有的所有权限', to='Permission', blank=True)

    def __str__(self):
        return self.title


class UserInfo(models.Model):
    """
    用户表
    """
    name = models.CharField(verbose_name='用户名', max_length=32)
    password = models.CharField(verbose_name='密码', max_length=64)
    email = models.CharField(verbose_name='邮箱', max_length=32)
    roles = models.ManyToManyField(verbose_name='拥有的所有角色', to='Role', blank=True)

    def __str__(self):
        return self.name
rbac/models.py 权限的管理,权限组件之权限控制 随笔 第13张
from django.db import models


class Customer(models.Model):
    """
    客户表
    """
    name = models.CharField(verbose_name='姓名', max_length=32)
    age = models.CharField(verbose_name='年龄', max_length=32)
    email = models.EmailField(verbose_name='邮箱', max_length=32)
    company = models.CharField(verbose_name='公司', max_length=32)


class Payment(models.Model):
    """
    付费记录
    """
    customer = models.ForeignKey(verbose_name='关联客户', to='Customer')
    money = models.IntegerField(verbose_name='付费金额')
    create_time = models.DateTimeField(verbose_name='付费时间', auto_now_add=True)
web/models.py

《客户管理》系统截图:基本增删改查和Excel导入源码下载猛击这里

权限的管理,权限组件之权限控制 随笔 第15张

权限的管理,权限组件之权限控制 随笔 第16张

权限的管理,权限组件之权限控制 随笔 第17张

以上简易版客户管理系统中的URL有:

  • 客户管理
    • 客户列表:/customer/list/
    • 添加客户:/customer/add/
    • 删除客户:/customer/list/(?P<cid>\d+)/
    • 修改客户:/customer/edit/(?P<cid>\d+)/
    • 批量导入:/customer/import/
    • 下载模板:/customer/tpl/
  • 账单管理
    • 账单列表:/payment/list/
    • 添加账单:/payment/add/
    • 删除账单:/payment/del/(?P<pid>\d+)/
    • 修改账单:/payment/edit/<?P<pid>\d+/

那么接下来,我们就在权限组件中录入相关信息:

  • 录入权限
  • 创建用户
  • 创建角色
  • 用户分配角色
  • 角色分配权限

这么一来,用户登录时,就可以根据自己的【用户】找到所有的角色,再根据角色找到所有的权限,再将权限信息放入session,以后每次访问时候需要先去session检查是否有权访问。

已录入权限数据源码下载猛击这里

含用户登录权限源码下载:猛击这里(简易版)

含用户登录权限源码下载猛击这里

至此,基本的权限控制已经完成,基本流程为:

  • 用户登录,获取权限信息并放入session
  • 用户访问,在中间件从session中获取用户权限信息,并进行权限验证。

所有示例中的账户信息:

账户一:
    用户名:alex
       密码:123

账户二:
    用户名:wupeiqi
       密码:123

客户管理之动态“一级”菜单

上述过程中的菜单是在程序中定义好的,无法根据用户权限进行动态配置。

那么,接下来我们来完成一个 “单级菜单”的功能:

权限的管理,权限组件之权限控制 随笔 第18张
from django.db import models


class Permission(models.Model):
    """
    权限表
    """
    title = models.CharField(verbose_name='标题', max_length=32)
    url = models.CharField(verbose_name='含正则的URL', max_length=128)

    icon = models.CharField(verbose_name='图标', max_length=32, null=True, blank=True, help_text='菜单才设置图标')
    is_menu = models.BooleanField(verbose_name='是否是菜单', default=False)

    def __str__(self):
        return self.title


class Role(models.Model):
    """
    角色
    """
    title = models.CharField(verbose_name='角色名称', max_length=32)
    permissions = models.ManyToManyField(verbose_name='拥有的所有权限', to='Permission', blank=True)

    def __str__(self):
        return self.title


class UserInfo(models.Model):
    """
    用户表
    """
    name = models.CharField(verbose_name='用户名', max_length=32)
    password = models.CharField(verbose_name='密码', max_length=64)
    email = models.CharField(verbose_name='邮箱', max_length=32)
    roles = models.ManyToManyField(verbose_name='拥有的所有角色', to='Role', blank=True)

    def __str__(self):
        return self.name
表结构

 这样可以开发出一个单级菜单的示例:

 权限的管理,权限组件之权限控制 随笔 第20张权限的管理,权限组件之权限控制 随笔 第21张

示例源码下载:猛击这里(无默认选中)

示例源码下载猛击这里(含默认选中)

客户管理之动态“二级”菜单

对于功能比较少的应用程序 “一级菜单” 基本可以满足需求,但是功能多的程序就需要 “二级菜单” 了,并且访问时候需要默认选中指定菜单。

权限的管理,权限组件之权限控制 随笔 第22张
from django.db import models


class Menu(models.Model):
    """
    菜单
    """
    title = models.CharField(verbose_name='菜单', max_length=32)
    icon = models.CharField(verbose_name='图标', max_length=32)

    def __str__(self):
        return self.title


class Permission(models.Model):
    """
    权限表
    """
    title = models.CharField(verbose_name='标题', max_length=32)
    url = models.CharField(verbose_name='含正则的URL', max_length=128)

    menu = models.ForeignKey(verbose_name='菜单', to='Menu', null=True, blank=True, help_text='null表示非菜单')

    def __str__(self):
        return self.title


class Role(models.Model):
    """
    角色
    """
    title = models.CharField(verbose_name='角色名称', max_length=32)
    permissions = models.ManyToManyField(verbose_name='拥有的所有权限', to='Permission', blank=True)

    def __str__(self):
        return self.title


class UserInfo(models.Model):
    """
    用户表
    """
    name = models.CharField(verbose_name='用户名', max_length=32)
    password = models.CharField(verbose_name='密码', max_length=64)
    email = models.CharField(verbose_name='邮箱', max_length=32)
    roles = models.ManyToManyField(verbose_name='拥有的所有角色', to='Role', blank=True)

    def __str__(self):
        return self.name
表结构

示例效果:

权限的管理,权限组件之权限控制 随笔 第24张权限的管理,权限组件之权限控制 随笔 第25张 

示例源码下载猛击这里

示例源码下载猛击这里(路飞线上录制代码示例)

客户管理之默认展开非菜单URL

由于很多URL都是不能作为菜单,所以当点击该类功能时,是无法默认展开菜单的,如:

  • 删除
  • 修改
  • ...

权限的管理,权限组件之权限控制 随笔 第26张权限的管理,权限组件之权限控制 随笔 第27张

此类页面只能通过菜单页面二次点击才能跳转,此时也应该为他们默认展开原菜单。

权限的管理,权限组件之权限控制 随笔 第28张
from django.db import models


class Menu(models.Model):
    """
    菜单
    """
    title = models.CharField(verbose_name='菜单', max_length=32)
    icon = models.CharField(verbose_name='图标', max_length=32)

    def __str__(self):
        return self.title


class Permission(models.Model):
    """
    权限表
    """
    title = models.CharField(verbose_name='标题', max_length=32)
    url = models.CharField(verbose_name='含正则的URL', max_length=128)

    pid = models.ForeignKey(verbose_name='默认选中权限', to='Permission', related_name='ps', null=True, blank=True,
                            help_text="对于无法作为菜单的URL,可以为其选择一个可以作为菜单的权限,那么访问时,则默认选中此权限",
                            limit_choices_to={'menu__isnull': False})
    
    menu = models.ForeignKey(verbose_name='菜单', to='Menu', null=True, blank=True, help_text='null表示非菜单')

    def __str__(self):
        return self.title


class Role(models.Model):
    """
    角色
    """
    title = models.CharField(verbose_name='角色名称', max_length=32)
    permissions = models.ManyToManyField(verbose_name='拥有的所有权限', to='Permission', blank=True)

    def __str__(self):
        return self.title


class UserInfo(models.Model):
    """
    用户表
    """
    name = models.CharField(verbose_name='用户名', max_length=32)
    password = models.CharField(verbose_name='密码', max_length=64)
    email = models.CharField(verbose_name='邮箱', max_length=32)
    roles = models.ManyToManyField(verbose_name='拥有的所有角色', to='Role', blank=True)

    def __str__(self):
        return self.name
表结构

功能完成后的示例如下:
权限的管理,权限组件之权限控制 随笔 第30张权限的管理,权限组件之权限控制 随笔 第31张

示例源码下载猛击这里

示例源码下载猛击这里(路飞线上录制代码示例)

客户管理之访问路径导航

如果想要保留放的地址,那么就可以通过在权限配置中获取此功能,示例如下:

 权限的管理,权限组件之权限控制 随笔 第32张权限的管理,权限组件之权限控制 随笔 第33张

示例源码下载猛击这里

客户管理之 权限粒度控制按钮级别

不同用户登录系统时候,根据权限不同来控制是否限制指定按钮,如:

权限的管理,权限组件之权限控制 随笔 第34张权限的管理,权限组件之权限控制 随笔 第35张

示例源码下载猛击这里 

示例源码下载猛击这里(路飞线上录制代码示例)

客户管理之 编辑权限和分配权限

给用户进行权限的分配。

1. 用户和角色管理

权限的管理,权限组件之权限控制 随笔 第36张 

示例源码下载猛击这里(用户和角色管理)

2. 一级菜单

权限的管理,权限组件之权限控制 随笔 第37张权限的管理,权限组件之权限控制 随笔 第38张

 

示例源码下载猛击这里(菜单和权限管理之一级菜单)

3. 二级菜单管理

 权限的管理,权限组件之权限控制 随笔 第39张

示例源码下载猛击这里(菜单和权限管理之二级菜单)

4. 三级菜单管理(权限管理)

权限的管理,权限组件之权限控制 随笔 第40张

示例源码下载猛击这里(菜单和权限管理之三级菜单)

5. django formset示例

权限的管理,权限组件之权限控制 随笔 第41张

源码示例下载猛击这里(django formset实现批量添加和编辑)

6. 批量操作权限的显示

权限的管理,权限组件之权限控制 随笔 第42张

源码示例下载猛击这里(批量操作权限的显示)

7. 批量操作权限:添加、删除、更新

 权限的管理,权限组件之权限控制 随笔 第43张 

源码示例下载猛击这里(权限的批量增删改) 

8. 权限分配

权限的管理,权限组件之权限控制 随笔 第44张

源码示例下载猛击这里(权限分配)

RBAC组件应用及文档

"""

1. 将rbac组件拷贝项目。


2. 将rbac/migrations目录中的数据库迁移记录删除


3. 业务系统中用户表结构的设计

    业务表结构中的用户表需要和rbac中的用户有继承关系,如:

    rbac/models.py
        class UserInfo(models.Model):
            # 用户表
            name = models.CharField(verbose_name='用户名', max_length=32)
            password = models.CharField(verbose_name='密码', max_length=64)
            email = models.CharField(verbose_name='邮箱', max_length=32)
            roles = models.ManyToManyField(verbose_name='拥有的所有角色', to=Role, blank=True) 严重提醒 Role 不要加引号

            def __str__(self):
                return self.name

            class Meta:
                # django以后再做数据库迁移时,不再为UserInfo类创建相关的表以及表结构了。
                # 此类可以当做"父类",被其他Model类继承。
                abstract = True

    业务/models.py
        class UserInfo(RbacUserInfo):
            phone = models.CharField(verbose_name='联系方式', max_length=32)
            level_choices = (
                (1, 'T1'),
                (2, 'T2'),
                (3, 'T3'),
            )
            level = models.IntegerField(verbose_name='级别', choices=level_choices)

            depart = models.ForeignKey(verbose_name='部门', to='Department')

4. 讲业务系统中的用户表的路径写到配置文件。

    # 业务中的用户表
    RBAC_USER_MODLE_CLASS = "app01.models.UserInfo"

    用于在rbac分配权限时,读取业务表中的用户信息。


5. 业务逻辑开发
    将所有的路由都设置一个name,如:
            url(r'^login/$', account.login, name='login'),
            url(r'^logout/$', account.logout, name='logout'),

            url(r'^index/$', account.index, name='index'),

            url(r'^user/list/$', user.user_list, name='user_list'),
            url(r'^user/add/$', user.user_add, name='user_add'),
            url(r'^user/edit/(?P<pk>\d+)/$', user.user_edit, name='user_edit'),
            url(r'^user/del/(?P<pk>\d+)/$', user.user_del, name='user_del'),
            url(r'^user/reset/password/(?P<pk>\d+)/$', user.user_reset_pwd, name='user_reset_pwd'),

            url(r'^host/list/$', host.host_list, name='host_list'),
            url(r'^host/add/$', host.host_add, name='host_add'),
            url(r'^host/edit/(?P<pk>\d+)/$', host.host_edit, name='host_edit'),
            url(r'^host/del/(?P<pk>\d+)/$', host.host_del, name='host_del'),
    用于反向生成URL以及粒度控制到按钮级别的权限控制。

6. 权限信息录入
    - 在url中添加rbac的路由分发,注意:必须设置namespace
        urlpatterns = [
            ...
            url(r'^rbac/', include('rbac.urls', namespace='rbac')),

        ]

    - rbac提供的地址进行操作
        - http://127.0.0.1:8000/rbac/menu/list/
        - http://127.0.0.1:8000/rbac/role/list/
        - http://127.0.0.1:8000/rbac/distribute/permissions/

    相关配置:自动发现URL时,排除的URL:

        # 自动化发现路由中URL时,排除的URL
        AUTO_DISCOVER_EXCLUDE = [
            '/admin/.*',
            '/login/',
            '/logout/',
            '/index/',
        ]


7. 编写用户登录的逻辑【进行权限初始化】

    from django.shortcuts import render, redirect
    from app01 import models
    from rbac.service.init_permission import init_permission


    def login(request):
        if request.method == 'GET':
            return render(request, 'login.html')

        user = request.POST.get('username')
        pwd = request.POST.get('password')

        user_object = models.UserInfo.objects.filter(name=user, password=pwd).first()
        if not user_object:
            return render(request, 'login.html', {'error': '用户名或密码错误'})

        # 用户权限信息的初始化
        init_permission(user_object, request)

        return redirect('/index/')


    相关配置: 权限和菜单的session key:

        setting.py
            PERMISSION_SESSION_KEY = "luffy_permission_url_list_key"
            MENU_SESSION_KEY = "luffy_permission_menu_key"

8. 编写一个首页的逻辑

    def index(request):
        return render(request, 'index.html')


    相关配置:需要登录但无需权限的URL

        # 需要登录但无需权限的URL
        NO_PERMISSION_LIST = [
            '/index/',
            '/logout/',
        ]

9. 通过中间件进行权限校验

    # 权限校验
    MIDDLEWARE = [
        'django.middleware.security.SecurityMiddleware',
        'django.contrib.sessions.middleware.SessionMiddleware',
        'django.middleware.common.CommonMiddleware',
        'django.middleware.csrf.CsrfViewMiddleware',
        'django.contrib.auth.middleware.AuthenticationMiddleware',
        'django.contrib.messages.middleware.MessageMiddleware',
        'django.middleware.clickjacking.XFrameOptionsMiddleware',
        'rbac.middlewares.rbac.RbacMiddleware',
    ]

    # 白名单,无需登录就可以访问
    VALID_URL_LIST = [
        '/login/',
        '/admin/.*'
    ]


10. 粒度到按钮级别的控制

        {% extends 'layout.html' %}
        {% load rbac %}

        {% block content %}
            <div class="luffy-container">
                <div class="btn-group" style="margin: 5px 0">

                    {% if request|has_permission:'host_add' %}
                        <a class="btn btn-default" href="{% memory_url request 'host_add' %}">
                            <i class="fa fa-plus-square" aria-hidden="true"></i> 添加主机
                        </a>
                    {% endif %}

                </div>
                <table class="table table-bordered table-hover">
                    <thead>
                    <tr>
                        <th>主机名</th>
                        <th>IP</th>
                        <th>部门</th>
                        {% if request|has_permission:'host_edit' or request|has_permission:'host_del' %}
                            <th>操作</th>
                        {% endif %}

                    </tr>
                    </thead>
                    <tbody>
                    {% for row in host_queryset %}
                        <tr>
                            <td>{{ row.hostname }}</td>
                            <td>{{ row.ip }}</td>
                            <td>{{ row.depart.title }}</td>
                            {% if request|has_permission:'host_edit' or request|has_permission:'host_del' %}
                                <td>
                                    {% if request|has_permission:'host_edit' %}
                                        <a style="color: #333333;" href="{% memory_url request 'host_edit' pk=row.id %}">
                                            <i class="fa fa-edit" aria-hidden="true"></i></a>
                                    {% endif %}
                                    {% if request|has_permission:'host_del' %}
                                        <a style="color: #d9534f;" href="{% memory_url request 'host_del' pk=row.id %}"><i
                                                class="fa fa-trash-o"></i></a>
                                    {% endif %}
                                </td>
                            {% endif %}
                        </tr>
                    {% endfor %}
                    </tbody>
                </table>
            </div>

        {% endblock %}




总结,目的是希望在任意系统中应用权限系统。
    - 用户登录 + 用户首页 + 用户注销 业务逻辑
    - 项目业务逻辑开发
        注意:开发时候灵活的去设置layout.html中的两个inclusion_tag
            <div class="pg-body">
                <div class="left-menu">
                    <div class="menu-body">
                        {% multi_menu request %}  # 开发时,去掉;上下时,取回。
                    </div>
                </div>
                <div class="right-body">
                    <div>
                        {% breadcrumb request %} # 开发时,去掉;上下时,取回。
                    </div>
                    {% block content %} {% endblock %}
                </div>
            </div>
    - 权限信息的录入
    - 配置文件
        # 注册APP
        INSTALLED_APPS = [
            ...
            'rbac.apps.RbacConfig'
        ]
        # 应用中间件
        MIDDLEWARE = [
            ...
            'rbac.middlewares.rbac.RbacMiddleware',
        ]

        # 业务中的用户表
        RBAC_USER_MODLE_CLASS = "app01.models.UserInfo"
        # 权限在Session中存储的key
        PERMISSION_SESSION_KEY = "luffy_permission_url_list_key"
        # 菜单在Session中存储的key
        MENU_SESSION_KEY = "luffy_permission_menu_key"

        # 白名单
        VALID_URL_LIST = [
            '/login/',
            '/admin/.*'
        ]

        # 需要登录但无需权限的URL
        NO_PERMISSION_LIST = [
            '/index/',
            '/logout/',
        ]

        # 自动化发现路由中URL时,排除的URL
        AUTO_DISCOVER_EXCLUDE = [
            '/admin/.*',
            '/login/',
            '/logout/',
            '/index/',
        ]

    - 粒度到按钮级别的控制
"""
,

1. 问:为什么程序需要权限控制?

   答:生活中的权限限制,① 看灾难片电影《2012》中富人和权贵有权登上诺亚方舟,穷苦老百姓只有等着灾难的来临;② 屌丝们,有没有想过为什么那些长得漂亮身材好的姑娘在你身边不存在呢?因为有钱人和漂亮姑娘都是珍贵稀有的,稀有的人在一起玩耍和解锁各种姿势。而你,无权拥有他们,只能自己玩自己了。
程序开发时的权限控制,对于不同用户使用系统时候就应该有不同的功能,如:

  • 普通员工
  • 部门主管
  • 总监
  • 总裁

所以,只要有不同角色的人员来使用系统,那么就肯定需要权限系统。

2. 问:为什么要开发权限组件?

   答:假设你今年25岁,从今天开始写代码到80岁,每年写5个项目,那么你的一生就会写275个项目,保守估计其中应该有150+个都需要用到权限控制,为了以后不再重复的写代码,所以就开发一个权限组件以便之后55年的岁月中使用。 亲,不要太较真哦,你觉得程序员能到80岁么,哈哈哈哈哈哈哈 
偷偷告诉你:老程序员开发速度快,其中一个原因是经验丰富,另外一个就是他自己保留了很多组件,新系统开发时,只需把组件拼凑起来基本就可以完成。

3. 问:web开发中权限指的是什么?

   答:web程序是通过 url 的切换来查看不同的页面(功能),所以权限指的其实就是URL,对url控制就是对权限的控制。

结论:一个人有多少个权限就取决于他有多少个URL的访问权限。

权限表结构设计:第一版

问答环节中已得出权限就是URL的结论,那么就可以开始设计表结构了。

  • 一个用户可以有多个权限。
  • 一个权限可以分配给多个用户。

你设计的表结构大概会是这个样子:

权限的管理,权限组件之权限控制 随笔 第45张

权限的管理,权限组件之权限控制 随笔 第46张

权限的管理,权限组件之权限控制 随笔 第47张

现在,此时此刻是不是觉得自己设计出的表结构棒棒哒!!!

But,无论是是否承认,你还是too young too native,因为老汉腚眼一看就有问题....

问题:假设 “老男孩”和“Alex” 这俩货都是老板,老板的权限一定是非常多。那么试想,如果给这俩货分配权限时需要在【用户权限关系表中】添加好多条数据。假设再次需要对老板的权限进行修改时,又需要在【用户权限关系表】中找到这俩人所有的数据进行更新,太他妈烦了吧!!! 类似的,如果给其他相同角色的人来分配权限时,必然会非常繁琐。

权限表结构设计:第二版

聪明机智的一定在上述的表述中看出了写门道,如果对用户进行角色的划分,然后对角色进行权限的分配,这不就迎刃而解了么。

  • 一个人可以有多个角色。
  • 一个角色可以有多个人。
  • 一个角色可以有多个权限。
  • 一个权限可以分配给多个角色。

表结构设计:

权限的管理,权限组件之权限控制 随笔 第48张

权限的管理,权限组件之权限控制 随笔 第49张

权限的管理,权限组件之权限控制 随笔 第50张

权限的管理,权限组件之权限控制 随笔 第51张

权限的管理,权限组件之权限控制 随笔 第52张

 这次调整之后,由原来的【基于用户的权限控制】转换成【基于角色的权限控制】,以后再进行分配权限时只需要给指定角色分配一次权限,给众多用户再次分配指定角色即可。

权限的管理,权限组件之权限控制 随笔 第53张
from django.db import models


class Permission(models.Model):
    """
    权限表
    """
    title = models.CharField(verbose_name='标题', max_length=32)
    url = models.CharField(verbose_name='含正则的URL', max_length=128)

    def __str__(self):
        return self.title


class Role(models.Model):
    """
    角色
    """
    title = models.CharField(verbose_name='角色名称', max_length=32)
    permissions = models.ManyToManyField(verbose_name='拥有的所有权限', to='Permission', blank=True)

    def __str__(self):
        return self.title


class UserInfo(models.Model):
    """
    用户表
    """
    name = models.CharField(verbose_name='用户名', max_length=32)
    password = models.CharField(verbose_name='密码', max_length=64)
    email = models.CharField(verbose_name='邮箱', max_length=32)
    roles = models.ManyToManyField(verbose_name='拥有的所有角色', to='Role', blank=True)

    def __str__(self):
        return self.name
models.py 示例

小伙子,告诉你一个事实,不经意间,你居然设计出了一个经典的权限访问控制系统:rbac(Role-Based Access Control)基于角色的权限访问控制。你这么优秀,为什么不来老男孩IT教育?路飞学城也行呀! 哈哈哈哈。

注意:现在的设计还不是最终版,但之后的设计都是在此版本基础上扩增的,为了让大家能够更好的理解,我们暂且再此基础上继续开发,直到遇到无法满足的情况,再进行整改。

源码示例猛击下载

客户管理之权限控制

学习知识最好的方式就是试错,坑踩多了那么学到的知识自然而然就多了,所以接下里下来我们用《客户管理》系统为示例,提出功能并实现,并且随着功能越来越多,一点点来找出问题,并解决问题。

目录结构:

luffy_permission/
├── db.sqlite3
├── luffy_permission
│   ├── __init__.py
│   ├── settings.py
│   ├── urls.py
│   └── wsgi.py
├── manage.py
├── rbac            # 权限组件,便于以后应用到其他系统
│   ├── __init__.py
│   ├── admin.py
│   ├── apps.py
│   ├── models.py
│   ├── tests.py
│   └── views.py
├── templates
└── web            # 客户管理业务
    ├── __init__.py
    ├── admin.py
    ├── apps.py
    ├── models.py
    ├── tests.py
    └── views.py
权限的管理,权限组件之权限控制 随笔 第55张
from django.db import models


class Permission(models.Model):
    """
    权限表
    """
    title = models.CharField(verbose_name='标题', max_length=32)
    url = models.CharField(verbose_name='含正则的URL', max_length=128)

    def __str__(self):
        return self.title


class Role(models.Model):
    """
    角色
    """
    title = models.CharField(verbose_name='角色名称', max_length=32)
    permissions = models.ManyToManyField(verbose_name='拥有的所有权限', to='Permission', blank=True)

    def __str__(self):
        return self.title


class UserInfo(models.Model):
    """
    用户表
    """
    name = models.CharField(verbose_name='用户名', max_length=32)
    password = models.CharField(verbose_name='密码', max_length=64)
    email = models.CharField(verbose_name='邮箱', max_length=32)
    roles = models.ManyToManyField(verbose_name='拥有的所有角色', to='Role', blank=True)

    def __str__(self):
        return self.name
rbac/models.py 权限的管理,权限组件之权限控制 随笔 第57张
from django.db import models


class Customer(models.Model):
    """
    客户表
    """
    name = models.CharField(verbose_name='姓名', max_length=32)
    age = models.CharField(verbose_name='年龄', max_length=32)
    email = models.EmailField(verbose_name='邮箱', max_length=32)
    company = models.CharField(verbose_name='公司', max_length=32)


class Payment(models.Model):
    """
    付费记录
    """
    customer = models.ForeignKey(verbose_name='关联客户', to='Customer')
    money = models.IntegerField(verbose_name='付费金额')
    create_time = models.DateTimeField(verbose_name='付费时间', auto_now_add=True)
web/models.py

《客户管理》系统截图:基本增删改查和Excel导入源码下载猛击这里

权限的管理,权限组件之权限控制 随笔 第59张

权限的管理,权限组件之权限控制 随笔 第60张

权限的管理,权限组件之权限控制 随笔 第61张

以上简易版客户管理系统中的URL有:

  • 客户管理
    • 客户列表:/customer/list/
    • 添加客户:/customer/add/
    • 删除客户:/customer/list/(?P<cid>\d+)/
    • 修改客户:/customer/edit/(?P<cid>\d+)/
    • 批量导入:/customer/import/
    • 下载模板:/customer/tpl/
  • 账单管理
    • 账单列表:/payment/list/
    • 添加账单:/payment/add/
    • 删除账单:/payment/del/(?P<pid>\d+)/
    • 修改账单:/payment/edit/<?P<pid>\d+/

那么接下来,我们就在权限组件中录入相关信息:

  • 录入权限
  • 创建用户
  • 创建角色
  • 用户分配角色
  • 角色分配权限

这么一来,用户登录时,就可以根据自己的【用户】找到所有的角色,再根据角色找到所有的权限,再将权限信息放入session,以后每次访问时候需要先去session检查是否有权访问。

已录入权限数据源码下载猛击这里

含用户登录权限源码下载:猛击这里(简易版)

含用户登录权限源码下载猛击这里

至此,基本的权限控制已经完成,基本流程为:

  • 用户登录,获取权限信息并放入session
  • 用户访问,在中间件从session中获取用户权限信息,并进行权限验证。

所有示例中的账户信息:

账户一:
    用户名:alex
       密码:123

账户二:
    用户名:wupeiqi
       密码:123

客户管理之动态“一级”菜单

上述过程中的菜单是在程序中定义好的,无法根据用户权限进行动态配置。

那么,接下来我们来完成一个 “单级菜单”的功能:

权限的管理,权限组件之权限控制 随笔 第62张
from django.db import models


class Permission(models.Model):
    """
    权限表
    """
    title = models.CharField(verbose_name='标题', max_length=32)
    url = models.CharField(verbose_name='含正则的URL', max_length=128)

    icon = models.CharField(verbose_name='图标', max_length=32, null=True, blank=True, help_text='菜单才设置图标')
    is_menu = models.BooleanField(verbose_name='是否是菜单', default=False)

    def __str__(self):
        return self.title


class Role(models.Model):
    """
    角色
    """
    title = models.CharField(verbose_name='角色名称', max_length=32)
    permissions = models.ManyToManyField(verbose_name='拥有的所有权限', to='Permission', blank=True)

    def __str__(self):
        return self.title


class UserInfo(models.Model):
    """
    用户表
    """
    name = models.CharField(verbose_name='用户名', max_length=32)
    password = models.CharField(verbose_name='密码', max_length=64)
    email = models.CharField(verbose_name='邮箱', max_length=32)
    roles = models.ManyToManyField(verbose_name='拥有的所有角色', to='Role', blank=True)

    def __str__(self):
        return self.name
表结构

 这样可以开发出一个单级菜单的示例:

 权限的管理,权限组件之权限控制 随笔 第64张权限的管理,权限组件之权限控制 随笔 第65张

示例源码下载:猛击这里(无默认选中)

示例源码下载猛击这里(含默认选中)

客户管理之动态“二级”菜单

对于功能比较少的应用程序 “一级菜单” 基本可以满足需求,但是功能多的程序就需要 “二级菜单” 了,并且访问时候需要默认选中指定菜单。

权限的管理,权限组件之权限控制 随笔 第66张
from django.db import models


class Menu(models.Model):
    """
    菜单
    """
    title = models.CharField(verbose_name='菜单', max_length=32)
    icon = models.CharField(verbose_name='图标', max_length=32)

    def __str__(self):
        return self.title


class Permission(models.Model):
    """
    权限表
    """
    title = models.CharField(verbose_name='标题', max_length=32)
    url = models.CharField(verbose_name='含正则的URL', max_length=128)

    menu = models.ForeignKey(verbose_name='菜单', to='Menu', null=True, blank=True, help_text='null表示非菜单')

    def __str__(self):
        return self.title


class Role(models.Model):
    """
    角色
    """
    title = models.CharField(verbose_name='角色名称', max_length=32)
    permissions = models.ManyToManyField(verbose_name='拥有的所有权限', to='Permission', blank=True)

    def __str__(self):
        return self.title


class UserInfo(models.Model):
    """
    用户表
    """
    name = models.CharField(verbose_name='用户名', max_length=32)
    password = models.CharField(verbose_name='密码', max_length=64)
    email = models.CharField(verbose_name='邮箱', max_length=32)
    roles = models.ManyToManyField(verbose_name='拥有的所有角色', to='Role', blank=True)

    def __str__(self):
        return self.name
表结构

示例效果:

权限的管理,权限组件之权限控制 随笔 第68张权限的管理,权限组件之权限控制 随笔 第69张 

示例源码下载猛击这里

示例源码下载猛击这里(路飞线上录制代码示例)

客户管理之默认展开非菜单URL

由于很多URL都是不能作为菜单,所以当点击该类功能时,是无法默认展开菜单的,如:

  • 删除
  • 修改
  • ...

权限的管理,权限组件之权限控制 随笔 第70张权限的管理,权限组件之权限控制 随笔 第71张

此类页面只能通过菜单页面二次点击才能跳转,此时也应该为他们默认展开原菜单。

权限的管理,权限组件之权限控制 随笔 第72张
from django.db import models


class Menu(models.Model):
    """
    菜单
    """
    title = models.CharField(verbose_name='菜单', max_length=32)
    icon = models.CharField(verbose_name='图标', max_length=32)

    def __str__(self):
        return self.title


class Permission(models.Model):
    """
    权限表
    """
    title = models.CharField(verbose_name='标题', max_length=32)
    url = models.CharField(verbose_name='含正则的URL', max_length=128)

    pid = models.ForeignKey(verbose_name='默认选中权限', to='Permission', related_name='ps', null=True, blank=True,
                            help_text="对于无法作为菜单的URL,可以为其选择一个可以作为菜单的权限,那么访问时,则默认选中此权限",
                            limit_choices_to={'menu__isnull': False})
    
    menu = models.ForeignKey(verbose_name='菜单', to='Menu', null=True, blank=True, help_text='null表示非菜单')

    def __str__(self):
        return self.title


class Role(models.Model):
    """
    角色
    """
    title = models.CharField(verbose_name='角色名称', max_length=32)
    permissions = models.ManyToManyField(verbose_name='拥有的所有权限', to='Permission', blank=True)

    def __str__(self):
        return self.title


class UserInfo(models.Model):
    """
    用户表
    """
    name = models.CharField(verbose_name='用户名', max_length=32)
    password = models.CharField(verbose_name='密码', max_length=64)
    email = models.CharField(verbose_name='邮箱', max_length=32)
    roles = models.ManyToManyField(verbose_name='拥有的所有角色', to='Role', blank=True)

    def __str__(self):
        return self.name
表结构

功能完成后的示例如下:
权限的管理,权限组件之权限控制 随笔 第74张权限的管理,权限组件之权限控制 随笔 第75张

示例源码下载猛击这里

示例源码下载猛击这里(路飞线上录制代码示例)

客户管理之访问路径导航

如果想要保留放的地址,那么就可以通过在权限配置中获取此功能,示例如下:

 权限的管理,权限组件之权限控制 随笔 第76张权限的管理,权限组件之权限控制 随笔 第77张

示例源码下载猛击这里

客户管理之 权限粒度控制按钮级别

不同用户登录系统时候,根据权限不同来控制是否限制指定按钮,如:

权限的管理,权限组件之权限控制 随笔 第78张权限的管理,权限组件之权限控制 随笔 第79张

示例源码下载猛击这里 

示例源码下载猛击这里(路飞线上录制代码示例)

客户管理之 编辑权限和分配权限

给用户进行权限的分配。

1. 用户和角色管理

权限的管理,权限组件之权限控制 随笔 第80张 

示例源码下载猛击这里(用户和角色管理)

2. 一级菜单

权限的管理,权限组件之权限控制 随笔 第81张权限的管理,权限组件之权限控制 随笔 第82张

 

示例源码下载猛击这里(菜单和权限管理之一级菜单)

3. 二级菜单管理

 权限的管理,权限组件之权限控制 随笔 第83张

示例源码下载猛击这里(菜单和权限管理之二级菜单)

4. 三级菜单管理(权限管理)

权限的管理,权限组件之权限控制 随笔 第84张

示例源码下载猛击这里(菜单和权限管理之三级菜单)

5. django formset示例

权限的管理,权限组件之权限控制 随笔 第85张

源码示例下载猛击这里(django formset实现批量添加和编辑)

6. 批量操作权限的显示

权限的管理,权限组件之权限控制 随笔 第86张

源码示例下载猛击这里(批量操作权限的显示)

7. 批量操作权限:添加、删除、更新

 权限的管理,权限组件之权限控制 随笔 第87张 

源码示例下载猛击这里(权限的批量增删改) 

8. 权限分配

权限的管理,权限组件之权限控制 随笔 第88张

源码示例下载猛击这里(权限分配)

RBAC组件应用及文档

"""

1. 将rbac组件拷贝项目。


2. 将rbac/migrations目录中的数据库迁移记录删除


3. 业务系统中用户表结构的设计

    业务表结构中的用户表需要和rbac中的用户有继承关系,如:

    rbac/models.py
        class UserInfo(models.Model):
            # 用户表
            name = models.CharField(verbose_name='用户名', max_length=32)
            password = models.CharField(verbose_name='密码', max_length=64)
            email = models.CharField(verbose_name='邮箱', max_length=32)
            roles = models.ManyToManyField(verbose_name='拥有的所有角色', to=Role, blank=True) 严重提醒 Role 不要加引号

            def __str__(self):
                return self.name

            class Meta:
                # django以后再做数据库迁移时,不再为UserInfo类创建相关的表以及表结构了。
                # 此类可以当做"父类",被其他Model类继承。
                abstract = True

    业务/models.py
        class UserInfo(RbacUserInfo):
            phone = models.CharField(verbose_name='联系方式', max_length=32)
            level_choices = (
                (1, 'T1'),
                (2, 'T2'),
                (3, 'T3'),
            )
            level = models.IntegerField(verbose_name='级别', choices=level_choices)

            depart = models.ForeignKey(verbose_name='部门', to='Department')

4. 讲业务系统中的用户表的路径写到配置文件。

    # 业务中的用户表
    RBAC_USER_MODLE_CLASS = "app01.models.UserInfo"

    用于在rbac分配权限时,读取业务表中的用户信息。


5. 业务逻辑开发
    将所有的路由都设置一个name,如:
            url(r'^login/$', account.login, name='login'),
            url(r'^logout/$', account.logout, name='logout'),

            url(r'^index/$', account.index, name='index'),

            url(r'^user/list/$', user.user_list, name='user_list'),
            url(r'^user/add/$', user.user_add, name='user_add'),
            url(r'^user/edit/(?P<pk>\d+)/$', user.user_edit, name='user_edit'),
            url(r'^user/del/(?P<pk>\d+)/$', user.user_del, name='user_del'),
            url(r'^user/reset/password/(?P<pk>\d+)/$', user.user_reset_pwd, name='user_reset_pwd'),

            url(r'^host/list/$', host.host_list, name='host_list'),
            url(r'^host/add/$', host.host_add, name='host_add'),
            url(r'^host/edit/(?P<pk>\d+)/$', host.host_edit, name='host_edit'),
            url(r'^host/del/(?P<pk>\d+)/$', host.host_del, name='host_del'),
    用于反向生成URL以及粒度控制到按钮级别的权限控制。

6. 权限信息录入
    - 在url中添加rbac的路由分发,注意:必须设置namespace
        urlpatterns = [
            ...
            url(r'^rbac/', include('rbac.urls', namespace='rbac')),

        ]

    - rbac提供的地址进行操作
        - http://127.0.0.1:8000/rbac/menu/list/
        - http://127.0.0.1:8000/rbac/role/list/
        - http://127.0.0.1:8000/rbac/distribute/permissions/

    相关配置:自动发现URL时,排除的URL:

        # 自动化发现路由中URL时,排除的URL
        AUTO_DISCOVER_EXCLUDE = [
            '/admin/.*',
            '/login/',
            '/logout/',
            '/index/',
        ]


7. 编写用户登录的逻辑【进行权限初始化】

    from django.shortcuts import render, redirect
    from app01 import models
    from rbac.service.init_permission import init_permission


    def login(request):
        if request.method == 'GET':
            return render(request, 'login.html')

        user = request.POST.get('username')
        pwd = request.POST.get('password')

        user_object = models.UserInfo.objects.filter(name=user, password=pwd).first()
        if not user_object:
            return render(request, 'login.html', {'error': '用户名或密码错误'})

        # 用户权限信息的初始化
        init_permission(user_object, request)

        return redirect('/index/')


    相关配置: 权限和菜单的session key:

        setting.py
            PERMISSION_SESSION_KEY = "luffy_permission_url_list_key"
            MENU_SESSION_KEY = "luffy_permission_menu_key"

8. 编写一个首页的逻辑

    def index(request):
        return render(request, 'index.html')


    相关配置:需要登录但无需权限的URL

        # 需要登录但无需权限的URL
        NO_PERMISSION_LIST = [
            '/index/',
            '/logout/',
        ]

9. 通过中间件进行权限校验

    # 权限校验
    MIDDLEWARE = [
        'django.middleware.security.SecurityMiddleware',
        'django.contrib.sessions.middleware.SessionMiddleware',
        'django.middleware.common.CommonMiddleware',
        'django.middleware.csrf.CsrfViewMiddleware',
        'django.contrib.auth.middleware.AuthenticationMiddleware',
        'django.contrib.messages.middleware.MessageMiddleware',
        'django.middleware.clickjacking.XFrameOptionsMiddleware',
        'rbac.middlewares.rbac.RbacMiddleware',
    ]

    # 白名单,无需登录就可以访问
    VALID_URL_LIST = [
        '/login/',
        '/admin/.*'
    ]


10. 粒度到按钮级别的控制

        {% extends 'layout.html' %}
        {% load rbac %}

        {% block content %}
            <div class="luffy-container">
                <div class="btn-group" style="margin: 5px 0">

                    {% if request|has_permission:'host_add' %}
                        <a class="btn btn-default" href="{% memory_url request 'host_add' %}">
                            <i class="fa fa-plus-square" aria-hidden="true"></i> 添加主机
                        </a>
                    {% endif %}

                </div>
                <table class="table table-bordered table-hover">
                    <thead>
                    <tr>
                        <th>主机名</th>
                        <th>IP</th>
                        <th>部门</th>
                        {% if request|has_permission:'host_edit' or request|has_permission:'host_del' %}
                            <th>操作</th>
                        {% endif %}

                    </tr>
                    </thead>
                    <tbody>
                    {% for row in host_queryset %}
                        <tr>
                            <td>{{ row.hostname }}</td>
                            <td>{{ row.ip }}</td>
                            <td>{{ row.depart.title }}</td>
                            {% if request|has_permission:'host_edit' or request|has_permission:'host_del' %}
                                <td>
                                    {% if request|has_permission:'host_edit' %}
                                        <a style="color: #333333;" href="{% memory_url request 'host_edit' pk=row.id %}">
                                            <i class="fa fa-edit" aria-hidden="true"></i></a>
                                    {% endif %}
                                    {% if request|has_permission:'host_del' %}
                                        <a style="color: #d9534f;" href="{% memory_url request 'host_del' pk=row.id %}"><i
                                                class="fa fa-trash-o"></i></a>
                                    {% endif %}
                                </td>
                            {% endif %}
                        </tr>
                    {% endfor %}
                    </tbody>
                </table>
            </div>

        {% endblock %}




总结,目的是希望在任意系统中应用权限系统。
    - 用户登录 + 用户首页 + 用户注销 业务逻辑
    - 项目业务逻辑开发
        注意:开发时候灵活的去设置layout.html中的两个inclusion_tag
            <div class="pg-body">
                <div class="left-menu">
                    <div class="menu-body">
                        {% multi_menu request %}  # 开发时,去掉;上下时,取回。
                    </div>
                </div>
                <div class="right-body">
                    <div>
                        {% breadcrumb request %} # 开发时,去掉;上下时,取回。
                    </div>
                    {% block content %} {% endblock %}
                </div>
            </div>
    - 权限信息的录入
    - 配置文件
        # 注册APP
        INSTALLED_APPS = [
            ...
            'rbac.apps.RbacConfig'
        ]
        # 应用中间件
        MIDDLEWARE = [
            ...
            'rbac.middlewares.rbac.RbacMiddleware',
        ]

        # 业务中的用户表
        RBAC_USER_MODLE_CLASS = "app01.models.UserInfo"
        # 权限在Session中存储的key
        PERMISSION_SESSION_KEY = "luffy_permission_url_list_key"
        # 菜单在Session中存储的key
        MENU_SESSION_KEY = "luffy_permission_menu_key"

        # 白名单
        VALID_URL_LIST = [
            '/login/',
            '/admin/.*'
        ]

        # 需要登录但无需权限的URL
        NO_PERMISSION_LIST = [
            '/index/',
            '/logout/',
        ]

        # 自动化发现路由中URL时,排除的URL
        AUTO_DISCOVER_EXCLUDE = [
            '/admin/.*',
            '/login/',
            '/logout/',
            '/index/',
        ]

    - 粒度到按钮级别的控制
"""
扫码关注我们
微信号:SRE实战
拒绝背锅 运筹帷幄