设计RESTful(flask)

本文总阅读量

前记

当前很多WEB框架都有一些库来支持RESTful了,但是不了解的话还是会用得一头雾水。在找了一些资料后,找了RESTful设计的规范,并用falsk框架尝试写出RESTful 的api

编写一个应用

在未使用RESTful之前,简单的web应用可能是这样的(我全部都使用了get)
这个应用目前记录的书的isbn,书的名字,还有书的数据(以时间来记录)
忽略<后的\

url:/post/<\int:isbn>/<\title> 建立一本书的信息
url:/get/<\int:isbn>/ 获取一本书的信息
url:/put/<\int:isbn>/<\data> 更新书的日期
url:/delete/<\int:isbn> 删除书的信息

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
import time
from flask import Flask
app = Flask(__name__)


class Book(object):
def __init__(self):
self.book_dict = {}


book = Book()

@app.route('/')
def Index():
return 'Index Page'

@app.route('/get/<int:isbn>/')
def get(isbn):
get_book = book.book_dict[isbn]
return "isbn:%d title:%s data:%s" %(isbn,get_book['title'],get_book['data'])

@app.route('/post/<int:isbn>/<title>')
def post(isbn,title):
data = time.strftime('%Y-%m-%d %H:%M',time.localtime(time.time()))
book.book_dict[isbn] = dict(title=title,data=data)

return "The new book title:%s,isbn:%d,data:%s" %(title,isbn,data)

@app.route('/put/<int:isbn>')
def put(isbn):
put_book = book.book_dict[isbn]
data_temp = put_book['data']
put_book['data'] = time.strftime('%Y-%m-%d %H:%M',time.localtime(time.time()))
return 'Updata data: %s to %s' % (data_temp,put_book['data'])

@app.route('/delete/<int:isbn>')
def delete(isbn):
book.book_dict[isbn]=None
return 'isbn:%d delete!' % isbn



if __name__ == '__main__':
app.run(debug=True)

url设计

统一的url

在RESTful设计中,统一接口,使用不同的http方法表达不同的行为:

  • GET:从服务器获出资源(一项或多项)
  • POST:在服务器新建一个资源
  • PUT:在服务器更新资源(用户提供完整数据)
  • PATCH:在服务器更新资源(用户供需要修改的资源数据)
  • DELETE:从服务器删除资源

其他URL设计

  • 版本
    RESTful给出的方法是使用HTTPheader中的accept来传递版本信息,但也有一些是在url中添加版本信息,如:

    /api/v1/ #v1就是指版本1

  • 使用名词
    设计url时,应使用名词而非动词

  • 资源合集
    获取资源合集时,有两种设计方法,一种是把要获取的资源当成一个子合集,另外一种是把需要的资源当成从总资源中过滤出来的。如书的例子,假设data有年,月,日三个值,我们要获取17年的十月的书时,url可以按照以下设计:

    • 子合集

      url: book/data/17/10

    • 过滤出来的资源

      url: book/data/?y=17&m=10

  • url区分大小写

  • api/demo和api/demo/不一样
    如果设计为:api/demo/时,用户输入api/demo会重定向到api/demo/。但是设计为api/demo时,用户输入api/demo/只能404 not found。

  • 使用-而不使用_
    由于网页的超链接会有下划线,所以_可能会让别人误会为空格

带有RESTful设计规范url的应用

按照之前的应用,HTTP方法也只使用了’POST’, ‘GET’,’PUT’,’DELETE’。(PUT是模拟出用户提供了日期时间,实际代码中是自己获取了- -。),可以把代码改为下面这样(其他URL设计中的资源合集并没有在这个例子体现出来)。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
import time
from flask import Flask, request
app = Flask(__name__)


class Book(object):
def __init__(self):
self.book_dict = {}


book = Book()

@app.route('/api/v1/book/<int:isbn>/', methods=['POST', 'GET','PUT','DELETE'])
def api(isbn):
error = None
elif request.method == 'POST':
title = request.args.get('title')
book.book_dict[isbn] = dict(title=title,data=time.strftime('%Y-%m-%d %H:%M',time.localtime(time.time())))
return "The new book title:%s,isbn:%d" %(title,isbn)
elif request.method == 'GET':
get_book = book.book_dict[isbn]
return "isbn:%d title:%s data:%s" %(isbn,get_book['title'],get_book['data'])
elif request.method == 'PUT':
data = time.strftime('%Y-%m-%d %H:%M',time.localtime(time.time()))
put_book = book.book_dict[isbn]
data_temp = put_book['data']
put_book['data'] = data
return 'Updata data: %s to %s' % (data_temp,put_book['data'])
elif request.method == 'DELETE':
book.book_dict[isbn]=None
return 'isbn:%d delete!' % isbn
else:
return 'methods not found'


