自建装饰器实现权限控制

在上一章 《登陆注册》中,我们为 REST 的 API 设置了新增、更新和删除的操作需要登陆才能完成。细想一下,这样未免太过草率,因为对于一个系统来说,用户肯定是分为不同的级别的,例如普通的用户也就只能查查数据,然后一些用户还能多一个增加数据的权限,再高级一点的还能修改数据,最高级的就是增删改查都能。

对于这些更加丰富的需求,我们目前的登陆可用明显还不能满足需求,因此,按常规本章应该会引入一个新的扩展,而 Flask 确实是有一款叫做 Flask-Principal,的扩展可以满足我们的需求,通过这个扩展,我们希望能够达到更细粒度得控制用户的权限。但是,我嫌弃这个扩展太累赘了,所以本章不准备使用这个扩展,而是自己编写一个权限控制的扩展进行权限的控制。

权限控制设计

我们这里的权限控制采用 RBAC 的方式,首先,我们会创建一个 Role 的 Model,然后给每个 User 分配一个 Role,这样的话,我们就可以限制某个操作需要某种 Role 才能执行,这样的话就实现了更细粒度的权限控制。

这里还有个实现细节需要先说明一下,我们的 Role 的权限是以二进制位来表示的,每一个二进制位表示一种权限:

  • 第一位表示可以读取记录
  • 第二位表示可以新建记录
  • 第三位表示可以更新记录
  • 第四位表示可以删除记录

这样的话,如果一个用户只能读取记录,那么他对应的 Role 的权限应该是 0000 0001b ,换算成十六进制的话就是: 0x01

如果一个用户所有操作都可以执行,那么它的权限应该对应于 0000 1111b,换算成十六进制的话就是:0x0f

那么,假如我们要判断一个用户时候可以进行新建操作,那么应该怎么实现这个逻辑?我这里的实现机制是如果是只有新建操作,那么对应的权限就是:0000 0010b,那如果我要判断一个用户时候有新建的权限,那么我只需要对这个用户的权限和这个操作所需要的权限进行 and 操作,如果得到的结果等于需要的权限的话,那么就表示该用户拥有权限,可能说得有点复杂,上一个简单的例子

  1. 用户 A 的权限: 0000 0001b 只有读取记录的权限
  2. 用户 B 的权限: 0000 1111b 拥有所有权限
  3. 新建记录需要权限: 0000 0010b 需要新建权限
  4. 用户A是否可以新建: 0000 0001b and 0000 0010b = 0000 0000b != 新建权限,所以不能新建
  5. 用户B时候可以新建: 0000 1111b and 0000 0010b = 0000 0010b == 新建权限,所以可以新建

大概就是这样一个场景,大家可以自己动手演练演练,看下是否可行。

创建 Role Model

之前已经在 《集成数据库》 章节中讲解过了如何创建 Model,所以这里直接根据之前的经验创建 Role Model,然后再往 User 中加上一个 Role 字段。

  1. class Permission:
  2. READ = 0x01
  3. CREATE = 0x02
  4. UPDATE = 0x04
  5. DELETE = 0x08
  6. DEFAULT = READ
  7. class Role(db.Document):
  8. name = db.StringField()
  9. permission = db.IntField()
  10. class User(db.Document):
  11. name = db.StringField()
  12. password = db.StringField()
  13. email = db.StringField()
  14. role = db.ReferenceField('Role' default=DEFAULT_ROLE)

这里就简单得创建了一个 Role 的 Model,而 Role 只有一个名称,用于标示这个角色,另外一个就是该角色拥有的权限了。然后就是在 User 中添加了一个 ReferenceField,这个在 MongoEngine 里面就表示是外引用的意思,我们可以直接通过这个成员变量访问到用户的 Role 的 permission。

同时,为了保持代码的可维护性,我们将 permission 都写在一个类中,还设置了一个默认的权限,默认为 READ。

