菜单和权限
  # 登陆校验
校验用户是否已登陆! 登陆校验.
# 注意事项
▲ 自己写的中间件AuthMiddleware在注册时,得注册在SessionMiddleware中间件的后面!!
因为在SessionMiddleware的process_request源码中是这样写的!
def process_request(self, request):
   # 取浏览器cookie中session_id对应的随机字符串
   session_key = request.COOKIES.get(settings.SESSION_COOKIE_NAME) 
   # 简单来说:从数据库或缓存中取到随机字符串所对应的值放到此次请求的request.session中
   # 实际上,request.session其实是self.SessionStore这个类实例化出的对象
   #       之所以可以像字典一样取值,是因为取值时会调用SessionStore的父类SessionBase里的 __getitem__ 等方法!
   request.session = self.SessionStore(session_key)
★ session中间件的源码分析详见: https://www.cnblogs.com/QQ279366/p/8525362.html
 1
2
3
4
5
6
7
8
9
10
2
3
4
5
6
7
8
9
10
# process_request
在用户名登陆和短信登陆的视图函数中,在登陆成功后,都会将用户信息存入session中. session的key值最好写setting配置文件中!
自己写的中间件AuthMiddleware的逻辑:
1.设置不需要登陆就能访问的url
2.在session中获取用户信息.若能获取,已登陆;否则,未登陆.
  2.1未登陆,跳转回登陆页面
  2.2若已登陆,最好将存储在session中的用户信息,进行封装.方便在视图函数中更好的取值!
     视图函数中取用户信息:
        未封装时: request.session[settings.NB_SESSION_KEY]["role"]
        已封装(会有提示!): request.nb_user.role 
 1
2
3
4
5
6
7
8
9
2
3
4
5
6
7
8
9
Ps: 针对权限的校验会写到中间件AuthMiddleware的process_view中!!
# 动态菜单
不同角色的用户看到不同的菜单!
# 菜单显示的美化
在自定义的AuthMiddleware中间件的process_request方法里,若已登陆,登陆成功的session信息会放到request.nb_user中!
写个inclusion_tag,模版中传递request对象给该inclusion_tag对应的函数.
在该函数中通过request.nb_user.role 取到当前登陆用户的角色.
并去settings配置文件中取到该角色对应的权限列表!!在模版中通过循环进行展示. 二级菜单需要两个循环.
注意:
  1. layout.html是基础的模版,home.html继承layout.html
  2. 为了美化,写了很多css的样式. 过程中,还使用了bootstrap和fontawesome.
  3. 用js代码实现了点击当前菜单,下级菜单的隐藏和显示的动态效果.
 1
2
3
4
5
6
7
8
9
10
2
3
4
5
6
7
8
9
10
# 默认选中(V1版)

web/templatetags/menu.py
from django import template
from django.conf import settings
import copy
register = template.Library()
@register.inclusion_tag("tag/nb_menu.html")
def nb_menu(request):
    menu_list = copy.deepcopy(settings.NB_MENU[request.nb_user.role])
    for item in menu_list:
        # item['class'] = "hide"
        for child in item["children"]:
            if child["url"] == request.path_info:
                child["class"] = "active"
                # item["class"] = ""
    return {"menu_list": menu_list}
★ 项目启动,NB_MENU是加载到内存中的!!
  后续每访问不同的url都会从内存中读取NB_MENU来加载动态菜单!! 都会!!
  若某次访问时,改变了内存中NB_MENU中的值,该改变会延续到接下来url的访问!
  So,使用了深拷贝!! copy.deepcopy(settings.NB_MENU[request.nb_user.role]) 不对内存中造成改变!
★ 上面注释的item['class'] = "hide"、item["class"] = ""的逻辑
  一开始,都不展开二级菜单!!点击某个一级菜单,会调用js展示对应的二级菜单,点击某个二级菜单,会访问对应的url!!
  又开始一个轮回,
     对内存中的NB_MENU["ADMIN"]进行拷贝,先是都不展开二级菜单,处于收缩状态!
     该url对应的二级菜单加上active样式,二级菜单对应的一级菜单取消hide样式!!
  再访问其他url时,再开始一个新的轮回...
