在現代 Web 開發中,資料序列化是不可或缺的一環。本文將探討如何在 Python 環境下安全地處理序列化,並結合 API 版本控制策略,確保系統的穩定性和安全性。我們將以 Sensor 生態系統為例,逐步說明如何使用 JSON 和 Pickle 進行序列化,以及如何使用 HMAC 進行簽章驗證以防止資料竄改。同時,我們也會探討如何設計和實作多版本 API,以因應不斷變化的需求,並確保向後相容性。最後,我們將提供程式碼重構和測試的最佳實踐,以提升程式碼品質和可維護性。

序列化問題的解決方案與實作細節

在前面的章節中,我們探討瞭如何利用WSGI伺服器將現有的Sensor生態系統進行整合,並著重於解決序列化問題。本章節將探討序列化問題的解決方案,並提供詳細的實作細節。

序列化格式的選擇

在我們的例子中,主要使用了JSON協定進行序列化。然而,並非所有的類別都能與JSON相容。因此,我們需要考慮使用更通用的序列化器,如pickle。

使用Pickle的風險

雖然pickle是一個強大的序列化工具,但它也存在安全風險。未經驗證的pickle資料可能會導致任意程式碼的執行。因此,在使用pickle時,我們必須謹慎處理。

使用HMAC進行簽章驗證

為了提高pickle的安全性,我們可以使用HMAC(Keyed-Hash Message Authentication Code)進行簽章驗證。這樣可以確保pickle資料的完整性和真實性。

簽章與驗證的實作

import hashlib
import hmac
import pickle

# 定義金鑰
secret = bytearray([0xb2, 0x56, 0xc4, 0x88, 0x09, 0xa0, 0x8a, 0x1e, 0x28, 0xe3, 0xa3, 0x25, 0xe9, 0x2b, 0x98, 0x6f, 0x13, 0x60, 0xfb, 0x26, 0x06, 0x9b, 0x9d, 0x6f, 0x3a, 0x01, 0x2c, 0x3f, 0x9d, 0x9f, 0x72, 0xcd])

# 簽章
def sign_pickle(data):
    untrusted_pickle = pickle.dumps(data)
    digest = hmac.digest(secret, untrusted_pickle, hashlib.sha256)
    signed_pickle = digest + b":" + untrusted_pickle
    return signed_pickle

# 驗證
def verify_pickle(signed_pickle):
    digest, untrusted = signed_pickle.split(b":", 1)
    expected_digest = hmac.digest(secret, untrusted, hashlib.sha256)
    if not hmac.compare_digest(digest, expected_digest):
        raise ValueError("Bad Signature")
    else:
        return pickle.loads(untrusted)

#### 簽章與驗證的邏輯解析
簽章過程中我們首先使用`pickle.dumps()`將資料序列化接著使用`hmac.digest()`計算序列化資料的HMAC值最後將HMAC值與序列化資料結合形成簽章後的pickle資料

在驗證過程中我們首先將簽章後的pickle資料分割為HMAC值和序列化資料然後重新計算序列化資料的HMAC值並與原始HMAC值進行比較如果兩者一致則驗證透過否則丟擲`ValueError`。

### 將所有內容整合在一起

在我們的平行世界中我們成功地將WSGI伺服器整合到現有的Sensor生態系統中主要的變更在於`sensor_values()`檢視函式中增加了if-else陳述式以處理序列化問題

#### WSGI伺服器與Fallback編碼的實作

