Flask 实现 RESTful Web 服务全解析
1. 多版本服务支持
在博客服务更新时,可能会改变博客文章的 JSON 格式。更新后,新的博客文章接口为
/api/v1.1/posts/
,同时为连接到
/api/v1.0/posts/
的旧客户端保留旧的 JSON 格式。在一段时间内,服务器会同时处理
v1.1
和
v1.0
版本的 URL 请求。虽然支持服务器的多个版本可能会带来维护负担,但在某些情况下,这是在不影响现有部署的前提下让应用程序发展的唯一方法。
2. Flask 实现 RESTful Web 服务
Flask 使创建 RESTful Web 服务变得非常容易。可以使用熟悉的
route()
装饰器及其可选的
methods
参数来声明处理服务暴露的资源 URL 的路由。处理 JSON 数据也很简单,请求中包含的 JSON 数据会自动作为
request.json
Python 字典暴露出来,而需要包含 JSON 的响应可以使用 Flask 的
jsonify()
辅助函数从 Python 字典轻松生成。
3. 创建 API 蓝图
与 RESTful API 相关的路由构成了应用程序的一个独立子集,因此将它们放在自己的蓝图中是保持良好组织的最佳方式。API 蓝图在应用程序中的一般结构如下:
|-flasky
|-app/
|-api_1_0
|-__init__.py
|-user.py
|-post.py
|-comment.py
|-authentication.py
|-errors.py
|-decorators.py
注意,用于 API 的包名中包含了版本号。当需要引入不兼容的 API 版本时,可以将其作为另一个具有不同版本号的包添加,并且可以同时提供两个 API。
API 蓝图在每个单独的模块中实现每个资源,还包括处理身份验证、错误处理和提供自定义装饰器的模块。蓝图构造函数如下:
from flask import Blueprint
api = Blueprint('api', __name__)
from . import authentication, posts, users, comments, errors
API 蓝图的注册代码如下:
def create_app(config_name):
# ...
from .api_1_0 import api as api_1_0_blueprint
app.register_blueprint(api_1_0_blueprint, url_prefix='/api/v1.0')
# ...
4. 错误处理
RESTful Web 服务通过在响应中发送适当的 HTTP 状态代码以及响应体中的任何附加信息来通知客户端请求的状态。客户端从 Web 服务可能看到的典型状态代码如下表所示:
| HTTP 状态码 | 名称 | 描述 |
| — | — | — |
| 200 | OK | 请求成功完成。 |
| 201 | Created | 请求成功完成,并创建了一个新资源。 |
| 400 | Bad request | 请求无效或不一致。 |
| 401 | Unauthorized | 请求不包含身份验证信息。 |
| 403 | Forbidden | 请求中发送的身份验证凭据不足以满足请求。 |
| 404 | Not found | URL 中引用的资源未找到。 |
| 405 | Method not allowed | 请求的方法对于给定资源不支持。 |
| 500 | Internal server error | 处理请求时发生意外错误。 |
处理状态码 404 和 500 会有一些小问题,因为这些错误是由 Flask 自己生成的,通常会返回 HTML 响应,这可能会使 API 客户端感到困惑。一种为所有客户端生成适当响应的方法是让错误处理程序根据客户端请求的格式调整其响应,这种技术称为内容协商。以下是一个改进的 404 错误处理程序示例:
@main.app_errorhandler(404)
def page_not_found(e):
if request.accept_mimetypes.accept_json and \
not request.accept_mimetypes.accept_html:
response = jsonify({'error': 'not found'})
response.status_code = 404
return response
return render_template('404.html'), 404
这个新的错误处理程序检查
Accept
请求头(Werkzeug 将其解码为
request.accept_mimetypes
)以确定客户端希望响应的格式。浏览器通常不会对响应格式进行任何限制,因此仅为接受 JSON 且不接受 HTML 的客户端生成 JSON 响应。
其余状态码由 Web 服务显式生成,因此可以在
errors.py
模块的蓝图中实现为辅助函数。以下是 403 错误的实现示例:
def forbidden(message):
response = jsonify({'error': 'forbidden', 'message': message})
response.status_code = 403
return response
现在,Web 服务中的视图函数可以调用这些辅助函数来生成错误响应。
5. 使用 Flask - HTTPAuth 进行用户身份验证
Web 服务和常规 Web 应用程序一样,需要保护信息并确保不将其提供给未经授权的方。因此,富互联网应用(RIAs)必须向用户索要登录凭据并将其传递给服务器进行验证。
RESTful Web 服务的一个特点是无状态,这意味着服务器在请求之间不允许“记住”关于客户端的任何信息。客户端需要在请求本身中提供执行请求所需的所有信息,因此所有请求都必须包含用户凭据。
当前使用 Flask - Login 实现的登录功能将数据存储在用户会话中,Flask 默认将其存储在客户端的 cookie 中,因此服务器不存储任何与用户相关的信息,而是要求客户端存储。虽然这种实现似乎符合 REST 的无状态要求,但在 RESTful Web 服务中使用 cookie 存在争议,因为对于非 Web 浏览器的客户端来说实现起来可能很麻烦,所以通常认为这是一个糟糕的设计选择。
REST 的无状态要求虽然看似过于严格,但并非随意规定。无状态服务器可以很容易地扩展。当服务器存储客户端信息时,需要一个所有服务器都可以访问的共享缓存,以确保同一个客户端的请求总是由同一台服务器处理,这两个问题都很难解决。
由于 RESTful 架构基于 HTTP 协议,HTTP 身份验证是发送凭据的首选方法,可以是基本身份验证或摘要身份验证。使用 HTTP 身份验证时,用户凭据会包含在所有请求的
Authorization
头中。
HTTP 身份验证协议足够简单,可以直接实现,但 Flask - HTTPAuth 扩展提供了一个方便的包装器,它将协议细节隐藏在一个类似于 Flask - Login 的
login_required
的装饰器中。
安装 Flask - HTTPAuth:
(venv) $ pip install flask-httpauth
为 HTTP 基本身份验证初始化扩展,需要创建一个
HTTPBasicAuth
类的对象。和 Flask - Login 一样,Flask - HTTPAuth 对验证用户凭据的过程没有任何假设,因此这些信息在回调函数中提供。以下是扩展初始化和验证回调的示例:
from flask.ext.httpauth import HTTPBasicAuth
auth = HTTPBasicAuth()
@auth.verify_password
def verify_password(email, password):
if email == '':
g.current_user = AnonymousUser()
return True
user = User.query.filter_by(email = email).first()
if not user:
return False
g.current_user = user
return user.verify_password(password)
由于这种用户身份验证仅在 API 蓝图中使用,因此 Flask - HTTPAuth 扩展在蓝图包中初始化,而不是像其他扩展一样在应用程序包中初始化。
电子邮件和密码使用
User
模型中的现有支持进行验证。验证回调在登录有效时返回
True
,否则返回
False
。支持匿名登录,客户端必须发送空白的电子邮件字段。
身份验证回调将经过身份验证的用户保存在 Flask 的
g
全局对象中,以便视图函数稍后可以访问它。注意,当收到匿名登录时,函数返回
True
并将 Flask - Login 使用的
AnonymousUser
类的一个实例保存到
g.current_user
中。
由于每个请求都要交换用户凭据,因此 API 路由通过安全的 HTTP 暴露非常重要,这样所有请求和响应都会被加密。
当身份验证凭据无效时,服务器向客户端返回 401 错误。Flask - HTTPAuth 默认生成带有此状态代码的响应,但为了确保响应与 API 返回的其他错误一致,可以自定义错误响应,示例如下:
@auth.error_handler
def auth_error():
return unauthorized('Invalid credentials')
要保护一个路由,可以使用
auth.login_required
装饰器:
@api.route('/posts/')
@auth.login_required
def get_posts():
pass
由于蓝图中的所有路由都需要以相同的方式进行保护,因此可以在蓝图的
before_request
处理程序中包含一次
login_required
装饰器,示例如下:
from .errors import forbidden_error
@api.before_request
@auth.login_required
def before_request():
if not g.current_user.is_anonymous and \
not g.current_user.confirmed:
return forbidden('Unconfirmed account')
现在,蓝图中所有路由的身份验证检查将自动完成。作为额外的检查,
before_request
处理程序还会拒绝未确认账户的已认证用户。
6. 基于令牌的身份验证
客户端必须在每个请求中发送身份验证凭据。为了避免不断传输敏感信息,可以提供基于令牌的身份验证解决方案。
在基于令牌的身份验证中,客户端将登录凭据发送到一个特殊的 URL 以生成身份验证令牌。一旦客户端获得了令牌,就可以使用它代替登录凭据来验证请求。出于安全原因,令牌会附带一个过期时间。当令牌过期时,客户端必须重新进行身份验证以获取新的令牌。由于令牌的生命周期较短,其落入坏人手中的风险有限。以下是
User
模型中支持生成和验证身份验证令牌的两个新方法:
class User(db.Model):
# ...
def generate_auth_token(self, expiration):
s = Serializer(current_app.config['SECRET_KEY'],
expires_in=expiration)
return s.dumps({'id': self.id})
@staticmethod
def verify_auth_token(token):
s = Serializer(current_app.config['SECRET_KEY'])
try:
data = s.loads(token)
except:
return None
return User.query.get(data['id'])
generate_auth_token()
方法返回一个签名的令牌,该令牌对用户的
id
字段进行编码,同时还使用以秒为单位的过期时间。
verify_auth_token()
方法接受一个令牌,如果有效则返回其中存储的用户。这是一个静态方法,因为只有在解码令牌后才能知道用户。
为了对带有令牌的请求进行身份验证,需要修改 Flask - HTTPAuth 的
verify_password
回调以接受令牌和常规凭据。更新后的回调如下:
@auth.verify_password
def verify_password(email_or_token, password):
if email_or_token == '':
g.current_user = AnonymousUser()
return True
if password == '':
g.current_user = User.verify_auth_token(email_or_token)
g.token_used = True
return g.current_user is not None
user = User.query.filter_by(email=email_or_token).first()
if not user:
return False
g.current_user = user
g.token_used = False
return user.verify_password(password)
在这个新版本中,第一个身份验证参数可以是电子邮件地址或身份验证令牌。如果该字段为空,则像以前一样假设为匿名用户。如果密码为空,则假设
email_or_token
字段是一个令牌并进行相应验证。如果两个字段都不为空,则假设是常规的电子邮件和密码身份验证。通过这种实现,基于令牌的身份验证是可选的,由每个客户端决定是否使用。为了让视图函数能够区分这两种身份验证方法,添加了一个
g.token_used
变量。
返回身份验证令牌给客户端的路由也添加到了 API 蓝图中,实现如下:
@api.route('/token')
def get_token():
if g.current_user.is_anonymous() or g.token_used:
return unauthorized('Invalid credentials')
return jsonify({'token': g.current_user.generate_auth_token(
expiration=3600), 'expiration': 3600})
由于这个路由在蓝图中,添加到
before_request
处理程序的身份验证机制也适用于它。为了防止客户端使用旧令牌请求新令牌,会检查
g.token_used
变量,这样可以拒绝使用令牌进行身份验证的请求。该函数在 JSON 响应中返回一个有效期为一小时的令牌,有效期也包含在 JSON 响应中。
7. 资源与 JSON 的序列化
编写 Web 服务时,经常需要将资源的内部表示与 JSON 进行相互转换,JSON 是 HTTP 请求和响应中使用的传输格式。以下是
Post
类中新增的
to_json()
方法:
class Post(db.Model):
# ...
def to_json(self):
json_post = {
'url': url_for('api.get_post', id=self.id, _external=True),
'body': self.body,
'body_html': self.body_html,
'timestamp': self.timestamp,
'author': url_for('api.get_user', id=self.author_id,
_external=True),
'comments': url_for('api.get_post_comments', id=self.id,
_external=True),
'comment_count': self.comments.count()
}
return json_post
url
、
author
和
comments
字段需要返回相应资源的 URL,因此这些 URL 通过调用
url_for()
生成,这些路由将在 API 蓝图中定义。注意,所有
url_for()
调用都添加了
_external=True
,以便返回完全限定的 URL,而不是传统 Web 应用程序上下文中通常使用的相对 URL。
这个例子还展示了如何在资源表示中返回“虚构”的属性。
comment_count
字段返回博客文章的评论数量,虽然这不是模型的真实属性,但为了方便客户端,将其包含在资源表示中。
User
模型的
to_json()
方法可以以类似的方式构造:
class User(UserMixin, db.Model):
# ...
def to_json(self):
json_user = {
'url': url_for('api.get_post', id=self.id, _external=True),
'username': self.username,
'member_since': self.member_since,
'last_seen': self.last_seen,
'posts': url_for('api.get_user_posts', id=self.id, _external=True),
'followed_posts': url_for('api.get_user_followed_posts',
id=self.id, _external=True),
'post_count': self.posts.count()
}
return json_user
注意,出于隐私原因,用户的一些属性(如电子邮件和角色)在响应中被省略。这个例子再次证明,提供给客户端的资源表示不需要与相应数据库模型的内部表示相同。
将 JSON 结构转换为模型时,客户端提供的一些数据可能无效、错误或不必要。以下是从 JSON 创建
Post
模型的方法:
from app.exceptions import ValidationError
class Post(db.Model):
# ...
@staticmethod
def from_json(json_post):
body = json_post.get('body')
if body is None or body == '':
raise ValidationError('post does not have a body')
return Post(body=body)
可以看到,这个实现只使用了 JSON 字典中的
body
属性。
body_html
属性被忽略,因为每当
body
属性被修改时,服务器端的 Markdown 渲染会由 SQLAlchemy 事件自动触发。
timestamp
属性不需要提供,除非允许客户端回溯文章发布时间,但这个应用程序没有这个功能。
author
字段不使用,因为客户端无权选择博客文章的作者,该字段的唯一可能值是经过身份验证的用户。
comments
和
comment_count
属性由数据库关系自动生成,因此其中没有创建模型所需的有用信息。最后,
url
字段被忽略,因为在这个实现中,资源 URL 由服务器定义,而不是客户端。
注意错误检查的方式。如果
body
字段缺失或为空,则会引发
ValidationError
异常。在这种情况下,抛出异常是处理错误的合适方式,因为这个方法没有足够的信息来正确处理这种情况。异常有效地将错误传递给调用者,使更高级别的代码能够处理错误。
ValidationError
类实现为 Python 的
ValueError
的简单子类。
Flask 实现 RESTful Web 服务全解析
8. 总结与操作建议
前面详细介绍了使用 Flask 构建 RESTful Web 服务的各个方面,下面为大家总结操作步骤和注意事项:
8.1 创建 API 蓝图步骤
- 定义蓝图结构 :按照如下结构创建 API 蓝图相关文件:
|-flasky
|-app/
|-api_1_0
|-__init__.py
|-user.py
|-post.py
|-comment.py
|-authentication.py
|-errors.py
|-decorators.py
-
编写蓝图构造函数
:在
app/api_1_0/__init__.py中编写如下代码:
from flask import Blueprint
api = Blueprint('api', __name__)
from . import authentication, posts, users, comments, errors
-
注册 API 蓝图
:在
app/_init_.py中使用以下代码注册蓝图:
def create_app(config_name):
# ...
from .api_1_0 import api as api_1_0_blueprint
app.register_blueprint(api_1_0_blueprint, url_prefix='/api/v1.0')
# ...
8.2 错误处理操作
-
处理 404 和 500 错误
:在
app/main/errors.py中实现内容协商的错误处理,示例如下:
@main.app_errorhandler(404)
def page_not_found(e):
if request.accept_mimetypes.accept_json and \
not request.accept_mimetypes.accept_html:
response = jsonify({'error': 'not found'})
response.status_code = 404
return response
return render_template('404.html'), 404
-
实现其他状态码辅助函数
:在
app/api/errors.py中实现如 403 错误的辅助函数:
def forbidden(message):
response = jsonify({'error': 'forbidden', 'message': message})
response.status_code = 403
return response
8.3 用户身份验证操作
- 安装 Flask - HTTPAuth :使用以下命令安装扩展:
(venv) $ pip install flask-httpauth
-
初始化扩展并设置验证回调
:在
app/api_1_0/authentication.py中编写如下代码:
from flask.ext.httpauth import HTTPBasicAuth
auth = HTTPBasicAuth()
@auth.verify_password
def verify_password(email, password):
if email == '':
g.current_user = AnonymousUser()
return True
user = User.query.filter_by(email = email).first()
if not user:
return False
g.current_user = user
return user.verify_password(password)
-
自定义错误响应
:在
app/api_1_0/authentication.py中添加如下代码:
@auth.error_handler
def auth_error():
return unauthorized('Invalid credentials')
-
保护蓝图路由
:在
app/api_1_0/authentication.py中使用以下代码:
from .errors import forbidden_error
@api.before_request
@auth.login_required
def before_request():
if not g.current_user.is_anonymous and \
not g.current_user.confirmed:
return forbidden('Unconfirmed account')
8.4 基于令牌的身份验证操作
-
添加令牌生成和验证方法到
User模型 :在app/models.py中添加如下方法:
class User(db.Model):
# ...
def generate_auth_token(self, expiration):
s = Serializer(current_app.config['SECRET_KEY'],
expires_in=expiration)
return s.dumps({'id': self.id})
@staticmethod
def verify_auth_token(token):
s = Serializer(current_app.config['SECRET_KEY'])
try:
data = s.loads(token)
except:
return None
return User.query.get(data['id'])
-
修改验证回调支持令牌
:在
app/api_1_0/authentication.py中修改verify_password函数:
@auth.verify_password
def verify_password(email_or_token, password):
if email_or_token == '':
g.current_user = AnonymousUser()
return True
if password == '':
g.current_user = User.verify_auth_token(email_or_token)
g.token_used = True
return g.current_user is not None
user = User.query.filter_by(email=email_or_token).first()
if not user:
return False
g.current_user = user
g.token_used = False
return user.verify_password(password)
-
添加令牌生成路由
:在
app/api_1_0/authentication.py中添加如下代码:
@api.route('/token')
def get_token():
if g.current_user.is_anonymous() or g.token_used:
return unauthorized('Invalid credentials')
return jsonify({'token': g.current_user.generate_auth_token(
expiration=3600), 'expiration': 3600})
8.5 资源与 JSON 序列化操作
-
添加
to_json()方法到模型 :在app/models.py中为Post和User模型添加to_json()方法,示例如下:
class Post(db.Model):
# ...
def to_json(self):
json_post = {
'url': url_for('api.get_post', id=self.id, _external=True),
'body': self.body,
'body_html': self.body_html,
'timestamp': self.timestamp,
'author': url_for('api.get_user', id=self.author_id,
_external=True),
'comments': url_for('api.get_post_comments', id=self.id,
_external=True),
'comment_count': self.comments.count()
}
return json_post
class User(UserMixin, db.Model):
# ...
def to_json(self):
json_user = {
'url': url_for('api.get_post', id=self.id, _external=True),
'username': self.username,
'member_since': self.member_since,
'last_seen': self.last_seen,
'posts': url_for('api.get_user_posts', id=self.id, _external=True),
'followed_posts': url_for('api.get_user_followed_posts',
id=self.id, _external=True),
'post_count': self.posts.count()
}
return json_user
-
实现从 JSON 创建模型方法
:在
app/models.py中为Post模型添加from_json()方法:
from app.exceptions import ValidationError
class Post(db.Model):
# ...
@staticmethod
def from_json(json_post):
body = json_post.get('body')
if body is None or body == '':
raise ValidationError('post does not have a body')
return Post(body=body)
9. 流程图展示
下面通过 mermaid 流程图展示基于令牌的身份验证流程:
graph TD;
A[客户端发送请求] --> B{是否包含令牌};
B -- 是 --> C{令牌是否有效};
C -- 是 --> D[处理请求];
C -- 否 --> E[返回 401 错误];
B -- 否 --> F{是否包含常规凭据};
F -- 是 --> G{凭据是否有效};
G -- 是 --> D;
G -- 否 --> E;
F -- 否 --> H[处理匿名请求或返回 401 错误];
10. 性能与安全考量
在使用 Flask 构建 RESTful Web 服务时,性能和安全是两个重要的考量因素。
10.1 性能方面
- 缓存机制 :可以使用缓存来减少对数据库的频繁查询。例如,对于一些不经常变化的数据,可以使用 Redis 等缓存工具进行缓存。
- 异步处理 :对于一些耗时的操作,如文件上传、复杂计算等,可以使用异步处理来提高服务的响应速度。Flask 可以结合 Celery 等异步任务队列来实现异步处理。
10.2 安全方面
- HTTPS 加密 :如前面所述,所有 API 路由都应该通过安全的 HTTP 暴露,确保所有请求和响应都被加密,防止用户凭据和数据在传输过程中被窃取。
- 输入验证 :在处理客户端请求时,要对输入数据进行严格的验证,防止 SQL 注入、XSS 攻击等安全问题。可以使用 Flask - WTF 等扩展来实现表单验证。
- 权限管理 :除了基本的身份验证,还需要进行细致的权限管理,确保不同用户角色只能访问其权限范围内的资源。
11. 未来扩展方向
随着业务的发展,RESTful Web 服务可能需要进行扩展。以下是一些可能的扩展方向:
11.1 支持更多 API 版本
当需要对 API 进行重大更新,且新的 API 与旧版本不兼容时,可以按照之前的方法创建新的 API 蓝图版本,如
api_1_1
,并同时支持多个版本的 API。
11.2 集成第三方服务
可以将 RESTful Web 服务与第三方服务集成,如支付网关、短信服务等,为用户提供更丰富的功能。
11.3 实现 API 文档自动化
使用工具如 Swagger 来自动生成 API 文档,方便开发者使用和测试 API。
通过以上的介绍,相信大家对使用 Flask 构建 RESTful Web 服务有了全面的了解。在实际开发中,可以根据具体需求灵活运用这些技术,构建出高效、安全、可扩展的 Web 服务。
超级会员免费看
1250

被折叠的 条评论
为什么被折叠?



