用APIFairy构建Flask API

363 阅读18分钟

本教程演示了如何使用FlaskAPIFairy轻松创建一个RESTful API。

目标

在本教程结束时,你将能够。

  1. 使用APIFairy提供的装饰器在Flask中创建API端点
  2. 利用Flask-Marshmallow来定义API端点的输入/输出的模式。
  3. 使用APIFairy生成API文档
  4. 将关系型数据库与API端点整合在一起
  5. 使用Flask-HTTPAuth实现基本和令牌认证

什么是APIFairy?

APIFairy是一个由Miguel Grinberg编写的API框架,它允许使用Flask轻松创建API。

APIFairy为在Flask中轻松创建API提供了四个关键组件。

  1. 装饰器
  2. 模式
  3. 认证
  4. 文档

让我们来详细探讨每一个组件...

装饰器

APIFairy提供了一组装饰器,用于定义每个API端点的输入、输出和认证。

APIFairy提供了五个核心装饰器。

  1. @arguments- 在URL的查询字符串中指定输入参数
  2. @body- 指定作为模式的输入JSON主体
  3. @response- 将输出的JSON体指定为一种模式
  4. @other_responses- 指定可以返回的额外响应(通常是错误)(仅有文档)。
  5. @authenticate- 指定认证过程

模式

一个API端点的输入(使用@body 装饰器)和输出(使用@response 装饰器)被定义为模式。

class EntrySchema(ma.Schema):
    """Schema defining the attributes in a journal entry."""
    id = ma.Integer()
    entry = ma.String()
    user_id = ma.Integer()

模式利用marshmallow将数据类型定义为类。

@authenticate 装饰器被用来检查每个API端点的URL请求中提供的认证头。认证方案是使用Flask-HTTPAuth实现的,它也是由Miguel Grinberg创建的。

一个典型的API认证方法是定义基本认证,以保护检索认证令牌的路线。

basic_auth = HTTPBasicAuth()

@basic_auth.verify_password
def verify_password(email, password):
    user = User.query.filter_by(email=email).first()
    if user.is_password_correct(password):
        return user

同时定义令牌认证,以保护大多数基于时间敏感的认证令牌的路由。

token_auth = HTTPTokenAuth()

@token_auth.verify_token
def verify_token(auth_token):
    return User.verify_auth_token(auth_token)

文档

APIFairy的一个伟大功能是自动生成的漂亮的API文档。

文档是根据源代码中的docstrings以及以下配置变量生成的。

  1. APIFAIRY_TITLE - 项目的名称
  2. APIFAIRY_VERSION - 项目的版本字符串
  3. APIFAIRY_UI - API文档的格式

对于APIFAIRY_UI ,你可以从以下的OpenAPI文档渲染器之一生成模板。

有关可用的配置变量的完整列表,请参阅配置文档。

我们在构建什么?

你将在本教程中开发一个日志API,允许用户记录每天的事件。你可以在GitLab上的flask-journal-api仓库中找到完整的源代码。

使用的关键Python包。

  1. Flask:用于Python网络应用程序开发的微型框架
  2. APIFairy
  3. Flask-SQLAlchemy:Flask的ORM(对象关系映射器)。

你将逐步开发API。

  1. 创建用于处理日记项的API端点
  2. 生成API文档
  3. 添加一个用于存储日志条目的关系型数据库
  4. 添加认证以保护API端点

API端点

让我们开始使用Flask和APIFairy创建一个API...

项目初始化

首先,创建一个新的项目文件夹和一个虚拟环境。

$ mkdir flask-journal-api
$ cd flask-journal-api
$ python3 -m venv venv
$ source venv/bin/activate
(venv)$

请随意将virtualenv和Pip换成PoetryPipenv。更多信息,请查看现代Python环境

继续添加以下文件和文件夹。

├── app.py
├── instance
│   └── .gitkeep
├── project
│   ├── __init__.py
│   └── journal_api
│       ├── __init__.py
│       └── routes.py
└── requirements.txt

接下来,为了安装必要的Python包,在项目根部的requirements.txt文件中添加依赖项。

apifairy==0.9.1
Flask==2.1.2
Flask-SQLAlchemy==2.5.1
marshmallow-sqlalchemy==0.28.0

安装。

(venv)$ pip install -r requirements.txt

这个Flask项目将利用Flask应用程序的两个最佳实践。

首先在project/__init__.py中定义应用工厂函数。

from apifairy import APIFairy
from flask import Flask, json
from flask_marshmallow import Marshmallow

# -------------
# Configuration
# -------------

# Create the instances of the Flask extensions in the global scope,
# but without any arguments passed in. These instances are not
# attached to the Flask application at this point.
apifairy = APIFairy()
ma = Marshmallow()


# ----------------------------
# Application Factory Function
# ----------------------------