```python
from abc import ABC, abstractmethod
import typing as t
import json
import flask
from apd.sensors.sensors import Sensor
from apd.sensors.cli import get_sensors
from apd.sensors.wsgi import require_api_key, set_up_config

app = flask.Flask(__name__)

T_value = t.TypeVar("T_value")

class SerializableSensor(ABC, t.Generic[T_value]):
    title: str

    @abstractmethod
    def value(self) -> T_value:
        pass

    @classmethod
    @abstractmethod
    def serialize(cls, value: T_value) -> str:
        pass

    @classmethod
    @abstractmethod
    def deserialize(cls, serialized: str) -> T_value:
        pass

class JSONSerializedSensor(SerializableSensor[t.Any]):
    @classmethod
    def serialize(cls, value: t.Any) -> str:
        try:
            return json.dumps(value)
        except TypeError:
            return json.dumps(None)

    @classmethod
    def deserialize(cls, serialized: str) -> t.Any:
        return json.loads(serialized)

class JSONWrappedSensor(JSONSerializedSensor):
    def __init__(self, sensor: Sensor[t.Any]):
        self.wrapped = sensor
        self.title = sensor.title

    def value(self) -> t.Any:
        return self.wrapped.value()

def get_serializable_sensors() -> t.Iterable[SerializableSensor[t.Any]]:
    sensors = get_sensors()
    found = []
    for sensor in sensors:
        if isinstance(sensor, SerializableSensor):
            found.append(sensor)
        else:
            found.append(JSONWrappedSensor(sensor))
    return found

@app.route("/sensors/")
@require_api_key
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_serializable_sensors():
        data[sensor.title] = sensor.serialize(sensor.value())
    return data, 200, headers

if __name__ == "__main__":
    import wsgiref.simple_server
    set_up_config(None, app)
    with wsgiref.simple_server.make_server("", 8000, app) as server:
        server.serve_forever()

#### 程式碼邏輯解析:
1. **定義抽象基礎類別`SerializableSensor`**該類別定義了序列化和反序列化的介面
2. **實作`JSONSerializedSensor`**該類別提供了根據JSON的序列化和反序列化實作
3. **建立`JSONWrappedSensor`**該類別用於包裝不支援序列化的Sensor使其能夠被序列化
4. **`get_serializable_sensors()`函式**該函式負責取得所有可序列化的Sensor
5. **`sensor_values()`檢視函式**該函式處理對`/sensors/`的請求傳回所有Sensor的值

## 軟體介面設計與JSON序列化實作

身為軟體的維護者我們在決定介面設計上有更大的彈性雖然仍需考慮實作的簡易性但我們有更強的權威來定義所需的函式功能本文將探討如何透過限制使用JSON API來提高資料的可讀性並實作相關的序列化和反序列化方法

### JSON序列化方法的設計與實作

為了使原始資料更容易被理解我們決定限制使用JSON API這樣做可以避免使用`serialize(...)``deserialize(...)`函式可能導致的不可讀取輸出相反地我們將建立能夠將值轉換為JSON可序列化格式的方法以及從該格式重建值的方法

#### 在Sensor基礎類別中新增JSON序列化方法

我們可以在`Sensor`基礎類別中定義這些方法並提供預設實作目前尚未保證任何感測器都相容於JSON序列化因此預設實作應當是引發例外

```python
@classmethod
def to_json_compatible(cls, value: T_value) -> Any:
    raise NotImplementedError

@classmethod
def from_json_compatible(cls, json_version: Any) -> T_value:
    raise NotImplementedError

為現有感測器實作JSON序列化方法

接下來,我們需要為現有的感測器提供這對方法的實作。主要分為三種不同的程式碼路徑需要更新。

1. 已經與JSON相容的感測器

對於大多數已經與JSON相容的感測器,我們可以建立一個新的混合類別JSONSensor

class JSONSensor(Sensor[T_value]):
    @classmethod
    def to_json_compatible(cls, value: T_value) -> t.Any:
        return value

    @classmethod
    def from_json_compatible(cls, json_version: t.Any) -> T_value:
        return cast(JSONT_value, json_version)
2. PythonVersion感測器

PythonVersion感測器使用了一個自定義類別,該類別無法直接例項化。因此,我們需要稍微修改感測器以便能夠從JSON轉換回原始值。

version_info_type = NamedTuple(
    "version_info_type",
    [
        ("major", int),
        ("minor", int),
        ("micro", int),
        ("releaselevel", str),
        ("serial", int),
    ],
)

class PythonVersion(JSONSensor[version_info_type]):
    title = "Python Version"

    def value(self) -> version_info_type:
        return version_info_type(*sys.version_info)

    @classmethod
    def format(cls, value: version_info_type) -> str:
        if value.micro == 0 and value.releaselevel == "alpha":
            return "{0.major}.{0.minor}.{0.micro}a{0.serial}".format(value)
        return "{0.major}.{0.minor}".format(value)
3. 溫度與太陽能功率感測器

這兩個感測器使用物理量作為其值型別,因此需要自定義的JSON方法。

class Temperature(Sensor[Optional[Any]]):
    ...

    @classmethod
    def to_json_compatible(cls, value: Optional[Any]) -> Any:
        #### 內容解密:
        # 將溫度值轉換為JSON可序列化的格式。
        # 如果值不為None,則傳回一個包含magnitude和unit的字典。
        if value is not None:
            return {"magnitude": value.magnitude, "unit": str(value.units)}
        else:
            return None

    @classmethod
    def from_json_compatible(cls, json_version: Any) -> Optional[Any]:
        #### 內容解密:
        # 從JSON可序列化的格式重建溫度值。
        # 如果json_version不為空,則使用magnitude和unit重建ureg.Quantity物件。
        if json_version:
            return ureg.Quantity(json_version["magnitude"], ureg[json_version["unit"]])
        else:
            return None