"""
NB_MENU = {
    "ADMIN": [
        {
            "text": "用户信息1",
            "icon": "fa-calendar-plus-o",
            // item['class'] = "hide" 相当于
            // 'class': "hide",
            "children": [
                // child["class"] = "active" 相当于
                // {"text": "级别管理1", "url": "/level/list/", "name": "level", "class":"active"}
                {"text": "级别管理1", "url": "/level/list/", "name": "level"},
                {"text": "订单管理1", "url": "/order/list/", "name": "order"},
            ]
        },
        {
            "text": "用户信息2",
            "icon": "fa-calendar-plus-o",
            "children": [
                {"text": "用户管理2", "url": "/user/list/", "name": "user"},
            ]
        },
    ],
    "CUSTOMER": [
        {
            "text": "用户信息3",
            "icon": "fa-calendar-plus-o",
            "children": [
                {"text": "订单管理3", "url": "/xxx/xx/"},
                {"text": "财务管理3", "url": "/xxx/xx/"},
            ]
        },
    ]
}
"""
 1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
web/templates/tag/nb_menu.html
<div class="multi-menu">
    {% for item in menu_list %}
        <div class="item">
            <div class="title">
                <span class="icon-wrap"><i class="fa {{ item.icon }}"></i></span> {{ item.text }}
            </div>
            <div class="body {{ item.class }}">  <!-- item.class 二级菜单是否展示 -->
                {% for child in item.children %}
                    <!-- child.class 是否默认选中 -->
                    <a class="{{ child.class }}" href="{{ child.url }}">{{ child.text }}</a>
                {% endfor %}
            </div>
        </div>
    {% endfor %}
</div>
 1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
2
3
4
5
6
7
8
9
10
11
12
13
14
15

# 顶部导航
左侧为网站logo+标题; 右侧为当前已登陆用户 + 头像 + 注销.
[实现展示头像的功能]
这里用的是写死的默认头像..若想实现展示头像的功能,需要在用户表里添加一个头像地址的链接!
登陆成功后,可以在session中放头像地址的链接,在页面中头像地址的链接也可以拿到! 跟拿当钱登陆用户的用户名的逻辑是一样的!!
[注销功能]
注销的本质是将session信息给清除!清除后,用户进来没有那个session,肯定就登陆失败啦!
逻辑: 清除request中的session信息(也就是清除原来登陆成功后放到session中的那个用户字典 ),重定向到登陆界面!
Ps: 登陆成功,S->C一串随机字符串;C下次访问时,携带着这串随机字符串,S可以依据该随机字符串找到S端中<<对应的那一条>>session!!
 1
2
3
4
5
6
7
8
9
10
2
3
4
5
6
7
8
9
10

# 权限校验
有无权限 ---> 即对url有无访问的权限!!
一个权限就是一个URL ; 用户具有多少权限,即拥有多少个URL.
# 简易版的
用字典,是因为字典的底层是哈希存储的,查询某个键时,速度非常快!!
用url的别名来代指某个权限!!而不是直接使用url.
简易版的实现逻辑:
   用户访问程序时,程序可以读取当前访问的url的name.
   再根据当前用户的所处的角色,看该角色是否有该name/权限!! 存在,有权访问;不存在,无权访问.
   在中间件中的process_view中实现这一过程:
      step1:根据用户所处角色获取该角色具备的所有权限
      step2:获取当前用户访问的url
      step3:看两者是否匹配!
 1
2
3
4
5
6
7
8
9
2
3
4
5
6
7
8
9
关键代码如下:
# 权限校验
NB_PERMISSION = {
    "ADMIN": {
        "home": None,
        "order": None,
        # "level": None,
        "user": None,
    },
    "CUSTOMER": {
        "home": None,
        "user": None,
    },
}
def process_view(self, request, callback, callback_args, callback_kwargs):
    user_permission_dict = settings.NB_PERMISSION.get(request.nb_user.role)
    current_name = request.resolver_match.url_name
    if current_name not in user_permission_dict:
        return render(request, "permission.html")
 1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
# 完整版的
要解决简易版存在的两个问题:
1> 访问子页面时, 关联菜单需要被选中;
2> 要有路径导航
# 默认选中(V2版)
选中关联菜单

关键代码如下:
def process_view(self, request, callback, callback_args, callback_kwargs):
    user_permission_dict = settings.NB_PERMISSION.get(request.nb_user.role)
    current_name = request.resolver_match.url_name
    if current_name not in user_permission_dict:
        return render(request, "permission.html")
    menu_name = current_name
    # !!该while循环,保证了哪怕层级很深,也能拿到菜单级别的值/当前访问的子页面对应菜单的名字
    while user_permission_dict[menu_name]["parent"]:
        menu_name = user_permission_dict[menu_name]["parent"]
    # 比如,访问/order/list/和访问/order/add/ 此处得到的menu_name都是"order"
    # print(menu_name)
    request.nb_user.menu_name = menu_name  # request.nb_user是我们自定义类UserInfo的实例化对象
    
    