if __name__ == '__main__':
app.run(debug=True)

PS:测试RESTful如果也是CHROME浏览器的话可以使用postman REST client
输入以下url测试,并选择好对应的按钮,再点击send:

http://127.0.0.1:5000/api/v1/book/123/?title=abc POST
http://127.0.0.1:5000/api/v1/book/123/ GET
http://127.0.0.1:5000/api/v1/book/123/ PUT
http://127.0.0.1:5000/api/v1/book/123/ DELETE

后端返回的内容

返回的数据类型

RESTful API规范统一的数据格式json或xml(一般都是json),如果API响应客户端请求后,
返回json,需要在header中添加Content-Type=application/json
返回xml,需要在header中添加Content-Type=application/atom+xml
在falsk中们可以使用json.jsonify返回json数据

1
2
3
4
#导入
from flask import json
#使用
return json.jsonify

返回的状态码

在客户端发送请求给服务器后,服务器也会返回信息给用户

  • 当GET, PUT和PATCH请求成功时,返回对应的数据,及状态码200
  • 当POST创建数据成功时,返回创建的数据,及状态码201
  • 当DELETE删除数据成功时,不返回数据,返回状态码204
  • 当GET 不到数据时,返回状态码404
  • 当验请求数据时发现错误,返回状态码 400
  • 当API 请求需要用户认证时,如果request中的认证信息不正确,返回状态码 401
  • 当API 请求需要验证用户权限时,如果当前用户无相应权限,返回状态码 403

Flask的路由函数可以选择额外返回两个值,这两个值将被分别设为HTTP状态码和自定义的HTTP响应标头
也就是使用

1
return "abc" ,200 ,{'Content-Type': 'application/json'}

就能返回文本,状态码,和http响应头,但是使用json.jsonify的话自带{‘Content-Type’: ‘application/json’}。

但如果要自己定义返回的错误信息的话,可以通过修改abort(状态码)和修饰器@app.errorhandler(状态码)来定义,不过比较繁琐,冗余比较多,可以自己定义一个类来自定义返回错误信息(原出处):
编写一个处理错误类

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
class CustomFlaskErr(Exception):

# 默认的返回码
status_code = 400

# 自己定义了一个 return_code,作为更细颗粒度的错误代码
def __init__(self, return_code=None, status_code=None, payload=None):
Exception.__init__(self)
self.return_code = return_code
if status_code is not None:
self.status_code = status_code
self.payload = payload

# 构造要返回的错误代码和错误信息的 dict
def to_dict(self):
rv = dict(self.payload or ())

# 增加 dict key: return code
rv['return_code'] = self.return_code

# 增加 dict key: message, 具体内容由常量定义文件中通过 return_code 转化而来
rv['message'] = J_MSG[self.return_code]

# 日志打印
logger.warning(J_MSG[self.return_code])

return rv

利用@app.errorhandler定义一个可以返回信息和状态码的函数,继承于我们编写的处理错误类

1
2
3
4
5
6
7
8
9
10
@app.errorhandler(CustomFlaskErr)
def handle_flask_error(error):

# response 的 json 内容为自定义错误代码和错误信息
response = jsonify(error.to_dict())

# response 返回 error 发生时定义的标准错误代码
response.status_code = error.status_code

return response

调用示例:

1
2
if user_name is None:
raise CustomFlaskErr(USER_NAME_ILLEGAL, status_code=400)

提供分页url

由于使用RESTful后是无状态的,所以要提供类似与分页的url,如上一页和下一页

如果服务器记录了应用的状态(stateful),那么你只要向服务询问『我要看下一页』,那么服务器自然就会返回第二页。类似的,如果你当前在第二页,想服务器请求『我要看下一页』,那就会得到第三页。但是REST的服务器恰恰是无状态的(stateless),服务器并没有保持你当前处于第几页,也就无法响应『下一页』这种具有状态性质的请求。因此客户端需要去维护当前应用的状态(application state),也就是『如何获取下一页资源』。当然,『下一页资源』的业务逻辑必然是由服务端来提供。服务器在文章列表的atom表征中加入一个URI超链接(hyper link),指向下一页文章列表对应的资源。客户端就可以使用统一接口(Uniform Interface)的方式,从这个URI中获取到他想要的下一页文章列表资源。上面的『能够进入下一页』就是应用的状态(State)。服务器把『能够进入下一页』这个状态以atom表征形式传输(Transfer)给客户端就是表征状态传输(REpresentational State Transfer)这个概念。
作者:季文昊
链接:https://www.zhihu.com/question/28557115/answer/48120528
来源:知乎

假设应用里的书可以给别人查看一页一页的查看,那返回的数据需要这些(例如现在处于第二页):
xml返回的格式(忽略\link中的\):

