Flask-REST接口开发 + marshmallow

滕英奕
2023-12-01

Flask-REST接口开发 + marshmallow

REST是Representational State Transfer三个单词的缩写,由Roy Fielding于2000年论文中提出,它代表着分布式服务的架构风格。

后端负责数据编造,而前端则负责数据渲染,前端静态页面调用指定api获取到有固定格式的数据,再将数据展示出来,这样呈现给用户的就是一个”动态“的过程。

REST设计原则

1 客户端-服务器:通过将用户UI与数据存储分开,我们可以简化服务器组件来提高跨多个平台的用户界面的可移植性并提高可伸缩性。 它可以表现成前后端分离的思想。

2 无状态:从客户端到服务器的每个请求都必须包含理解请求所需的所有信息,并且不能利用服务器上任何存储的上下文。 这表示你应该尽可能的避免使用session,由客户端自己标识会话状态。(token)

3 规范接口:REST接口约束定义:资源识别; 请求动作; 响应信息; 它表示通过uri标出你要操作的资源,通过请求动作(http method)标识要执行的操作,通过返回的状态码来表示这次请求的执行结果。

4 可缓存: 缓存约束要求将对请求的响应中的数据隐式或显式标记为可缓存或不可缓存。如果响应是可缓存的,则客户端缓存有权重用该响应数据以用于以后的等效请求。 它表示get请求响应头中应该表示有是否可缓存的头(Cache-Control)

uri规范

资源的描述构成了uri,它一般有以下约束:
1 使用名词。如 user, student, class

http://api.example.com/class-management/students 

http://api.example.com/device-management/managed-devices/{device-id}

http://api.example.com/user-management/users/

http://api.example.com/user-management/users/id

2 http method对应不同的请求动作(数据库或者业务逻辑)
GET:查询操作:
HTTP GET /devices?startIndex=0&size=20
HTTP GET /configurations?startIndex=0&size=20
HTTP GET devices{id}/configurations
HTTP GET devices{id}

POST:新增操作:
HTTP POST /device

PUT 更新操作(代表更新一个实体的所有属性)
HTTP PUT devices{id}

PATCH 部分更新(代表更新一个实体的部分属性)由于有的浏览器兼容性问题,一般推荐使用put
HTTP PATCH devices{id}

DELETE 删除操作
HTTP DELETE devices{id}

3 使用连字符"-“而不是”_"来提高URI的可读性

http://api.example.com/inventory-management/managed-entities //更易读

http://api.example.com/inventory_management/managed_entities //更容易出错

4 在URI中使用小写字母

http://api.example.org/my-folder/my-doc 

5 不要使用文件扩展名 文件扩展名看起来很糟糕,不会增加任何优势。删除它们也会减少URI的长度。没理由保留它们。

http://api.example.com/device-management/managed-devices.xml / /不要使用它/ /

http://api.example.com/device-management/managed-devices  / *这是正确的URI * /

6 使用查询组件过滤URI集合
很多时候,我们会遇到需要根据某些特定资源属性对需要排序,过滤或限制的资源集合的要求。为此,请不要创建新的API - 而是在资源集合API中启用排序,过滤和分页功能,并将输入参数作为查询参数传递。例如

http://api.example.com/device-management/managed-devices
 
http://api.example.com/device-management/managed-devices?region=USA

http://api.example.com/device-management/managed-devices?region=USA&brand=XYZ

http://api.example.com/device-management/managed-devices?region=USA&brand=XYZ&sort=installation-date

7 不要在末尾使用/
作为URI路径中的最后一个字符,正斜杠(/)不会添加语义值,并可能导致混淆。最好完全放弃它们。

8 使用http状态码定义api执行结果
1xx:信息
通信传输协议级信息。
2xx:成功
表示客户端的请求已成功接受。
3xx:重定向
表示客户端必须执行一些其他操作才能完成其请求。
4xx:客户端错误
此类错误状态代码指向客户端。
5xx:服务器错误
服务器负责这些错误状态代码。
另外完整的更为详细的状态码此处不做赘述。一般简化版本的api会使用200,400,500,其中400代表客户端调用有误,将错误信息放入response:
{
“error”: “username.or.password.error”
}

