自建装饰器实现权限控制
在上一章 《登陆注册》中,我们为 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
8. class Role(db.Document):
9. name = db.StringField()
10. permission = db.IntField()
12. class User(db.Document):
13. name = db.StringField()
14. password = db.StringField()
15. email = db.StringField()
16. 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)
9. READ_ROLE.save()
10. CREATE_ROLE.save()
11. UPDATE_ROLE.save()
12. DELETE_ROLE.save()
13. DEFAULT_ROLE.save()
14. else:
15. READ_ROLE = Role.objects(permission=Permission.READ).first()
16. CREATE_ROLE = Role.objects(permission=Permission.CREATE).first()
17. UPDATE_ROLE = Role.objects(permission=Permission.UPDATE).first()
18. DELETE_ROLE = Role.objects(permission=Permission.DELETE).first()
19. 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
3. {
4. "email": "liqianglau@outlook.com",
5. "name": "tyrael",
6. "password": "password"
7. }
然后发现响应是:

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