本文將會使用python的Flask框架輕鬆實現一個RESTful的服務。html
Servers can provide executable code or scripts for clients to execute in their context. This constraint is the only one that is optional.(沒看明白)python
REST架構就是爲了HTTP協議設計的。RESTful web services的核心概念是管理資源。資源是由URIs來表示,客戶端使用HTTP當中的'POST, OPTIONS, GET, PUT, DELETE'等方法發送請求到服務器,改變相應的資源狀態。git
HTTP請求方法一般也十分合適去描述操做資源的動做:github
HTTP方法 | 動做 | 例子 |
GET | 獲取資源信息 | http://example.com/api/ordersweb (檢索訂單清單)數據庫 |
GET | 獲取資源信息 | http://example.com/api/orders/123json (檢索訂單 #123)flask |
POST | 建立一個次的資源 | http://example.com/api/orderswindows (使用帶數據的請求,建立一個新的訂單)api |
PUT | 更新一個資源 | http://example.com/api/orders/123 (使用帶數據的請求,更新#123訂單) |
DELETE | 刪除一個資源 | http://example.com/api/orders/123 刪除訂單#123 |
REST請求並不須要特定的數據格式,一般使用JSON做爲請求體,或者URL的查詢參數的一部份。
下面的任務將會練習設計以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] | 刪除一個任務 |
咱們定義任務清單有如下字段:
以上基本完成了設計部份,接下來咱們將會實現它!
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!
使用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 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" } ] }
這樣就調用了一個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 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產生了默認的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 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" }
接下來咱們實現 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 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命令行提示符,上面的命令會正確執行。若是是在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 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" } ] }
剩餘的兩個函數以下:
@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 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" } ] }
當前咱們還有一個問題,客戶端有可能須要從返回的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 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。
咱們已經完成了整個功能,可是咱們還有一個問題。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 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加密的通信方式,客戶端與服務器端傳輸認證信息都是加密過的,防止第三方的人去看到。
當使用瀏覽器去訪問這個接口,會彈出一個醜醜的登陸對話框,若是密碼錯誤就回返回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
只是看到教程寫得很詳細,試試拿來翻譯理解,未經做者贊成。