在現代 Web 應用中,API 設計與驗證至關重要。本文將探討 API 金鑰驗證機制及如何使用 Flask 微框架構建安全的 API。同時,我們會深入研究 Python 裝飾器和閉包的應用,以實作更精細的存取控制。最後,我們將探討如何使用 Flask 建立 Web 伺服器,提供感測器資料,並探討如何使用裝飾器增強 API 的安全性。我們將會使用 API 金鑰來驗證使用者,並使用 Python 的裝飾器來簡化驗證邏輯。
API 設計與驗證機制
在建立一個可靠且適合生產環境的 Web 應用程式時,我們需要規劃一個完善的 API。首先,我們希望能夠擷取所有的感測器資料,但也需要考慮取得單一特定值的可能性,因為擷取感測器資料可能需要耗費一些時間。同時,我們也需要決定這個 API 的驗證機制,因為它不再受到伺服器登入限制的保護。
API 金鑰驗證
大多數的 API 並不使用傳統的使用者名稱和密碼登入系統,而是使用單一的 API 金鑰作為憑證。選擇授權系統的考量因素對於使用者(可能是人類或是其他程式)都是一樣的。人類通常使用使用者名稱和密碼,而其他程式則使用 API 金鑰。
使用者授權的三種方式
- 扁平許可權結構:適用於簡單的應用程式,使用者只需要登入即可存取所有功能。
- 群組和許可權系統:如 Django 的完整授權系統,使用者可以直接或透過群組成員資格獲得許可權。
- 彈性許可權關係:許可權與使用者和資料之間的關係是彈性的,許可權是在特定資料的背景下分配給使用者或群組。
選擇合適的驗證方法
我們的 API 將是唯讀的,我們只需要保護讀取感測器資料的功能。由於我們的使用案例是從多個來源收集資訊並集中儲存,我們希望以最少的 HTTP 請求載入所有感測器值。因此,選擇一個對所有使用者一視同仁的驗證解決方案是最合適的。我們不需要區分不同許可權級別的使用者,只需要驗證使用者是否有效。
驗證框架的選擇
我們選擇的驗證框架應該與我們預期與 API 伺服器互動的方式相比對。使用者最熟悉的形式是專用的登入頁面,提供一個會話憑證,通常以 Cookie 的形式存在。另一種更常見於 API 的方式是每個請求都包含驗證資訊,可以是專用的 HTTP 標頭或使用 HTTP 基本和摘要驗證功能。
由於我們的 API 將被自動化程式存取,且登入資訊預期不會改變,因此 API 金鑰式的驗證系統最符合我們的需要。
Flask 微框架
Flask 微框架的靈感來自於一個愚人節玩笑,一個名為 denied 的微框架,它被設計成一個單一檔案,具有非常簡單的介面。作者 Armin Ronacher 編寫了一個 160 行的框架,強調市場行銷而非進階功能。在大多數 Web 框架專注於大型、功能齊全的應用程式的時代,許多人對簡單的 Web 程式設計介面感興趣。一年後,Flask 誕生了,它是一個高品質的 Web 框架,旨在滿足那些對 denied 感興趣的人的需求。
# 使用 Flask 建立一個簡單的 API
from flask import Flask, jsonify
app = Flask(__name__)
# 假設的感測器資料
sensor_data = {
"Python Version": [3, 9, 0],
"Temperature": 25.0,
# 其他感測器資料...
}
@app.route('/api/sensors', methods=['GET'])
def get_sensors():
return jsonify(sensor_data)
if __name__ == '__main__':
app.run(debug=True)
內容解密:
這段程式碼展示瞭如何使用 Flask 建立一個簡單的 API。首先,我們匯入必要的模組並建立一個 Flask 應用程式。然後,我們定義了一些假設的感測器資料。get_sensors 函式處理對 /api/sensors 的 GET 請求,並以 JSON 格式傳回感測器資料。最後,我們在 __main__ 下執行應用程式,並啟用除錯模式。
API 驗證流程
@startuml
skinparam backgroundColor #FEFEFE
skinparam defaultTextAlignment center
skinparam rectangleBackgroundColor #F5F5F5
skinparam rectangleBorderColor #333333
skinparam arrowColor #333333
title 內容解密:
rectangle "請求" as node1
rectangle "驗證 API 金鑰" as node2
rectangle "是" as node3
rectangle "否" as node4
node1 --> node2
node2 --> node3
node3 --> node4
@enduml此圖示展示了客戶端向 API 伺服器請求感測器資料的流程。伺服器驗證客戶端提供的 API 金鑰,如果驗證成功,則傳回感測器資料;否則,傳回錯誤訊息。
使用Flask建立Web伺服器
Flask是一個流行的Python網頁框架,提供了豐富的功能來簡化Web開發。它支援使用Jinja2範本語言生成HTML、管理請求和回應頭、管理URL路由以及在需要時生成錯誤。本章節將介紹如何使用Flask建立一個簡單的Web伺服器來提供感測器資料。
將Flask新增到專案依賴中
首先,需要將Flask新增到專案的依賴中。這可以透過在setup.cfg檔案中定義一個名為webapp的額外依賴來實作。這個額外依賴將允許使用者在安裝專案時選擇是否包含Flask。
[options.extras_require]
webapp = flask
接下來,使用pipenv install -e .[webapp]命令來安裝這個額外依賴。這將把Flask新增到專案的依賴列表中並安裝到環境中。
建立Flask應用程式
建立一個基本的Flask應用程式非常簡單,如下所示:
import json
import typing as t
from flask import Flask
from apd.sensors.cli import get_sensors
app = Flask(__name__)
@app.route("/sensors/")
def sensor_values() -> t.Tuple[t.Dict[str, t.Any], int, t.Dict[str, str]]:
headers = {"Content-Security-Policy": "default-src 'none'"}
data = {}
for sensor in get_sensors():
data[sensor.title] = sensor.value()
return data, 200, headers
if __name__ == "__main__":
import wsgiref.simple_server
with wsgiref.simple_server.make_server("", 8000, app) as server:
server.serve_forever()
內容解密:
- 匯入必要的模組:程式碼首先匯入了必要的模組,包括
json、typing、Flask以及來自apd.sensors.cli的get_sensors函式。 - 建立Flask應用程式例項:使用
Flask(__name__)建立了一個Flask應用程式例項,並將其指定給變數app。 - 定義路由:使用
@app.route("/sensors/")裝飾器定義了一個路由。當使用者存取/sensors/路徑時,將呼叫sensors_value函式。 - 實作
sensors_value函式:這個函式負責傳回感測器的資料。它首先建立了一個字典來儲存感測器的資料,然後遍歷所有感測器,將其標題和值存入字典中。最後,它傳回包含感測器資料的字典、HTTP狀態碼200以及自定義的HTTP頭。 - 執行Flask應用程式:在
if __name__ == "__main__":區塊中,程式碼使用wsgiref.simple_server.make_server建立了一個簡單的WSGI伺服器,並將其組態為在埠8000上執行。
使用Flask的優勢
使用Flask有幾個優勢:
- 路由管理:Flask提供了方便的路由管理功能,使得將不同的URL對映到不同的處理函式變得容易。
- 自動JSON轉換:Flask能夠自動將傳回的字典轉換為JSON格式,並設定正確的
Content-Type頭。 - 靈活性:透過使用裝飾器註冊處理函式,Flask提供了一種靈活的方式來管理Web應用程式的邏輯。
測試Flask應用程式
要測試這個Flask應用程式,需要匯入app物件而不是sensors_value函式,因為sensors_value已經成為了一個實作細節。同時,確保傳送GET請求到正確的URL。
import pytest
from yourapplication import app
@pytest.fixture
def client():
with app.test_client() as client:
yield client
def test_sensor_values(client):
response = client.get('/sensors/')
assert response.status_code == 200
# 進一步斷言response的內容
內容解密:
- 測試客戶端:使用
app.test_client()建立一個測試客戶端,這使得模擬HTTP請求變得容易。 - 測試
sensors_value端點:傳送一個GET請求到/sensors/,並斷言回應的狀態碼是200。進一步,可以斷言回應內容符合預期。
後續改進方向
- 安全性增強:進一步增強應用的安全性,例如透過驗證和授權機制。
- 擴充套件性改進:考慮如何使應用程式能夠處理更多的請求和負載,可能需要引入負載平衡和快取機制。
- 監控和日誌記錄:實施監控和日誌記錄機制,以便能夠跟蹤應用的效能和除錯問題。
這些改進可以幫助確保應用程式在生產環境中的穩定性和可靠性。
Python 函式裝飾器與閉包的應用
在將 API 投入生產環境之前,我們需要實作之前討論過的存取控制。為此,我們可以使用 Python 的裝飾器(decorator),就像 Flask 使用路由裝飾器將函式與特定的 URL 模式繫結一樣。裝飾器是一種 Python 函式,它接受一個可呼叫的物件或類別作為引數,並傳回相同型別的物件。
裝飾器模式的優勢
裝飾器模式允許開發者為函式撰寫自定義的前導或後續程式碼,這些程式碼會在函式主體執行前或後執行。雖然無法直接存取函式內部的變數,但可以對輸入或輸出進行額外的錯誤檢查或轉換。此外,部分裝飾器的程式碼會在函式定義時執行,可用於在應用程式啟動時設定元資料,例如 URL 路由。
使用裝飾器的好處
使用裝飾器可以將複雜的邏輯封裝在裝飾器內部,讓使用該裝飾器的函式保持簡潔和易於理解。下面是一個例子,展示瞭如何使用裝飾器檢查函式的引數是否為負數:
def disallow_negative(func):
def inner(*args):
for arg in args:
if arg < 0:
return 0
return func(*args)
return inner
@disallow_negative
def power(x, y):
return x ** y
內容解密:
disallow_negative裝飾器接受一個函式func作為引數,並傳回一個新的函式inner。inner函式檢查傳入的引數是否包含負數,如果有,則直接傳回 0;否則,呼叫原始函式func並傳回其結果。- 使用
@disallow_negative裝飾power函式,使其具備檢查負數引數的功能。
閉包(Closures)的工作原理
裝飾器的實作依賴於 Python 的閉包特性。閉包是指在一個函式內部定義另一個函式,並且內部函式使用了外部函式的變數。當外部函式傳回內部函式時,內部函式仍然可以存取外部函式的變數,即使外部函式已經結束執行。
def example(x, y):
a = x + y
b = x * y
c = b * a
def get_value_of_c():
return c
return get_value_of_c
getter = example(1, 2)
print(getter()) # 輸出:6
內容解密:
example函式內部定義了一個變數c和一個內部函式get_value_of_c。get_value_of_c函式傳回變數c的值。- 當
example傳回get_value_of_c時,get_value_of_c仍然可以存取變數c,即使example已經結束執行。
修改父作用域中的變數
Python 提供了 nonlocal 關鍵字,允許內部函式修改外部函式的變數。這使得開發者可以實作更複雜的閉包邏輯。
def private_variable():
value = None
def set(new_value):
nonlocal value
value = new_value
def get():
return value
return set, get
set, get = private_variable()
set(10)
print(get()) # 輸出:10
內容解密:
private_variable函式傳回兩個內部函式:set和get。set函式使用nonlocal修改外部變數value。get函式傳回外部變數value的值。
裝飾器(Decorator)技術深度解析
裝飾器是 Python 中一個強大且靈活的功能,能夠在不修改原始函式的情況下擴充套件其功能。本文將探討裝飾器的基本原理、實務應用及設計考量。
基礎閉包(Closure)原理
在理解裝飾器之前,必須先了解閉包的概念。閉包允許內部函式存取外部函式的變數,實作資料封裝和功能擴充套件。
def private_variable():
var = None
def set(value):
nonlocal var
var = value
def get():
return var
return set, get
# 示範閉包的獨立性
a_set, a_get = private_variable()
b_set, b_get = private_variable()
a_set(10)
print(f"a={a_get()} b={b_get()}") # 輸出:a=10 b=None
b_set(4)
print(f"a={a_get()} b={b_get()}") # 輸出:a=10 b=4
內容解密:
private_variable()函式傳回兩個內部函式:set和getnonlocal關鍵字允許set函式修改外部變數var- 每個
private_variable()呼叫建立獨立的變數空間 a_set和b_set操作各自獨立的變數
基本裝飾器實作
裝飾器本質上是一個接受函式作為引數並傳回新函式的高階函式。
def outer(func):
print(f"裝飾 {func}")
def inner(*args, **kwargs):
print(f"呼叫 {func}(*{args}, **{kwargs})")
value = func(*args, **kwargs)
print(f"傳回 {value}")
return value
return inner
@outer
def add_five(num):
return num + 5
# 示範裝飾器的運作
print(add_five(1))
內容解密:
outer函式是裝飾器工廠,接受原始函式funcinner函式包裝原始函式,實作功能擴充套件- 使用
*args和**kwargs確保函式簽名相容性 - 裝飾器在函式定義時執行,而非呼叫時
引數化裝飾器設計
有時需要根據不同場景定製裝飾器行為,這時可以使用帶引數的裝飾器。
def add_integer_to_all_arguments(offset):
def decorator(func):
def inner(*args):
args = [arg + offset for arg in args]
return func(*args)
return inner
return decorator
@add_integer_to_all_arguments(10)
def power(x, y):
return x ** y
@add_integer_to_all_arguments(3)
def add(x, y):
return x + y
# 示範不同偏移量的效果
print(power(0, 0)) # 經過計算後結果為 10 ** 10
print(add(0, 0)) # 經過計算後結果為 6
內容解密:
add_integer_to_all_arguments是裝飾器工廠的工廠- 第一層巢狀實作引數傳遞 (
offset) - 第二層巢狀實作函式裝飾 (
decorator) - 第三層巢狀實作最終的函式包裝 (
inner)
最佳實踐與設計建議
保持函式簽名相容性
- 使用
*args和**kwargs確保靈活性 - 在必要時明確指定引數以提高可讀性
- 使用
合理使用裝飾器
- 用於日誌記錄、許可權檢查等橫切關注點
- 簡化重複性程式碼邏輯
注意效能影響
- 裝飾器會增加函式呼叫開銷
- 在效能敏感場景需謹慎使用