Flask-BBS论坛项目实战5-权限设计及前台模型创建

二进制及其相关运算

认识二进制

1
2
0,1,2,3,4,5,6,7,8,9,10:逢101
0,1:逢21

二进制转十进制

十进制二进制
00
11
210
311
4100
25511111111

二进制之间的与(&)或(|)运算

1
2
与运算:0&1=0 1&1=1 相当于python if条件中的and
或运算:0|1=1 0|0=0 相当于Python if条件中的or

判断某个用户有没有某个权限(a)

只要将需要对比的用户的权限和a权限的二进制码进行与运算,如果得到的结果和a相等,那么就代表这个用户有a这个权限,否则代表没有

权限和角色模型定义

用户是和角色绑定,而不是直接拥有权限,通过角色拥有的权限,则该用户就拥有什么权限
一个用户可以同时拥有多个角色(如:运营和销售),一个角色也可以对应多个用户,因此用户和角色是多对多的关系
权限,这里直接用类固定写好。

配置模型,编辑cms.modles.py

定义一个权限的类

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
class CMSPersmission(object):
#255的二进制方式表示11111111
ALL_PERMISSION = 0b11111111
#访问者权限
VISITOR = 0b00000001
#管理帖子权限
POSTER = 0b00000010
#管理评论的权限
COMMENTER = 0b00000100
#管理板块的权限
BOARDER = 0b00001000
#管理前台用户的权限
FRONTUSER = 0b00010000
#管理后台用户的权限
CMSUSER = 0b00100000
#管理后台管理员的权限
ADMIN = 0b01000000

定义角色模型并且和CMSUser模型组成多对多的关系

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
cms_role_user = db.Table(
'cms_role_user',
db.Column('cms_role_id', db.Integer, db.ForeignKey('cms_role.id'), primary_key=True),
db.Column('cms_user_id', db.Integer, db.ForeignKey('cms_user.id'), primary_key=True)
)

class CMSRole(db.Model):
__tablename__ = 'cms_role'
id = db.Column(db.Integer, primary_key=True, autoincrement=True)
name = db.Column(db.String(50), nullable=False)
desc = db.Column(db.String(200), nullable=True)
create_time = db.Column(db.DateTime, default=datetime.now)
permissions = db.Column(db.Integer, default=CMSPersmission.VISITOR)

users = db.relationship('CMSUser', secondary=cms_role_user, backref='roles')

编辑CMSUser模型,该用户拥有的权限,是否拥有某权限,是不是开发者(拥有所有权限)

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
class CMSUser(db.Model):
__tablename__ = 'cms_user'
id = db.Column(db.Integer, primary_key=True, autoincrement=True)
username = db.Column(db.String(50), nullable=False)
_password = db.Column(db.String(100), nullable=False)
email = db.Column(db.String(50), nullable=False, unique=True) #unique表示必须是唯一的
join_time = db.Column(db.DateTime, default=datetime.now)

def __init__(self, username, password, email):
self.username = username
self.password = password
self.email = email

@property
def password(self):
return self._password

@password.setter
def password(self, raw_password):
self._password = generate_password_hash(raw_password)

def check_password(self, raw_password):
result = check_password_hash(self.password, raw_password)
return result

@property
def permissions(self):
# 如果该用户没有任何角色,则没有权限
if not self.roles:
return 0
#遍历该用户拥有的角色,获取该角色权限,并所有所含所有角色权限通过或运算组合在一起
all_permissions = 0
for role in self.roles:
permissions = role.permissions
all_permissions |= permissions
return all_permissions

def has_permission(self, permission):
# 把传过来的权限和该用户所拥有的权限进行与运算,得出的结果和传过来的权限进行比较,一致的话则拥有该权限
# 0b00000011 & 0b00000001 ---->1 (0b00000001)
return self.permissions & permission == permission

@property
def is_developer(self):
#判断该用户是否是开发者,开发者拥有所有权限
return self.has_permission(CMSPersmission.ALL_PERMISSION)

映射表到数据库

1
2
python manage.py db migrate
python manage.py db upgrade

创建角色,并把用户加入到角色

编辑manage.py

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
...
CMSRole = cms_models.CMSRole
CMSPermission = cms_models.CMSPersmission

@manager.command
def create_role():
#访问者(可以修改个人信息)
visitor = CMSRole(name='访问者',desc='可以修改个人信息')
visitor.permissions = CMSPermission.VISITOR

