創建博客-使用REST Web服務

使用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格式的響應也包含過期時間

發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章