在使用flask設(shè)計(jì)restful api的時(shí)候,有一個(gè)很重要的問題就是如何進(jìn)行權(quán)限管理乘盼,以及如何進(jìn)行角色的定義兔跌,在網(wǎng)上找了一下沒有發(fā)現(xiàn)有類似的資料,雖然有些針對(duì)網(wǎng)站進(jìn)行的權(quán)限管理設(shè)計(jì)婉商,但是跟restful api接口的權(quán)限管理還是有很多不同的,于是乎自己動(dòng)手渣叛,豐衣足食丈秩。為方便后來者,特撰此文淳衙!
權(quán)限的設(shè)計(jì)
從本質(zhì)上思考蘑秽,我需要為每個(gè)API接口設(shè)定相應(yīng)的權(quán)限,所以針對(duì)API的權(quán)限列表跟普通網(wǎng)站的權(quán)限設(shè)計(jì)是不同的箫攀,普通網(wǎng)站的權(quán)限設(shè)計(jì)是針對(duì)某個(gè)功能肠牲,比如是否可以comment功能,通常的權(quán)限定義如下:
class Permission:
"""
權(quán)限表
"""
COMMENT = 0x01 # 評(píng)論
MODERATE_COMMENT = 0x02 # 移除評(píng)論
但是針對(duì)restful api靴跛,我們更希望權(quán)限是針對(duì)我們的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í)會(huì)將角色存儲(chǔ)在數(shù)據(jù)庫當(dāng)中绝葡,并會(huì)通過或運(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)
# 該用戶角色對(duì)應(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í)我一直沒有搞明白,為什么要將角色存儲(chǔ)于數(shù)據(jù)庫當(dāng)中藏畅,在我看來這只會(huì)導(dǎo)致更多的I/O操作從而影響系統(tǒng)的性能敷硅,因此我在設(shè)計(jì)角色的時(shí)候根本沒有考慮存儲(chǔ)到數(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ì)也不太相同坦辟,所以我針對(duì)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í)都會(huì)帶有用戶信息锉走,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存儲(chǔ)于全局變量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)證,大家可以先不用理會(huì)窍蓝。到這里我們已經(jīng)可以針對(duì)每個(gè)接口自動(dòng)判斷該用戶是否有權(quán)限訪問了腋颠,而所有權(quán)限的變化,都可以通過修改Role中的權(quán)限來進(jìn)行更改吓笙,而不需要更改原來的代碼秕豫,很爽吧,有木有观蓄?
不過,筆者在項(xiàng)目中還遇到了另外一個(gè)問題祠墅,有時(shí)候針對(duì)一個(gè)接口所有的user都應(yīng)該有權(quán)限侮穿,但是針對(duì)特定的resource,只能resource owner可以操作毁嗦,舉個(gè)栗子亲茅,比如我們要?jiǎng)h除某個(gè)評(píng)論,但是只允許發(fā)布評(píng)論的人才有權(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)贊哦氨鹏!