菜单和权限
# 登陆校验
校验用户是否已登陆! 登陆校验.
# 注意事项
▲ 自己写的中间件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