10.9. Авторизація для програмних клієнтів¶
Токени-носії — це спосіб, яким телефони, супутні мікроконтролери та хмарні обробники автентифікуються на сервері: клієнт передає токен у заголовку Authorization при кожному запиті, а сервер його перевіряє. Супутній додаток на телефоні надсилає POST з підтвердженням «ack», коли власник натискає «переглянуто» на сповіщенні про рух; ці запити потребують токена.
10.9.1. Секрет підпису¶
Секрет — це приватний ключ камери для підписання та верифікації токенів. Згенеруйте 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() є криптографічно придатним джерелом випадкових чисел на кожному порті камери — на більшості портів він безпосередньо зчитує апаратний генератор випадкових чисел чипа. Файл зберігається у робочому каталозі камери (/sdcard, якщо вставлена SD-картка, або /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}
jwt.decode() генерує виняток jwt.exceptions.PyJWTError (або підклас) коли підпис неправильний, токен має невірний формат або поле exp минуло. Верифікатор перехоплює всі ці випадки та повертає None, тому microdot відповідає 401, і обробник нічого не бачить.
Коли верифікатор повертає значення, відмінне від None, microdot зберігає його в request.g.current_user, щоб обробник міг його прочитати — у цьому випадку це поле sub (тема) JWT.
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-автентифікації.