9 api版本定义
当我们需要对现有的api接口升级的时候,因为该api接口已经投入使用,所以新添加的业务可能无法保证兼容原来的逻辑,这个时候就需要新的接口,而这个接口一般表示对原来的接口的升级(不同版本),那版本怎么定义呢?
URI版本控制(推荐)

http://api.example.com/v1/users
http://apiv1.example.com 

使用自定义请求标头进行版本控制

Accept-version:v1
Accept-version:v2

使用Accept header 进行版本控制

Accept:application / vnd.example.v1 + json
Accept:application / vnd.example + json; version = 1.0

安装flask-marshmallow,映射models到json输出

marshmallow主要完成这些任务:
1.序列化
2.反序列化
3.过滤输出
4.验证

渣翻marshmallow文档 - 简书
marshmallow轻量自动化验证和序列化 - 简书

pip install flask-marshmallow
pip install marshmallow-sqlalchemy

创建映射文件schemas.py

from application.users.models import Users, Class
from application import db
from marshmallow_sqlalchemy import ModelSchema
from marshmallow import fields


# 定义Users映射文件
class UsersSchema(ModelSchema):
    class Meta(ModelSchema.Meta):
        model = Users
        sqla_session = db.session

    # dump_only表示id字段是只读字段(只允许从Model序列化为字典,不允许反序列)
    id = fields.Integer(dump_only=True)
    username = fields.String(required=True, error_messages={'required': '用户名必填'})
    fullname = fields.String(required=True, error_messages={'required': '姓名必填'})
    # load_only表示密码字段只能写入,不能读取
    password = fields.String(load_only=True)
    status = fields.Integer(required=True)
    created_time = fields.DateTime(dump_only=True)
    vote_type = fields.String()

Nesting Schemas

如果你想返回的对象里面嵌套对象
使用Nested field表示外键对象

# 在UsersSchema中添加
# Nesting Schemas
uclass = fields.Nested(ClassSchema)

创建REST方法

# REST /users GET 查询用户列表
@users.route('/', methods=['GET'])
def index():
    get_users = Users.query.all()
    users_schema = UsersSchema(many=True)
    users = users_schema.dump(get_users)
    return make_response(jsonify({'users': users}))
# REST /users/<id> GET 查询单个用户
@users.route('/<int:id>', methods=['GET'])
def get_user_by_id(id):
    get_user = Users.query.get(id)
    users_schema = UsersSchema()
    user = users_schema.dump(get_user)
    return make_response(jsonify({'user': user}))
# REST /users POST 新增用户
@users.route('/', methods=['POST'])
def create_user():
    data = request.get_json()
    data['password'] = generate_password_hash(data['password'])
    users_schema = UsersSchema()
    user = None
    user = users_schema.load(data)
    db.session.add(user)
    db.session.commit()
    result = users_schema.dump(user)
    return make_response(jsonify({'user': result}), 200)
# REST /users PUT 修改用户
@users.route('/<int:id>', methods=['PUT'])
def update_user(id):
    data = request.get_json()
    get_user = Users.query.get(id)
    if data.get('fullname'):
        get_user.fullname = data.get('fullname')

    if data.get('password'):
        get_user.password = generate_password_hash(data.get('password'))

    if data.get('vote_type'):
        get_user.vote_type = data.get('vote_type')

    db.session.add(get_user)
    db.session.commit()
    
    users_schema = UsersSchema(only=['id', 'username', 'fullname', 'vote_type'])
    user = users_schema.dump(get_user)

    return make_response(jsonify({'user': user}), 200)

一般接口开发都需要定义统一的异常处理,便于做一些集中处理后返回调用方

在application包中创建common包,创建application_common_error_handlers.py文件

from marshmallow.exceptions import ValidationError
from flask import jsonify
from flask import Blueprint, jsonify

error = Blueprint('error', __name__)

