Flask 中的自定义身份验证和参数检查

Posted

技术标签:

【中文标题】Flask 中的自定义身份验证和参数检查【英文标题】:Custom authentication and parameter checking in Flask 【发布时间】:2018-03-31 16:35:15 【问题描述】:

我目前正在重构我大约两年前编写的 Flask 应用程序,我怀疑我所做的一些事情不像使用该库那样优雅和干净。因此,我想就如何改善现状征求一些建议:

    该应用提供了多个 API 端点,每个端点都可以通过 /<category>/<endpoint> 形式的路由访问,其中 <category> 是 10 个不同类别之一。 对于每个<category>,我创建了一个独特的Python模块<category>.py,将它放置在一个api/子目录和一个独特的flask.Blueprint(实际上是它的一个子类,见下文)并将其注册到主@ 987654327@. 每个模块<category>.py 包含许多函数,它们充当各个类别的端点,并且每个都用路由装饰器进行装饰。

到目前为止一切顺利。

每个端点函数都接受许多参数,这些参数可以作为 GET 请求的参数传递,也可以作为 POST 请求的 JSON 有效负载中 parameter 字段的一部分传递。因此,在调用相应的端点函数之前,它会检查是否提供了正确数量的具有正确名称的参数。

此外,应用程序需要检查是否允许客户端调用某个函数。为此,读取由网络服务器设置的环境变量SSL_CLIENT_CERT(在我的例子中是通过 FCGI 的 lighttpd),并将其指纹与一些内部权限文件进行比较。

由于我不太清楚将上述逻辑放在哪里,我将flask.Blueprint 子类化并编写了我自己的(修改后的)route 装饰器(custom_route)。这个装饰器现在要么返回自定义错误响应(flask.Response 对象),要么返回自定义成功响应(从而使用从客户端传递的参数调用端点函数)。

所以模块category_xy.py 看起来像这样:

category_xy = CustomBlueprint('category_xy', __name__)    

@category_xy.custom_route('/category_xy/some_endpoint',
                          auth_required=True,
                          methods=['GET', 'POST'])
def some_endpoint(foo, bar):
    # do stuff 
    return '123'

CustomBlueprint 在单独的文件中定义为(部分伪代码):

from flask import Blueprint, Response, g, request
from functools import wraps


class CustomBlueprint(Blueprint):
    def custom_route(self, rule, **options):
        def decorator(f):

            # don't pass the custom parameter 'auth_required' on to
            # self.route
            modified_options = dict(options)
            if 'auth_required' in modified_options:
                del modified_options['auth_required']

            @self.route(rule, **modified_options)
            @wraps(f)
            def wrapper():
                # do some authentication checks...
                if not authenticiated():
                    return Response(...)

                # check if correct paramters have been passed
                if not correct_paramters():
                    return Response(...)

                # extract parameter values from either GET or POST request
                arg_values = get_arg_values(request)

                # call the decorated function with these parameters
                result = f(*arg_values)

                return Response(result, ...)
            return wrapper
        return decorator

这行得通,但感觉一点也不干净,我认为应该有一种更好、更清洁的方法来做到这一点。将所有这些逻辑放在自定义装饰器中感觉非常错误。

有没有比 Flask 更有经验的人提供一些想法和/或最佳实践?

【问题讨论】:

我想你们的类别有一些共同点:相似的端点名称、授权机制、验证功能。对吗? 是的!每个端点函数都会触发服务器上的某些操作。一个类别中的所有操作都有些相关,并且通常也具有相似的端点名称。 【参考方案1】:

首先,您可以切换到Pluggable Views — 允许您将视图函数编写为类的 API:

from flask import Blueprint, Response, render_template
from flask.views import View

from models import db, CategoryXY # Let`s have here a database model

category_blueprint = Blueprint('category_xy', __name__)

class CategoryXYView(View):
    def get_list(self):
        items = db.session.query(CategoryXY).all()
        return render_template('category_xy_list.html', items=items)

    def get_one(self, item_id):
        item = db.session.query(CategoryXY).first()
        if item is None:
            return Response(status=404)
        return render_template('category_xy_edit.html', item=item)

    def dispatch_request(self, item_id):
        """Required method which handles incoming requests"""
        if item_id is None:
            return self.get_list()
        return self.get_one(item_id)

category_xy_view = CategoryXYView.as_view('category_xy')
category_blueprint.add_url_rule(
    'category_xy',
    view_func=category_xy_view,
    defaults='item_id': None
)
category_blueprint.add_url_rule(
    'category_xy/<item_id>',
    view_func=category_xy_view
)

当您拥有具有通用方法(数据库对象处理、验证、授权检查)的基视图类并且从它继承“类别”类时,它会变得非常有用:

class BaseView(View):
    model = None
    edit_template = None
    list_template = None

    def get_one(self, item_id):
        item = db.session.query(self.model).filter_by(id=item_id).first()
        if item is None:
            return Response(status=404)
        return render_template(self.edit_template, item=item)

    def get_list(self):
        items = db.session.query(self.model).all()
        return render_template(self.list_template, items=items)

    def dispatch_request(self, item_id):
        if item_id is None:
            return self.get_list()
        return self.get_one(item_id)

class CategoryXYView(BaseView):
    model = CategoryXY
    edit_template = 'category_xy_edit.html'
    list_template = 'category_xy_list.html'

category_xy_view = CategoryXYView.as_view('category_xy')
category_blueprint.add_url_rule(
    'category_xy',
    view_func=category_xy_view,
    defaults='item_id': None
)
category_blueprint.add_url_rule(
    'category_xy/<item_id>',
    view_func=category_xy_view
)

如果其中一个类别具有扩展功能,您只需覆盖其中一种常用方法:

class CategoryXXView(BaseView):
    model = CategoryXX
    edit_template = 'category_xx_edit.html'
    list_template = 'category_xx_list.html'

    def get_one(self, item_id):
        item = db.session.query(self.model).first()
        if item is None:
            return Response(status=404)
        xy_items = db.session.query(CategoryXY).all()
        return render_template(self.edit_template, item=item, xy_items=xy_items)

为了自动生成路由,您可以为BaseView 编写一个元类。每个新类的路由都会在类定义之后生成:

class ViewMeta(type):
    def __new__(mcs, name, bases, attributes):
        cls = type(name, bases, attributes)

        if cls.route is not None:
            view_function = cls.as_view(cls.route)
            category_blueprint.add_url_rule(
                cls.route,
                view_func=view_function,
                defaults='item_id': None
            )
            category_blueprint.add_url_rule(
                '/<item_id>'.format(cls.route),
                view_func=view_function
            )
        return cls

class BaseView(View):
    __metaclass__ = ViewMeta
    route = None
    model = None
    # Other common attributes

class CategoryXYView(BaseView):
    route = 'category_xy'
    model = CategoryXY

【讨论】:

以上是关于Flask 中的自定义身份验证和参数检查的主要内容,如果未能解决你的问题,请参考以下文章

在电子邮件密码身份验证中进行额外的自定义检查 (Flutter - Firebase)

具有 Spring Security 和 Java Config 的自定义身份验证提供程序

Azure 门户的自定义身份验证

与 parse-server 和 auth0 的自定义身份验证集成

Kong API 网关中的自定义身份验证服务

JWT 身份验证不适用于 Django 中的自定义控制器