目录

前文列表

用 Flask 来写个轻博客 (1) — 创建项目

用 Flask 来写个轻博客 (2) — Hello World!

用 Flask 来写个轻博客 (3) — (M)VC_连接 MySQL 和 SQLAlchemy

用 Flask 来写个轻博客 (4) — (M)VC_创建数据模型和表

用 Flask 来写个轻博客 (5) — (M)VC_SQLAlchemy 的 CRUD 详解

用 Flask 来写个轻博客 (6) — (M)VC_models 的关系(one to many)

用 Flask 来写个轻博客 (7) — (M)VC_models 的关系(many to many)

用 Flask 来写个轻博客 (8) — (M)VC_Alembic 管理数据库结构的升级和降级

用 Flask 来写个轻博客 (9) — M(V)C_Jinja 语法基础快速概览

用 Flask 来写个轻博客 (10) — M(V)C_Jinja 常用过滤器与 Flask 特殊变量及方法

用 Flask 来写个轻博客 (11) — M(V)C_创建视图函数

用 Flask 来写个轻博客 (12) — M(V)C_编写和继承 Jinja 模板

用 Flask 来写个轻博客 (13) — M(V)C_WTForms 服务端表单检验

用 Flask 来写个轻博客 (14) — M(V)C_实现项目首页的模板

用 Flask 来写个轻博客 (15) — M(V)C_实现博文页面评论表单

用 Flask 来写个轻博客 (16) — MV(C)_Flask Blueprint 蓝图

用 Flask 来写个轻博客 (17) — MV(C)_应用蓝图来重构项目

用 Flask 来写个轻博客 (18) — 使用工厂模式来生成应用对象

用 Flask 来写个轻博客 (19) — 以 Bcrypt 密文存储账户信息与实现用户登陆表单

用 Flask 来写个轻博客 (20) — 实现注册表单与应用 reCAPTCHA 来实现验证码

用 Flask 来写个轻博客 (21) — 结合 reCAPTCHA 验证码实现用户注册与登录

用 Flask 来写个轻博客 (22) — 实现博客文章的添加和编辑页面

用 Flask 来写个轻博客 (23) — 应用 OAuth 来实现 Facebook 第三方登录

用 Flask 来写个轻博客 (24) — 使用 Flask-Login 来保护应用安全

扩展阅读

Flask Principal — Flask Principal 0.4.0 documentation

Flask-Login — Flask-Login 0.4.0 documentation

Flask-Principal

Flask-Principal 是一个 Flask 扩展(用户权限框架), 框架主要部分包含身份(Identity),需求(Needs),权限(Permission),和包含身份信息的上下文环境(IdentityContext)。

Flask 中的每一个 user 都会拥有一种 Identity, 而每一种 Identity 又会被关联到一个 Needs. Flask-Principal 提供了两种 Needs(RoleNeed/UserNeed). Needs 本质上是一个 namedtuple(具名元组) EG. ("role", "admin"), 其定义了在这个 Identity 能做什么事情. 也就是说 Permission 其实是通过 Needs 来定义和初始化的, 其中 Permission 可以是一个权限的集合.

除此之外, Flask-Principal 是通过信号(signal)来与 Flask 应用进行交互的,满足了低耦合的理念。其定义了两个重要的signal:

  • identity_changed:一般在用户身份变化时发送该信号, 在用户登录认证成功后,通过发送 identity_changed 信号告知 Flask-Principal 用户登录成功, 需要对用户的权限进行改变

  • identity_loaded:一般在用户权限需要被载入时发送该信息. 通常在用户身份改变时, 就需要载入相应的权限.

使用 Flask-Principal 来实现角色权限功能

添加 Role Model

