POST
方法。 此请求的响应也是JSON格式,服务器仅返回所请求的信息,客户端负责将此信息呈现给用户。@login_required
装饰器, 这会将用户的登录状态存储在Flask用户会话中。GET
请求表示客户想要检索资源信息,POST
请求表示客户想要创建新资源,PUT
或PATCH
请求定义对现有资源的修改,DELETE
表示删除资源的请求。 目标资源被指定为请求的URL,并在HTTP头,URL的查询字符串部分或请求主体中提供附加信息。__init__.py
文件中创建blueprint对象,这与应用程序中的其他blueprint类似:app/api/__init__.py
: API blueprint 构造器。GET
GET
GET
GET
POST
PUT
app/__init__.py
:应用中注册API blueprint。password
字段的特殊之处在于,它仅在注册新用户时才会使用。 回顾第五章,用户密码不存储在数据库中,只存储一个散列字符串,所以密码永远不会被返回。email
字段也被专门处理,因为我不想公开用户的电子邮件地址。 只有当用户请求自己的条目时,才会返回email
字段,但是当他们检索其他用户的条目时不会返回。post_count
,follower_count
和followed_count
字段是“虚拟”字段,它们在数据库字段中不存在,提供给客户端是为了方便。 这是一个很好的例子,它演示了资源表示不需要和服务器中资源的实际定义一致。_links
部分,它实现了超媒体要求。 定义的链接包括指向当前资源的链接,用户的粉丝列表链接,用户关注的用户列表链接,最后是指向用户头像图像的链接。 将来,如果我决定向这个API添加用户动态,那么用户的动态列表链接也应包含在这里。json
包负责Python数据结构和JSON之间的转换。因此,为了生成这些表示,我将在User
模型中添加一个名为to_dict()
的方法,该方法返回一个Python字典:email
字段需要特殊处理,因为我只想在用户请求自己的数据时才包含电子邮件。 所以我使用include_email
标志来确定该字段是否包含在表示中。last_seen
字段的生成。 对于日期和时间字段,我将使用ISO 8601格式,Python的datetime
对象可以通过isoformat()
方法生成这样格式的字符串。 但是因为我使用的datetime
对象的时区是UTC,且但没有在其状态中记录时区,所以我需要在末尾添加Z
,即ISO 8601的UTC时区代码。url_for()
生成URL(目前指向我在app/api/users.py中定义的占位符视图函数)。 头像链接是特殊的,因为它是应用外部的Gravatar URL。 对于这个链接,我使用了与渲染网页中的头像的相同avatar()
方法。to_dict()
方法将用户对象转换为Python表示,以后会被转换为JSON。 我还需要其反向处理的方法,即客户端在请求中传递用户表示,服务器需要解析并将其转换为User
对象。 以下是实现从Python字典到User
对象转换的from_dict()
方法:username
,email
和about_me
。 对于每个字段,我检查它是否存在于data
参数中,如果存在,我使用Python的setattr()
在对象的相应属性中设置新值。password
字段被视为特例,因为它不是对象中的字段。 new_user
参数确定了这是否是新的用户注册,这意味着data
中包含password
。 要在用户模型中设置密码,需要调用set_password()
方法来创建密码哈希。items
是用户资源的列表,每个用户资源的定义如前一节所述。 _meta
部分包含集合的元数据,客户端在向用户渲染分页控件时就会用得上。 _links
部分定义了相关链接,包括集合本身的链接以及上一页和下一页链接,也能帮助客户端对列表进行分页。SearchableMixin
类,任何需要全文索引的模型都可以从中继承。 我会故技重施,实现一个新的mixin类,我命名为PaginatedAPIMixin
:to_collection_dict()
方法产生一个带有用户集合表示的字典,包括items
,_meta
和_links
部分。 你可能需要仔细检查该方法以了解其工作原理。 前三个参数是Flask-SQLAlchemy查询对象,页码和每页数据数量。 这些是决定要返回的条目是什么的参数。 该实现使用查询对象的paginate()
方法来获取该页的条目,就像我对主页,发现页和个人主页中的用户动态所做的一样。url_for('api.get_users', id=id, page=page)
这样的代码来生成自链接(译者注:因为这样就固定成用户资源专用了)。 url_for()
的参数将取决于特定的资源集合,所以我将依赖于调用者在endpoint
参数中传递的值,来确定需要发送到url_for()
的视图函数。 由于许多路由都需要参数,我还需要在kwargs
中捕获更多关键字参数,并将它们传递给url_for()
。 page
和per_page
查询字符串参数是明确给出的,因为它们控制所有API路由的分页。error_response()
函数:HTTP_STATUS_CODES
字典,它为每个HTTP状态代码提供一个简短的描述性名称。 我在错误表示中使用这些名称作为error
字段的值,所以我只需要操心数字状态码和可选的长描述。 jsonify()
函数返回一个默认状态码为200的FlaskResponse
对象,因此在创建响应之后,我将状态码设置为对应的错误代码。bad_request()
占位符:id
来检索指定用户开始吧:id
作为URL中的动态参数。 查询对象的get_or_404()
方法是以前见过的get()
方法的一个非常有用的变体,如果用户存在,它返回给定id
的对象,当id不存在时,它会中止请求并向客户端返回一个404错误,而不是返回None
。 get_or_404()
比get()
更有优势,它不需要检查查询结果,简化了视图函数中的逻辑。to_dict()
方法用于生成用户资源表示的字典,然后Flask的jsonify()
函数将该字典转换为JSON格式的响应以返回给客户端。id
值来查看SQLAlchemy查询对象的get_or_404()
方法如何触发404错误(我将在稍后向你演示如何扩展错误处理,以便返回这些错误 JSON格式)。id
为1
的用户(可能是你自己),命令如下:PaginatedAPIMixin
的to_collection_dict()
方法:page
和per_page
,如果它们没有被定义,则分别使用默认值1和10。 per_page
具有额外的逻辑,以100为上限。 给客户端控件请求太大的页面并不是一个好主意,因为这可能会导致服务器的性能问题。 然后page
和per_page
以及query对象(在本例中,该查询只是User.query
,是返回所有用户的最通用的查询)参数被传递给to_collection_query()
方法。 最后一个参数是api.get_users
,这是我在表示中使用的三个链接所需的endpoint名称。id
动态参数。 id
用于从数据库中获取用户,然后将user.followers
和user.followed
关系查询提供给to_collection_dict()
,所以希望现在你可以看到,花费一点点额外的时间,并以通用的方式设计该方法,对于获得的回报而言是值得的。 to_collection_dict()
的最后两个参数是endpoint名称和id
,id
将在kwargs
中作为一个额外关键字参数,然后在生成链接时将它传递给url_for()
。_links
部分。POST
请求将用于注册新的用户帐户。 你可以在下面看到这条路由的实现:request.get_json()
方法从请求中提取JSON并将其作为Python结构返回。 如果在请求中没有找到JSON数据,该方法返回None
,所以我可以使用表达式request.get_json() or {}
确保我总是可以获得一个字典。username
, email
和password
。 如果其中任何一个缺失,那么我使用app/api/errors.py模块中的bad_request()
辅助函数向客户端返回一个错误。 除此之外,我还需要确保username
和email
字段尚未被其他用户使用,因此我尝试使用获得的用户名和电子邮件从数据库中加载用户,如果返回了有效的用户,那么我也将返回错误给客户端。User
模型中的from_dict()
方法,new_user
参数被设置为True
,所以它也接受通常不存在于用户表示中的password
字段。to_dict()
产生它的有效载荷。 创建资源的POST
请求的响应状态代码应该是201,即创建新实体时使用的代码。 此外,HTTP协议要求201响应包含一个值为新资源URL的Location
头部。id
,所以我可以加载指定的用户或返回404错误(如果找不到)。 就像注册新用户一样,我需要验证客户端提供的username
和email
字段是否与其他用户发生了冲突,但在这种情况下,验证有点棘手。 首先,这些字段在此请求中是可选的,所以我需要检查字段是否存在。 第二个复杂因素是客户端可能提供与目前字段相同的值,所以在检查用户名或电子邮件是否被采用之前,我需要确保它们与当前的不同。 如果任何验证检查失败,那么我会像之前一样返回400错误给客户端。User
模型的from_dict()
方法导入客户端提供的所有数据,然后将更改提交到数据库。 该请求的响应会将更新后的用户表示返回给用户,并使用默认的200状态代码。about_me
字段:@login_required
装饰器,但是这种方法存在一些问题。 装饰器检测到未通过身份验证的用户时,会将用户重定向到HTML登录页面。 在API中没有HTML或登录页面的概念,如果客户端发送带有无效或缺少凭证的请求,服务器必须拒绝请求并返回401状态码。 服务器不能假定API客户端是Web浏览器,或者它可以处理重定向,或者它可以渲染和处理HTML登录表单。 当API客户端收到401状态码时,它知道它需要向用户询问凭证,但是它是如何实现的,服务器不需要关心。User
模型:token
属性,并且因为我需要通过它搜索数据库,所以我为它设置了唯一性和索引。 我还添加了token_expiration
字段,它保存token过期的日期和时间。 这使得token不会长时间有效,以免成为安全风险。get_token()
方法为用户返回一个token。 以base64编码的24位随机字符串来生成这个token,以便所有字符都处于可读字符串范围内。 在创建新token之前,此方法会检查当前分配的token在到期之前是否至少还剩一分钟,并且在这种情况下会返回现有的token。revoke_token()
方法使得当前分配给用户的token失效,只需设置到期时间为当前时间的前一秒。check_token()
方法是一个静态方法,它将一个token作为参数传入并返回此token所属的用户。 如果token无效或过期,则该方法返回None
。HTTPBasicAuth
类实现了基本的认证流程。 这两个必需的函数分别通过verify_password
和error_handler
装饰器进行注册。True
,否则返回False
。 我依赖User
类的check_password()
方法来检查密码,它在Web应用的认证过程中,也会被Flask-Login使用。 我将认证用户保存在g.current_user
中,以便我可以从API视图函数中访问它。error_response()
函数生成的401错误。 401错误在HTTP标准中定义为“未授权”错误。 HTTP客户端知道当它们收到这个错误时,需要重新发送有效的凭证。HTTPBasicAuth
实例中的@basic_auth.login_required
装饰器,它将指示Flask-HTTPAuth验证身份(通过我上面定义的验证函数),并且仅当提供的凭证是有效的才运行下面的视图函数。 该视图函数的实现依赖于用户模型的get_token()
方法来生成token。 数据库提交在生成token后发出,以确保token及其到期时间被写回到数据库。basic_auth_error()
函数中定义的错误负载。 下面请求带上了基本认证需要的凭证:<username>:<password>
。 用户名和密码需要以冒号作为分隔符。HTTPTokenAuth
类的第二个身份验证实例,并提供token验证回调:verify_token
装饰器注册验证函数,除此之外,token认证的工作方式与基本认证相同。 我的token验证函数使用User.check_token()
来定位token所属的用户。 该函数还通过将当前用户设置为None
来处理缺失token的情况。返回值是True
还是False
,决定了Flask-HTTPAuth是否允许视图函数的运行。@token_auth.login_required
装饰器:create_user()
之外的所有API视图函数中,显而易见,这个函数不能使用token认证,因为用户都不存在时,更不会有token了。Authorization
头部,其值是请求 /api/tokens 获得的token的值。Flask-HTTPAuth期望的是"不记名"token,但是它没有被HTTPie直接支持。就像针对基本认证,HTTPie提供了--auth
选项来接受用户名和密码,但是token的头部则需要显式地提供了。下面是发送不记名token的格式:DELETE
请求,以使token失效。此路由的身份验证是基于token的,事实上,在Authorization
头部中发送的token就是需要被撤销的。撤销使用了User
类中的辅助方法,该方法重新设置token过期日期来实现撤销操作。之后提交数据库会话,以确保将更改写入数据库。这个请求的响应没有正文,所以我可以返回一个空字符串。Return语句中的第二个值设置状态代码为204,该代码用于成功请求却没有响应主体的响应。Accept
头部,指示格式首选项。然后,服务器查看自身格式列表并使用匹配客户端格式列表中的最佳格式进行响应。request.accept_mimetypes
来完成:wants_json_response()
辅助函数比较客户端对JSON和HTML格式的偏好程度。 如果JSON比HTML高,那么我会返回一个JSON响应。 否则,我会返回原始的基于模板的HTML响应。 对于JSON响应,我将使用从API blueprint中导入error_response
辅助函数,但在这里我要将其重命名为api_error_response()
,以便清楚它的作用和来历。