@register.inclusion_tag("tag/nb_menu.html")
def nb_menu(request):
    menu_list = copy.deepcopy(settings.NB_MENU[request.nb_user.role])
    for item in menu_list:
        for child in item["children"]:
            # if child["url"] == request.path_info:  # v1版本 当前的url跟菜单是否匹配
            if child["name"] == request.nb_user.menu_name:  # v2版本 当前的url的name跟配置的动态菜单里的name是否匹配
                child["class"] = "active"
 1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24

# 路径导航
注意: settings.NB_PERMISSION_PUBLIC中提取了所有角色都有的权限!!
# 权限校验
NB_PERMISSION_PUBLIC = {
    "home": {"text": "主页", 'parent': None},
    "logout": {"text": "注销", 'parent': None},
}
NB_PERMISSION = {
    "ADMIN": {
        "order": {"text": "订单列表", "parent": None},
        "order_add": {"text": "创建订单", "parent": "order"},
        "level": {"text": "级别列表", "parent": None},
        "user": {"text": "用户列表", "parent": None},
    },
    "CUSTOMER": {
        "user": {"text": "用户列表", "parent": None},
    },
}
def process_view(self, request, callback, callback_args, callback_kwargs):
    current_name = request.resolver_match.url_name
    # -- 若不加该判断,注销登陆是无权访问的.
    if current_name in settings.NB_PERMISSION_PUBLIC:
        # request.nb_user.nav_list = [("首页", "home")]
        return
      
    # -- 若不加该判断,会报错,'WSGIRequest' object has no attribute 'nb_user'
    #    无论是直接访问/login/,还是注销跳转访问到/login/,都是一次新的请求哦!会先经历中间件.
    #    process_request直接通过,request里可是没有nb_user的!!
    if request.path_info in settings.NB_WHITE_URL:
        return
    user_permission_dict = settings.NB_PERMISSION.get(request.nb_user.role)
    if current_name not in user_permission_dict:
        return render(request, "permission.html")
    nav_list = [
        # -- 当然可以直接只加text,只不过放弃了路径导航a标签的点击效果罢了!
        (user_permission_dict[current_name]["text"], current_name)
    ]
    menu_name = current_name
    while user_permission_dict[menu_name]["parent"]:
        menu_name = user_permission_dict[menu_name]["parent"]
        text = user_permission_dict[menu_name]["text"]
        nav_list.append((text, menu_name))
    if menu_name != "home":
        nav_list.append(("首页", "home"))
    nav_list.reverse()
    # 当前默认选中的菜单
    request.nb_user.menu_name = menu_name
    # 路径导航
    request.nb_user.nav_list = nav_list
    
<div class="pg-body">
    <!-- 动态菜单 -->
    <div class="left-menu">
        <div class="menu-body">
            {% nb_menu request %}
        </div>
    </div>
    <div class="right-body">
        <!-- 路径导航 -->
        {% if request.nb_user.nav_list %}
            <ol class="breadcrumb">
                {% for nav in request.nb_user.nav_list %}
                    <!-- 之所以添加这个判断,是因为像编辑级别的路由,路由反向需要传pk,不传会报错 -->
                    {% if forloop.last %}
                        <li><a href="#">{{ nav.0 }}</a></li>
                    {% else %}
                        <li><a href="{% url nav.1 %}">{{ nav.0 }}</a></li>
                    {% endif %}
                {% endfor %}
            </ol>
        {% endif %}
        <div style="padding: 15px">
            {% block content %}{% endblock %}
        </div>
    </div>
</div>
 1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84

# 小结
home页面继承layout母版;
在layout.html中使用inclusiontag做动态菜单数据的展示!!
自定义中间件中的方法 process_request、process_view
   settings配置文件里定义专属配置.注意,名称必须得是大写!!
   菜单&权限 --> 数据结构的设计 --> 处理和判断 (while循环一直找最上级)
   将后续需要用到的数据封装到了request.nb_user里!
   若项目比较复杂,出现了多个app,会用到路由分发、通过namespace进行拆分.
   
1> 登陆成功,用户信息写入session
2> 中间件中读取session,进行校验.
   process_request中进行登陆的校验;
   prcess_view中判断是否有访问该url的权限 & 路径导航的实现 & 以及动态默认选中的问题.
3> 中间件处理完数据后,往request.nb_user中添加/封装了一系列的实例属性.
   模版语法 + 自定义的inclusion_tag + 读取request.nb_user里的属性值 进行页面菜单等的展示!
 1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
2
3
4
5
6
7
8
9
10
11
12
13
14
15