再次理解Flask中RESTfulAPI

再次理解 Flask 中 RESTful API

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

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

  • 初始化扩展

    1
    2
    3
    from flask_restful import Api

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

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    from flask_restful import Resource

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

    1
    2
    3
    4
    5
    6
    7
    8
    9
    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 对象。

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    17
    18
    19
    20
    21
    22
    23
    24
    25
    26
    27
    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

1
2
3
4
5
6
7
8
9
10
11
12
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

1
2
3
4
5
6
7
8
9
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 来进行相应的权限操作。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
# 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 类中添加一个类方法

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
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 对象。