而且 Role 和 User 应该是 many to many 的关系.

  • models.py
  1. users_roles = db.Table('users_roles',
  2. db.Column('user_id', db.String(45), db.ForeignKey('users.id')),
  3. db.Column('role_id', db.String(45), db.ForeignKey('roles.id')))
  4. class User(db.Model):
  5. """Represents Proected users."""
  6. # Set the name for table
  7. __tablename__ = 'users'
  8. id = db.Column(db.String(45), primary_key=True)
  9. username = db.Column(db.String(255))
  10. password = db.Column(db.String(255))
  11. # one to many: User ==> Post
  12. # Establish contact with Post's ForeignKey: user_id
  13. posts = db.relationship(
  14. 'Post',
  15. backref='users',
  16. lazy='dynamic')
  17. roles = db.relationship(
  18. 'Role',
  19. secondary=users_roles,
  20. backref=db.backref('users', lazy='dynamic'))
  21. def __init__(self, id, username, password):
  22. self.id = id
  23. self.username = username
  24. self.password = self.set_password(password)
  25. # Setup the default-role for user.
  26. default = Role.query.filter_by(name="default").one()
  27. self.roles.append(default)
  28. def __repr__(self):
  29. """Define the string format for instance of User."""
  30. return "<Model User `{}`>".format(self.username)
  31. def set_password(self, password):
  32. """Convert the password to cryptograph via flask-bcrypt"""
  33. return bcrypt.generate_password_hash(password)
  34. def check_password(self, password):
  35. return bcrypt.check_password_hash(self.password, password)
  36. def is_authenticated(self):
  37. """Check the user whether logged in."""
  38. # Check the User's instance whether Class AnonymousUserMixin's instance.
  39. if isinstance(self, AnonymousUserMixin):
  40. return False
  41. else:
  42. return True
  43. def is_active():
  44. """Check the user whether pass the activation process."""
  45. return True
  46. def is_anonymous(self):
  47. """Check the user's login status whether is anonymous."""
  48. if isinstance(self, AnonymousUserMixin):
  49. return True
  50. else:
  51. return False
  52. def get_id(self):
  53. """Get the user's uuid from database."""
  54. return unicode(self.id)
  55. class Role(db.Model):
  56. """Represents Proected roles."""
  57. __tablename__ = 'roles'
  58. id = db.Column(db.String(45), primary_key=True)
  59. name = db.Column(db.String(255), unique=True)
  60. description = db.Column(db.String(255))
  61. def __init__(self, id, name):
  62. self.id = id
  63. self.name = name
  64. def __repr__(self):
  65. return "<Model Role `{}`>".format(self.name)

NOTE: 这样的话我们可以为 user 指定一个 role 集. 用来代表该用户所拥有的 Identity, 这也是之后为 user 对象绑定 Needs 所需要的前提.

在 Manager shell 中手动的添加角色

  • 创建 roles 数据表
  1. (env) jmilkfan@JmilkFan-Devstack:/opt/JmilkFan-s-Blog$ python manage.py shell
  2. >>> db.create_all()
  • 创建新用户
  1. >>> from uuid import uuid4
  2. >>> user = User(id=str(uuid4()), username='jmilkfan_2016', password="fanguiju")
  3. >>> db.session.add(user)
  4. >>> db.session.commit()
  • 创建新角色并与新用户建立关联
  1. >>> role_admin = Role(id=str(uuid4()), name="admin")
  2. >>> role_poster = Role(id=str(uuid4()), name="poster")
  3. >>> role_default = Role(id=str(uuid4()), name="default")
  4. >>> user
  5. <Model User `jmilkfan_2016`>
  6. >>> role_admin.users = [user]
  7. >>> role_poster.users = [user]
  8. >>> db.session.add(role_admin)
  9. >>> db.session.add(role_poster)
  10. >>> db.session.add(role_default)
  11. >>> db.session.commit()

初始化 Flask-Principal 和 Permission

  • extensions.py
  1. from flask.ext.principal import Principal, Permission, RoleNeed
  2. # Create the Flask-Principal's instance
  3. principals = Principal()
  4. # 这里设定了 3 种权限, 这些权限会被绑定到 Identity 之后才会发挥作用.
  5. # Init the role permission via RoleNeed(Need).
  6. admin_permission = Permission(RoleNeed('admin'))
  7. poster_permission = Permission(RoleNeed('poster'))
  8. default_permission = Permission(RoleNeed('default'))

