10.9. 程式化用戶端的驗證¶
Bearer 權杖是手機、配套微控制器以及雲端工作程序向伺服器進行驗證的方式——用戶端在每個請求的 Authorization 標頭中放入一個權杖,伺服器則加以檢查。配套手機應用在擁有者點擊動作偵測通知上的「已看到」時會 POST 一個「ack」;這些請求需要權杖。
10.9.1. 簽章密鑰¶
這個密鑰是相機用於簽署及驗證權杖的私鑰。在相機首次開機時從硬體 RNG 產生 32 個隨機位元組,將其持久化到檔案系統中,並在之後每次開機時重複使用同一組位元組:
# auth/tokens.py
import os
try:
with open('secret.bin', 'rb') as f:
SECRET = f.read()
except OSError:
SECRET = os.urandom(32)
with open('secret.bin', 'wb') as f:
f.write(SECRET)
os.urandom() 是每個相機埠上適合用於加密的隨機來源——在大多數埠上它會直接讀取晶片的硬體亂數產生器。這個檔案位於相機的工作目錄中(若掛載了 SD 卡則為 /sdcard,否則為 /flash),且永遠不會離開相機。刪除 secret.bin 並重新開機會輪換密鑰,並使在舊密鑰下簽發的每一個權杖失效。
備註
machine.unique_id() 並非替代方案。在某些相機埠上,它也被用來衍生網路 MAC 位址,這表示它的值會隨著相機送出的每個封包一同傳遞——這並非簽章密鑰所需要的特性。
10.9.2. 簽發權杖¶
手機首先需要有辦法取得一個權杖。一個短期有效的 JSON Web Token(JWT)——一段以 base64 編碼的 JSON 酬載並附加一個簽章——對於初步實作而言便已足夠:擁有者以密碼換取一個權杖,接著在權杖過期之前一直使用它:
import jwt
import time
@app.post('/api/login')
async def api_login(request):
creds = request.json
if creds.get('user') != 'owner' or creds.get('pass') != load_password():
abort(401)
token = jwt.encode({
'sub': 'owner',
'exp': int(time.time()) + 3600,
}, SECRET)
return {'token': token}
手機一次性 POST {user, pass},將回傳的權杖儲存在本機儲存空間中,並在之後的每個請求中以 Authorization: Bearer <token> 的形式包含它。
exp 宣告是一個 Unix 時間戳記。下一節中的驗證器依賴此宣告來自動拒絕已過期的權杖——這表示相機的系統時鐘必須設定好,否則每個權杖看起來要嘛在遙遠的未來,要嘛早已過期。NTP 同步的做法請參閱 時間與 NTP。
10.9.3. 使用 TokenAuth 驗證權杖¶
microdot.auth.TokenAuth 是用於需要 Authorization: Bearer <token> 標頭之路由的裝飾器工廠。驗證器回呼會接收權杖字串,並回傳應附加到請求上的任何身分物件——或回傳 None 以拒絕:
from microdot.auth import TokenAuth
tokens = TokenAuth()
@tokens.authenticate
async def check_token(request, token):
try:
claims = jwt.decode(token, SECRET)
except jwt.exceptions.PyJWTError:
return None
return claims['sub']
@app.post('/api/ack')
@tokens
async def ack(request):
body = request.json
if not body or 'count' not in body:
abort(400, 'missing count')
return {'ok': True, 'user': request.g.current_user}
當簽章錯誤、權杖格式不正確,或 exp 宣告已過期時,jwt.decode() 會引發 jwt.exceptions.PyJWTError(或其子類別)。驗證器會吞下所有這些例外並回傳 None,因此 microdot 會回應 401 而處理常式完全看不到任何東西。
當驗證器回傳一個非 None 的值時,microdot 會將其儲存在 request.g.current_user 上,以便處理常式讀取——在此情況下它是 JWT 的 sub(subject,主旨)宣告。
10.9.4. 封閉網路的 BasicAuth¶
對於一個只會從受信任網路存取的管理端點而言,microdot.auth.BasicAuth 較為簡單——瀏覽器會彈出原生的使用者名稱/密碼對話框,並在每個請求中以 Authorization: Basic ... 標頭送出:
from microdot.auth import BasicAuth
basic = BasicAuth(realm='Backyard cam admin')
@basic.authenticate
async def check_basic(request, username, password):
if username == 'owner' and password == load_password():
return 'owner'
return None
@app.get('/admin')
@basic
async def admin(request):
return 'hi ' + request.g.current_user
除非連線是 HTTPS,否則這些憑證會以明文傳輸,因此這種做法只有在受信任的網路上或搭配 HTTPS 時才合理。
/api/ack 與 /api/login 現在能理解權杖了;/admin 則需要 Basic 驗證。