Flask-Principal作为一个Flask扩展的框架,首先,要了解包含的四个组件:Identity,Needs,Permission,IdentityContext。
-
Identity(身份)
源码解释:
Represent the user's identity.
:param id: The user id
:param auth_type: The authentication type used to confirm the user's identity.
The identity is used to represent the user's identity in the system. This
object is created on login, or on the start of the request as loaded from
the user's session.
Once loaded it is sent using theidentity-loaded
signal, and should be
populated with additional required information.
Needs that are provided by this identity should be added to theprovides
set after loading.
Identity代码一个用户,每一次的请求,用户的信息(id,auth_type)都会被记录在session中,它包含着用户拥有的访问的权限。一旦被加载,它将使用identity-loaded
的信号发送,然后填充了其他所需信息
-
Needs(需求)
Need 是访问控制的最小粒度,并代表特殊的操作权限。 如 “管理员角色”,“可以编辑博客帖子”
用户可以灵活设计很多自定义的需求,它的值是以(method,value)元组形式定义,比如:('role', 'admin')、('role', 'Editor')、('user','zhangsan')、('user','lisi')等等
Need = namedtuple('Need', ['method', 'value'])
UserNeed = partial(Need, 'id')
UserNeed.__doc__ = """A need with the method preset to `"id"`."""
RoleNeed = partial(Need, 'role')
RoleNeed.__doc__ = """A need with the method preset to `"role"`."""
TypeNeed = partial(Need, 'type')
TypeNeed.__doc__ = """A need with the method preset to `"type"`."""
ActionNeed = partial(Need, 'action')
TypeNeed.__doc__ = """A need with the method preset to `"action"`."""
ItemNeed = namedtuple('ItemNeed', ['method', 'value', 'type'])
"""A required item need
-
Permission (权限)
用一个set表示,包含了对资源的访问控制。 Permission 其实是通过 Needs 来定义和初始化的, 其中 Permission 可以是一个权限的集合.
admin_permission = Permission(RoleNeed('admin'))
admin_permission = Permission(RoleNeed('editor'))
admin_permission = Permission(RoleNeed('reader'))
-
IdentityContext (身份上下文)
IdentityContext 是针对某一特定身份和某一特定权限的上下文环境,可以作为 context manager 或者 decorator 使用。
应用场景:
1.用于保护资源的访问
比如blog管理员界面只有拥有admin权限的用户才可以访问,或者管理blog所有文章界面,可以通过decorator 和context manager。
实例:
from flask import Flask, Response
from flask_principal import Principal, Permission, RoleNeed
app = Flask(__name__)
principals = Principal(app)
# 创建定义为'admin'的权限
admin_permission = Permission(RoleNeed('admin'))
# 使用admin_permission.require()作为decorator装饰视图保护是否有相应的权限
@app.route('/admin')
@admin_permission.require()
def do_admin_index():
return Response('Only if you are an admin')
# this time protect with a context manager
@app.route('/articles')
def do_articles():
with admin_permission.require():
return Response('Only if you are admin')
2.用于实现身份改变信号逻辑
Flask-principle结合Flask-Login,当登录用户改变时,认证 providers 应该使用 identity-changed 信号来表明这个请求已经被认证。
实例:
from flask import Flask, current_app, request, session
from flask_login import LoginManager, login_user, logout_user, login_required, current_user
from flask_wtf import FlaskForm, TextField, PasswordField, Required, Email
from flask_principal import Principal, Identity, AnonymousIdentity, identity_changed
app = Flask(__name__)
Principal(app)
login_manager = LoginManager(app)
@login_manager.user_loader
def load_user(userid):
# 返回一个User实例
return datastore.find_user(id=userid)
class LoginForm(FlaskForm):
email = TextField()
password = PasswordField()
@app.route('/login', methods=['GET', 'POST'])
def login():
form = LoginForm()
if form.validate_on_submit():
user = datastore.find_user(email=form.email.data)
if form.password.data == user.password:
# Flask-Login的login_user方法将登录用户信息保存于session中
login_user(user)
# 当前应用对象 app 和当前要登录的用户对象作为身份对象, 以信号的形式发送出去
identity_changed.send(current_app._get_current_object(),
identity=Identity(user.id))
return redirect(request.args.get('next') or '/')
return render_template('login.html', form=form)
@app.route('/logout')
@login_required
def logout():
# 登出时删除保存在session中的用户信息
logout_user()
# 删除Flask-Principal设置的会话密钥
for key in ('identity.name', 'identity.auth_type'):
session.pop(key, None)
# 告诉Flask-Principal用户是匿名的
identity_changed.send(current_app._get_current_object(),
identity=AnonymousIdentity())
return redirect(request.args.get('next') or '/')
3.用于实现权限载入信号逻辑
与上面例子想结合,当用户登录后,通过identity_loaded添加一切附加信息到 Identity 实例,然后改变权限信息
实例:
from flask_login import current_user
from flask_principal import identity_loaded, RoleNeed, UserNeed
@identity_loaded.connect_via(app)
def on_identity_loaded(sender, identity):
# Set the identity user object
identity.user = current_user
# Add the UserNeed to the identity
if hasattr(current_user, 'id'):
identity.provides.add(UserNeed(current_user.id))
# Assuming the User model has a list of roles, update the
# identity with the roles that the user provides
if hasattr(current_user, 'roles'):
for role in current_user.roles:
identity.provides.add(RoleNeed(role.name))
on_identity_loaded()
函数在用户身份发生了变化, 需要重载权限的时候被调用. 首先将当前的用户绑定到一个 Identity 的实例化对象中, 然后将该用户 id 的 UserNeed 和该用户所拥有的 roles 对应的 RoleNeed 绑定到该 Identity 中. 实现了将数据库中 user 所拥有的 roles 都以 Needs 的形式绑定到其自身中.
4.用于实现权限保护
举个例子:要实现只有博客帖子的作者才能编辑文章。这个可以通过创建必需的 Need 和
Permission 对象来实现,并且添加更多逻辑到 identity_loaded 信号函数上
实例:
from collections import namedtuple
from functools import partial
from flask_login import current_user
from flask_principal import identity_loaded, Permission, RoleNeed, \
UserNeed
BlogPostNeed = namedtuple('blog_post', ['method', 'value'])
EditBlogPostNeed = partial(BlogPostNeed, 'edit')
class EditBlogPostPermission(Permission):
def __init__(self, post_id):
need = EditBlogPostNeed(unicode(post_id))
super(EditBlogPostPermission, self).__init__(need)
@identity_loaded.connect_via(app)
def on_identity_loaded(sender, identity):
# Set the identity user object
identity.user = current_user
# Add the UserNeed to the identity
if hasattr(current_user, 'id'):
identity.provides.add(UserNeed(current_user.id))
# Assuming the User model has a list of roles, update the
# identity with the roles that the user provides
if hasattr(current_user, 'roles'):
for role in current_user.roles:
identity.provides.add(RoleNeed(role.name))
# Assuming the User model has a list of posts the user
# has authored, add the needs to the identity
if hasattr(current_user, 'posts'):
for post in current_user.posts:
identity.provides.add(EditBlogPostNeed(unicode(post.id)))
接下来,保护这个endpoint允许某个用户编辑一篇文章,这是通过使用资源ID即时创建权限(Permission)对象来进行的 ,在这个博客文章例子中:
@app.route('/posts/<post_id>', methods=['PUT', 'PATCH'])
def edit_post(post_id):
permission = EditBlogPostPermission(post_id)
if permission.can():
# Save the edits ...
return render_template('edit_post.html')
abort(403) # HTTP Forbidden