实现权限载入信号逻辑

  • jmilkfannsblog.__init__.py
  1. def create_app(object_name):
  2. """Create the app instance via `Factory Method`"""
  3. app = Flask(__name__)
  4. # Set the config for app instance
  5. app.config.from_object(object_name)
  6. # Will be load the SQLALCHEMY_DATABASE_URL from config.py to db object
  7. db.init_app(app)
  8. # Init the Flask-Bcrypt via app object
  9. bcrypt.init_app(app)
  10. # Init the Flask-OpenID via app object
  11. openid.init_app(app)
  12. # Init the Flask-Login via app object
  13. login_manager.init_app(app)
  14. # Init the Flask-Prinicpal via app object
  15. principals.init_app(app)
  16. @identity_loaded.connect_via(app)
  17. def on_identity_loaded(sender, identity):
  18. """Change the role via add the Need object into Role.
  19. Need the access the app object.
  20. """
  21. # Set the identity user object
  22. identity.user = current_user
  23. # Add the UserNeed to the identity user object
  24. if hasattr(current_user, 'id'):
  25. identity.provides.add(UserNeed(current_user.id))
  26. # Add each role to the identity user object
  27. if hasattr(current_user, 'roles'):
  28. for role in current_user.roles:
  29. identity.provides.add(RoleNeed(role.name))
  30. # Register the Blueprint into app object
  31. app.register_blueprint(blog.blog_blueprint)
  32. app.register_blueprint(main.main_blueprint)
  33. return app
  • NOTE 1: 因为 identity_loaded 信号实现函数,需要访问 app 对象, 所以直接在 __init\_\_.create_app() 中实现.

  • NOTE 2: on_identity_loaded() 函数在用户身份发生了变化, 需要重载权限的时候被调用. 首先将当前的用户绑定到一个 Identity 的实例化对象中, 然后将该用户 id 的 UserNeed 和该用户所拥有的 roles 对应的 RoleNeed 绑定到该 Identity 中. 实现了将数据库中 user 所拥有的 roles 都以 Needs 的形式绑定到其自身中.

实现身份改变信号逻辑

  • jmilkfsnsblog.controllers.main.py
  1. from flask.ext.principal import Identity, AnonymousIdentity, identity_changed, current_app
  2. @main_blueprint.route('/login', methods=['GET', 'POST'])
  3. @openid.loginhandler
  4. def login():
  5. """View function for login.
  6. Flask-OpenID will be receive the Authentication-information
  7. from relay party.
  8. """
  9. ...
  10. # Will be check the account whether rigjt.
  11. if form.validate_on_submit():
  12. # Using session to check the user's login status
  13. # Add the user's name to cookie.
  14. # session['username'] = form.username.data
  15. user = User.query.filter_by(username=form.username.data).one()
  16. # Using the Flask-Login to processing and check the login status for user
  17. # Remember the user's login status.
  18. login_user(user, remember=form.remember.data)
  19. identity_changed.send(
  20. current_app._get_current_object(),
  21. identity=Identity(user.id))
  22. flash("You have been logged in.", category="success")
  23. return redirect(url_for('blog.home'))
  24. ...
  • NOTE 1: identity_changed一般在用户的身份发生变化时发送, 所以我们一般选择 login()视图函数中实现.

  • NOTE 2: identity_changed.send() 函数会将 sender: current_app._get_current_object() 当前应用对象 app 和身份对象 identity: Identity(user.id) 当前要登录的用户对象, 以信号的新式发送出去, 表示应用 app 对象中的 user 用户对象的 identity 被改变了.

  • NOTE 3: 在 identity_changed 信息被发送之后, 被装饰器 identity_loaded.connect_via(app) 装饰的函数 on_identity_loaded(sender, identity) 就会接受该信号, 并为 user 绑定应有 Needs, 以此来赋予其权限.

NOTE 4: 在用户认证通过后,Flask-Principal 会将用户的身份(identity) 存储在 session 中。

除了登录的时候用户身份会被改变, 登出也是一样的.

  1. @main_blueprint.route('/logout', methods=['GET', 'POST'])
  2. def logout():
  3. """View function for logout."""
  4. # Remove the username from the cookie.
  5. # session.pop('username', None)
  6. # Using the Flask-Login to processing and check the logout status for user.
  7. logout_user()
  8. identity_changed.send(
  9. current_app._get_current_object(),
  10. identity=AnonymousIdentity())
  11. flash("You have been logged out.", category="success")
  12. return redirect(url_for('main.login'))

NOTE: 用户登出系统后清理 session,Flask-Principal 会将用户的身份变为 AnonymousIdentity(匿名身份)。

