使用python的Flask实现一个RESTful API服务器端[翻译]
最近这些年,REST已经成为web services和APIs的标准架构,很多APP的架构基本上是使用RESTful的形式了。
本文将会使用python的Flask框架轻松实现一个RESTful的服务。
REST的六个特性:
- Client-Server:服务器端与客户端分离。
- Stateless(无状态):每次客户端请求必需包含完整的信息,换句话说,每一次请求都是独立的。
- Cacheable(可缓存):服务器端必需指定哪些请求是可以缓存的。
- Layered System(分层结构):服务器端与客户端通讯必需标准化,服务器的变更并不会影响客户端。
- Uniform Interface(统一接口):客户端与服务器端的通讯方法必需是统一的。
- Code on demand(按需执行代码?):服务器端可以在上下文中执行代码或者脚本?
Servers can provide executable code or scripts for clients to execute in their context. This constraint is the only one that is optional.(没看明白)
RESTful web service的样子
REST架构就是为了HTTP协议设计的。RESTful web services的核心概念是管理资源。资源是由URIs来表示,客户端使用HTTP当中的'POST, OPTIONS, GET, PUT, DELETE'等方法发送请求到服务器,改变相应的资源状态。
HTTP请求方法通常也十分合适去描述操作资源的动作:
HTTP方法 | 动作 | 例子 |
GET | 获取资源信息 |
http://example.com/api/orders (检索订单清单) |
GET | 获取资源信息 |
http://example.com/api/orders/123 (检索订单 #123) |
POST | 创建一个次的资源 |
http://example.com/api/orders (使用带数据的请求,创建一个新的订单) |
PUT | 更新一个资源 |
http://example.com/api/orders/123 (使用带数据的请求,更新#123订单) |
DELETE | 删除一个资源 |
http://example.com/api/orders/123 删除订单#123 |
REST请求并不需要特定的数据格式,通常使用JSON作为请求体,或者URL的查询参数的一部份。
设计一个简单的web service
下面的任务将会练习设计以REST准则为指引,通过不同的请求方法操作资源,标识资源的例子。
我们将写一个To Do List 应用,并且设计一个web service。第一步,规划一个根URL,例如:
http://[hostname]/todo/api/v1.0/
上面的URL包括了应用程序的名称、API版本,这是十分有用的,既提供了命名空间的划分,同时又与其它系统区分开来。版本号在升级新特性时十分有用,当一个新功能特性增加在新版本下面时,并不影响旧版本。
第二步,规划资源的URL,这个例子十分简单,只有任务清单。
规划如下:
HTTP方法 | URI | 动作 |
GET | http://[hostname]/todo/api/v1.0/tasks | 检索任务清单 |
GET | http://[hostname]/todo/api/v1.0/tasks/[task_id] | 检索一个任务 |
POST | http://[hostname]/todo/api/v1.0/tasks | 创建一个新任务 |
PUT | http://[hostname]/todo/api/v1.0/tasks/[task_id] | 更新一个已存在的任务 |
DELETE | http://[hostname]/todo/api/v1.0/tasks/[task_id] | 删除一个任务 |
我们定义任务清单有以下字段:
- id:唯一标识。整型。
- title:简短的任务描述。字符串型。
- description:完整的任务描述。文本型。
- done:任务完成状态。布尔值型。
以上基本完成了设计部份,接下来我们将会实现它!
简单了解Flask框架
Flask好简单,但是又很强大的Python web 框架。这里有一系列教程Flask Mega-Tutorial series。(注:Django\Tornado\web.py感觉好多框:()
在我们深入实现web service之前,让我们来简单地看一个Flask web 应用的结构示例。
这里都是在Unix-like(Linux,Mac OS X)操作系统下面的演示,但是其它系统也可以跑,例如windows下的Cygwin。可能命令有些不同吧。(注:忽略Windows吧。)
先使用virtualenv安装一个Flask的虚拟环境。如果没有安装virtualenv,开发python必备,最好去下载安装。https://pypi.python.org/pypi/virtualenv
$ mkdir todo-api
$ cd todo-api
$ virtualenv flask
New python executable in flask/bin/python
Installing setuptools............................done.
Installing pip...................done.
$ flask/bin/pip install flask
这样做好了一个Flask的开发环境,开始创建一个简单的web应用,在当前目录里面创建一个app.py文件:
#!flask/bin/python
from flask import Flask app = Flask(__name__) @app.route('/')
def index():
return "Hello, World!" if __name__ == '__main__':
app.run(debug=True)
去执行app.py:
$ chmod a+x app.py
$ ./app.py
* Running on http://127.0.0.1:5000/
* Restarting with reloader
现在可以打开浏览器,输入http://localhost:5000去看看这个Hello,World!
好吧,十分简单吧。我们开始转换到RESTful service!
使用Python 和 Flask实现RESTful services
使用Flask建立web services超级简单。
当然,也有很多Flask extensions可以帮助建立RESTful services,但是这个例实在太简单了,不需要使用任何扩展。
这个web service提供增加,删除、修改任务清单,所以我们需要将任务清单存储起来。最简单的做法就是使用小型的数据库,但是数据库并不是本文涉及太多的。可以参考原文作者的完整教程。Flask Mega-Tutorial series
在这里例子我们将任务清单存储在内存中,这样只能运行在单进程和单线程中,这样是不适合作为生产服务器的,若非就必需使用数据库了。
现在我们准备实现第一个web service的入口点:
#!flask/bin/python
from flask import Flask, jsonify app = Flask(__name__) tasks = [
{
'id': 1,
'title': u'Buy groceries',
'description': u'Milk, Cheese, Pizza, Fruit, Tylenol',
'done': False
},
{
'id': 2,
'title': u'Learn Python',
'description': u'Need to find a good Python tutorial on the web',
'done': False
}
] @app.route('/todo/api/v1.0/tasks', methods=['GET'])
def get_tasks():
return jsonify({'tasks': tasks}) if __name__ == '__main__':
app.run(debug=True)
正如您所见,并没有改变太多代码。我们将任务清单存储在list内(内存),list存放两个非常简单的数组字典。每个实体就是我们上面定义的字段。
而 index 入口点有一个get_tasks函数与/todo/api/v1.0/tasks URI关联,只接受http的GET方法。
这个响应并非一般文本,是JSON格式的数据,是经过Flask框架的 jsonify模块格式化过的数据。
使用浏览器去测试web service并不是一个好的办法,因为要创建不同类弄的HTTP请求,事实上,我们将使用curl命令行。如果没有安装curl,快点去安装一个。
像刚才一样运行app.py。
打开一个终端运行以下命令:
$ curl -i http://localhost:5000/todo/api/v1.0/tasks
HTTP/1.0 OK
Content-Type: application/json
Content-Length:
Server: Werkzeug/0.8. Python/2.7.
Date: Mon, May :: GMT {
"tasks": [
{
"description": "Milk, Cheese, Pizza, Fruit, Tylenol",
"done": false,
"id": ,
"title": "Buy groceries"
},
{
"description": "Need to find a good Python tutorial on the web",
"done": false,
"id": ,
"title": "Learn Python"
}
]
}
这样就调用了一个RESTful service方法!
现在,我们写第二个版本的GET方法获取特定的任务。获取单个任务:
from flask import abort @app.route('/todo/api/v1.0/tasks/<int:task_id>', methods=['GET'])
def get_task(task_id):
task = filter(lambda t: t['id'] == task_id, tasks)
if len(task) == 0:
abort(404)
return jsonify({'task': task[0]})
第二个函数稍稍复杂了一些。任务的id包含在URL内,Flask将task_id参数传入了函数内。
通过参数,检索tasks数组。如果参数传过来的id不存在于数组内,我们需要返回错误代码404,按照HTTP的规定,404意味着是"Resource Not Found",资源未找到。
如果找到任务在内存数组内,我们通过jsonify模块将字典打包成JSON格式,并发送响应到客户端上。就像处理一个实体字典一样。
试试使用curl调用:
$ curl -i http://localhost:5000/todo/api/v1.0/tasks/2
HTTP/1.0 OK
Content-Type: application/json
Content-Length:
Server: Werkzeug/0.8. Python/2.7.
Date: Mon, May :: GMT {
"task": {
"description": "Need to find a good Python tutorial on the web",
"done": false,
"id": ,
"title": "Learn Python"
}
}
$ curl -i http://localhost:5000/todo/api/v1.0/tasks/3
HTTP/1.0 NOT FOUND
Content-Type: text/html
Content-Length:
Server: Werkzeug/0.8. Python/2.7.
Date: Mon, May :: GMT <!DOCTYPE HTML PUBLIC "-//W3C//DTD HTML 3.2 Final//EN">
<title> Not Found</title>
<h1>Not Found</h1>
<p>The requested URL was not found on the server.</p><p>If you entered the URL manually please check your spelling and try again.</p>
当我们请求#2 id的资源时,可以获取,但是当我们请求#3的资源时返回了404错误。并且返回了一段奇怪的HTML错误,而不是我们期望的JSON,这是因为Flask产生了默认的404响应。客户端需要收到的都是JSON的响应,因此我们需要改进404错误处理:
from flask import make_response @app.errorhandler(404)
def not_found(error):
return make_response(jsonify({'error': 'Not found'}), 404)
这样我们就得到了友好的API错误响应:
$ curl -i http://localhost:5000/todo/api/v1.0/tasks/3
HTTP/1.0 NOT FOUND
Content-Type: application/json
Content-Length:
Server: Werkzeug/0.8. Python/2.7.
Date: Mon, May :: GMT {
"error": "Not found"
}
接下来我们实现 POST 方法,插入一个新的任务到数组中:
from flask import request @app.route('/todo/api/v1.0/tasks', methods=['POST'])
def create_task():
if not request.json or not 'title' in request.json:
abort(400)
task = {
'id': tasks[-1]['id'] + 1,
'title': request.json['title'],
'description': request.json.get('description', ""),
'done': False
}
tasks.append(task)
return jsonify({'task': task}), 201
request.json里面包含请求数据,如果不是JSON或者里面没有包括title字段,将会返回400的错误代码。
当创建一个新的任务字典,使用最后一个任务id数值加1作为新的任务id(最简单的方法产生一个唯一字段)。这里允许不带description字段,默认将done字段值为False。
将新任务附加到tasks数组里面,并且返回客户端201状态码和刚刚添加的任务内容。HTTP定义了201状态码为“Created”。
测试上面的新功能:
$ curl -i -H "Content-Type: application/json" -X POST -d '{"title":"Read a book"}' http://localhost:5000/todo/api/v1.0/tasks
HTTP/1.0 Created
Content-Type: application/json
Content-Length:
Server: Werkzeug/0.8. Python/2.7.
Date: Mon, May :: GMT {
"task": {
"description": "",
"done": false,
"id": ,
"title": "Read a book"
}
}
注意:如果使用原生版本的curl命令行提示符,上面的命令会正确执行。如果是在Windows下使用Cygwin bash版本的curl,需要将body部份添加双引号:
curl -i -H "Content-Type: application/json" -X POST -d "{"""title""":"""Read a book"""}" http://localhost:5000/todo/api/v1.0/tasks
基本上在Windows中需要使用双引号包括body部份在内,而且需要三个双引号转义序列。
完成上面的事情,就可以看到更新之后的list数组内容:
$ curl -i http://localhost:5000/todo/api/v1.0/tasks
HTTP/1.0 OK
Content-Type: application/json
Content-Length:
Server: Werkzeug/0.8. Python/2.7.
Date: Mon, May :: GMT {
"tasks": [
{
"description": "Milk, Cheese, Pizza, Fruit, Tylenol",
"done": false,
"id": ,
"title": "Buy groceries"
},
{
"description": "Need to find a good Python tutorial on the web",
"done": false,
"id": ,
"title": "Learn Python"
},
{
"description": "",
"done": false,
"id": ,
"title": "Read a book"
}
]
}
剩余的两个函数如下:
@app.route('/todo/api/v1.0/tasks/<int:task_id>', methods=['PUT'])
def update_task(task_id):
task = filter(lambda t: t['id'] == task_id, tasks)
if len(task) == 0:
abort(404)
if not request.json:
abort(400)
if 'title' in request.json and type(request.json['title']) != unicode:
abort(400)
if 'description' in request.json and type(request.json['description']) is not unicode:
abort(400)
if 'done' in request.json and type(request.json['done']) is not bool:
abort(400)
task[0]['title'] = request.json.get('title', task[0]['title'])
task[0]['description'] = request.json.get('description', task[0]['description'])
task[0]['done'] = request.json.get('done', task[0]['done'])
return jsonify({'task': task[0]}) @app.route('/todo/api/v1.0/tasks/<int:task_id>', methods=['DELETE'])
def delete_task(task_id):
task = filter(lambda t: t['id'] == task_id, tasks)
if len(task) == 0:
abort(404)
tasks.remove(task[0])
return jsonify({'result': True})
delete_task函数没什么太特别的。update_task函数需要检查所输入的参数,防止产生错误的bug。确保是预期的JSON格式写入数据库里面。
测试将任务#2的done字段变更为done状态:
$ curl -i -H "Content-Type: application/json" -X PUT -d '{"done":true}' http://localhost:5000/todo/api/v1.0/tasks/2
HTTP/1.0 OK
Content-Type: application/json
Content-Length:
Server: Werkzeug/0.8. Python/2.7.
Date: Mon, May :: GMT {
"task": [
{
"description": "Need to find a good Python tutorial on the web",
"done": true,
"id": ,
"title": "Learn Python"
}
]
}
改进Web Service接口
当前我们还有一个问题,客户端有可能需要从返回的JSON中重新构造URI,如果将来加入新的特性时,可能需要修改客户端。(例如新增版本。)
我们可以返回整个URI的路径给客户端,而不是任务的id。为了这个功能,创建一个小函数生成一个“public”版本的任务URI返回:
from flask import url_for def make_public_task(task):
new_task = {}
for field in task:
if field == 'id':
new_task['uri'] = url_for('get_task', task_id=task['id'], _external=True)
else:
new_task[field] = task[field]
return new_task
通过Flask的url_for模块,获取任务时,将任务中的id字段替换成uri字段,并且把值改为uri值。
当我们返回包含任务的list时,通过这个函数处理后,返回完整的uri给客户端:
@app.route('/todo/api/v1.0/tasks', methods=['GET'])
def get_tasks():
return jsonify({'tasks': map(make_public_task, tasks)})
现在看到的检索结果:
$ curl -i http://localhost:5000/todo/api/v1.0/tasks
HTTP/1.0 OK
Content-Type: application/json
Content-Length:
Server: Werkzeug/0.8. Python/2.7.
Date: Mon, May :: GMT {
"tasks": [
{
"title": "Buy groceries",
"done": false,
"description": "Milk, Cheese, Pizza, Fruit, Tylenol",
"uri": "http://localhost:5000/todo/api/v1.0/tasks/1"
},
{
"title": "Learn Python",
"done": false,
"description": "Need to find a good Python tutorial on the web",
"uri": "http://localhost:5000/todo/api/v1.0/tasks/2"
}
]
}
这种办法避免了与其它功能的兼容,拿到的是完整uri而不是一个id。
RESTful web service的安全认证
我们已经完成了整个功能,但是我们还有一个问题。web service任何人都可以访问的,这不是一个好主意。
当前service是所有客户端都可以连接的,如果有别人知道了这个API就可以写个客户端随意修改数据了。 大多数教程没有与安全相关的内容,这是个十分严重的问题。
最简单的办法是在web service中,只允许用户名和密码验证通过的客户端连接。在一个常规的web应用中,应该有登录表单提交去认证,同时服务器会创建一个会话过程去进行通讯。这个会话过程id会被存储在客户端的cookie里面。不过这样就违返了我们REST中无状态的规则,因此,我们需求客户端每次都将他们的认证信息发送到服务器。
为此我们有两种方法表单认证方法去做,分别是 Basic 和 Digest。
这里有有个小Flask extension可以轻松做到。首先需要安装 Flask-HTTPAuth :
$ flask/bin/pip install flask-httpauth
假设web service只有用户 ok 和密码为 python 的用户接入。下面就设置了一个Basic HTTP认证:
from flask.ext.httpauth import HTTPBasicAuth
auth = HTTPBasicAuth() @auth.get_password
def get_password(username):
if username == 'ok':
return 'python'
return None @auth.error_handler
def unauthorized():
return make_response(jsonify({'error': 'Unauthorized access'}), 401)
get_password函数是一个回调函数,获取一个已知用户的密码。在复杂的系统中,函数是需要到数据库中检查的,但是这里只是一个小示例。
当发生认证错误之后,error_handler回调函数会发送错误的代码给客户端。这里我们自定义一个错误代码401,返回JSON数据,而不是HTML。
将@auth.login_required装饰器添加到需要验证的函数上面:
@app.route('/todo/api/v1.0/tasks', methods=['GET'])
@auth.login_required
def get_tasks():
return jsonify({'tasks': tasks})
现在,试试使用curl调用这个函数:
$ curl -i http://localhost:5000/todo/api/v1.0/tasks
HTTP/1.0 UNAUTHORIZED
Content-Type: application/json
Content-Length:
WWW-Authenticate: Basic realm="Authentication Required"
Server: Werkzeug/0.8. Python/2.7.
Date: Mon, May :: GMT {
"error": "Unauthorized access"
}
这里表示了没通过验证,下面是带用户名与密码的验证:
$ curl -u ok:python -i http://localhost:5000/todo/api/v1.0/tasks
HTTP/1.0 OK
Content-Type: application/json
Content-Length:
Server: Werkzeug/0.8. Python/2.7.
Date: Mon, May :: GMT {
"tasks": [
{
"title": "Buy groceries",
"done": false,
"description": "Milk, Cheese, Pizza, Fruit, Tylenol",
"uri": "http://localhost:5000/todo/api/v1.0/tasks/1"
},
{
"title": "Learn Python",
"done": false,
"description": "Need to find a good Python tutorial on the web",
"uri": "http://localhost:5000/todo/api/v1.0/tasks/2"
}
]
}
这个认证extension十分灵活,可以随指定需要验证的APIs。
为了确保登录信息的安全,最好的办法还是使用https加密的通讯方式,客户端与服务器端传输认证信息都是加密过的,防止第三方的人去看到。
当使用浏览器去访问这个接口,会弹出一个丑丑的登录对话框,如果密码错误就回返回401的错误代码。为了防止浏览器弹出验证对话框,客户端应该处理好这个登录请求。
有一个小技巧可以避免这个问题,就是修改返回的错误代码401。例如修改成403(”Forbidden“)就不会弹出验证对话框了。
@auth.error_handler
def unauthorized():
return make_response(jsonify({'error': 'Unauthorized access'}), 403)
当然,同时也需要客户端知道这个403错误的意义。
最后
还有很多办法去改进这个web service。
事实上,一个真正的web service应该使用真正的数据库。使用内存数据结构有非常多的限制,不要用在实际应用上面。
另外一方面,处理多用户。如果系统支持多用户认证,则任务清单也是对应多用户的。同时我们需要有第二种资源,用户资源。当用户注册时使用POST请求。使用GET返回用户信息到客户端。使用PUT请求更新用户资料,或者邮件地址。使用DELETE删除用户账号等。
通过GET请求检索任务清单时,有很多办法可以进扩展。第一,可以添加分页参数,使客户端只请求一部份数据。第二,可以添加筛选关键字等。所有这些元素可以添加到URL上面的参数。
原文来自:http://blog.miguelgrinberg.com/post/designing-a-restful-api-with-python-and-flask
只是看到教程写得很详细,试试拿来翻译理解,未经作者同意。
使用python的Flask实现一个RESTful API服务器端[翻译]的更多相关文章
- 使用python的Flask实现一个RESTful API服务器端
使用python的Flask实现一个RESTful API服务器端 最近这些年,REST已经成为web services和APIs的标准架构,很多APP的架构基本上是使用RESTful的形式了. 本文 ...
- 转:使用python的Flask实现一个RESTful API服务器端
提示:可以学习一下flask框架中对于密码进行校验的部分.封装了太多操作. 最近这些年,REST已经成为web services和APIs的标准架构,很多APP的架构基本上是使用RESTful的形式了 ...
- Python的Flask框架开发RESTful API
web框架选择 Django,流行但是笨重,还麻烦,人生苦短,肯定不选 web.py,轻量,但据说作者仙逝无人维护,好吧,先pass tornado,据说倡导自己造轮子,虽然是facebook开源的吧 ...
- 使用Flask设计带认证token的RESTful API接口[翻译]
上一篇文章, 使用python的Flask实现一个RESTful API服务器端 简单地演示了Flask实的现的api服务器,里面提到了因为无状态的原则,没有session cookies,如果访问 ...
- Flask设计带认证token的RESTful API接口[翻译]
上一篇文章, 使用python的Flask实现一个RESTful API服务器端 简单地演示了Flask实的现的api服务器,里面提到了因为无状态的原则,没有session cookies,如果访问 ...
- Flask 学习篇一: 搭建Python虚拟环境,安装flask,并设计RESTful API。
前些日子,老师给我看了这本书,于是便开始了Flask的学习 GitHub上的大神,于是我也在GitHub上建了一个Flask的项目. 有兴趣可以看看: https://github.com/Silen ...
- 一个Restful Api的访问控制方法
最近在做的两个项目,都需要使用Restful Api,接口的安全性和访问控制便成为一个问题,看了一下别家的API访问控制办法. 新浪的API访问控制使用的是AccessToken,有两种方式来使用该A ...
- 实现一个 RESTful API 服务器
RESTful 是目前最为流行的一种互联网软件结构.因为它结构清晰.符合标准.易于理解.扩展方便,所以正得到越来越多网站的采用. 什么是 REST REST(REpresentational Stat ...
- 转:一个Restful Api的访问控制方法(简单版)
最近在做的两个项目,都需要使用Restful Api,接口的安全性和访问控制便成为一个问题,看了一下别家的API访问控制办法. 新浪的API访问控制使用的是AccessToken,有两种方式来使用该A ...
随机推荐
- js自动提示查询添加功能(不是自动补全)
在工作中遇到查询某些数据,并添加到一个列表里的时候,写了一个小功能. 优点: 1.纯手工JS代码,不需要任何js框架,复制下来就能测试,无毒副作用. 2.通过模糊查询快速定位数据,并添加到列表里. 缺 ...
- WCF学习之旅—TCP双工模式(二十一)
WCF学习之旅—请求与答复模式和单向模式(十九) WCF学习之旅—HTTP双工模式(二十) 五.TCP双工模式 上一篇文章中我们学习了HTTP的双工模式,我们今天就学习一下TCP的双工模式. 在一个基 ...
- .NetCore之EF跳过的坑
我在网上看到很多.netCore的信息,就动手自己写一个例子测试哈,但是想不到其中这么多坑: 1.首先.netCore和EF的安装就不用多说了,网上有很多的讲解可以跟着一步一步的下载和安装,但是需要注 ...
- 常用的Webpack配置
官方文档: http://webpack.github.io/docs/ 1. 安装python2. 安装node.js msi3. npm自动打包在最新的node.js安装包里 被封的包用国内镜像下 ...
- Hadoop Shell命令大全
hadoop支持命令行操作HDFS文件系统,并且支持shell-like命令与HDFS文件系统交互,对于大多数程序猿/媛来说,shell-like命令行操作都是比较熟悉的,其实这也是Hadoop的极大 ...
- 【CSS进阶】CSS 颜色体系详解
说到 CSS 颜色,相比大家都不会陌生,本文是我个人对 CSS 颜色体系的一个系统总结与学习,分享给大家. 先用一张图直观的感受一下与 CSS 颜色相关大概覆盖了哪些内容. 接下来的行文内容大概会按照 ...
- Vertica环境安装R-Lang包提示缺少libgfortran.so.1
环境:RHEL 6.4 + Vertica 7.0.0-11.最终确认安装compat-libgfortran-41-4.1.2-39.el6.x86_64.rpm即可解决. # rpm -ivh v ...
- 【知识积累】try-catch-finally+return总结
一.前言 对于找Java相关工作的读者而言,在笔试中肯定免不了遇到try-catch-finally + return的题型,需要面试这清楚返回值,这也是这篇博文产生的由来.本文将从字节码层面来解释为 ...
- 自己动手之使用反射和泛型,动态读取XML创建类实例并赋值
前言: 最近小匹夫参与的游戏项目到了需要读取数据的阶段了,那么觉得自己业余时间也该实践下数据相关的内容.那么从哪入手呢?因为用的是Unity3d的游戏引擎,思来想去就选择了C#读取XML文件这个小功能 ...
- c#编程基础之ref、out参数
引例: 先看这个源码,函数传递后由于传递的是副本所以真正的值并没有改变. 源码如下: using System; using System.Collections.Generic; using Sys ...