def create_app():
    # Create the Flask application
    app = Flask(__name__)

    initialize_extensions(app)
    register_blueprints(app)
    return app


# ----------------
# Helper Functions
# ----------------

def initialize_extensions(app):
    # Since the application instance is now created, pass it to each Flask
    # extension instance to bind it to the Flask application instance (app)
    apifairy.init_app(app)
    ma.init_app(app)


def register_blueprints(app):
    # Import the blueprints
    from project.journal_api import journal_api_blueprint

    # Since the application instance is now created, register each Blueprint
    # with the Flask application instance (app)
    app.register_blueprint(journal_api_blueprint, url_prefix='/journal')

在定义了应用工厂函数后,可以在项目顶层文件夹的app.py中调用该函数。

from project import create_app


# Call the application factory function to construct a Flask application
# instance using the development configuration
app = create_app()

蓝图

让我们来定义journal_api 蓝图。首先在project/journal_api/__init__.py中定义journal_api 蓝图。

"""
The 'journal_api' blueprint handles the API for managing journal entries.
Specifically, this blueprint allows for journal entries to be added, edited,
and deleted.
"""
from flask import Blueprint

journal_api_blueprint = Blueprint('journal_api', __name__, template_folder='templates')

from . import routes

现在是时候在project/journal_api/routes.py中定义日记的API端点了。

首先是必要的导入。

from apifairy import body, other_responses, response
from flask import abort

from project import ma
from . import journal_api_blueprint

对于这个初始版本的Flask日志API,数据库将是一个日志条目的列表。

# --------
# Database
# --------

messages = [
    dict(id=1, entry='The sun was shining when I woke up this morning.'),
    dict(id=2, entry='I tried a new fruit mixture in my oatmeal for breakfast.'),
    dict(id=3, entry='Today I ate a great sandwich for lunch.')
]

接下来,定义创建新日志条目和返回日志条目的模式。

# -------
# Schemas
# -------

class NewEntrySchema(ma.Schema):
    """Schema defining the attributes when creating a new journal entry."""
    entry = ma.String(required=True)


class EntrySchema(ma.Schema):
    """Schema defining the attributes in a journal entry."""
    id = ma.Integer()
    entry = ma.String()


new_entry_schema = NewEntrySchema()
entry_schema = EntrySchema()
entries_schema = EntrySchema(many=True)

这两个模式类都继承自ma.Schema,它是由Flask-Marshmallow提供的。创建这些模式的对象也是一个好主意,因为这允许你定义一个可以返回多个条目的模式(使用many=True 参数)。

现在我们准备好定义API端点了!

路由

首先是检索所有的日记条目。

@journal_api_blueprint.route('/', methods=['GET'])
@response(entries_schema)
def journal():
    """Return all journal entries"""
    return messages

这个视图函数使用@response 装饰器来定义返回多个条目。该视图函数返回完整的日记条目列表(return messages)。

接下来,创建添加新日志条目的API端点。

@journal_api_blueprint.route('/', methods=['POST'])
@body(new_entry_schema)
@response(entry_schema, 201)
def add_journal_entry(kwargs):
    """Add a new journal entry"""
    new_message = dict(**kwargs, id=messages[-1]['id']+1)
    messages.append(new_message)
    return new_message

这个视图函数使用@body 装饰器来定义API端点的输入,使用@response 装饰器来定义API端点的输出。

@body 装饰器中解析出的输入数据作为kwargs (关键词 参数)参数被传入add_journal_entry() 视图函数。然后,这些数据被用来创建一个新的日志条目并将其添加到数据库中。

new_message = dict(**kwargs, id=messages[-1]['id']+1)
messages.append(new_message)

然后,新创建的日记条目被返回(return new_message)。请注意,@response 装饰器将返回代码定义为201(创建),以表明日记条目已被添加到数据库中。

创建API端点以检索特定的日记条目。

@journal_api_blueprint.route('/', methods=['GET'])
@response(entry_schema)
@other_responses({404: 'Entry not found'})
def get_journal_entry(index):
    """Return a journal entry"""
    if index >= len(messages):
        abort(404)

    return messages[index]

这个视图函数使用@other_responses 装饰器来指定非标准的响应。

@other_responses 装饰器仅用于文档编制!它不提供任何功能。它在返回错误代码方面不提供任何功能。

创建用于更新日记账的API端点。

@journal_api_blueprint.route('/', methods=['PUT'])
@body(new_entry_schema)
@response(entry_schema)
@other_responses({404: 'Entry not found'})
def update_journal_entry(data, index):
    """Update a journal entry"""
    if index >= len(messages):
        abort(404)

    messages[index] = dict(data, id=index+1)
    return messages[index]

该视图函数使用@body@response 装饰器来定义该API端点的输入和输出(分别)。此外,@other_responses 装饰器定义了在未找到日志条目时的非标准响应。

最后,创建删除日志条目的API端点。

@journal_api_blueprint.route('/', methods=['DELETE'])
@other_responses({404: 'Entry not found'})
def delete_journal_entry(index):
    """Delete a journal entry"""
    if index >= len(messages):
        abort(404)

    messages.pop(index)
    return '', 204

这个视图函数没有使用@body@response 装饰器,因为这个API端点没有输入或输出。如果日记条目被成功删除,则返回204(无内容)状态代码,没有数据。

为了测试,在一个终端窗口中,配置Flask应用程序并运行开发服务器。

(venv) $ export FLASK_APP=app.py
(venv) $ export FLASK_ENV=development
(venv) $ flask run

然后,在另一个终端窗口中,你可以与API进行交互。在这里可以随意使用你选择的工具,如cURL、HTTPieRequestsPostman

请求的例子。

$ python3

>>> import requests
>>>
>>> r = requests.get('http://127.0.0.1:5000/journal/')
>>> print(r.text)
>>>
>>> post_data = {'entry': "some message"}
>>> r = requests.post('http://127.0.0.1:5000/journal/', json=post_data)
>>> print(r.text)

想更容易地测试API的端点吗?看看这个脚本,它添加了CLI命令,用于与API端点互动,以检索、创建、更新和删除日志条目。

文档

APIFairy的一个令人难以置信的功能是自动创建API文档!

配置API文档有三个关键方面。

  1. API端点(即视图函数)的文档字符串
  2. 整个API项目的文档串
  3. 用于指定API文档外观的配置变量

我们在上一节已经涵盖了第一项内容,因为我们包含了每个视图函数的文档字符串。例如,journal() 视图函数有一个关于这个API端点的目的的简短描述。

@journal_api_blueprint.route('/', methods=['GET'])
@response(entries_schema)
def journal():
    """Return all journal entries"""
    return messages

接下来,我们需要在project/__init__.py文件的最上方加入描述整个项目的docstring。

"""
Welcome to the documentation for the Flask Journal API!

## Introduction

The Flask Journal API is an API (Application Programming Interface) for creating a **daily journal** that documents events that happen each day.

## Key Functionality

The Flask Journal API has the following functionality:

1. Work with journal entries:
  * Create a new journal entry
  * Update a journal entry
  * Delete a journal entry
  * View all journal entries
2. 

## Key Modules

This project is written using Python 3.10.1.

The project utilizes the following modules:

* **Flask**: micro-framework for web application development which includes the following dependencies:
  * **click**: package for creating command-line interfaces (CLI)
  * **itsdangerous**: cryptographically sign data
  * **Jinja2**: templating engine
  * **MarkupSafe**: escapes characters so text is safe to use in HTML and XML
  * **Werkzeug**: set of utilities for creating a Python application that can talk to a WSGI server
* **APIFairy**: API framework for Flask which includes the following dependencies:
  * **Flask-Marshmallow** - Flask extension for using Marshmallow (object serialization/deserialization library)
  * **Flask-HTTPAuth** - Flask extension for HTTP authentication
  * **apispec** - API specification generator that supports the OpenAPI specification
* **pytest**: framework for testing Python projects
"""
...

这个docstring是用来描述整个项目的,包括提供的关键功能和项目使用的关键Python包。

最后,需要定义一些配置变量来指定 API 文档的外观。更新project/__init__.py 中的create_app() 函数。

def create_app():
    # Create the Flask application
    app = Flask(__name__)

    # Configure the API documentation
    app.config['APIFAIRY_TITLE'] = 'Flask Journal API'
    app.config['APIFAIRY_VERSION'] = '0.1'
    app.config['APIFAIRY_UI'] = 'elements'

    initialize_extensions(app)
    register_blueprints(app)

    return app

准备好查看项目的文档了吗?通过flask run ,启动Flask开发服务器,然后导航到http://127.0.0.1:5000/docs,查看由APIFairy创建的API文档。

在左边的窗格中,有一个journal_api 蓝图的API端点的列表。点击其中一个端点,可以看到关于该端点的所有细节。

这个API文档的奇妙之处在于能够看到API端点是如何工作的(假设Flask开发服务器正在运行)。在文档的右侧窗格,输入一个日志条目索引,然后点击 "发送API请求"。然后就会显示API响应。

这种交互式文档使用户很容易理解API!

数据库

出于演示目的,本教程将使用一个SQLite数据库。

配置

由于Flask-SQLAlchemy已经在本教程开始时安装了,我们需要在project/__init__.py文件中对其进行配置。

首先在 "配置 "部分创建一个SQLAlchemy() 对象。

...

from apifairy import APIFairy
from flask import Flask, json
from flask_marshmallow import Marshmallow
from flask_sqlalchemy import SQLAlchemy  # <-- NEW!!


# -------------
# Configuration
# -------------

# Create the instances of the Flask extensions in the global scope,
# but without any arguments passed in. These instances are not
# attached to the Flask application at this point.
apifairy = APIFairy()
ma = Marshmallow()
database = SQLAlchemy()  # <-- NEW!!

...

接下来,更新create_app() 函数以指定必要的配置变量。

def create_app():
    # Create the Flask application
    app = Flask(__name__)

    # Configure the API documentation
    app.config['APIFAIRY_TITLE'] = 'Flask Journal API'
    app.config['APIFAIRY_VERSION'] = '0.1'
    app.config['APIFAIRY_UI'] = 'elements'

    # NEW!
    # Configure the SQLite database (intended for development only!)
    app.config['SQLALCHEMY_DATABASE_URI'] = f"sqlite:///{os.path.join(os.getcwd(), 'instance', 'app.db')}"
    app.config['SQLALCHEMY_TRACK_MODIFICATIONS'] = False

    initialize_extensions(app)
    register_blueprints(app)

    return app

将导入的内容添加到顶部。

SQLALCHEMY_DATABASE_URI 配置变量对于确定SQLite数据库的位置至关重要。在本教程中,数据库被存储在instance/app.db中。

最后,更新initialize_extensions() 函数来初始化Flask-SQLAlchemy对象。

def initialize_extensions(app):
    # Since the application instance is now created, pass it to each Flask
    # extension instance to bind it to the Flask application instance (app)
    apifairy.init_app(app)
    ma.init_app(app)
    database.init_app(app)  # <-- NEW!!

想了解更多关于这个Flask应用是如何连接起来的吗?请看我关于如何构建、测试和部署Flask应用程序的课程。

数据库模型

创建一个新的project/models.py文件来定义数据库表来表示日记账。

from project import database


class Entry(database.Model):
    """Class that represents a journal entry."""
    __tablename__ = 'entries'

    id = database.Column(database.Integer, primary_key=True)
    entry = database.Column(database.String, nullable=False)

    def __init__(self, entry: str):
        self.entry = entry

    def update(self, entry: str):
        self.entry = entry

    def __repr__(self):
        return f''

这个新的类,Entry ,指定entries 数据库表将包含两个元素(现在!)来表示一个日记条目。

  1. id - 表的主键 ( ),这意味着它是表中每个元素(行)的唯一标识。primary_key=True
  2. entry - 用于存储日志文本的字符串

虽然models.py定义了数据库表,但它并没有在SQLite数据库中创建表。要创建这些表,请在终端窗口中启动Flask shell。

(venv)$ flask shell

>>> from project import database
>>> database.drop_all()
>>> database.create_all()
>>> quit()

(venv)$

由于我们要进展到使用SQLite数据库,首先要删除project/journal_api/routes.py中定义的临时database (Python列表)。

# --------
# Database
# --------

messages = [
    dict(id=1, entry='The sun was shining when I woke up this morning.'),
    dict(id=2, entry='I tried a new fruit mixture in my oatmeal for breakfast.'),
    dict(id=3, entry='Today I ate a great sandwich for lunch.')
]

接下来,我们需要更新每个API端点(即视图函数)以利用SQLite数据库。

首先更新journal() 视图函数。

@journal_api_blueprint.route('/', methods=['GET'])
@response(entries_schema)
def journal():
    """Return all journal entries"""
    return Entry.query.all()

现在可以从SQLite数据库中获取完整的日记条目列表了。请注意,这个视图函数的模式或装饰器不需要改变......只是获取用户的基本过程发生了变化

添加导入。

from project.models import Entry

接下来,更新add_journal_entry() 视图函数。

@journal_api_blueprint.route('/', methods=['POST'])
@body(new_entry_schema)
@response(entry_schema, 201)
def add_journal_entry(kwargs):
    """Add a new journal entry"""
    new_message = Entry(**kwargs)
    database.session.add(new_message)
    database.session.commit()
    return new_message

这个视图函数的输入是由new_entry_schema

class NewEntrySchema(ma.Schema):
    """Schema defining the attributes when creating a new journal entry."""
    entry = ma.String(required=True)

new_entry_schema = NewEntrySchema()

entry 字符串被用来创建一个新的Entry 类的实例(定义在models.py中),然后这个日记条目被添加到数据库中。

添加导入的内容。

from project import database

接下来,更新get_journal_entry()

@journal_api_blueprint.route('/', methods=['GET'])
@response(entry_schema)
@other_responses({404: 'Entry not found'})
def get_journal_entry(index):
    """Return a journal entry"""
    entry = Entry.query.filter_by(id=index).first_or_404()
    return entry

这个函数现在尝试查找指定的日志条目(基于index )。

entry = Entry.query.filter_by(id=index).first_or_404()

如果该条目存在,就会返回给用户。如果该条目不存在,则返回404(未找到)错误。

接下来,更新update_journal_entry()

@journal_api_blueprint.route('/', methods=['PUT'])
@body(new_entry_schema)
@response(entry_schema)
@other_responses({404: 'Entry not found'})
def update_journal_entry(data, index):
    """Update a journal entry"""
    entry = Entry.query.filter_by(id=index).first_or_404()
    entry.update(data['entry'])
    database.session.add(entry)
    database.session.commit()
    return entry

update_journal_entry() 视图函数现在尝试检索指定的日志条目。

entry = Entry.query.filter_by(id=index).first_or_404()

如果日志条目存在,该条目将被更新为新的文本,然后保存到数据库中。

最后,更新delete_journal_entry()

@journal_api_blueprint.route('/', methods=['DELETE'])
@other_responses({404: 'Entry not found'})
def delete_journal_entry(index):
    """Delete a journal entry"""
    entry = Entry.query.filter_by(id=index).first_or_404()
    database.session.delete(entry)
    database.session.commit()
    return '', 204

如果找到了指定的日记条目,那么就从数据库中删除它。

运行开发服务器。测试每一个端点,以确保它们仍然工作。

错误处理

由于这个Flask项目是一个API,错误代码应该以JSON格式而不是典型的HTML格式返回。

在Flask项目中,这可以通过使用一个自定义的错误处理程序来实现。在project/__init__.py中,在文件的底部定义一个新的函数(register_error_handlers())。

def register_error_handlers(app):
    @app.errorhandler(HTTPException)
    def handle_http_exception(e):
        """Return JSON instead of HTML for HTTP errors."""
        # Start with the correct headers and status code from the error
        response = e.get_response()
        # Replace the body with JSON
        response.data = json.dumps({
            'code': e.code,
            'name': e.name,
            'description': e.description,
        })
        response.content_type = 'application/json'
        return response

这个函数注册了一个新的错误处理程序,当HTTPException ,将输出转换为JSON格式。

添加导入的内容。

from werkzeug.exceptions import HTTPException

同时,更新应用程序工厂函数,create_app() ,以调用这个新函数。

def create_app():
    # Create the Flask application
    app = Flask(__name__)

    # Configure the API documentation
    app.config['APIFAIRY_TITLE'] = 'Flask Journal API'
    app.config['APIFAIRY_VERSION'] = '0.1'
    app.config['APIFAIRY_UI'] = 'elements'

    # Configure the SQLite database (intended for development only!)
    app.config['SQLALCHEMY_DATABASE_URI'] = f"sqlite:///{os.path.join(os.getcwd(), 'instance', 'app.db')}"
    app.config['SQLALCHEMY_TRACK_MODIFICATIONS'] = False

    initialize_extensions(app)
    register_blueprints(app)
    register_error_handlers(app)  # NEW!!

    return app

认证是验证试图访问一个系统的用户的身份的过程,在这种情况下就是API。

另一方面,授权是验证一个特定用户应该访问哪些特定资源的过程。

APIFairy利用Flask-HTTPAuth来支持认证。在本教程中,我们将以两种方式使用Flask-HTTPAuth。

通过Flask-HTTPAuth使用的令牌认证通常被称为承载者认证,因为这个过程会调用授予令牌的 "承载者 "访问权。令牌必须包含在HTTP头的授权头中,例如 "授权。承载者"。

下图说明了一个典型的流程,即新用户如何与应用程序互动以获取认证令牌。

配置

由于Flask-HTTPAuth已经在本教程开始时安装了APIFairy,我们只需要在project/__init__.py文件中对其进行配置。

首先,为基本认证和令牌认证创建单独的对象。

...

import os

from apifairy import APIFairy
from flask import Flask, json
from flask_httpauth import HTTPBasicAuth, HTTPTokenAuth  # NEW!!
from flask_marshmallow import Marshmallow
from flask_sqlalchemy import SQLAlchemy
from werkzeug.exceptions import HTTPException


# -------------
# Configuration
# -------------

# Create the instances of the Flask extensions in the global scope,
# but without any arguments passed in. These instances are not
# attached to the Flask application at this point.
apifairy = APIFairy()
ma = Marshmallow()
database = SQLAlchemy()
basic_auth = HTTPBasicAuth()  # NEW!!
token_auth = HTTPTokenAuth()  # NEW!!

...

project/__init__.py中不需要进一步更新。

数据库模型

project/models.py中,需要创建一个新的User 模型来代表一个用户。

class User(database.Model):
    __tablename__ = 'users'

    id = database.Column(database.Integer, primary_key=True)
    email = database.Column(database.String, unique=True, nullable=False)
    password_hashed = database.Column(database.String(128), nullable=False)
    entries = database.relationship('Entry', backref='user', lazy='dynamic')
    auth_token = database.Column(database.String(64), index=True)
    auth_token_expiration = database.Column(database.DateTime)

    def __init__(self, email: str, password_plaintext: str):
        """Create a new User object."""
        self.email = email
        self.password_hashed = self._generate_password_hash(password_plaintext)

    def is_password_correct(self, password_plaintext: str):
        return check_password_hash(self.password_hashed, password_plaintext)

    def set_password(self, password_plaintext: str):
        self.password_hashed = self._generate_password_hash(password_plaintext)

    @staticmethod
    def _generate_password_hash(password_plaintext):
        return generate_password_hash(password_plaintext)

    def generate_auth_token(self):
        self.auth_token = secrets.token_urlsafe()
        self.auth_token_expiration = datetime.utcnow() + timedelta(minutes=60)
        return self.auth_token

    @staticmethod
    def verify_auth_token(auth_token):
        user = User.query.filter_by(auth_token=auth_token).first()
        if user and user.auth_token_expiration > datetime.utcnow():
            return user

    def revoke_auth_token(self):
        self.auth_token_expiration = datetime.utcnow()

    def __repr__(self):
        return f''

添加导入的内容。

import secrets
from datetime import datetime, timedelta

from werkzeug.security import check_password_hash, generate_password_hash

User 模型使用werkzeug.security 在将用户的密码存储到数据库之前对其进行散列。

记住。永远不要将明文密码存储在数据库中!

User 模型使用secrets ,为特定的用户生成一个认证令牌。这个令牌是在generate_auth_token() 方法中创建的,包括一个未来60分钟的失效日期/时间。

def generate_auth_token(self):
    self.auth_token = secrets.token_urlsafe()
    self.auth_token_expiration = datetime.utcnow() + timedelta(minutes=60)
    return self.auth_token

有一个静态方法,verify_auth_token() ,用来验证认证令牌(同时考虑过期时间),并从一个有效的令牌中返回用户。

@staticmethod
def verify_auth_token(auth_token):
    user = User.query.filter_by(auth_token=auth_token).first()
    if user and user.auth_token_expiration > datetime.utcnow():
        return user

还有一个值得关注的方法是revoke_auth_token() ,它用于撤销特定用户的认证令牌。

def revoke_auth_token(self):
    self.auth_token_expiration = datetime.utcnow()

入口模型

为了在用户("一")和他们的条目("多")之间建立一对多的关系,需要更新Entry 模型,将entriesusers 表连接起来。

class Entry(database.Model):
    """Class that represents a journal entry."""
    __tablename__ = 'entries'

    id = database.Column(database.Integer, primary_key=True)
    entry = database.Column(database.String, nullable=False)
    user_id = database.Column(database.Integer, database.ForeignKey('users.id'))  # <-- NEW!!

    def __init__(self, entry: str):
        self.entry = entry

    def update(self, entry: str):
        self.entry = entry

    def __repr__(self):
        return f''

User 模型已经包含了返回到entries 表的链接。

entries = database.relationship('Entry', backref='user', lazy='dynamic')

用户API蓝图

Flask项目的用户管理功能将被定义在一个单独的蓝图中,名为users_api_blueprint

首先在 "项目 "中创建一个名为" users_api "的新目录。在该目录下创建一个*__init__.py*文件。

from flask import Blueprint


users_api_blueprint = Blueprint('users_api', __name__)

from . import authentication, routes

这个新的蓝图需要在projects/__init__.pyregister_blueprints() 函数中与Flaskapp 注册。

def register_blueprints(app):
    # Import the blueprints
    from project.journal_api import journal_api_blueprint
    from project.users_api import users_api_blueprint  # NEW!!

    # Since the application instance is now created, register each Blueprint
    # with the Flask application instance (app)
    app.register_blueprint(journal_api_blueprint, url_prefix='/journal')
    app.register_blueprint(users_api_blueprint, url_prefix='/users')  # NEW!!

为了使用Flask-HTTPAuth,需要定义几个函数来处理检查用户凭证。

创建一个新的project/users_api/authentication.py文件来处理基本认证和token认证。

对于基本认证(检查用户的电子邮件和密码)。

from werkzeug.exceptions import Forbidden, Unauthorized

from project import basic_auth, token_auth
from project.models import User


@basic_auth.verify_password
def verify_password(email, password):
    user = User.query.filter_by(email=email).first()
    if user is None:
        return None

    if user.is_password_correct(password):
        return user


@basic_auth.error_handler
def basic_auth_error(status=401):
    error = (Forbidden if status == 403 else Unauthorized)()
    return {
        'code': error.code,
        'message': error.name,
        'description': error.description,
    }, error.code, {'WWW-Authenticate': 'Form'}

verify_password() 函数用来检查一个用户是否存在,以及他们的密码是否正确。这个函数将被Flask-HTTPAuth用于在需要基本认证时验证密码(感谢@basic_auth.verify_password 装饰器。)

此外,为基本认证定义了一个错误处理程序,以JSON格式返回错误信息。

对于令牌认证(处理一个令牌以确定用户是否有效)。

@token_auth.verify_token
def verify_token(auth_token):
    return User.verify_auth_token(auth_token)


@token_auth.error_handler
def token_auth_error(status=401):
    error = (Forbidden if status == 403 else Unauthorized)()
    return {
        'code': error.code,
        'message': error.name,
        'description': error.description,
    }, error.code

verify_token() 函数被用来检查认证令牌是否有效。这个函数将被Flask-HTTPAuth用来在需要令牌认证时验证令牌(感谢@token_auth.verify_token 装饰器。)

此外,为令牌认证定义了一个错误处理程序,以JSON格式返回错误信息。

用户路线

users_api_blueprint ,将有两个路由。

  1. 注册一个新用户
  2. 检索一个认证令牌

首先,需要在projects/users_api/routes.py中定义一组新的模式(使用marshmallow)。

from project import ma

from . import users_api_blueprint

# -------
# Schemas
# -------

class NewUserSchema(ma.Schema):
    """Schema defining the attributes when creating a new user."""
    email = ma.String()
    password_plaintext = ma.String()


class UserSchema(ma.Schema):
    """Schema defining the attributes of a user."""
    id = ma.Integer()
    email = ma.String()


class TokenSchema(ma.Schema):
    """Schema defining the attributes of a token."""
    token = ma.String()


new_user_schema = NewUserSchema()
user_schema = UserSchema()
token_schema = TokenSchema()

这些模式将被用于定义该文件中定义的视图函数的输入和输出。

注册一个新用户

接下来,定义注册一个新用户的视图函数。

@users_api_blueprint.route('/', methods=['POST'])
@body(new_user_schema)
@response(user_schema, 201)
def register(kwargs):
    """Create a new user"""
    new_user = User(**kwargs)
    database.session.add(new_user)
    database.session.commit()
    return new_user

添加进口。

from apifairy import authenticate, body, other_responses, response

from project import basic_auth, database, ma
from project.models import User

这个API端点使用new_user_schema ,指定电子邮件和密码为输入。

注意:由于电子邮件和密码被发送到这个API端点,现在是记住在开发测试期间使用HTTP是可以接受的,但在生产中应始终使用HTTPS(安全)。

然后,电子邮件和密码(定义为kwargs - 关键字参数)被解包,创建一个新的User 对象,并保存到数据库中。

new_user = User(**kwargs)
database.session.add(new_user)
database.session.commit()

API端点的输出是由user_schema ,它是新用户的ID和电子邮件。

projects/users_api/routes.py中要定义的另一个视图函数是用于检索认证令牌。

@users_api_blueprint.route('/get-auth-token', methods=['POST'])
@authenticate(basic_auth)
@response(token_schema)
@other_responses({401: 'Invalid username or password'})
def get_auth_token():
    """Get authentication token"""
    user = basic_auth.current_user()
    token = user.generate_auth_token()
    database.session.add(user)
    database.session.commit()
    return dict(token=token)

@authenticate 装饰器在本教程中是第一次使用,它指定了基本认证应该被用来守护这个路由。

@authenticate(basic_auth)

当用户想检索他们的认证令牌时,他们需要向这个API端点发送一个POST请求,在 "授权 "头中嵌入电子邮件和密码。作为一个例子,可以使用Requests包向这个API端点发出以下Python命令。

>>> import requests
>>> r = requests.post(
    'http://127.0.0.1:5000/users/get-auth-token',
    auth=('[email protected]', 'FlaskIsAwesome123')
)

如果基本认证成功,视图函数使用Flask-HTTPAuth提供的current_user() 方法检索当前用户。

user = basic_auth.current_user()

为该用户创建一个新的认证令牌。

token = user.generate_auth_token()

并且该令牌被保存到数据库中,以便将来可以用来验证用户(至少在接下来的60分钟内!)。

最后,新的认证令牌将被返回给用户,以便为所有后续的API调用保存。

有了认证过程,现在是时候给现有的API端点添加一些防护措施,以确保只有有效的用户才能访问应用程序。

这些更新是针对projects/journal_api/routes.py中定义的视图函数。

首先,更新journal() ,只返回当前用户的日志条目。

@journal_api_blueprint.route('/', methods=['GET'])
@authenticate(token_auth)
@response(entries_schema)
def journal():
    """Return journal entries"""
    user = token_auth.current_user()
    return Entry.query.filter_by(user_id=user.id).all()

在顶部的导入项中这样更新。

from apifairy import authenticate, body, other_responses, response
from flask import abort

from project import database, ma, token_auth
from project.models import Entry

from . import journal_api_blueprint

@authenticate 装饰器指定访问该API端点时需要使用令牌认证。作为一个例子,下面的GET请求可以使用Requests*(在获取了认证令牌之后*)进行。

>>> import requests
>>> headers = {'Authorization': f'Bearer {auth_token}'}
>>> r = requests.get('http://127.0.0.1:5000/journal/', headers=headers)

一旦用户通过认证,就会根据用户的ID从数据库中检索出完整的日记条目列表。

user = token_auth.current_user()
return Entry.query.filter_by(user_id=user.id).all()

这个API端点的输出是由@response 装饰器定义的,它是一个日记条目的列表(ID,条目,用户ID)。

接下来,更新add_journal_entry()

@journal_api_blueprint.route('/', methods=['POST'])
@authenticate(token_auth)
@body(new_entry_schema)
@response(entry_schema, 201)
def add_journal_entry(kwargs):
    """Add a new journal entry"""
    user = token_auth.current_user()
    new_message = Entry(user_id=user.id, **kwargs)
    database.session.add(new_message)
    database.session.commit()
    return new_message

与之前的视图函数一样,@authenticate 装饰器被用来指定访问这个API端点时需要使用令牌认证。此外,现在通过指定应与日记条目相关的用户ID来添加日记条目。

user = token_auth.current_user()
new_message = Entry(user_id=user.id, **kwargs)

新的日志条目被保存到数据库,并返回日志条目(由@response 装饰器定义)。

接下来,更新get_journal_entry()

@journal_api_blueprint.route('/', methods=['GET'])
@authenticate(token_auth)
@response(entry_schema)
@other_responses({403: 'Forbidden', 404: 'Entry not found'})
def get_journal_entry(index):
    """Return a journal entry"""
    user = token_auth.current_user()
    entry = Entry.query.filter_by(id=index).first_or_404()

    if entry.user_id != user.id:
        abort(403)
    return entry

@authenticate 装饰器被添加,以指定访问该API端点需要令牌认证。

当试图检索一个日志条目时,增加了一个额外的检查,以确保试图访问该日志条目的用户是该条目的实际 "所有者"。如果不是,则会通过Flask的abort() 函数返回一个403(禁止)错误代码。

if entry.user_id != user.id:
        abort(403)

请注意,这个API端点有两个由@other_responses 装饰器指定的非名词性响应。

@other_responses({403: 'Forbidden', 404: 'Entry not found'})

提醒您。@other_responses 装饰器只用于记录;引发这些错误是视图函数的责任。

接下来,更新update_journal_entry()

@journal_api_blueprint.route('/', methods=['PUT'])
@authenticate(token_auth)
@body(new_entry_schema)
@response(entry_schema)
@other_responses({403: 'Forbidden', 404: 'Entry not found'})
def update_journal_entry(data, index):
    """Update a journal entry"""
    user = token_auth.current_user()
    entry = Entry.query.filter_by(id=index).first_or_404()

    if entry.user_id != user.id:
        abort(403)

    entry.update(data['entry'])
    database.session.add(entry)
    database.session.commit()
    return entry

这个视图函数的更新与本节中的其他视图函数类似。

  1. @authenticate decorator指定访问这个API端点需要令牌认证
  2. 只有 "拥有 "该日志条目的用户才被允许更新该条目(否则,403 (Forbidden))

最后,更新delete_journal_entry()

@journal_api_blueprint.route('/', methods=['DELETE'])
@authenticate(token_auth)
@other_responses({403: 'Forbidden', 404: 'Entry not found'})
def delete_journal_entry(index):
    """Delete a journal entry"""
    user = token_auth.current_user()
    entry = Entry.query.filter_by(id=index).first_or_404()

    if entry.user_id != user.id:
        abort(403)

    database.session.delete(entry)
    database.session.commit()
    return '', 204

结论

本教程介绍了如何使用APIFairy在Flask中轻松快速地构建一个API。

装饰器是定义API端点的关键。

  • 输入
    • @arguments - 来自URL查询字符串的输入参数
    • @body - JSON请求的结构
  • 输出
    • @response - JSON响应的结构
  • 认证
    • @authenticate - 使用Flask-HTTPAuth的认证方法
  • 错误
    • @other_responses - 非名义上的响应,如HTTP错误代码

另外,由APIFairy生成的API文档非常好,为应用程序的用户提供了关键信息。

如果你有兴趣学习更多关于Flask的知识,请查看我的课程,了解如何构建、测试和部署Flask应用程序。