实现只有文章作者才能编辑文章

  • jmilkfansblog.controllers.blog.py
  1. @blog_blueprint.route('/edit/<string:id>', methods=['GET', 'POST'])
  2. @login_required
  3. @poster_permission.require(http_exception=403)
  4. def edit_post(id):
  5. """View function for edit_post."""
  6. post = Post.query.get_or_404(id)
  7. # Ensure the user logged in.
  8. if not current_user:
  9. return redirect(url_for('main.login'))
  10. # Only the post onwer can be edit this post.
  11. if current_user != post.users:
  12. return redirect(url_for('blog.post', post_id=id))
  13. # 当 user 是 poster 或者 admin 时, 才能够编辑文章
  14. # Admin can be edit the post.
  15. permission = Permission(UserNeed(post.users.id))
  16. if permission.can() or admin_permission.can():
  17. form = PostForm()
  18. #if current_user != post.users:
  19. # abort(403)
  20. if form.validate_on_submit():
  21. post.title = form.title.data
  22. post.text = form.text.data
  23. post.publish_date = datetime.now()
  24. # Update the post
  25. db.session.add(post)
  26. db.session.commit()
  27. return redirect(url_for('blog.post', post_id=post.id))
  28. else:
  29. abort(403)
  30. # Still retain the original content, if validate is false.
  31. form.title.data = post.title
  32. form.text.data = post.text
  33. return render_template('edit_post.html', form=form, post=post)

实现效果

  • 以具有 poster identity 的 jmilkfan_2016 登录

  • 创建新的文章

  • jmilkfansblog.controllers.blog:edit_port()中打个断点, 我们来看看此时 permision 和 admin_permission 对象的值.

  1. (Pdb) l
  2. 165 return redirect(url_for('blog.post', post_id=id))
  3. 166
  4. 167 import pdb
  5. 168 pdb.set_trace()
  6. 169 # Admin can be edit the post.
  7. 170 -> permission = Permission(UserNeed(post.users.id))
  8. 171 if permission.can() or admin_permission.can():
  9. 172 form = PostForm()
  10. 173
  11. 174 #if current_user != post.users:
  12. 175 # abort(403)
  13. (Pdb) n
  14. > /opt/JmilkFan-s-Blog/jmilkfansblog/controllers/blog.py(171)edit_post()
  15. -> if permission.can() or admin_permission.can():
  16. (Pdb) permission
  17. <Permission needs=set([Need(method='id', value=u'b003f813-abfa-46d6-babc-2033b0b43f7e')]) excludes=set([])>
  18. (Pdb) permission.can()
  19. True

可以看见 permission 对象所对应的 user id == b003f813-abfa-46d6-babc-2033b0b43f7e, 而该 user 在数据库中对应的 roles == [87d180cc-bfa5-4c6a-87d4-01decb9c8649, 4b8b5c13-76fa-47e1-8403-623d284b2db7], 所以 user 在登录时由于其自身 Identity 的改变而触发了 on_identity_loaded() 方法, 将 admin/poster 两个 roles 对应的 RoleNeed 绑定到 user 自身的 identity 对象上, 从而拥有了编辑文章的权限.

否则, 如果是匿名用户想要编辑该文章的话就会触发 403

