转载于:http://pdf.us/2017/10/05/492.html,感谢这位大神
《Flask Web开发:基于Python的Web应用开发实战》学习笔记
这里是第二部分的学习笔记。第二部分:实例:社交博客程序
第八章 用户认证
用到的扩展
Werkzeug:计算密码散列并核对
istdangerous:生成并核对加密安全令牌Flask-Mail:发送与认证相关的密码
Flask-Bootstrap:HTML模板
Flask-WTF:Web表单
使用Werkzeug实现密码散列
Werkzeug的security模块可以实现计算密码散列。主要用于用户注册和验证用户。
generate_password_hash(password,method=pbkdf2:sha1,salt_length=8) 以密码作为输入,输出密码的散列值
check_password_hash(hash,password) 返回True即表示验证通过
程序从7a版本开始推进。数据库改用mysql。先不要建表,db init;db migrate;db upgrade生成当前数据库。
对app/models.py中User模型做改造:
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
|
from
werkzeug
.
security
import
generate_password_hash
,
check_password_hash
#...
#class User(db.Model):
# __tablename__ = 'users'
# id = db.Column(db.Integer, primary_key=True)
# username = db.Column(db.String(64), unique=True, index=True)
password_hash
=
db
.
Column
(
db
.
String
(
128
)
)
# role_id = db.Column(db.Integer, db.ForeignKey('roles.id'))
# def __repr__(self):
# return '<User %r>' % self.username
@
property
def
password
(
self
)
:
raise
AttributeError
(
'password is not a readable attribute'
)
@
password
.
setter
def
password
(
self
,
password
)
:
self
.
password_hash
=
generate_password_hash
(
password
)
def
verify_password
(
self
,
password
)
:
return
check_password_hash
(
self
.
password_hash
,
password
)
|
把一个getter方法变成属性,只需要加上@property就可以了,此时,@property本身又创建了另一个装饰器@password.setter,负责把一个setter方法变成属性赋值
简单讲,@property附加到那个方法上,该方法变为同名属性,并只具有读属性
而要设置属性的值,需要使用另外一个方法,并附加@方法名.setter,这样提供了写属性
一句话就是对属性读写分别处理,如果没有setter,则属性为只读
hash后的加密串,即使相同的密码加密,hash串也不相同
该功能的单元测试用例:
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
|
import
unittest
from
app
.
models
import
User
class
UserModelTestCase
(
unittest
.
TestCase
)
:
def
test_password_setter
(
self
)
:
u
=
User
(
password
=
'cat'
)
self
.
assertTrue
(
u
.
password_hash
is
not
None
)
def
test_password_getter
(
self
)
:
u
=
User
(
password
=
'cat'
)
with
self
.
assertRaises
(
AttributeError
)
:
u
.
password
def
test_password_verfication
(
self
)
:
u
=
User
(
password
=
'cat'
)
self
.
assertTrue
(
u
.
verify_password
(
'cat'
)
)
self
.
assertFalse
(
u
.
verify_password
(
'dog'
)
)
def
test_password_salts_are_random
(
self
)
:
u1
=
User
(
password
=
'cat'
)
u2
=
User
(
password
=
'cat'
)
self
.
assertTrue
(
u1
.
password_hash
!=
u2
.
password_hash
)
|
创建认证蓝本
对于不同的程序功能,使用不同的蓝本,这样可以使代码保持整齐有序。
蓝本:auth/__init__.py
1
2
3
|
from
flask
import
Blueprint
auth
=
Blueprint
(
'auth'
,
__name__
)
from
.
import
views
|
auth/views.py
1
2
3
4
5
6
|
from
flask
import
render_template
from
.
import
auth
@
auth
.
route
(
'/login'
)
def
login
(
)
:
return
render_template
(
'auth/login.html'
)
|
auth/login.html位于app/templates/目录下。当然,蓝本也可以定义自己的模板文件夹,此时,render_template()会先搜索程序文件夹,再搜索蓝本配置的模板文件夹。
在create_app函数中附加蓝本auth到程序:app/__init__.py
1
2
3
4
5
6
7
8
9
10
|
#...
#def create_app(config_name):
#...
# from .main import main as main_blueprint
# app.register_blueprint(main_blueprint)
from
.
auth
import
auth
as
auth_blueprint
app
.
register_blueprint
(
auth_blueprint
,
url_prefix
=
'/auth'
)
# return app
|
url_prefix是可选参数,使用该参数后,蓝本中定义的所有路由都会加上指定前缀,这里,/login变成了/auth/login。
使用Flask-Login认证用户
pip install flask-login
使用Flask-Login扩展,User模型需要实现如下几个方法:
属性/方法 | 说明 |
is_authenticated | 若用户已登录,则返回True,否则返回False |
is_active | 若允许用户登录,则返回True,否则返回False;禁用用户,可返回False |
is_anonymous | 对普通用户返回False |
get_id() | 必须返回用户唯一标识符,使用Unicode编码 |
这四个方法可以直接在User类中实现,更简单的方法是使用Flask-Login提供的UserMixin类。
app/modles.py
1
2
3
4
5
6
7
8
9
10
11
|
#...
from
flask_login
import
UserMixin
#...
class
User
(
UserMixin
,
db
.
Model
)
:
# __tablename__ = 'users'
# id = db.Column(db.Integer, primary_key=True)
email
=
db
.
Column
(
db
.
String
(
64
)
,
unique
=
True
,
index
=
True
)
# username = db.Column(db.String(64), unique=True, index=True)
# password_hash=db.Column(db.String(128))
# role_id = db.Column(db.Integer, db.ForeignKey('roles.id'))
#...
|
初始化:app/__init__.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
|
#from flask import Flask
#from flask_bootstrap import Bootstrap
#from flask_mail import Mail
#from flask_moment import Moment
#from flask_sqlalchemy import SQLAlchemy
#from config import config
from
flask_login
import
LoginManager
#bootstrap = Bootstrap()
#mail = Mail()
#moment = Moment()
#db = SQLAlchemy()
login_manager
=
LoginManager
(
)
login_manager
.
session_protection
=
'strong'
login_manager
.
login_view
=
'auth.login'
#def create_app(config_name):
# app = Flask(__name__)
# app.config.from_object(config[config_name])
# config[config_name].init_app(app)
# bootstrap.init_app(app)
# mail.init_app(app)
# moment.init_app(app)
# db.init_app(app)
login_manager
.
init_app
(
app
)
# from .main import main as main_blueprint
# app.register_blueprint(main_blueprint)
# from .auth import auth as auth_blueprint
# app.register_blueprint(auth_blueprint,url_prefix='/auth')
# return app
|
session_protection可设置为None,'basic','strong',当设置为‘strong'时,会记录客户端IP和浏览器用户代理信息,发现异动就登出用户。
Flask-Login要求实现一个回调函数,使用指定的标识符加载用户:app/models.py
1
2
3
4
5
|
from
.
import
login_manager
#...
@
login_manager
.
user_loader
def
load_user
(
user_id
)
:
return
User
.
query
.
get
(
int
(
user_id
)
)
|
回调函数接收以Unicode字符串形式表示的用户标识符,若存在该用户,则返回用户对象,否则返回None
保护路由
让一个路由仅让认证的用户能访问,未认证用户访问,Flask-Login会拦截请求,把用户发往登录页面,示例如下:
1
2
3
4
5
6
|
from
flask_login
import
login
_required
@
app
.
route
(
'/secret'
)
@
login_required
def
secret
(
)
:
return
'only authenticated users are allowed!'
|
添加登录表单
app/auth/forms.py
1
2
3
4
5
6
7
8
9
|
from
flask_wtf
import
Form
from
wtforms
import
StringField
,
PasswordField
,
BooleanField
,
SubmitField
from
wtforms
.
validators
import
Required
,
Length
,
Email
class
LoginForm
(
Form
)
:
email
=
StringField
(
'Email'
,
validators
=
[
Required
(
)
,
Length
(
1
,
64
)
,
Email
(
)
]
)
password
=
PasswordField
(
'Password'
,
validators
=
[
Required
(
)
]
)
remeber_me
=
BooleanField
(
'Keep me logged in'
)
submit
=
SubmitField
(
'Log in'
)
|
app/templates/base.html
{% if current_user.is_authenticated %}
<li><a href="{{ url_for('auth.logout') }} ">Sign Out</a></li>
{% else %}
<li><a href="{{ url_for('auth.login') }}">Sign In</a></li>
{% endif %}
</ul>
current_user由Flask-Login定义, 在视图函数和模板中自动可用,这个变量的值是当前登录的用户,若未登录,则是匿名用户代理对像。
登入用户
app/auth/views.py
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
|
from
flask
import
render_template
,
redirect
,
request
,
url_for
,
flash
from
flask_login
import
login_user
from
.
import
auth
from
.
.
models
import
User
from
.
forms
import
LoginForm
@
auth
.
route
(
'/login'
,
methods
=
[
'GET'
,
'POST'
]
)
def
login
(
)
:
form
=
LoginForm
(
)
if
form
.
validate_on_submit
(
)
:
user
=
User
.
query
.
filter_by
(
email
=
form
.
email
.
data
)
.
first
(
)
if
user
is
not
None
and
user
.
verify_password
(
form
.
password
.
data
)
:
login_user
(
user
,
form
.
remember_me
.
data
)
return
redirect
(
request
.
args
.
get
(
'next'
)
or
url_for
(
'main.index'
)
)
flash
(
'Invalid username or password.'
)
return
render_template
(
'auth/login.html'
,
form
=
form
)
|
app/templates/auth/login.html
{% extends "base.html" %}
{% import "bootstrap/wtf.html" as wtf %}
{% block title %}Flasky - Login{% endblock %}
{% block page_content %}
<div class="page-header">
<h1>Login</h1>
</div>
<div class="col-md-4">
{{ wtf.quick_form(form) }}
</div>
{% endblock %}
登出用户
app/auth/views.py
1
2
3
4
5
6
7
8
|
from
flask_login
import
logout_user
,
login_required
#...
@
auth
.
route
(
'/logout'
)
@
login_required
#保护路由
def
logout
(
)
:
logout_user
(
)
flash
(
'You have been logged out.'
)
return
redirect
(
url_for
(
'main.index'
)
)
|
测试登录
shell中注册新用户
>>> db.session.add(u)
>>> db.session.commit()
{{ current_user.username }}
{% else %}
Stranger
{% endif %}
注册新用户
用户注册表单:
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
|
from
flask_wtf
import
FlaskForm
from
wtforms
import
StringField
,
PasswordField
,
SubmitField
from
wtforms
.
validators
import
Required
,
Length
,
Email
,
Regexp
,
EqualTo
from
wtforms
import
ValidationError
from
.
.
models
import
User
#...
class
RegistrationForm
(
FlaskForm
)
:
email
=
StringField
(
'Email'
,
validators
=
[
Required
(
)
,
Length
(
1
,
64
)
,
Email
(
)
]
)
username
=
StringField
(
'Username'
,
validators
=
[
Required
(
)
,
Length
(
1
,
64
)
,
Regexp
(
'^[A-Za-z0-9_.]*$'
,
0
,
'Usernames must have only letters,numbers,dots or underscores'
)
]
)
password
=
PasswordField
(
'Password'
,
validators
=
[
Required
(
)
,
EqualTo
(
'password2'
,
message
=
'Passwords must match.'
)
]
)
password2
=
PasswordField
(
'Confirm password'
,
validators
=
[
Required
(
)
]
)
submit
=
SubmitField
(
'Register'
)
def
validate_email
(
self
,
field
)
:
if
User
.
query
.
filter_by
(
email
=
field
.
data
)
.
first
(
)
:
raise
ValidationError
(
'Email already registed.'
)
def
validate_username
(
self
,
field
)
:
if
User
.
query
.
filter_by
(
username
=
field
.
data
)
.
first
(
)
:
raise
ValidationError
(
'Username already in use.'
)
|
注意,validator是复数:validators
其中,验证函数Regexp是正则表达式验证,第一个参数是正则表达式(包含字母、数字、下划线和点),第二个是表达式的旗标(通常为0),第三个是匹配失败时的错误消息。
密码的验证使用EqualTo,放到任意一个就可以,另一个字段做为参数传入。
自定义的验证函数:以validate_开头,后面跟字段名的方法。该方法会和常规验证函数一起调用。
注册表单的渲染:
{{ wtf.quick_form(form) }}
注册新用户的视图函数:
1
2
3
4
5
6
7
8
9
10
11
12
|
from
.
forms
import
RegistrationForm
from
.
.
import
db
#...
@
auth
.
route
(
'/register'
,
methods
=
[
'GET'
,
'POST'
]
)
def
register
(
)
:
form
=
RegistrationForm
(
)
if
form
.
validate_on_submit
(
)
:
user
=
User
(
email
=
form
.
email
.
data
,
username
=
form
.
username
.
data
,
password
=
form
.
password
.
data
)
db
.
session
.
add
(
user
)
flash
(
'You can now login.'
)
return
redirect
(
url_for
(
'auth.login'
)
)
return
render_template
(
'auth/register.html'
,
form
=
form
)
|
确认帐户
验证邮箱,通过点击包含令牌的URL,修改标记状态。
itsdangerous提供多种生成令牌方法,其中TimedJSONWebSignatureSerializer类生成具有过期时间的JSON Web签名,该类构造函数接收参数是一个密钥和过期时间(秒)。dumps方法为指定数据生成加密的令牌字符串,load方法解码令牌。
from itsdangerous import TimedJSONWebSignatureSerializer as Serializer
s=Serializer(app.config['SECRET_KEY'],expires_in=3600)
token=s.dumps({'confirm':23}) #生成token,签名字符串
data=s.loads(token) #data={u'confirm':23}
修改模型:app/models.py
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
|
from
itsdangerous
import
TimedJSONWebSignatureSerializer
as
Serializer
from
flask
import
current_app
from
.
import
db
#...
class
User
(
UserMixin
,
db
.
Model
)
:
#...
confirmed
=
db
.
Column
(
db
.
Boolean
,
default
=
False
)
def
generate_confirmation_token
(
self
,
expiration
=
3600
)
:
s
=
Serializer
(
current_app
.
config
[
'SECRET_KEY'
]
,
expiration
)
return
s
.
dumps
(
{
'confirm'
:
self
.
id
}
)
def
confirm
(
self
,
token
)
:
s
=
Serializer
(
current_app
.
config
[
'SECRET_KEY'
]
)
try
:
data
=
s
.
loads
(
token
)
except
:
return
False
if
data
.
get
(
'confirm'
)
!=
self
.
id
:
return
False
self
.
confirmed
=
True
db
.
session
.
add
(
self
)
return
True
|
发送确认邮件
app/auth/views.py
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
|
#...
from
.
.
email
import
send_email
#...
#@auth.route('/register',methods=['GET','POST'])
#def register():
# form = RegistrationForm()
# if form.validate_on_submit():
# user=User(email=form.email.data,username=form.username.data,password=form.password.data)
# db.session.add(user)
db
.
session
.
commit
(
)
token
=
user
.
generate_confirmation_token
(
)
#send_email(user.email,'Confirm your account','auth/email/confirm',user=user,token=token) 发送邮件不好模拟,暂用print替代
#print url_for('auth.confirm',token=token,_external=True)
flash
(
'A confirmation email has been sent to you by email'
)
# return redirect(url_for('auth.login'))
# return render_template('auth/register.html',form=form)
|
因为只有提交数据库后才能够得到新用户id,而生成token需要用到用户id,所以需要添加db.session.commit()
模板:{{ url_for('auth.confirm',token=token,_external=True) }}
确认token:
1
2
3
4
5
6
7
8
9
10
11
12
|
from
flask_login
import
current_user
#...
@
auth
.
route
(
'/confirm/<token>'
)
@
login_required
def
confirm
(
token
)
:
if
current_user
.
confirmed
:
return
redirect
(
url_for
(
'main.index'
)
)
if
current_user
.
confirm
(
token
)
:
flash
(
'You have confirmed your account.Thanks!'
)
else
:
flash
(
'The confirmation link is ivalid or has expired.'
)
return
redirect
(
url_for
(
'main.index'
)
)
|
蓝本中的程序全局请求钩子-before_app_request
1
2
3
4
5
6
7
8
9
10
11
12
|
@
auth
.
before_app_request
def
before_request
(
)
:
if
current_user
.
is_authenticated
and
not
current_user
.
confirmed
and
\
request
.
endpoint
[
:
5
]
!=
'auth.'
and
request
.
endpoint
!=
'static'
:
return
redirect
(
url_for
(
'auth.unconfirmed'
)
)
#如果当前用户已登录and帐号未激活and请求端点不在认证蓝本中and不是静态文件,则跳转
@
auth
.
route
(
'/unconfirmed'
)
def
unconfirmed
(
)
:
if
current_user
.
is_anonymous
or
current_user
.
confirmed
:
return
redirect
(
url_for
(
'main.index'
)
)
return
render_template
(
'auth/unconfirmed.html'
)
#如果是匿名用户or帐号已激活则正常跳转
|
重新发送确认邮件
1
2
3
4
5
6
7
8
|
@
auth
.
route
(
'/confirm'
)
@
login_required
def
resend_confirmation
(
)
:
token
=
current_user
.
generate_confirmation_token
(
)
#send_email(current_user.email,'Confirm Your Account','auth/email/confirm',user=current_user,token=token)
print
url_for
(
'auth.confirm'
,
token
=
token
,
_external
=
True
)
flash
(
'A new confirmation email has been sent to you by email.'
)
return
redirect
(
url_for
(
'main.index'
)
)
|
管理帐户
修改密码
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
|
#表单
class
ChangePasswordForm
(
FlaskForm
)
:
old_password
=
PasswordField
(
'Old password'
,
validators
=
[
Required
(
)
]
)
password
=
PasswordField
(
'New password'
,
validators
=
[
Required
(
)
,
EqualTo
(
'password2'
,
message
=
'Passwords must match'
)
]
)
password2
=
PasswordField
(
'Confirm new password'
,
validators
=
[
Required
(
)
]
)
submit
=
SubmitField
(
'Update Password'
)
#视图
@
auth
.
route
(
'/change-password'
,
methods
=
[
'GET'
,
'POST'
]
)
@
login_required
def
change_password
(
)
:
form
=
ChangePasswordForm
(
)
if
form
.
validate_on_submit
(
)
:
if
current_user
.
verify_password
(
form
.
old_password
.
data
)
:
current_user
.
password
=
form
.
password
.
data
db
.
session
.
add
(
current_user
)
flash
(
'Your password has been updated.'
)
return
redirect
(
url_for
(
'main.index'
)
)
else
:
flash
(
'Invalid password.'
)
return
render_template
(
"auth/change_password.html"
,
form
=
form
)
|
重设密码
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
|
#模型:
def
reset_password
(
self
,
token
,
new_password
)
:
s
=
Serializer
(
current_app
.
config
[
'SECRET_KEY'
]
)
try
:
data
=
s
.
loads
(
token
)
except
:
return
False
if
data
.
get
(
'reset'
)
!=
self
.
id
:
return
False
self
.
password
=
new_password
db
.
session
.
add
(
self
)
return
True
#表单
class
PasswordResetRequestForm
(
FlaskForm
)
:
email
=
StringField
(
'Email'
,
validators
=
[
Required
(
)
,
Length
(
1
,
64
)
,
Email
(
)
]
)
submit
=
SubmitField
(
'Reset Password'
)
class
PasswordResetForm
(
FlaskForm
)
:
email
=
StringField
(
'Email'
,
validators
=
[
Required
(
)
,
Length
(
1
,
64
)
,
Email
(
)
]
)
password
=
PasswordField
(
'New Password'
,
validators
=
[
Required
(
)
,
EqualTo
(
'password2'
,
message
=
'Passwords must match'
)
]
)
password2
=
PasswordField
(
'Confirm password'
,
validators
=
[
Required
(
)
]
)
submit
=
SubmitField
(
'Reset Password'
)
def
validate_email
(
self
,
field
)
:
if
User
.
query
.
filter_by
(
email
=
field
.
data
)
.
first
(
)
is
None
:
raise
ValidationError
(
'Unknown email address.'
)
#视图
@
auth
.
route
(
'/reset'
,
methods
=
[
'GET'
,
'POST'
]
)
def
password_reset_request
(
)
:
if
not
current_user
.
is_anonymous
:
return
redirect
(
url_for
(
'main.index'
)
)
form
=
PasswordResetRequestForm
(
)
if
form
.
validate_on_submit
(
)
:
user
=
User
.
query
.
filter_by
(
email
=
form
.
email
.
data
)
.
first
(
)
if
user
:
token
=
user
.
generate_reset_token
(
)
send_email
(
user
.
email
,
'Reset Your Password'
,
'auth/email/reset_password'
,
user
=
user
,
token
=
token
,
next
=
request
.
args
.
get
(
'next'
)
)
flash
(
'An email with instructions to reset your password has been '
'sent to you.'
)
return
redirect
(
url_for
(
'auth.login'
)
)
return
render_template
(
'auth/reset_password.html'
,
form
=
form
)
@
auth
.
route
(
'/reset/<token>'
,
methods
=
[
'GET'
,
'POST'
]
)
def
password_reset
(
token
)
:
if
not
current_user
.
is_anonymous
:
return
redirect
(
url_for
(
'main.index'
)
)
form
=
PasswordResetForm
(
)
if
form
.
validate_on_submit
(
)
:
user
=
User
.
query
.
filter_by
(
email
=
form
.
email
.
data
)
.
first
(
)
if
user
is
None
:
return
redirect
(
url_for
(
'main.index'
)
)
if
user
.
reset_password
(
token
,
form
.
password
.
data
)
:
flash
(
'Your password has been updated.'
)
return
redirect
(
url_for
(
'auth.login'
)
)
else
:
return
redirect
(
url_for
(
'main.index'
)
)
return
render_template
(
'auth/reset_password.html'
,
form
=
form
)
|
修改电子邮件
先确认邮件进行确认,输入新邮件地址后,向该邮件地址发送一封包含令牌的邮件。服务器发送令牌前,可先将邮件地址存到临时表或者是直接存到token中。
第九章 用户角色
角色在数据库中的表示
app/models.py,添加两个属性
permissions = db.Column(db.Integer)
其中permissions字段使用二进制位表示不同的权限。
权限常量:
FOLLOW=0x01 #0b00000001关注其它用户
COMMENT=0x02 #0b00000010在他人文章后发表评论
WRITE_ARTICLES=0x04 #0b00000100写文章
MODERATE_COMMENTS=0x08 #0b00001000管理他人发表的评论
ADMINISTER=0x80 #0b10000000管理员
用户角色:
用户角色 | 权限 | 权限 | 说明 |
匿名 | 0b00000000 | 0x00 | 未登录用户,仅阅读权限 |
用户 | 0b00000111 | 0x07 | 写文章,写评论,关注其他用户 |
协管员 | 0b00001111 | 0x0f | 增加管理他人评论功能 |
管理员 | 0b11111111 | 0xff | 所有权限,包括修改其它用户权限 |
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
|
#class Role(db.Model):
# __tablename__ = 'roles'
# id = db.Column(db.Integer, primary_key=True)
# name = db.Column(db.String(64), unique=True)
default
=
db
.
Column
(
db
.
Boolean
,
default
=
False
,
index
=
True
)
permissions
=
db
.
Column
(
db
.
Integer
)
# users = db.relationship('User', backref='role', lazy='dynamic')
# def __repr__(self):
# return '<Role %r>' % self.name
@
staticmethod
def
insert_roles
(
)
:
roles
=
{
'User'
:
(
Permission
.
FOLLOW
|
Permission
.
COMMENT
|
Permission
.
WRITE_ARTICLES
,
True
)
,
'Moderator'
:
(
Permission
.
FOLLOW
|
Permission
.
COMMENT
|
Permission
.
WRITE_ARTICLES
|
Permission
.
MODERATE_COMMENTS
,
False
)
,
'Administrator'
:
(
0xff
,
False
)
}
for
r
in
roles
:
role
=
Role
.
query
.
filter_by
(
name
=
r
)
.
first
(
)
if
role
is
None
:
role
=
Role
(
name
=
r
)
role
.
permissions
=
roles
[
r
]
[
0
]
role
.
default
=
roles
[
r
]
[
1
]
db
.
session
.
add
(
role
)
db
.
session
.
commit
(
)
|
通过insert_roles方法添加角色,使用shell操作,Role.insert_roles()
赋予角色
app/models.py
1
2
3
4
5
6
7
8
9
10
|
class
User
(
UserMixin
,
db
.
Model
)
:
#...
def
__init__
(
self
,
*
*
kwargs
)
:
super
(
User
,
self
)
.
__init__
(
*
*
kwargs
)
if
self
.
role
is
None
:
if
self
.
email
==
current_app
.
config
[
'FLASKY_ADMIN'
]
:
self
.
role
=
Role
.
query
.
filter_by
(
permissions
=
0xff
)
.
first
(
)
if
self
.
role
is
None
:
self
.
role
=
Role
.
query
.
filter_by
(
default
=
True
)
.
first
(
)
#...
|
角色验证
添加辅助方法:app/models.py
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
|
#...
from
flask_login
import
UserMixin
,
AnonymousUserMixin
#...
class
User
(
UserMixin
,
db
.
Model
)
:
#...
def
can
(
self
,
permissions
)
:
return
self
.
role
is
not
None
and
(
self
.
role
.
permissions
&
permissions
)
==
permissions
def
is_administrator
(
self
)
:
return
self
.
can
(
Permission
.
ADMINISTER
)
#...
class
AnonymousUser
(
AnonymousUserMixin
)
:
def
can
(
self
,
permissions
)
:
return
False
def
is_administrator
(
self
)
:
return
False
#...
login_manager
.
anonymous_user
=
AnonymousUser
|
can方法使用位与操作,检查用户权限。Anonymous类出于一致性考虑,无论用户是否登录,均可使用current_user.can()和current_user.is_administrator()方法来验证用户权限。
检查用户权限的自定义修饰器
app/decorators.py
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
|
from
functools
import
wraps
from
flask
import
abort
from
flask_login
import
current_user
def
permission_required
(
permission
)
:
def
decorator
(
f
)
:
@
wraps
(
f
)
def
decorated_function
(
*
args
,
*
*
kwargs
)
:
if
not
current_user
.
can
(
permission
)
:
abort
(
403
)
return
f
(
*
args
,
*
*
kwargs
)
return
decorated_function
return
decorator
def
admin_required
(
f
)
:
return
permission_required
(
Permission
.
ADMINISTER
)
(
f
)
|
自定义修饰器的使用方法
1
2
3
4
5
6
7
8
9
10
11
12
13
14
|
from
decorators
import
admin_required
,
permission_required
from
.
models
import
Permission
@
main
.
route
(
'/admin'
)
@
login
_required
@
admin_required
def
for_admins_only
(
)
:
return
"For administrator"
@
main
.
route
(
'/moderator'
)
@
login
_required
@
permission_required
(
Permission
.
MODERATE_COMMENTS
)
def
for_moderators_only
(
)
:
return
"For comment moderators!"
|
模板中也需要检查权限,为避免每次调用render_template()时都多添加一个模板参数,可以使用上下文处理器,上下文处理器能让变量在所有模板中全局可访问。
app/main/__init__.py
def inject_permissions():
return dict(Permission=Permission)