程式碼整理

目前sensors.py檔案包含了兩個基礎類別和一些實際的感測器。為了使程式碼函式庫更容易導覽,我們將支援程式碼移至base.py

定義Sensor基礎類別

base.py中,我們定義了Sensor基礎類別,如清單5-13所示。

import typing as t

T_value = t.TypeVar("T_value")

class Sensor(t.Generic[T_value]):
    name: str
    title: str

    def value(self) -> T_value:
        raise NotImplementedError

JSON API的改進

為了使JSON API與感測器入口點使用相同的鍵,我們在Sensor基礎類別中新增了一個name屬性。這樣可以更容易地反序列化資料,因為我們可以輕易地查詢定義它的感測器類別。

版本控制與API管理

在軟體開發過程中,版本控制是確保API穩定性和向後相容性的關鍵。隨著功能的增加和技術的演進,API需要進行更新以滿足新的需求,但同時也要考慮到已有的使用者和舊版本的相容性。

版本控制的重要性

當API需要進行重大變更時,直接修改現有的API可能會導致依賴該API的使用者端出現問題。因此,建立多個版本的API成為了一個必要的選擇。

版本控制策略

  1. URL版本控制:將版本號直接包含在URL中,如/v/1.0/sensors/v/2.0/sensors。這種方法簡單明瞭,使用者可以根據需要選擇合適的版本。

  2. 版本號管理:可以使用簡單的數字版本(如1.0、2.0),也可以採用日曆版本控制(Calendar Versioning)。選擇合適的版本控制策略取決於專案的需求和團隊的偏好。

實作多版本API

為了支援多個版本的API,需要對程式碼進行重構,將不同版本的API邏輯分離到不同的檔案中,並使用Flask的Blueprint功能來註冊和管理不同版本的API。

程式碼結構範例

# v10.py
from flask import Blueprint

version = Blueprint(__name__, __name__)

@version.route("/sensors/")
@require_api_key
def sensor_values() -> t.Tuple[t.Dict[str, t.Any], int, t.Dict[str, str]]:
    # API實作細節
    ...

# __init__.py
from flask import Flask
from .v10 import version as v10_blueprint

app = Flask(__name__)
app.register_blueprint(v10_blueprint, url_prefix="/v/1.0")

檔案結構

src/apd/sensors/wsgi/
├── __init__.py
├── base.py
├── serve.py
├── v10.py
└── v20.py

測試多版本API

支援多版本API的同時,也需要確保每個版本的API都經過充分的測試。可以透過為每個版本建立獨立的測試類別來實作這一點。

測試類別範例

# test_v10_api.py
from apd.sensors.wsgi import v10

class TestV10API:
    @pytest.fixture
    def subject(self, api_key):
        app = flask.Flask("testapp")
        app.register_blueprint(v10.version)
        set_up_config({"APD_SENSORS_API_KEY": api_key}, to_configure=app)
        return app
    
    @pytest.fixture
    def api_server(self, subject):
        return TestApp(subject)

class CommonTests:
    @pytest.mark.functional
    def test_sensor_values_fails_on_missing_api_key(self, api_server):
        response = api_server.get("/sensors/", expect_errors=True)
        assert response.status_code == 403
        assert response.json["error"] == "Supply API key in X-API-Key header"

重構Sensor類別以支援JSON序列化

為了使Sensor類別能夠支援JSON序列化,需要實作to_json_compatiblefrom_json_compatible方法。

Sensor類別範例

class Sensor:
    @classmethod
    def to_json_compatible(cls, value: T_value) -> t.Any:
        raise NotImplementedError
    
    @classmethod
    def from_json_compatible(cls, json_version: t.Any) -> T_value:
        raise NotImplementedError

class JSONSensor(Sensor[T_value]):
    @classmethod
    def to_json_compatible(cls, value: T_value) -> t.Any:
        return value
    
    @classmethod
    def from_json_compatible(cls, json_version: t.Any) -> T_value:
        return t.cast(T_value, json_version)

內容解密:

  1. to_json_compatible方法:將感測器的值轉換為可JSON序列化的格式。這使得感測器的資料可以輕鬆地被轉換為JSON格式,以便於網路傳輸或儲存。

  2. from_json_compatible方法:將JSON相容的資料轉換回感測器的值。這是to_json_compatible的逆操作,用於從JSON資料中還原感測器的值。

  3. JSONSensor類別:繼承自Sensor,並為其提供了一個預設的JSON序列化實作。這意味著任何繼承自JSONSensor的感測器類別都自動獲得了JSON序列化的能力。