用 Flask 来写个轻博客 (25) — 使用 Flask-Principal 实现角色权限功能的更多相关文章

  1. 用 Flask 来写个轻博客

    用 Flask 来写个轻博客 用 Flask 来写个轻博客 (1) — 创建项目 用 Flask 来写个轻博客 (2) — Hello World! 用 Flask 来写个轻博客 (3) — (M)V ...

  2. 用 Flask 来写个轻博客 (37) — 在 Github 上为第一阶段的版本打 Tag

    Blog 项目源码:https://github.com/JmilkFan/JmilkFan-s-Blog 目录 目录 前文列表 第一阶段结语 打 Tag 前文列表 用 Flask 来写个轻博客 (1 ...

  3. 用 Flask 来写个轻博客 (36) — 使用 Flask-RESTful 来构建 RESTful API 之五

    目录 目录 前文列表 PUT 请求 DELETE 请求 测试 对一条已经存在的 posts 记录进行 update 操作 删除一条记录 前文列表 用 Flask 来写个轻博客 (1) - 创建项目 用 ...

  4. 用 Flask 来写个轻博客 (35) — 使用 Flask-RESTful 来构建 RESTful API 之四

    Blog 项目源码:https://github.com/JmilkFan/JmilkFan-s-Blog 目录 目录 前文列表 POST 请求 身份认证 测试 前文列表 用 Flask 来写个轻博客 ...

  5. 用 Flask 来写个轻博客 (34) — 使用 Flask-RESTful 来构建 RESTful API 之三

    目录 目录 前文列表 应用请求中的参数实现 API 分页 测试 前文列表 用 Flask 来写个轻博客 (1) - 创建项目 用 Flask 来写个轻博客 (2) - Hello World! 用 F ...

  6. 用 Flask 来写个轻博客 (33) — 使用 Flask-RESTful 来构建 RESTful API 之二

    Blog 项目源码:https://github.com/JmilkFan/JmilkFan-s-Blog 目录 目录 前文列表 扩展阅读 构建 RESTful Flask API 定义资源路由 格式 ...

  7. 用 Flask 来写个轻博客 (32) — 使用 Flask-RESTful 来构建 RESTful API 之一

    目录 目录 前文列表 扩展阅读 RESTful API REST 原则 无状态原则 面向资源 RESTful API 的优势 REST 约束 前文列表 用 Flask 来写个轻博客 (1) - 创建项 ...

  8. 用 Flask 来写个轻博客 (31) — 使用 Flask-Admin 实现 FileSystem 管理

    Blog 项目源码:https://github.com/JmilkFan/JmilkFan-s-Blog 目录 目录 前文列表 扩展阅读 编写 FileSystem Admin 页面 Flask-A ...

  9. 用 Flask 来写个轻博客 (30) — 使用 Flask-Admin 增强文章管理功能

    Blog 项目源码:https://github.com/JmilkFan/JmilkFan-s-Blog 目录 目录 前文列表 扩展阅读 实现文章管理功能 实现效果 前文列表 用 Flask 来写个 ...

随机推荐

  1. JavaScript.import

    // --file.js-- function getJSON(url, callback) {   let xhr = new XMLHttpRequest();   xhr.onload = fu ...

  2. zookeeper+dubbo+demo

    zookeeper下载地址 https://archive.apache.org/dist/zookeeper/ zookeeper安装和使用 windows环境 https://blog.csdn. ...

  3. Android关于Activity生命周期详解

    子曰:溫故而知新,可以為師矣.<論語> 学习技术也一样,对于技术文档或者经典的技术书籍来说,指望看一遍就完全掌握,那基本不大可能,所以我们需要经常回过头再仔细研读几遍,以领悟到作者的思想精 ...

  4. 记录使用node启用微信公众平台服务器配置

    在微信公众平台的基本信息里面的,修改服务器配置后会有下面的界面. EncodingAESKey是随机生成的,加密方式根据需要自己选择. 关于URL和Token: URL:填写之后会向这个URL发送一个 ...

  5. Spring Boot 2 Webflux的全局异常处理

    https://www.jianshu.com/p/6f631f3e00b9 本文首先将会回顾Spring 5之前的SpringMVC异常处理机制,然后主要讲解Spring Boot 2 Webflu ...

  6. 洛谷 P2863 [USACO06JAN]牛的舞会The Cow Prom(Tarjan)

    一道tarjan的模板水题 在这里还是着重解释一下tarjan的代码 #include<iostream> #include<cstdio> #include<algor ...

  7. Sass函数:数字函数-ceil()函数

    ceil() 函数将一个数转换成最接近于自己的整数,会将一个大于自身的任何小数转换成大于本身 1 的整数.也就是只做入,不做舍的计算: >> ceil(2.0) 2 >> ce ...

  8. Vue--按键修饰符(逐个学习按键修饰符)

    在监听键盘事件时,我们经常需要检查常见的键值.Vue 允许为 v-on 在监听键盘事件时添加按键修饰符: <!-- 只有在 `keyCode` 是 13 时调用 `vm.submit()` -- ...

  9. CF555E Case of Computer Network

    题面:https://www.luogu.com.cn/problem/CF555E 题意:给定一张\(n\)个点\(m\)条边的无向图. 给定\(q\)组有向点对\((s,t)\). 询问是否存在使 ...

  10. ElasticSearch java api -单例模式

    //单例模式 private static Settings getSettingInstance(){ if(settings==null){ synchronized (Settings.clas ...