#运营角色(修改个人信息,管理帖子,管理评论,管理前台用户)
operator = CMSRole(name='运营', desc='管理帖子,评论,前台用户')
operator.permissions = (CMSPermission.VISITOR|
CMSPermission.POSTER|
CMSPermission.COMMENTER|
CMSPermission.FRONTUSER)
#管理员(拥有绝大部分权限)
admin = CMSRole(name='管理员', desc='拥有本系统所有权限')
admin.permissions = (CMSPermission.VISITOR|
CMSPermission.POSTER|
CMSPermission.COMMENTER|
CMSPermission.BOARDER|
CMSPermission.FRONTUSER|
CMSPermission.CMSUSER)
#开发者
developer = CMSRole(name='开发者', desc='开发人员专用')
developer.permissions = CMSPermission.ALL_PERMISSION

db.session.add_all([visitor, operator, admin, developer])
db.session.commit()

@manager.option('-e', '--email', dest='email')
@manager.option('-n', '--name', dest='nmae')
def add_user_to_rule(email, name):
user = CMSUser.query.filter_by(email=email).first()
if user:
role = CMSRole.query.filter_by(name=name).first()
if role:
role.users.append(user)
db.session.commit()
print('用户{}添加到角色{}成功'.format(email, name))
else:
print('没有这个角色:{}'.format(name))
else:
print('没有这个用户:{}'.format(email))

@manager.command
def test_permission():
user = CMSUser.query.first() #目前我数据库只有一个账号
if user.is_developer:
print('用户{}有开发者的权限'.format(user.email))
else:
print('用户{}没有开发权限'.format(user.email))

执行创建角色

1
python manage.py create_role

客户端权限


编辑cms_base.html

1
2
3
4
<li><a href="#">{{ g.cms_user.username }}<span>[超级管理员]</span></a></li>

改为
<li><a href="{{ url_for('cms.profile') }}">{{ g.cms_user.username }}</a></li>

1
2
3
4
5
<li class="nav-group user-manage"><a href="#">用户管理</a></li>
<li class="role-manage"><a href="#">组管理</a></li>

改为
<li class="nav-group user-manage"><a href="#">前台用户管理</a></li>

编辑cms_profile.html

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
...
<tr>
<td>角色</td>
<td>
{% for role in g.cms_user.roles %}
{{ role.name }}
{% if not loop.last %},{% endif %}
{% endfor %}
</td>
</tr>
<tr>
<td>权限</td>
<td>
{% for role in g.cms_user.roles %}
{{ role.desc }}
{% if not loop.last %}/{% endif %}
{% endfor %}
</td>
</tr>
...

客户端权限:不同的权限展示不同的页面

首先,添加测试用户并加入到角色中

usernameemailpasswordrule
adminhewqeoan@qq.com123456开发者
gl13123@123.com111111管理员
sd\111@123.con111111运营
ddwq@11.com11111访问者

添加完成后。编辑cms_base.html
比如只有用户含有访问者权限才能看到个人中心,我们可能会写CMSPermission.VISITOR

但是模板中并不能直接使用CMSPermission.VISITOR变量,因为变量只有从后台传过来才能使用,然后CMSPermission.VISITOR只是在models中的一个类属性。因此要使用它,我们必须在视图函数中把它传递过来。为了避免单独在每个需要此类属性值的视图函数中都要传递,我们可以利用上下文钩子函数,这样,每个模板都自动有了此钩子函数设置的变量值了。

之前,我们写了个文件cms/hook.py专门用来写cms 下的钩子函数,所以编辑cms/hook.py

1
2
3
4
5
6
...
from .models import CMSPersmission

@bp.context_processor
def context_processor():
return {'CMSPersmission': CMSPersmission}

修改后cms_base.html如下

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
<li class="unfold"><a href="{{ url_for('cms.index') }}">首页</a></li>

{% if g.cms_user.has_permission(CMSPersmission.VISITOR) %}
<li class="profile-li">
<a href="#">个人中心<span></span></a>
<ul class="subnav">
<li><a href="{{ url_for('cms.profile') }}">个人信息</a></li>
<li><a href="{{ url_for('cms.resetpwd') }}">修改密码</a></li>
<li><a href="{{ url_for('cms.resetemail') }}">修改邮箱</a></li>
</ul>
</li>
{% endif %}

{% if g.cms_user.has_permission(CMSPersmission.POSTER) %}
<li class="nav-group post-manage"><a href="#">帖子管理</a></li>
{% endif %}

{% if g.cms_user.has_permission(CMSPersmission.COMMENTER) %}
<li class="comments-manage"><a href="#">评论管理</a></li>
{% endif %}