<\link href=”http://***/book/page?pn=2” rel=”self” />
<\link href=”http://***/book/page?pn=3” rel=”next” />
<\link href=”http://***/book/page?pn=1” rel=”prev” />

json返回的数据要包含:

{“link”: {
“rel”: “http:///book/page?pn=2”,
“next”: “http://
/book/page?pn=3”,
“prev”: “http://***/book/page?pn=1”
“title”: “book.title”,
“type”: “application/vnd.yourformat+json”
}}

或者

{
“page”: 2, # 当前是第几页
“pages”: 3, # 总共多少页
“per_page”: 20, # 每页多少数据
“has_next”: true, # 是否有下一页数据
“has_prev”: true, # 是否有前一页数据
“total”: 59 # 总共多少数据
}

按照以上规范进行修改,就可以得出一段代码不太好看的应用了- -(没有从数据库抽取数据,数据都是post生成的,很多判断都叠加在一起了)

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
import time
from flask import Flask, request, json
app = Flask(__name__)


class Book(object):
def __init__(self):
self.book_dict = {}


book = Book()

def check(isbn):
'''
第一个布尔量代表book_dict有没有资源,第二个代表isbn是否存在
'''
if not book.book_dict:
return [False,False]

for k,v in book.book_dict.items():
if isbn == k:
return [True,True,v]
else:
return [True,False,404]


@app.route('/')
def Index():
return 'Index Page'

@app.route('/api/v1/book/<int:isbn>/', methods=['POST', 'GET','PUT','DELETE'])
def api(isbn):
if request.method == 'POST':
if check(isbn)[0]:
message = dict(message="isbn:%d Has been created" % isbn)
return json.jsonify(message), 422
title = request.args.get('title')
page_info = ['page1','page2','page3'] #为书添加分页功能
book.book_dict[isbn] = dict(title=title,page=page_info,data=time.strftime('%Y-%m-%d %H:%M',time.localtime(time.time())))
data_json = dict(isbn=isbn,data=book.book_dict[isbn])
return json.jsonify(data_json), 201


elif request.method == 'GET':
page_data =None
if not check(isbn)[1]:
message = dict(message="isbn:%d not found!" % isbn)
return json.jsonify(message), check(isbn)[2]
elif request.args.get('page'):
page = int(request.args.get('page'))
page_info = book.book_dict[isbn]['page']
if page > len(page_info):
message = dict(message="isbn:%d,page%d not found!" % (isbn,page))
return json.jsonify(message), check(isbn)[2]
else:
if page_info[page]:
has_next = True
else:
has_next = False
if page_info[page-2]:
has_prev = True
else:
has_prev = False
page_data = dict(page=page,
pages=len(page_info),
per_page=20,
has_next=has_next,
has_prev=has_prev,
total=len(page_info)*20
)
else:
page_data = book.book_dict[isbn]['page']

get_book = check(isbn)[2]
data_json = dict(isbn=isbn,data=get_book,page_data=page_data)
return json.jsonify(data_json), 200


elif request.method == 'PUT':
if not check(isbn)[1]:
message = dict(message="isbn:%d not found!" % isbn)
return json.jsonify(message), check(isbn)[2]
data = time.strftime('%Y-%m-%d %H:%M',time.localtime(time.time()))
put_book = book.book_dict[isbn]
data_temp = put_book['data']
put_book['data'] = data
data_json = dict(data=book.book_dict[isbn],change_data=dict(old_data=data_temp,new_data=data))
return json.jsonify(data_json), 200


elif request.method == 'DELETE':
if not check(isbn)[0]:
message = dict(message="not isbn:%d" % isbn)
return json.jsonify(message), 410

if not book.book_dict[isbn]:
message = dict(message="isbn:%d not found!" % isbn)
return json.jsonify(message), 404

book.book_dict[isbn]=None
return '', 204

else:
return 'methods not found'


if __name__ == '__main__':
app.run(debug=True)

如果返回的是要有url的话,如查看的是书的第二页,但书还有第三页,需要返回第三页的url,这个url可以使用url_for()来构造,在这个例子中的api是
api/v1/book/isbn/page
也就是调用api时能直接显示到书的对应页数的内容
用到的url_for()要引用

1
from flask import url_for

api_url调用的函数是这样

1
2
@app.route('/api/v1/book/<int:isbn>/<int:id>/', methods=['POST', 'GET','PUT','DELETE'])
def new_api(isbn):

构造URL的函数:
注意的是url_for中的’new_api’这样url_for()才知道要构建的是哪个URL

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
def make_url(page):
page = int(page)
page_info = book.book_dict[isbn]['page']
if page_info[page]:
url_next = url_for('new_api', page_info[page], _external=True)
else:
url_next = None
if page_info[page-2]:
url_prev = url_for('new_api', page_info[page-2], _external=True)
else:
url_prev = None
page_data = dict(page=page,
pages=len(page_info),
per_page=20,
has_next=url_next,
has_prev=url_prev,
total=len(page_info)*20
)
return page_data

