使用Flask創建REST Web服務很簡單,使用熟悉的route()
裝飾器及其methods
可選參數可以聲明服務所提供資源URL的路由,處理JSON數據同樣簡單,因爲請求中包含的JSON數據可通過request.json
這個Python字典獲取,並且需要包含JSON的響應可以使用Flask提供的輔助函數jsonify()
從Python字典中生成
創建API藍本
REST API相關的路由是一個自成一體的程序子集,所以爲了更好的組織代碼,我們最好把這些路由放到獨立的藍本中,這個程序API藍本的基本結構如下:
|-flasky
|-app/
|-api_1_0
|-__init__.py
|-users.py
|-posts.py
|-comments.py
|-authentication.py
|-errors.py
|-decorators.py
注意,API包的名字中有個版本號,如果需要創建一個向前兼容的API版本,可以添加一個版本號不同的包,讓程序同時支持兩個版本的API
在這個API藍本中,各資源分別在不同的模塊中實現,藍本中還包含處理認證、錯誤以及提供自定義裝飾器的模塊,藍本的構造文件如下所示:
# app/api_1_0/__init__.py
from flask import Blueprint
api = Blueprint('api', __name__)
from . import authentication, posts, users, comments, errors
#...
註冊API藍本的代碼如下:
# app/__init__.py
def create_app(config_name):
#...
from .api_1_0 import api as api_1_0_blueprint
app.register_blueprint(api_1_0_blueprint, url_prefix='/api/v1.0')
return app
錯誤處理
REST Web服務將請求的狀態告知客戶端時,會在響應中發送適當的HTTP狀態碼,並將額外信息放入響應主體,客戶端能從Web服務得到的常見狀態碼如下表
HTTP狀態碼 | 名稱 | 說明 |
---|---|---|
200 | OK(成功) | 請求成功完成 |
201 | Created(已創建) | 請求成功完成並創建了一個新資源 |
400 | Bad request(壞請求) | 請求不可用或不一致 |
401 | Unauthorized(未授權) | 請求中爲包含認證信息 |
403 | Forbidden(禁止) | 請求中發送的認證密令無權訪問目標 |
404 | Notfound(未找到) | URL對應資源不存在 |
405 | Methods not allowed(不允許使用的方法) | 指定資源不支持請求使用方法 |
500 | Internal server error(內部服務器錯誤) | 處理請求的過程中發生意外錯誤 |
處理404和500狀態碼時會有點小麻煩,因爲這兩個錯誤是由Flask自己生成的,而且一般會返回HTML響應,這很可能會讓API客戶端困惑
爲所有客戶端生成適當相應的一種方式是,在錯誤處理程序中根據客戶端請求的格式改寫響應,這種技術成爲內容協商, 下例是改進後的404錯誤處理程序,它向Web服務客戶端發送JSON格式響應,除此之外都發送HTML格式響應,500錯誤處理程序的寫法類似
app/main/errors.py
@main.app_errorhandler(404)
def page_not_found(e):
if request.accept_mimetypes.accept_json and \
not request.accept_mimetypes.accept_html:
response = jsonify({'error': 'not found'})
response.status_code = 404
return response
return render_template('404.html'), 404
這個新版錯誤處理程序檢查Accept請求首部(Werkzeug將其編碼爲request.accept_mimetypes
),根據首部的值決定客戶端期望接受的響應格式,瀏覽器一般不限制響應的格式,所以只爲接受JSON格式而不接受HTML格式的客戶端生成JSON響應
其他狀態碼都是由Web服務生成,因此可在藍本的errors.py
模塊作爲輔助函數實現,下例是403錯誤的處理程序,其他錯誤處理程序的寫法類似
# app/api_1_0/errors.py
def forbidden(message):
response = jsonify({'error':'forbidden', 'message': message})
response.status_code = 403
return response
現在,Web服務的視圖函數可以調用這些輔助函數生成錯誤響應了
使用Flask-HTTPAuth認證用戶
和普通的Web程序一樣,Web服務也需要保護信息,確保未經授權的用戶無法訪問,爲此RIA必須詢問用戶的登錄密令,並將其傳給服務器進行驗證
REST Web服務的特徵之一是無狀態,即在服務器在兩次請求之間不能“記住”客戶端的任何信息,客戶端必須在發出的請求中包含所有必要信息,因此所有請求都必須包含用戶密令
程序當前的登錄功能是在Flask-Login的幫助下實現的,可以把數據存儲在用戶會話中,默認情況下,Flask把會話保存在客戶端cookie中,因此服務器沒有保存任何用戶相關的信息,都轉交給客戶端保存,這種實現方式看起來遵守了REST架構的無狀態要求,但在REST Web服務中使用cookie有點不現實,因爲Web瀏覽器之外的客戶端很難提供對cookie的支持,鑑於此,使用cookie並不是一個很好的設計選擇
REST架構的無狀態看起來似乎過於嚴格,但這並是不隨意提出的要求,無狀態的服務器伸縮起來更加簡單,如果服務器保存了客戶端的相關信息,就必須提供一個所有服務器都能訪問的共享緩存,這樣才能保證一直使用同一臺服務器處理特定客戶端的請求,這樣的需求很難實現
因爲REST架構基於HTTP協議,所以發送密令的最佳方式是使用HTTP認證,基本認證和摘要認證都可以,在HTTP認證中,用戶密令包含在請求的Authorization首部中
HTTP認證協議很簡單,可以直接實現,不過Flask-HTTPAuth拓展提供了一個便利的包裝,可以把協議的細節隱藏在裝飾器之中,類似於Flask-Login提供的login_required裝飾器
Flask-HTTPAuth使用pip安裝,在將HTTP基本認證的擴展進行初始化之前,我們先要創建一個HTTPBasicAuth類對象,和Flask-Login一樣,Flask-HTTPAuth不對驗證用戶命令所需的步驟做任何假設,因此所需的信息在回調函數中提供,下例展示瞭如何初始化Flask-HTTPAuth擴展,以及如何在回調函數中驗證密令
# app/api_1_0/authentication.py
from flask_httpauth import HTTPBasicAuth
auth = HTTPBasicAuth()
@auth.verify_password
def verify_password(email, password):
if email == '':
g.current_user = AnonymousUser()
return True
user = User.query.filter_by(email = email).first()
if not user:
return False
g.current_user = user
return user.verify_password(password)
由於這種用戶認證方法只在API藍本中使用,所以Flask-HTTPAuth擴展只能在藍本包中初始化,而不像其他擴展那樣要在程序包中初始化
電子郵件和密碼使用User模型中現有的方法驗證,如果登錄密令正確,這個驗證回調函數就返回True,否則返回False,API藍本也支持匿名用戶訪問,此時客戶端發送的電子郵件字段必須爲空
驗證回調函數把通過認證的用戶保存在Flask的全局對象g中,這樣一來,視圖函數便能進行訪問,注意在匿名登錄時,這個函數返回True並把Flask-Login提供的AnonymousUser類實例賦值給g.current_user
由於每次請求時都要傳送用戶密令,所以API路由最好通過安全的HTTP提供,加密所有的請求和響應
如果認證密令不正確,服務器向客戶端返回401錯誤,默認情況下,Flask-HTTPAuth自動生成這個狀態碼,但爲了和API返回的其他錯誤保持一致,我們可以自定義這個錯誤響應:
#app/api_1_0/authentication.py
#...
@auth.error_headler
def auth_error():
return unauthorized('Invalid credentials')
爲了保護路由,可使用裝飾器auth.login_required
@api.route('/posts')
@auth.login_required
def get_posts():
pass
不過,這個藍本中的所有路由都要使用相同的方式進行保護,所以我們可以在before_request
處理程序中使用一次login_required
裝飾器,應用到整個藍本,如下例所示:
#app/api_1_0/authentication.py
from .errors import forbidden
@api.before_request
@auth.login_required
def before_request():
if not g.current_user.is_anonymous and \
not g.current_user.comfirmed:
return forbidden('Uncofirmed account')
現在,API藍本中的所有路由都能進行自動認證,而且作爲附加認證,before_request
處理程序還會拒絕已通過認證但沒有確認賬戶的用戶
基於令牌的認證
每次請求時,客戶端都要發送認證密令,爲了避免總是發送敏感信息,我們可以提供一種基於令牌的認證方案
使用基於令牌的認證方案時,客戶端要先把登錄密令發送給一個特殊的URL,從而生成認證令牌,一旦客戶端獲得令牌,就可用令牌代替登錄密令認證請求,處於安全考慮,令牌有過期時間,令牌過期後,客戶端必須重新發送登陸密令以生成新令牌,令牌落入他人之手所帶來的安全隱患受限於令牌的短暫使用期限,爲了生成和驗證認證令牌,我們要在User模型中定義兩個新方法,這兩個新方法用到了itsdangerous
包,如下
# app/models.py
class User(db.Model):
#....
def generate_auth_token(self, expiration):
s = Serializer(current_app.config['SECRET_KEY'],
expires_in=expiration)
return s.dumps({'id': self.id})
@staticmethod
def verify_auth_token(token):
s = Serializer(current_app.config['SECRET_KEY'])
try:
data = s.loads(token)
except:
return None
return User.query.get(data['id'])
generate_auth_token()
方法使用編碼後的用戶id字段值生成一個簽名令牌,還指定了以秒爲單位的過期時間,verify_auth_token()
方法接受的參數是一個令牌,如果令牌可用就返回對應的對象,verify_auth_token()
是靜態方法,因爲只有解碼令牌後才能知道用戶是誰
爲了能夠認證包含令牌的請求,我們必須修改Flask-HTTPAuth提供的verify_password
回調,除了普通的密令之外,還要接受令牌,修改後的回調函數如下:
# app/api_1_0/authentication.py
@auth.verify_password
def verify_password(email_or_token, password):
if email_or_token == '':
g.current_user = AnonymousUser()
return True
if password == '':
g.current_user = User.verify_auth_token(email_or_token)
g.token_used = True
return g.current_user is not None
user = User.query.filter_by(email = email_or_token).first()
if not user:
return False
g.current_user = user
g.token_used = False
return user.verify_password(password)
在這個新版本中,第一個認證參數可以是電子郵件地址或認證令牌,如果這個參數爲空,那就和之前一樣,假定是匿名用戶,如果密碼爲空,那就假定email_or_token
參數提供的是令牌,按照令牌的方式進行認證,如果兩個參數都不爲空,假定使用常規的郵件地址和密碼進行認證,在這種實現方式中,基於令牌的認證是可選的,由客戶端決定是否使用,爲了讓視圖函數能區分這兩種認證方式,我們添加了g.token_used
變量
把認證令牌發送給客戶端的路由也要添加到API藍本中,具體實現如下:
# app/api_1_0/authentication.py
#...
@api.route('/token')
def get_token():
if g.current_user.is_anonymous() or g.token_used:
return unauthorized("Invalid credentials")
return jsonify({'token': g.current_user.generate_auth_token(
expiration=3600), 'expiration': 3600})
這個路由也在藍本中,所以添加到before_request
處理程序上的認證機制也會用在這個路由上,爲了避免客戶端使用舊令牌申請新令牌,要在視圖函數中檢查g.token_used
變量的值,如果使用令牌進行認證就拒絕請求,這個視圖函數返回JSON格式的響應,其中包含了過期時間爲1小時的令牌,JSON格式的響應也包含過期時間