{% if g.cms_user.has_permission(CMSPersmission.BOARDER) %}
<li class="board-manage"><a href="#">板块管理</a></li>
{% endif %}

{% if g.cms_user.has_permission(CMSPersmission.FRONTUSER) %}
<li class="nav-group user-manage"><a href="#">前台用户管理</a></li>
{% endif %}

{% if g.cms_user.has_permission(CMSPersmission.CMSUSER) %}
<li class="nav-group cmsuser-manage"><a href="#">CMS用户管理</a></li>
{% endif %}

{% if g.cms_user.has_permission(CMSPersmission.ADMIN) %}
<li class="cmsrole-manage"><a href="#">CMS组管理</a></li>
{% endif %}

完成后即可登录上面创建的账号查看效果。

服务器权限

完成服务器权限验证之前,我们先如下页面先补上

cms_posts.html

帖子管理

1
2
3
4
5
6
7
8
9
10
11
12
13
{% extends 'cms/cms_base.html' %}

{% block title %}
帖子管理-CMS管理系统
{% endblock %}

{% block page_title %}
帖子管理
{% endblock %}

{% block main_content %}
这是帖子管理页面
{% endblock %}

评论管理

cms_comments.html

1
2
3
4
5
6
7
8
9
10
11
12
13
{% extends 'cms/cms_base.html' %}

{% block title %}
评论管理-CMS管理系统
{% endblock %}

{% block page_title %}
评论管理
{% endblock %}

{% block main_content %}
这是评论管理页面
{% endblock %}

板块管理

cms_boards.html

1
2
3
4
5
6
7
8
9
10
11
12
13
{% extends 'cms/cms_base.html' %}

{% block title %}
板块管理-CMS管理系统
{% endblock %}

{% block page_title %}
板块管理
{% endblock %}

{% block main_content %}
这是板块管理页面
{% endblock %}

前台用户管理

cms_fusers.html

1
2
3
4
5
6
7
8
9
10
11
12
13
{% extends 'cms/cms_base.html' %}

{% block title %}
前台用户管理-CMS管理系统
{% endblock %}

{% block page_title %}
前台用户管理
{% endblock %}

{% block main_content %}
这是前台用户管理页面
{% endblock %}

CMS用户管理

cms_cusers.html

1
2
3
4
5
6
7
8
9
10
11
12
13
{% extends 'cms/cms_base.html' %}

{% block title %}
cms用户管理-CMS管理系统
{% endblock %}

{% block page_title %}
cms用户管理
{% endblock %}

{% block main_content %}
这是cms用户管理页面
{% endblock %}

CMS组管理

cms_roles.html

1
2
3
4
5
6
7
8
9
10
11
12
13
{% extends 'cms/cms_base.html' %}

{% block title %}
CMS组管理-CMS管理系统
{% endblock %}

{% block page_title %}
CMS组管理
{% endblock %}

{% block main_content %}
这是CMS组管理页面
{% endblock %}

编辑cms.views.编写对应的视图函数
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
@bp.route('/posts/')
@login_required
def posts():
return render_template('cms/cms_posts.html')

@bp.route('/comments/')
@login_required
def comments():
return render_template('cms/cms_comments.html')

@bp.route('/boards/')
@login_required
def boards():
return render_template('cms/cms_boards.html')

@bp.route('/fusers/')
@login_required
def fusers():
return render_template('cms/cms_fusers.html')

@bp.route('/cusers/')
@login_required
def cusers():
return render_template('cms/cms_cusers.html')

@bp.route('/croles/')
@login_required
def croles():
return render_template('cms/cms_croles.html')
编辑cms_base.html,修改对应导航的url
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
...
{% if g.cms_user.has_permission(CMSPersmission.POSTER) %}
<li class="nav-group post-manage"><a href="{{ url_for('cms.posts') }}">帖子管理</a></li>
{% endif %}

{% if g.cms_user.has_permission(CMSPersmission.COMMENTER) %}
<li class="comments-manage"><a href="{{ url_for('cms.comments') }}">评论管理</a></li>
{% endif %}

{% if g.cms_user.has_permission(CMSPersmission.BOARDER) %}
<li class="board-manage"><a href="{{ url_for('cms.boards') }}">板块管理</a></li>
{% endif %}

{% if g.cms_user.has_permission(CMSPersmission.FRONTUSER) %}
<li class="nav-group user-manage"><a href="{{ url_for('cms.fusers') }}">前台用户管理</a></li>
{% endif %}