当输入:api/v1/book/123/2/时,就会返回url

api/v1/book/123/1/
api/v1/book/123/3/

验证与限制

数据检验

数据校验,有些是在前端进行校验,但是如果是给其他用户使用(如开发者API)时,数据校验需要放在后端进行,如果提交的字段不符合时,则需要返回对应的错误。虽然数据检验是RESTful设计中的一个可选项,但它对API的安全、服务器的开销和交互的友好性而言都具有重要意义。常见的数据类型校验如下:

  • 数据类型校验,如规定的字段类型是int或者str
  • 数据格式校验,如字段需要满足相应的正则表达式
  • 数据逻辑校验,如数据包含出生日期和年龄两个字段,如果这两个字段的数据不一致,则数据校验失败

上面那段代码的check()函数就数据数据校验了

速度限制

为了避免请求泛滥,给API设置速度限制很重要。为此 RFC 6585 引入了HTTP状态码429(too many requests)。加入速度设置之后,应该提示用户,至于如何提示标准上没有说明,不过流行的方法是使用HTTP的返回头。
下面是几个必须的返回头(依照twitter的命名规则):

X-Rate-Limit-Limit :当前时间段允许的并发请求数
X-Rate-Limit-Remaining:当前时间段保留的请求数。
X-Rate-Limit-Reset:当前时间段剩余秒数
为什么使用当前时间段剩余秒数而不是时间戳?

时间戳保存的信息很多,但是也包含了很多不必要的信息,用户只需要知道还剩几秒就可以再发请求了这样也避免了clock skew问题。

在flask中有个拓展提供了类似的功能传送门

用户认证与权限机制

一般都使用OAuth认证授权。Oauth认证会生成一个令牌,可以授权用户或者第三方系统在一定的时间内获取特定的资源。目前使用的几乎是Oauth 2.0。由于Oauth比较安全,现在Oauth已成为RESTful设计中最常用的认证机制。
关于使用RESTFUL认证,这里有篇文章写得不错——传送门

其他

到了这里,就已经能写出一个初步成形RESTful规范的后台应用了,为了更加深入RESTful和减少开发时间,可以使用一些RESTful库。

python中web框架的RESTful库:

  • Django
  • django-rest-framework:一个强大灵活的工具,用来构建 web API。
  • django-tastypie:为Django 应用开发API。
  • django-formapi:为 Django 的表单验证,创建 JSON APIs 。
  • Flask
  • flask-api:为 flask 开发的,可浏览 Web APIs 。
  • flask-restful:为 flask 快速创建REST APIs 。
  • flask-restless:为 SQLAlchemy 定义的数据库模型创建 RESTful APIs 。
  • flask-api-utils:为 Flask 处理 API 表示和验证。
  • eve:REST API 框架,由 Flask, MongoDB 等驱动。
  • Pyramid
  • cornice:一个Pyramid 的 REST 框架 。
  • 与框架无关的
  • falcon:一个用来建立云 API 和 web app 后端的高性能框架。
  • sandman:为现存的数据库驱动系统自动创建 REST APIs 。
  • restless:框架无关的 REST 框架 ,基于从 Tastypie 学到的知识。
  • ripozo:快速创建 REST/HATEOAS/Hypermedia APIs。

HTTP状态码

HTTP定义了很多有意义的状态码,你可以在你的API中使用。这些状态码可以帮助API消费者用来路由它们获取到的响应内容。整理了一个你肯定会用到的状态码列表:

200 OK - 对成功的GET、PUT、PATCH或DELETE操作进行响应。也可以被用在不创建新资源的POST操作上
201 Created - 对创建新资源的POST操作进行响应。应该带着指向新资源地址的Location header)
204 No Content - 对不会返回响应体的成功请求进行响应(比如DELETE请求)
304 Not Modified - HTTP缓存header生效的时候用
400 Bad Request - 请求异常,比如请求中的body无法解析
401 Unauthorized - 没有进行认证或者认证非法。当API通过浏览器访问的时候,可以用来弹出一个认证对话框
403 Forbidden - 当认证成功,但是认证过的用户没有访问资源的权限
404 Not Found - 当一个不存在的资源被请求
405 Method Not Allowed - 所请求的HTTP方法不允许当前认证用户访问
410 Gone - 表示当前请求的资源不再可用。当调用老版本API的时候很有用
415 Unsupported Media Type - 如果请求中的内容类型是错误的
422 Unprocessable Entity - 用来表示校验错误
429 Too Many Requests - 由于请求频次达到上限而被拒绝访问

查看评论