再次理解Flask中RESTfulAPI

Author Avatar
patrickcty 8月 10, 2017

再次理解 Flask 中 RESTful API

用 Flask 构建 RESTful 有两种方法,一种是狗书中用到的方法,把视图“包装”成 API —— 视图函数完成 API 所需要的功能,但最后返回的不再是一个 HTML 文件,而是由所需要的数据构成的 JSON。

而另一种方法就是使用 flask-restful 扩展来构建 API 了,利用专有的类来作为 REST 的资源部分,通过资源类的不同方法来处理不同的 http 请求,最后以 JSON 格式返回数据,具体步骤如下:

  • 初始化扩展

    from flask_restful import Api
    
    rest_api = Api()
  • 自定义资源类以及相应的请求方法(对应了一个路由)

    from flask_restful import Resource
    
    class PostApi(Resource):
        # 如果用没有定义的请求方法访问则会返回 405 错误
        def get(self):
            pass
        def post(self):
            pass
        def put(self):
            pass
  • 初始化 API 对象路由

    from .extensions import rest_api
    from .controller.rest.post import PostApi
    
    def create_app(object_name):
        ...
        # 如果是多个路由则要分别绑定
        rest_api.add_resource(PostApi, '/api/post', '/api/post/<int:post_id>')
        rest_api.add_resource(AnotherApi, '/api/another')
        rest_api.init_app(app)
  • JSON 化输出格式

JSON 化也同样有两种方法:

  • 第一种是使用 flask.jsonify() 把数据手动转换成 JSON 对象再返回。
  • 第二种则是使用 field 对象和 mashal_with 函数来进行转换,返回直接返回原来的对象即可。这种方法通过一个由 field 定义的 dict 来说明要转化输出的内容,并且把它传递给 mashal_with 装饰器,最后在返回之前自动转换成相应的 JSON 对象。

    from flask_restful import Resource, fields, mashal_with
    
    nested_tag_fields = {
        'id': fields.Integer(),
        'title': fields.String()
    }
    
    post_fields = {
        # 字符串 field,从对象的 .user.username 中取出值作为值
        'author': fields.String(attribute=lambda x: x.user.username), 
        'title': fields.String(),
        'text': HTMLField(),  # 自定义 field
        # list field,转换后成为 JSON list
        # 内层为一个特殊的 field,是另外一个 JSON 对象,它是用另一个字段对象构成的
        'tags': fields.List(fields.Nested(nested_tag_fields)),
        'publish_time': fields.DateTime(dt_format='iso8601')
    }
    
    class PostApi(Resource):
        @marshal_with(post_fields)  # 对象的属性会根据这个 dict 来转换
        def get(self, post_id=None):
            if post_id:
                post = Post.query.get(post_id)
                if not post:
                    abort(404)
                return post  # 返回的 post 的内容已经被转换格式了
            ...
  • 接收请求参数

定义 parser

post_get_parser = reqparse.RequestParser()  # 初始化 parser
post_get_parser.add_argument(  # 在 parser 中添加参数以及相应规则
    'page',
    type=int,
    location=['args', 'header'],
    required=False
)
post_get_parser.add_argument(
    'user',
    type=str,
    location=['json', 'args', 'headers']
)

使用 parser

from .parsers import post_get_parser

class PostApi(Resource):
    def get(self, post_id=None):
        ...
        # 使用 parser 解析参数
        # 解析出来的可以像 dict 引用
        # 例如 args['user']
        args = post_get_parser.parse_args()

  • 身份验证

使用 access token 来进行身份认证,要完成身份认证要先通过一个 API 来进行登录验证,如果通过就返回 token,之后就根据 token 来进行相应的权限操作。

# parser 也是要定义的
class AuthApi(Resource):
    def post(self):
        args = user_post_parser.parse_args()
        user = User.query.filter_by(
            username=args['username']
        ).one()

        # 检验密码
        if user.verify_password(args['password']):
            # 通过了才生成令牌
            s = Serializer(
                current_app.config['SECRET_KEY'],
                expires_in=600
            )
            # 返回令牌,注意 dumps 出来的数据是 byte 类型的,要根据编码改成 utf-8 才能成为 JSON 内容
            return {"token": s.dumps({'id': user.id}).decode('utf-8')}

        else:
            abort(401)

验证令牌的话在 User 类中添加一个类方法

class User(db.Model, UserMixin):
    ...

    @staticmethod
    def verify_auth_token(token):
        s = Serializer(current_app.config['SECRET_KEY'])

        try:
            data = s.loads(token)
        except SignatureExpired:
            return None
        except BadSignature:
            return None
        user = User.query.get(data['id'])
        return user

要使用的话就在资源类的方法中调用这个类方法来返回 user 对象。