使用python的Flask实现一个RESTful API服务器端
使用python的Flask實現(xiàn)一個RESTful API服務(wù)器端[翻譯]
最近這些年,REST已經(jīng)成為web services和APIs的標準架構(gòu),很多APP的架構(gòu)基本上是使用RESTful的形式了。
本文將會使用python的Flask框架輕松實現(xiàn)一個RESTful的服務(wù)。
REST的六個特性:
- Client-Server:服務(wù)器端與客戶端分離。
- Stateless(無狀態(tài)):每次客戶端請求必需包含完整的信息,換句話說,每一次請求都是獨立的。
- Cacheable(可緩存):服務(wù)器端必需指定哪些請求是可以緩存的。
- Layered System(分層結(jié)構(gòu)):服務(wù)器端與客戶端通訊必需標準化,服務(wù)器的變更并不會影響客戶端。
- Uniform Interface(統(tǒng)一接口):客戶端與服務(wù)器端的通訊方法必需是統(tǒng)一的。
- Code on demand(按需執(zhí)行代碼?):服務(wù)器端可以在上下文中執(zhí)行代碼或者腳本?
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架構(gòu)就是為了HTTP協(xié)議設(shè)計的。RESTful web services的核心概念是管理資源。資源是由URIs來表示,客戶端使用HTTP當中的'POST, OPTIONS, GET, PUT, DELETE'等方法發(fā)送請求到服務(wù)器,改變相應(yīng)的資源狀態(tài)。
HTTP請求方法通常也十分合適去描述操作資源的動作:
| HTTP方法 | 動作 | 例子 |
| GET | 獲取資源信息 | http://example.com/api/orders (檢索訂單清單) |
| GET | 獲取資源信息 | http://example.com/api/orders/123 (檢索訂單 #123) |
| POST | 創(chuàng)建一個次的資源 | http://example.com/api/orders (使用帶數(shù)據(jù)的請求,創(chuàng)建一個新的訂單) |
| PUT | 更新一個資源 | http://example.com/api/orders/123 (使用帶數(shù)據(jù)的請求,更新#123訂單) |
| DELETE | 刪除一個資源 | http://example.com/api/orders/123 刪除訂單#123 |
REST請求并不需要特定的數(shù)據(jù)格式,通常使用JSON作為請求體,或者URL的查詢參數(shù)的一部份。
設(shè)計一個簡單的web service
下面的任務(wù)將會練習(xí)設(shè)計以REST準則為指引,通過不同的請求方法操作資源,標識資源的例子。
我們將寫一個To Do List 應(yīng)用,并且設(shè)計一個web service。第一步,規(guī)劃一個根URL,例如:
http://[hostname]/todo/api/v1.0/
上面的URL包括了應(yīng)用程序的名稱、API版本,這是十分有用的,既提供了命名空間的劃分,同時又與其它系統(tǒng)區(qū)分開來。版本號在升級新特性時十分有用,當一個新功能特性增加在新版本下面時,并不影響舊版本。
第二步,規(guī)劃資源的URL,這個例子十分簡單,只有任務(wù)清單。
規(guī)劃如下:
| HTTP方法 | URI | 動作 |
| GET | http://[hostname]/todo/api/v1.0/tasks | 檢索任務(wù)清單 |
| GET | http://[hostname]/todo/api/v1.0/tasks/[task_id] | 檢索一個任務(wù) |
| POST | http://[hostname]/todo/api/v1.0/tasks | 創(chuàng)建一個新任務(wù) |
| PUT | http://[hostname]/todo/api/v1.0/tasks/[task_id] | 更新一個已存在的任務(wù) |
| DELETE | http://[hostname]/todo/api/v1.0/tasks/[task_id] | 刪除一個任務(wù) |
我們定義任務(wù)清單有以下字段:
- id:唯一標識。整型。
- title:簡短的任務(wù)描述。字符串型。
- description:完整的任務(wù)描述。文本型。
- done:任務(wù)完成狀態(tài)。布爾值型。
以上基本完成了設(shè)計部份,接下來我們將會實現(xiàn)它!
?簡單了解Flask框架
Flask好簡單,但是又很強大的Python web 框架。這里有一系列教程Flask Mega-Tutorial series。(注:Django\Tornado\web.py感覺好多框:()
在我們深入實現(xiàn)web service之前,讓我們來簡單地看一個Flask web 應(yīng)用的結(jié)構(gòu)示例。
這里都是在Unix-like(Linux,Mac OS X)操作系統(tǒng)下面的演示,但是其它系統(tǒng)也可以跑,例如windows下的Cygwin。可能命令有些不同吧。(注:忽略Windows吧。)
先使用virtualenv安裝一個Flask的虛擬環(huán)境。如果沒有安裝virtualenv,開發(fā)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的開發(fā)環(huán)境,開始創(chuàng)建一個簡單的web應(yīng)用,在當前目錄里面創(chuàng)建一個app.py文件:
#!flask/bin/python from flask import Flaskapp = Flask(__name__)@app.route('/') def index():return "Hello, World!"if __name__ == '__main__':app.run(debug=True)去執(zhí)行app.py:
$ chmod a+x app.py $ ./app.py* Running on http://127.0.0.1:5000/* Restarting with reloader現(xiàn)在可以打開瀏覽器,輸入http://localhost:5000去看看這個Hello,World!
好吧,十分簡單吧。我們開始轉(zhuǎn)換到RESTful service!
使用Python 和 Flask實現(xiàn)RESTful services
使用Flask建立web services超級簡單。
當然,也有很多Flask extensions可以幫助建立RESTful services,但是這個例實在太簡單了,不需要使用任何擴展。
這個web service提供增加,刪除、修改任務(wù)清單,所以我們需要將任務(wù)清單存儲起來。最簡單的做法就是使用小型的數(shù)據(jù)庫,但是數(shù)據(jù)庫并不是本文涉及太多的。可以參考原文作者的完整教程。Flask Mega-Tutorial series
在這里例子我們將任務(wù)清單存儲在內(nèi)存中,這樣只能運行在單進程和單線程中,這樣是不適合作為生產(chǎn)服務(wù)器的,若非就必需使用數(shù)據(jù)庫了。
現(xiàn)在我們準備實現(xiàn)第一個web service的入口點:
#!flask/bin/python from flask import Flask, jsonifyapp = 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)正如您所見,并沒有改變太多代碼。我們將任務(wù)清單存儲在list內(nèi)(內(nèi)存),list存放兩個非常簡單的數(shù)組字典。每個實體就是我們上面定義的字段。
而 index 入口點有一個get_tasks函數(shù)與/todo/api/v1.0/tasks?URI關(guān)聯(lián),只接受http的GET方法。
這個響應(yīng)并非一般文本,是JSON格式的數(shù)據(jù),是經(jīng)過Flask框架的?jsonify模塊格式化過的數(shù)據(jù)。
使用瀏覽器去測試web service并不是一個好的辦法,因為要創(chuàng)建不同類弄的HTTP請求,事實上,我們將使用curl命令行。如果沒有安裝curl,快點去安裝一個。
像剛才一樣運行app.py。
打開一個終端運行以下命令:
$ curl -i http://localhost:5000/todo/api/v1.0/tasks HTTP/1.0 200 OK Content-Type: application/json Content-Length: 294 Server: Werkzeug/0.8.3 Python/2.7.3 Date: Mon, 20 May 2013 04:53:53 GMT{"tasks": [{"description": "Milk, Cheese, Pizza, Fruit, Tylenol","done": false,"id": 1,"title": "Buy groceries"},{"description": "Need to find a good Python tutorial on the web","done": false,"id": 2,"title": "Learn Python"}] }這樣就調(diào)用了一個RESTful service方法!
現(xiàn)在,我們寫第二個版本的GET方法獲取特定的任務(wù)。獲取單個任務(wù):
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]})?第二個函數(shù)稍稍復(fù)雜了一些。任務(wù)的id包含在URL內(nèi),Flask將task_id參數(shù)傳入了函數(shù)內(nèi)。
通過參數(shù),檢索tasks數(shù)組。如果參數(shù)傳過來的id不存在于數(shù)組內(nèi),我們需要返回錯誤代碼404,按照HTTP的規(guī)定,404意味著是"Resource Not Found",資源未找到。
如果找到任務(wù)在內(nèi)存數(shù)組內(nèi),我們通過jsonify模塊將字典打包成JSON格式,并發(fā)送響應(yīng)到客戶端上。就像處理一個實體字典一樣。
試試使用curl調(diào)用:
$ curl -i http://localhost:5000/todo/api/v1.0/tasks/2 HTTP/1.0 200 OK Content-Type: application/json Content-Length: 151 Server: Werkzeug/0.8.3 Python/2.7.3 Date: Mon, 20 May 2013 05:21:50 GMT{"task": {"description": "Need to find a good Python tutorial on the web","done": false,"id": 2,"title": "Learn Python"} } $ curl -i http://localhost:5000/todo/api/v1.0/tasks/3 HTTP/1.0 404 NOT FOUND Content-Type: text/html Content-Length: 238 Server: Werkzeug/0.8.3 Python/2.7.3 Date: Mon, 20 May 2013 05:21:52 GMT<!DOCTYPE HTML PUBLIC "-//W3C//DTD HTML 3.2 Final//EN"> <title>404 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產(chǎn)生了默認的404響應(yīng)。客戶端需要收到的都是JSON的響應(yīng),因此我們需要改進404錯誤處理:
from flask import make_response@app.errorhandler(404) def not_found(error):return make_response(jsonify({'error': 'Not found'}), 404)這樣我們就得到了友好的API錯誤響應(yīng):
$ curl -i http://localhost:5000/todo/api/v1.0/tasks/3 HTTP/1.0 404 NOT FOUND Content-Type: application/json Content-Length: 26 Server: Werkzeug/0.8.3 Python/2.7.3 Date: Mon, 20 May 2013 05:36:54 GMT{"error": "Not found" }接下來我們實現(xiàn)?POST?方法,插入一個新的任務(wù)到數(shù)組中:
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里面包含請求數(shù)據(jù),如果不是JSON或者里面沒有包括title字段,將會返回400的錯誤代碼。
當創(chuàng)建一個新的任務(wù)字典,使用最后一個任務(wù)id數(shù)值加1作為新的任務(wù)id(最簡單的方法產(chǎn)生一個唯一字段)。這里允許不帶description字段,默認將done字段值為False。
將新任務(wù)附加到tasks數(shù)組里面,并且返回客戶端201狀態(tài)碼和剛剛添加的任務(wù)內(nèi)容。HTTP定義了201狀態(tài)碼為“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 201 Created Content-Type: application/json Content-Length: 104 Server: Werkzeug/0.8.3 Python/2.7.3 Date: Mon, 20 May 2013 05:56:21 GMT{"task": {"description": "","done": false,"id": 3,"title": "Read a book"} }注意:如果使用原生版本的curl命令行提示符,上面的命令會正確執(zhí)行。如果是在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部份在內(nèi),而且需要三個雙引號轉(zhuǎn)義序列。
完成上面的事情,就可以看到更新之后的list數(shù)組內(nèi)容:
$ curl -i http://localhost:5000/todo/api/v1.0/tasks HTTP/1.0 200 OK Content-Type: application/json Content-Length: 423 Server: Werkzeug/0.8.3 Python/2.7.3 Date: Mon, 20 May 2013 05:57:44 GMT{"tasks": [{"description": "Milk, Cheese, Pizza, Fruit, Tylenol","done": false,"id": 1,"title": "Buy groceries"},{"description": "Need to find a good Python tutorial on the web","done": false,"id": 2,"title": "Learn Python"},{"description": "","done": false,"id": 3,"title": "Read a book"}] }剩余的兩個函數(shù)如下:
@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函數(shù)沒什么太特別的。update_task函數(shù)需要檢查所輸入的參數(shù),防止產(chǎn)生錯誤的bug。確保是預(yù)期的JSON格式寫入數(shù)據(jù)庫里面。
測試將任務(wù)#2的done字段變更為done狀態(tài):
$ curl -i -H "Content-Type: application/json" -X PUT -d '{"done":true}' http://localhost:5000/todo/api/v1.0/tasks/2 HTTP/1.0 200 OK Content-Type: application/json Content-Length: 170 Server: Werkzeug/0.8.3 Python/2.7.3 Date: Mon, 20 May 2013 07:10:16 GMT{"task": [{"description": "Need to find a good Python tutorial on the web","done": true,"id": 2,"title": "Learn Python"}] }改進Web Service接口
當前我們還有一個問題,客戶端有可能需要從返回的JSON中重新構(gòu)造URI,如果將來加入新的特性時,可能需要修改客戶端。(例如新增版本。)
我們可以返回整個URI的路徑給客戶端,而不是任務(wù)的id。為了這個功能,創(chuàng)建一個小函數(shù)生成一個“public”版本的任務(wù)URI返回:
from flask import url_fordef 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模塊,獲取任務(wù)時,將任務(wù)中的id字段替換成uri字段,并且把值改為uri值。
當我們返回包含任務(wù)的list時,通過這個函數(shù)處理后,返回完整的uri給客戶端:
@app.route('/todo/api/v1.0/tasks', methods=['GET']) def get_tasks():return jsonify({'tasks': map(make_public_task, tasks)})現(xiàn)在看到的檢索結(jié)果:
$ curl -i http://localhost:5000/todo/api/v1.0/tasks HTTP/1.0 200 OK Content-Type: application/json Content-Length: 406 Server: Werkzeug/0.8.3 Python/2.7.3 Date: Mon, 20 May 2013 18:16:28 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的安全認證
我們已經(jīng)完成了整個功能,但是我們還有一個問題。web service任何人都可以訪問的,這不是一個好主意。
當前service是所有客戶端都可以連接的,如果有別人知道了這個API就可以寫個客戶端隨意修改數(shù)據(jù)了。 大多數(shù)教程沒有與安全相關(guān)的內(nèi)容,這是個十分嚴重的問題。
最簡單的辦法是在web service中,只允許用戶名和密碼驗證通過的客戶端連接。在一個常規(guī)的web應(yīng)用中,應(yīng)該有登錄表單提交去認證,同時服務(wù)器會創(chuàng)建一個會話過程去進行通訊。這個會話過程id會被存儲在客戶端的cookie里面。不過這樣就違返了我們REST中無狀態(tài)的規(guī)則,因此,我們需求客戶端每次都將他們的認證信息發(fā)送到服務(wù)器。
?為此我們有兩種方法表單認證方法去做,分別是 Basic 和 Digest。
這里有有個小Flask extension可以輕松做到。首先需要安裝?Flask-HTTPAuth?:
$ flask/bin/pip install flask-httpauth假設(shè)web service只有用戶?ok?和密碼為?python?的用戶接入。下面就設(shè)置了一個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函數(shù)是一個回調(diào)函數(shù),獲取一個已知用戶的密碼。在復(fù)雜的系統(tǒng)中,函數(shù)是需要到數(shù)據(jù)庫中檢查的,但是這里只是一個小示例。
當發(fā)生認證錯誤之后,error_handler回調(diào)函數(shù)會發(fā)送錯誤的代碼給客戶端。這里我們自定義一個錯誤代碼401,返回JSON數(shù)據(jù),而不是HTML。
將@auth.login_required裝飾器添加到需要驗證的函數(shù)上面:
@app.route('/todo/api/v1.0/tasks', methods=['GET']) @auth.login_required def get_tasks():return jsonify({'tasks': tasks})現(xiàn)在,試試使用curl調(diào)用這個函數(shù):
$ curl -i http://localhost:5000/todo/api/v1.0/tasks HTTP/1.0 401 UNAUTHORIZED Content-Type: application/json Content-Length: 36 WWW-Authenticate: Basic realm="Authentication Required" Server: Werkzeug/0.8.3 Python/2.7.3 Date: Mon, 20 May 2013 06:41:14 GMT{"error": "Unauthorized access" }這里表示了沒通過驗證,下面是帶用戶名與密碼的驗證:
$ curl -u ok:python -i http://localhost:5000/todo/api/v1.0/tasks HTTP/1.0 200 OK Content-Type: application/json Content-Length: 316 Server: Werkzeug/0.8.3 Python/2.7.3 Date: Mon, 20 May 2013 06:46:45 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加密的通訊方式,客戶端與服務(wù)器端傳輸認證信息都是加密過的,防止第三方的人去看到。
當使用瀏覽器去訪問這個接口,會彈出一個丑丑的登錄對話框,如果密碼錯誤就回返回401的錯誤代碼。為了防止瀏覽器彈出驗證對話框,客戶端應(yīng)該處理好這個登錄請求。
有一個小技巧可以避免這個問題,就是修改返回的錯誤代碼401。例如修改成403(”Forbidden“)就不會彈出驗證對話框了。
@auth.error_handler def unauthorized():return make_response(jsonify({'error': 'Unauthorized access'}), 403)當然,同時也需要客戶端知道這個403錯誤的意義。
最后
還有很多辦法去改進這個web service。
事實上,一個真正的web service應(yīng)該使用真正的數(shù)據(jù)庫。使用內(nèi)存數(shù)據(jù)結(jié)構(gòu)有非常多的限制,不要用在實際應(yīng)用上面。
另外一方面,處理多用戶。如果系統(tǒng)支持多用戶認證,則任務(wù)清單也是對應(yīng)多用戶的。同時我們需要有第二種資源,用戶資源。當用戶注冊時使用POST請求。使用GET返回用戶信息到客戶端。使用PUT請求更新用戶資料,或者郵件地址。使用DELETE刪除用戶賬號等。
通過GET請求檢索任務(wù)清單時,有很多辦法可以進擴展。第一,可以添加分頁參數(shù),使客戶端只請求一部份數(shù)據(jù)。第二,可以添加篩選關(guān)鍵字等。所有這些元素可以添加到URL上面的參數(shù)。
原文來自:http://blog.miguelgrinberg.com/post/designing-a-restful-api-with-python-and-flask
?
轉(zhuǎn)載于:https://www.cnblogs.com/sea520/p/9993254.html
總結(jié)
以上是生活随笔為你收集整理的使用python的Flask实现一个RESTful API服务器端的全部內(nèi)容,希望文章能夠幫你解決所遇到的問題。
- 上一篇: 【.NET Core项目实战-统一认证平
- 下一篇: 细说Mammut大数据系统测试环境Doc