@error.app_errorhandler(ValidationError)
def handle_valid_error(e):
    result_dict = {'status_code': 400, 'error_message': e.messages}
    return jsonify(result_dict)

在application/init.py文件中添加

app.register_blueprint(error, url_prefix='/error')

api访问验证使用JWT(JSON Web Tokens)

JWT是一个含签名并携带用户相关信息的加密串,页面请求校验登录接口时,请求头中携带JWT串到后端服务,后端通过签名加密串匹配校验,保证信息未被篡改。校验通过则认为是可靠的请求,将正常返回数据。

JWT由三部分组成,分别是头信息、有效载荷、签名,中间以(.)分隔,如下格式:

xxx.yyy.zzz

10分钟了解JSON Web令牌(JWT)

安装flask-jwt-extended

$ pip install flask-jwt-extended
# application/__init__.py
from flask_jwt_extended import JWTManager
...

jwt = JWTManager()

jwt.init_app(app)

# api/users_api.py文件中
from flask_jwt_extended import create_access_token, jwt_required

# REST /users/login POST 用户登录
@users_api.route('/login', methods=['POST'])
def login():
    data = request.get_json()
    user = Users.query.filter_by(username=data['username']).first()
    if not user:
        return jsonify({'status_code': 403, 'status_message': '用户名或密码错误'})

    if check_password_hash(user.password, data['password']):
        access_toten = create_access_token(identity=data['username'])
        return jsonify({'status_code': 200, 'status_message': '登录成功', 'access_toten': access_toten})
    return jsonify({'status_code': 403, 'status_message': '用户名或密码错误'})    

返回

{
  "access_toten": "eyJ0eXAiOiJKV1QiLCJhbGciOiJIUzI1NiJ9.eyJpYXQiOjE1NzE2NzI3NDYsIm5iZiI6MTU3MTY3Mjc0NiwianRpIjoiNGYyYTM4YWEtMGYxMy00YTI2LWEwMWItYWU3ZGM0NGQ2ZmFlIiwiZXhwIjoxNTcxNjczNjQ2LCJpZGVudGl0eSI6Imxpc2EiLCJmcmVzaCI6ZmFsc2UsInR5cGUiOiJhY2Nlc3MifQ.GN1ZJP9JVmotF6HtGD8jCn98UFz5fumVpLTHnkEgtAY", 
  "status_code": 200, 
  "status_message": "\u767b\u5f55\u6210\u529f"
}
@jwt_required装饰器
规定加载视图的前提需要通过jwt验证才能访问
# 如果没有access token会报错
{
  "msg": "Missing Authorization Header"
}

后续接口访问需要在请求头中加入,有空格
Authorization=Bearer <jwt_token>

注意JWT一定要配合https使用

文件上传

在application_users_models.py中加入

# 用户头像 
avatar = db.Column(db.String(128), nullable=True)

在application_users_schemas.py中加入

avatar = fields.String(dump_only=True)

为保证安全,上传文件一定要做文件类型检查和文件名的加密

from werkzeug.utils import secure_filename

# 获取图片
@users_api.route('/avatar/<filename>')
def uploaded_file(filename):
    filepath = os.path.join(get_root_path(current_app), current_app.config['UPLOAD_FOLDER'])
    print(filepath)
    return send_from_directory(filepath, filename)

# 添加修改用户头像
@users_api.route('/avatar/<int:user_id>', methods=['POST'])
def change_user_avatar(user_id):
    file = request.files['avatar']
    get_user = Users.query.get_or_404(user_id)
    if get_user and allowed_pic_file(file.content_type):
        filename = secure_filename(file.filename)
        file.save(os.path.join(current_app.config['UPLOAD_FOLDER'], filename))
        get_user.avatar = url_for('users_api.uploaded_file', filename=filename, _external=True)
        db.session.add(get_user)
        db.session.commit()

        users_schema = UsersSchema()
        user = users_schema.dump(get_user)
        return jsonify({'status_code': 200, 'user': user})

 类似资料: