本教程演示了如何使用Flask和APIFairy轻松创建一个RESTful API。
目标
在本教程结束时,你将能够。
- 使用APIFairy提供的装饰器在Flask中创建API端点
- 利用Flask-Marshmallow来定义API端点的输入/输出的模式。
- 使用APIFairy生成API文档
- 将关系型数据库与API端点整合在一起
- 使用Flask-HTTPAuth实现基本和令牌认证
什么是APIFairy?
APIFairy是一个由Miguel Grinberg编写的API框架,它允许使用Flask轻松创建API。
APIFairy为在Flask中轻松创建API提供了四个关键组件。
- 装饰器
- 模式
- 认证
- 文档
让我们来详细探讨每一个组件...
装饰器
APIFairy提供了一组装饰器,用于定义每个API端点的输入、输出和认证。
APIFairy提供了五个核心装饰器。
- @arguments- 在URL的查询字符串中指定输入参数
- @body- 指定作为模式的输入JSON主体
- @response- 将输出的JSON体指定为一种模式
- @other_responses- 指定可以返回的额外响应(通常是错误)(仅有文档)。
- @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以及以下配置变量生成的。
APIFAIRY_TITLE- 项目的名称APIFAIRY_VERSION- 项目的版本字符串APIFAIRY_UI- API文档的格式
对于APIFAIRY_UI ,你可以从以下的OpenAPI文档渲染器之一生成模板。
有关可用的配置变量的完整列表,请参阅配置文档。
我们在构建什么?
你将在本教程中开发一个日志API,允许用户记录每天的事件。你可以在GitLab上的flask-journal-api仓库中找到完整的源代码。
使用的关键Python包。
- Flask:用于Python网络应用程序开发的微型框架
- APIFairy。
- Flask-SQLAlchemy:Flask的ORM(对象关系映射器)。
你将逐步开发API。
- 创建用于处理日记项的API端点
- 生成API文档
- 添加一个用于存储日志条目的关系型数据库
- 添加认证以保护API端点
API端点
让我们开始使用Flask和APIFairy创建一个API...
项目初始化
首先,创建一个新的项目文件夹和一个虚拟环境。
$ mkdir flask-journal-api
$ cd flask-journal-api
$ python3 -m venv venv
$ source venv/bin/activate
(venv)$
请随意将virtualenv和Pip换成Poetry或Pipenv。更多信息,请查看现代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、HTTPie、Requests或Postman。
请求的例子。
$ 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文档有三个关键方面。
- API端点(即视图函数)的文档字符串
- 整个API项目的文档串
- 用于指定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 数据库表将包含两个元素(现在!)来表示一个日记条目。
id- 表的主键 ( ),这意味着它是表中每个元素(行)的唯一标识。primary_key=Trueentry- 用于存储日志文本的字符串
虽然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 模型,将entries 和users 表连接起来。
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__.py的register_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 ,将有两个路由。
- 注册一个新用户
- 检索一个认证令牌
首先,需要在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
这个视图函数的更新与本节中的其他视图函数类似。
@authenticatedecorator指定访问这个API端点需要令牌认证- 只有 "拥有 "该日志条目的用户才被允许更新该条目(否则,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应用程序。