Flask Restful API 權限管理設計與實現

在使用 flask 設計 restful api 的時候,有一個很重要的問題就是如何進行權限管理,以及如何進行角色的定義,在網上找了一下沒有發現有類似的資料,雖然有些針對網站進行的權限管理設計,但是跟 restful api 接口的權限管理還是有很多不同的,于是乎自己動手,豐衣足食。為方便后來者,特撰此文!

權限的設計

從本質上思考,我需要為每個 API 接口設定相應的權限,所以針對 API 的權限列表跟普通網站的權限設計是不同的,普通網站的權限設計是針對某個功能,比如是否可以 comment 功能,通常的權限定義如下:

class Permission:
    """
    權限表
    """
    COMMENT = 0x01  # 評論
    MODERATE_COMMENT = 0x02  # 移除評論

但是針對 restful api,我們更希望權限是針對我們的 api 接口,而 restful api 接口是跟我們路由的 endpoint 以及 http method 相關的,所以我們的權限設計應該是類似如下示例中的樣子:

# 這里comments是路由的endpoint,接口在判斷用戶是否有權限的時候
# 可以先獲取到endpoint和http method,然后就可以查看其是否有權限
comment_permission = {"comments": {"post": True, "get": True, "delete": False}}

角色的設計

通常,我們在做網站的角色設計時會將角色存儲在數據庫當中,并會通過或運算 (|) 賦予角色以特定權限,如下:

class Role(db.Model):
    """
    用戶角色
    """
    id = db.Column(db.Integer, primary_key=True)
    # 該用戶角色名稱
    name = db.Column(db.String(164))
    # 該用戶角色是否為默認
    default = db.Column(db.Boolean, default=False, index=True)
    # 該用戶角色對應的權限
    permissions = db.Column(db.Integer)
    # 該用戶角色和用戶的關系
    # 角色為該用戶角色的所有用戶
    users = db.relationship('User', backref='role', lazy='dynamic')

    @staticmethod
    def insert_roles():
        """
        創建用戶角色
        """
        roles = {
            # 定義了兩個用戶角色(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:
                # 如果用戶角色沒有創建: 創建用戶角色
                role = Role(name=r)
            role.permissions = roles[r][0]
            role.default = roles[r][1]
            db.session.add(role)
            db.session.commit()

這里其實我一直沒有搞明白,為什么要將角色存儲于數據庫當中,在我看來這只會導致更多的 I/O 操作從而影響系統的性能,因此我在設計角色的時候根本沒有考慮存儲到數據庫中,角色的數據結構在系統運行時,直接存在內存當中,這樣在接口調用時,可以直接使用角色相關的數據結構。而且由于我們的權限設計也不太相同,所以我針對 restful api 設計的 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}

那么接口如何判斷用戶是否有權限訪問呢? 首先用戶訪問接口時都會帶有用戶信息,restful api 一般是通過 token 來表明身份,系統通過 token 來獲取用戶的信息,比如用戶名,然后我們可以通過用戶名來獲取用戶的角色 role,假設我們訪問的接口是 comments endpoint 的 post 接口,那么就可以如下判斷:

def access_control(user):
    """判斷用戶是否有訪問權限,有就返回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,那么我們把上邊的代碼稍作改變,讓它成為一個 decorator,同時,user 信息也可以直接獲取而不需要從參數傳遞,如下:

from functools import wraps

def get_role():
    # 這里get_resource_by_name用于從數據庫中獲取該用戶的信息,這個需要自己去定義
    # 另外我們可以在登錄驗證的時候或者token驗證的時候講user name存儲于全局變量g中,這樣我們可以隨時獲取該用戶名
    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

以下是一個獲取圖片 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

這里另外一個 decortor @token_auth.login_required 用于 token 驗證,大家可以先不用理會。到這里我們已經可以針對每個接口自動判斷該用戶是否有權限訪問了,而所有權限的變化,都可以通過修改 Role 中的權限來進行更改,而不需要更改原來的代碼,很爽吧,有木有? 不過,筆者在項目中還遇到了另外一個問題,有時候針對一個接口所有的 user 都應該有權限,但是針對特定的 resource,只能 resource owner 可以操作,舉個栗子,比如我們要刪除某個評論,但是只允許發布評論的人才有權限刪除,也就是 comment resource 的 owner 才可以使用 delete 接口刪除,但是我們所有的用戶在 Role 定義的時候 delete 接口都是 True,這個怎么辦呢? 這就需要我們在 access_control 檢測完了之后再進一步檢測該用戶是否是 resource owner,所以我們就需要進一步檢測,這里添加一個 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 權限管理相關的設計就完成了,如果文章給你帶來了啟發,記得點贊哦!

本文鏈接:參與評論 ?

--EOF--

提醒:本文最后更新于 415 天前,文中所描述的信息可能已發生改變,請謹慎使用。

專題「web開發」的其它文章 ?

Comments

好日子高手社区广东好日子