在使用flask設(shè)計(jì)restful api的時(shí)候,有一個(gè)很重要的問題就是如何進(jìn)行權(quán)限管理,以及如何進(jìn)行角色的定義,在網(wǎng)上找了一下沒有發(fā)現(xiàn)有類似的資料,雖然有些針對網(wǎng)站進(jìn)行的權(quán)限管理設(shè)計(jì),但是跟restful api接口的權(quán)限管理還是有很多不同的,于是乎自己動手,豐衣足食。為方便后來者,特撰此文!
權(quán)限的設(shè)計(jì)
從本質(zhì)上思考,我需要為每個(gè)API接口設(shè)定相應(yīng)的權(quán)限,所以針對API的權(quán)限列表跟普通網(wǎng)站的權(quán)限設(shè)計(jì)是不同的,普通網(wǎng)站的權(quán)限設(shè)計(jì)是針對某個(gè)功能,比如是否可以comment功能,通常的權(quán)限定義如下:
class Permission:
"""
權(quán)限表
"""
COMMENT = 0x01 # 評論
MODERATE_COMMENT = 0x02 # 移除評論
但是針對restful api,我們更希望權(quán)限是針對我們的api接口,而restful api接口是跟我們路由的endpoint以及http method相關(guān)的,所以我們的權(quán)限設(shè)計(jì)應(yīng)該是類似如下示例中的樣子:
# 這里comments是路由的endpoint,接口在判斷用戶是否有權(quán)限的時(shí)候
# 可以先獲取到endpoint和http method,然后就可以查看其是否有權(quán)限
comment_permission = {"comments": {"post": True, "get": True, "delete": False}}
角色的設(shè)計(jì)
通常,我們在做網(wǎng)站的角色設(shè)計(jì)時(shí)會將角色存儲在數(shù)據(jù)庫當(dāng)中,并會通過或運(yùn)算(|)賦予角色以特定權(quán)限,如下:
class Role(db.Model):
"""
用戶角色
"""
id = db.Column(db.Integer, primary_key=True)
# 該用戶角色名稱
name = db.Column(db.String(164))
# 該用戶角色是否為默認(rèn)
default = db.Column(db.Boolean, default=False, index=True)
# 該用戶角色對應(yīng)的權(quán)限
permissions = db.Column(db.Integer)
# 該用戶角色和用戶的關(guān)系
# 角色為該用戶角色的所有用戶
users = db.relationship('User', backref='role', lazy='dynamic')
@staticmethod
def insert_roles():
"""
創(chuàng)建用戶角色
"""
roles = {
# 定義了兩個(gè)用戶角色(User, Admin)
'User': (Permission.COMMENT, True),
'Admin': (Permission.COMMENT |
Permission.MODERATE_COMMENT, False)
}
for r in roles:
role = Role.query.filter_by(name=r).first()
if role is None:
# 如果用戶角色沒有創(chuàng)建: 創(chuàng)建用戶角色
role = Role(name=r)
role.permissions = roles[r][0]
role.default = roles[r][1]
db.session.add(role)
db.session.commit()
這里其實(shí)我一直沒有搞明白,為什么要將角色存儲于數(shù)據(jù)庫當(dāng)中,在我看來這只會導(dǎo)致更多的I/O操作從而影響系統(tǒng)的性能,因此我在設(shè)計(jì)角色的時(shí)候根本沒有考慮存儲到數(shù)據(jù)庫中,角色的數(shù)據(jù)結(jié)構(gòu)在系統(tǒng)運(yùn)行時(shí),直接存在內(nèi)存當(dāng)中,這樣在接口調(diào)用時(shí),可以直接使用角色相關(guān)的數(shù)據(jù)結(jié)構(gòu)。而且由于我們的權(quán)限設(shè)計(jì)也不太相同,所以我針對restful api設(shè)計(jì)的Role如下:
USER = 1
ADMIN = 2
VISITOR = 3
Role = {
USER: {
"comment": {"post": True, "patch": True, "get": True, "delete": True},
"share": {"post": True}
},
ADMIN: {
"comment": {"post": True, "patch": True, "get": True, "delete": True},
"share": {"post": True}
},
VISITOR: {
"comment": {"get": True},
"share": {"post": True}
}
}
用戶可以被賦予特定的role,如下:
userA = {"name": "John", "role": USER}
那么接口如何判斷用戶是否有權(quán)限訪問呢?
首先用戶訪問接口時(shí)都會帶有用戶信息,restful api一般是通過token來表明身份,系統(tǒng)通過token來獲取用戶的信息,比如用戶名,然后我們可以通過用戶名來獲取用戶的角色role,假設(shè)我們訪問的接口是comments endpoint的post接口,那么就可以如下判斷:
def access_control(user):
"""判斷用戶是否有訪問權(quán)限,有就返回True,沒有返回False"""
# 首先要獲取到API的endpoint和http method,此處代碼省略
...
role = user.get('role', VISITOR)
try:
if not Role[role][endpoint][http_method]:
return False
return True
except KeyError:
return False
由于基本所有的接口都需要access control,那么我們把上邊的代碼稍作改變,讓它成為一個(gè)decorator,同時(shí),user信息也可以直接獲取而不需要從參數(shù)傳遞,如下:
from functools import wraps
def get_role():
# 這里get_resource_by_name用于從數(shù)據(jù)庫中獲取該用戶的信息,這個(gè)需要自己去定義
# 另外我們可以在登錄驗(yàn)證的時(shí)候或者token驗(yàn)證的時(shí)候講user name存儲于全局變量g中,這樣我們可以隨時(shí)獲取該用戶名
user = UserModel.get_resource_by_name(g.user_name)
return user.get("role", VISITOR)
def access_control(func):
@wraps(func)
def wrap_func(*args, **kwargs):
# 同樣要先獲取到API的endpoint和http method,此處代碼省略
...
try:
if not Role[role][endpoint][http_method]:
return make_response(
jsonify({'error': 'no permission'}), 403)
return func(*args, **kwargs)
except KeyError:
return make_response(
jsonify({'error': 'no permission'}), 403)
return wrap_func
以下是一個(gè)獲取圖片resource的使用示例
from flask_restful import Resource
class ImageResource(Resource):
def __init__(self):
super(ImageResource, self).__init__()
@token_auth.login_required
@access_control
def get(self, resource_id):
response = resource_get(resource_id)
return response
這里另外一個(gè)decortor @token_auth.login_required用于token驗(yàn)證,大家可以先不用理會。到這里我們已經(jīng)可以針對每個(gè)接口自動判斷該用戶是否有權(quán)限訪問了,而所有權(quán)限的變化,都可以通過修改Role中的權(quán)限來進(jìn)行更改,而不需要更改原來的代碼,很爽吧,有木有?
不過,筆者在項(xiàng)目中還遇到了另外一個(gè)問題,有時(shí)候針對一個(gè)接口所有的user都應(yīng)該有權(quán)限,但是針對特定的resource,只能resource owner可以操作,舉個(gè)栗子,比如我們要刪除某個(gè)評論,但是只允許發(fā)布評論的人才有權(quán)限刪除,也就是comment resource的owner才可以使用delete接口刪除,但是我們所有的用戶在Role定義的時(shí)候delete接口都是True,這個(gè)怎么辦呢?
這就需要我們在access_control檢測完了之后再進(jìn)一步檢測該用戶是否是resource owner,所以我們就需要進(jìn)一步檢測,這里添加一個(gè)decorator如下:
def get_resource_owner():
"""獲取resource的owner"""
# 自定義,代碼省略
...
def owner_permission_required(func):
@wrap(func)
def wrap_func(*args, **kwargs):
if g.user_name == get_resource_owner():
return func(*args, **kwargs)
return make_response(
jsonify({'error': 'no permission'}), 403)
return wrap_func
使用如下:
from flask_restful import Resource
class CommentResource(Resource):
def __init__(self):
super(CommentResource, self).__init__()
@token_auth.login_required
@access_control
@owner_permission_required
@marshal_with(image_fields)
def delete(self, resource_id):
response = resource_delete(resource_id)
return response
注意:decorator的順序是不能改變的。
至此,Restful API權(quán)限管理相關(guān)的設(shè)計(jì)就完成了,如果文章給你帶來了啟發(fā),記得點(diǎn)贊哦!