{% if g.cms_user.has_permission(CMSPersmission.CMSUSER) %}
<li class="nav-group cmsuser-manage"><a href="{{ url_for('cms.cusers') }}">CMS用户管理</a></li>
{% endif %}

{% if g.cms_user.has_permission(CMSPersmission.ADMIN) %}
<li class="cmsrole-manage"><a href="{{ url_for('cms.croles') }}">CMS组管理</a></li>
{% endif %}

现在点击各导航已经能够正确调到自己的页面了,但是有些导航样式并不是选中的状态,需要修改cms_base.js

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
...
$(function () {
var url = window.location.href;
if(url.indexOf('profile') >= 0){
var profileLi = $('.profile-li');
profileLi.addClass('unfold').siblings().removeClass('unfold');
profileLi.children('.subnav').children().eq(0).addClass('active').siblings().removeClass('active');
} else if(url.indexOf('resetpwd') >= 0){
var profileLi = $('.profile-li');
profileLi.addClass('unfold').siblings().removeClass('unfold');
profileLi.children('.subnav').children().eq(1).addClass('active').siblings().removeClass('active');
} else if(url.indexOf('resetemail') >= 0){
var profileLi = $('.profile-li');
profileLi.addClass('unfold').siblings().removeClass('unfold');
profileLi.children('.subnav').children().eq(2).addClass('active').siblings().removeClass('active');
} else if(url.indexOf('posts') >= 0){
var postManageLi = $('.post-manage');
postManageLi.addClass('unfold').siblings().removeClass('unfold');
}else if(url.indexOf('boards') >= 0){
var boardManageLi = $('.board-manage');
boardManageLi.addClass('unfold').siblings().removeClass('unfold');
}else if(url.indexOf('fusers') >= 0){
var userManageLi = $('.user-manage');
userManageLi.addClass('unfold').siblings().removeClass('unfold');
}else if(url.indexOf('cusers') >= 0){
var cmsuserManageLi = $('.cmsuser-manage');
cmsuserManageLi.addClass('unfold').siblings().removeClass('unfold');
}else if(url.indexOf('croles') >= 0){
var cmsroleManageLi = $('.cmsrole-manage');
cmsroleManageLi.addClass('unfold').siblings().removeClass('unfold');
}else if(url.indexOf('comments') >= 0) {
var commentsManageLi = $('.comments-manage');
commentsManageLi.addClass('unfold').siblings().removeClass('unfold');
}
});

服务端权限验证

cms.decoratoras.py
1
2
3
4
5
6
7
8
9
10
11
12
13
...
def permission_required(permission):
def outter(func):
@wraps(func)
def inner(*args, **kwargs):
#判断该用户是否有权限
if g.cms_user.has_permission(permission):
return func(*args, **kwargs)
else:
#没有权限则返回到首页
return redirect(url_for('cms.index'))
return inner
return outter

最后,编辑user.views.py在视图函数中加入上面的装饰器,并传入对应的权限

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
....
@bp.route('/profile/')
@login_required
@permission_required(CMSPersmission.VISITOR) #这个装饰器要放在login_required下面,因为只有先登录了,才能进下一步验证,
def profile():
return render_template('cms/cms_profile.html')


@bp.route('/posts/')
@login_required
@permission_required(CMSPersmission.POSTER)
def posts():
return render_template('cms/cms_posts.html')

@bp.route('/comments/')
@login_required
@permission_required(CMSPersmission.COMMENTER)
def comments():
return render_template('cms/cms_comments.html')

@bp.route('/boards/')
@login_required
@permission_required(CMSPersmission.BOARDER)
def boards():
return render_template('cms/cms_boards.html')

@bp.route('/fusers/')
@login_required
@permission_required(CMSPersmission.FRONTUSER)
def fusers():
return render_template('cms/cms_fusers.html')

@bp.route('/cusers/')
@login_required
@permission_required(CMSPersmission.CMSUSER)
def cusers():
return render_template('cms/cms_cusers.html')

@bp.route('/croles/')
@login_required
@permission_required(CMSPersmission.ADMIN)
def croles():
return render_template('cms/cms_croles.html')

大功告成!

前台模型创建

安装shortuui
1
pip install shortuuid
编辑front.models.py
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
from exts import db
import shortuuid
from werkzeug.security import generate_password_hash, check_password_hash
import enum
from datetime import datetime

class GenderEnum(enum.Enum):
MALE = 1
FEMALE = 2
SECRET = 3
UNKNOW = 4