因为我们现在的数据库中还没有 Role 相关的记录,所以我们需要在启动应用的时候进行插入数据,所以我做了这样的一个操作:

  1. # init roles
  2. if Role.objects.count() <= 0:
  3. READ_ROLE = Role('READER', Permission.READ)
  4. CREATE_ROLE = Role('CREATER', Permission.CREATE)
  5. UPDATE_ROLE = Role('UPDATER', Permission.UPDATE)
  6. DELETE_ROLE = Role('DELETER', Permission.DELETE)
  7. DEFAULT_ROLE = Role('DEFAULT', Permission.DEFAULT)
  8. READ_ROLE.save()
  9. CREATE_ROLE.save()
  10. UPDATE_ROLE.save()
  11. DELETE_ROLE.save()
  12. DEFAULT_ROLE.save()
  13. else:
  14. READ_ROLE = Role.objects(permission=Permission.READ).first()
  15. CREATE_ROLE = Role.objects(permission=Permission.CREATE).first()
  16. UPDATE_ROLE = Role.objects(permission=Permission.UPDATE).first()
  17. DELETE_ROLE = Role.objects(permission=Permission.DELETE).first()
  18. DEFAULT_ROLE = Role.objects(permission=Permission.DEFAULT).first()

虽然这段代码有不严谨的地方,但是作为讲解的话无关大雅,通过这段代码,我们可以保证在下面的代码中我们有五种 Role 的对象,分别对应着增删改查,还有一个默认的角色,他为读取权限。同时,我们也应该修改一下我们的 API,让他能够增加用户的默认权限。

  1. @app.route('/', methods=['POST'])
  2. @login_required
  3. def create_record():
  4. record = json.loads(request.data)
  5. user = User(name=record['name'],
  6. password=record['password'],
  7. email=record['email'],
  8. role=DEFAULT_ROLE)
  9. user.save()
  10. return jsonify(user.to_json())

这段代码只增加了一行,就是:

  1. role=DEFAULT_ROLE

权限控制

好,到这里算是完成了一半了,我们的角色已经算是有了,然后就是怎么进行权限控制了,我希望权限控制代码能够竟可能得简单,最好是能用装饰器实现,对于一些默认权限就能访问的,我希望不用加权限控制的代码就好了。没有不能实现的需求,只是实现得好坏而已,所以,既然我们都能描述出需求,那么就能够写出满足需求的代码。

首先,我们是需要编写一个权限控制的装饰器的,我们希望这个装饰器可以很方便得进行权限控制,最好是可以这样:

  1. @creater_required()
  2. def create_model():
  3. ... ...

或者这样也可以接受:

  1. @permission_required(CREATE_PERMISSION):
  2. def create_model():
  3. ... ...

那么,就先写一个较为简单的版本试试先:

  1. def permission_required(permission):
  2. def decorator(func):
  3. @wraps(func)
  4. def decorated_function(*args, **kwargs):
  5. if not current_user.is_authenticated:
  6. abort(401)
  7. user_permission = current_user.role.permission
  8. if user_permission & permission == permission:
  9. return func(*args, **kwargs)
  10. else:
  11. abort(403)
  12. return decorated_function
  13. return decorator

这一版本我们可以简单得看这几句关键的代码:

  1. if not current_user.is_authenticated:
  2. abort(401)
  3. user_permission = current_user.role.permission
  4. if user_permission & permission == permission:
  5. return func(*args, **kwargs)
  6. else:
  7. abort(403)

首先用户没有登陆肯定是没有权限的了,所以返回 401 未授权错误,如果用户没有权限(权限设计中的描述),那么就返回 403 禁止访问。

接着我们就在我们的 REST API 中尝试一下这个权限,这里相对新增用户进行尝试:

  1. @app.route('/', methods=['POST'])
  2. @permission_required(Permission.CREATE)
  3. def create_record():
  4. record = json.loads(request.data)
  5. user = User(name=record['name'],
  6. password=record['password'],
  7. email=record['email'],
  8. role=DEFAULT_ROLE)
  9. user.save()
  10. return jsonify(user.to_json()

这里只将 @login_required 的装饰器换成了

  1. @permission_required(Permission.CREATE)

然后我们尝试一下新建记录:

  1. POST http://localhost:8080
  2. {
  3. "email": "liqianglau@outlook.com",
  4. "name": "tyrael",
  5. "password": "password"
  6. }

然后发现响应是:

Image-2016-05-26-020312001.png

说明我们的权限控制生效啦。