class FrontUser(db.Model):
__tablename__ = 'front_user'
#这里的id如果我们还使用自动增长,就会存在商业安全隐患,用户可以根据id推算出我们网站的人数
#不使用自动增长,又要保证id的唯一性,我们就可以使用uuid
#虽然uuid好用,但是它太长了,查找的效率会降低
#这时我们就可以使用shortuuid这个库,它既满足了唯一性,又没有uuid那么长
id = db.Column(db.String(100), primary_key=True, default=shortuuid.uuid)
telephone = db.Column(db.String(11), nullable=False)
username = db.Column(db.String(50), nullable=False)
_password = db.Column(db.String(100), nullable=False)
email = db.Column(db.String(50), unique=True)
realname = db.Column(db.String(50))
avatar = db.Column(db.String(50))
signature = db.Column(db.String(100))
gender = db.Column(db.Enum(GenderEnum), default=GenderEnum.UNKNOW)
join_time = db.Column(db.DateTime, default=datetime.now)

#对password对应的是_password,所以要做处理
def __init__(self, *args, **kwargs):
if "password" in kwargs:
self.password = kwargs.get('password')
kwargs.pop("password")
super(FrontUser, self).__init__(*args, **kwargs)


@property
def password(self):
return self._password

@password.setter
def password(self, newpwd):
self._password = generate_password_hash(newpwd)

def check_password(self, rawpwd):
return check_password_hash(self._password, rawpwd)

同步到数据库

1
2
python manage.py db migrate
python manage.py db upgrade

然后我可以在manage.py中配置命令来添加前台用户

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
from apps.front import models as front_models

FrontUser = front_models.FrontUser
...

@manager.option('-t', '--telephone', dest='telephone')
@manager.option('-u', '--username', dest='username')
@manager.option('-p', '--password', dest='password')
def create_front_user(telephone, username, password):
user = FrontUser(telephone=telephone, username=username, password=password)
db.session.add(user)
db.session.commit()
print( '成功创建前台用户:{}'.format(username))

...

创建一个前台用户

查看一下数据库

前台注册页面

在template下创建目录front,该目录用于存放前台页面的所有模板
在front下创建登录模板

front_signup.html
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
85
86
87
88
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<title>注册-BBS论坛</title>

<script src="http://cdn.bootcss.com/jquery/3.1.1/jquery.min.js"></script>
<link href="http://cdn.bootcss.com/bootstrap/3.3.7/css/bootstrap.min.css" rel="stylesheet">
<script src="http://cdn.bootcss.com/bootstrap/3.3.7/js/bootstrap.min.js"></script>

<style>
body{
background-color: #8CD4F5;
}

.page-title {
text-align: center;
padding-top: 50px;
}
.out-box{
width: 845px;
background-color: white;
margin: 0 auto;
margin-top: 50px;

}

.page-title {
text-align: center;
padding-top: 80px;
}

.form-box{
padding-top: 50px;
padding-bottom: 50px;
width: 300px;
margin: 0 auto;
}
</style>
</head>
<body>

<div class="out-box">
<h2 class="page-title">注册BBS论坛</h2>
<div class="form-box">
<div class="form-group">
<div class="input-group">
<input type="text" class="form-control" name="telephone" placeholder="手机号码">
<span class="input-group-btn">
<button id="sms-captcha-btn" class="btn btn-default">发送验证码</button>
</span>
</div>
</div>

<div class="form-group">
<input type="text" class="form-control" name="sms_captcha" placeholder="短信验证码">
</div>

<div class="form-group">
<input type="text" class="form-control" name="username" placeholder="用户名">
</div>

<div class="form-group">
<input type="password" class="form-control" name="password1" placeholder="密码">
</div>

<div class="form-group">
<input type="password" class="form-control" name="password2" placeholder="确认密码">
</div>

<div class="form-group">
<div class="input-group">
<input type="text" class="form-control" name="graph_captcha" placeholder="图形验证码">
<span class="input-group-addon">
图形验证码
</span>
</div>
</div>

<div class="form-group">
<button class="btn btn-warning btn-block" id="submit-btn">立即注册</button>
</div>
</div>

</div>

</body>
</html>

在front中编写视图,编辑front.views.py

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
from flask import (
Blueprint,
views,
render_template
)

bp = Blueprint('front', __name__)

@bp.route('/')
def index():
return 'front index'


class SignUpViews(views.MethodView):
def get(self):
return render_template('front/front_signup.html')

def post(self):
pass


bp.add_url_rule('/signup/', view_func=SignUpViews.as_view('signup'))

访问http://127.0.0.1:5000/signup/

-------------本文结束感谢您的阅读-------------