10.10. Logowanie do panelu¶
Panel internetowy potrzebuje formularza logowania – przypadkowe osoby w sieci LAN nie powinny widzieć podwórka. Właśnie do tego służą sesje oparte na ciasteczkach oraz dekorator logowania.
Sesja to mały słownik, który kamera zapisuje w ciasteczku. Ciasteczko jest podpisane sekretem do podpisywania JWT wczytanym wcześniej w tym rozdziale, więc przeglądarka może je przenosić, ale nie może zmienić jego zawartości bez unieważnienia podpisu.
10.10.1. Konfiguracja obiektów sesji i logowania¶
microdot.session.Session instaluje mechanizm sesji w obiekcie app. microdot.login.Login dodaje dekorator w stylu login_required oraz pomocnicze funkcje login_user / logout_user:
# auth/login.py
from microdot.session import Session
from microdot.login import Login
Session(app, secret_key=SECRET,
cookie_options={'http_only': True, 'secure': False})
login = Login()
USERS = {
'owner': {'id': 'owner', 'password_hash': load_password_hash()},
}
@login.user_loader
async def load_user(user_id):
return USERS.get(user_id)
http_only=True uniemożliwia kodowi JavaScript na stronie odczyt ciasteczka – to warstwowa obrona przed atakującym XSS przejmującym sesję. secure=False jest tymczasowym ustawieniem, dopóki nie zostanie wdrożone HTTPS; zmień je na True, gdy serwer będzie działał przez TLS, aby ciasteczko nigdy nie wędrowało zwykłym HTTP.
Informacja
Cross-site scripting (XSS) to klasa ataków, w których atakujący doprowadza do wykonania kodu JavaScript wewnątrz widoku zaufanej strony oglądanej przez użytkownika – zwykle poprzez nieoczyszczone pole formularza, komentarz renderowany jako HTML lub podatny widget innej firmy. Flaga ciasteczka http_only nie zapobiega atakom XSS; sprawia jedynie, że ciasteczko sesji jest poza zasięgiem wstrzykniętego skryptu, więc udany XSS nie da się trywialnie przekształcić w przejęcie sesji.
user_loader jest wywoływany przy każdym chronionym żądaniu, aby zamienić identyfikator zapisany w sesji na rekord użytkownika, który zobaczy procedura obsługi. Niech będzie tani – działa na gorącej ścieżce.
10.10.2. Formularz logowania, żądanie POST logowania, wylogowanie¶
Sam formularz logowania to statyczna strona serwowana z /sdcard/static/, tak jak panel. Formularz wysyła POST-em do /login:
from microdot import redirect
@app.get('/login')
async def login_form(request):
return Response.send_file('/sdcard/static/login.html')
@app.post('/login')
async def do_login(request):
form = request.form
user = USERS.get(form.get('user'))
if not user or not check_password(user, form.get('pass')):
return redirect('/login?error=1')
return await login.login_user(request, user, remember=True)
@app.post('/logout')
async def logout(request):
await login.logout_user(request)
return redirect('/login')
microdot.login.Login.login_user() zapisuje ciasteczko sesji i zwraca przekierowanie 302 na stronę, którą klient pierwotnie chciał odwiedzić (argument zapytania next=, z domyślnym powrotem do /). remember=True zapisuje też dłużej żyjące ciasteczko _remember, dzięki czemu sesja przetrwa ponowne uruchomienie przeglądarki.
microdot.login.Login.logout_user() czyści oba ciasteczka, a następujące po nim przekierowanie odsyła przeglądarkę z powrotem do formularza.
10.10.3. Ochrona panelu¶
Udekoruj każdą trasę dotyczącą panelu dekoratorem @login, tak aby nieuwierzytelnione żądanie otrzymało przekierowanie 302 do /login zamiast treści:
@app.get('/<path:filename>')
@login
async def static(request, filename):
...
@app.get('/config')
@login
async def get_config(request):
...
@app.post('/config')
@login
async def set_config(request):
...
@app.get('/events')
@login
@with_sse
async def events(request, sse):
...
@app.get('/control')
@login
@with_websocket
async def control(request, ws):
...
Punkty końcowe API oparte na tokenach (/api/login, /api/ack) pozostają oparte na tokenach – są przeznaczone dla aplikacji telefonicznej, a nie przeglądarki. Uwierzytelnianie tokenowe i sesyjne bez problemu współistnieją w tym samym obiekcie app.
10.10.4. Świeże a zapamiętane sesje¶
Ciasteczko _remember utrzymuje użytkownika zalogowanym pomimo ponownych uruchomień przeglądarki, ale jest to słabsza forma uwierzytelniania – przeglądarka mogła zostać pozostawiona w kawiarni. W przypadku tras, które zmieniają hasła, rejestrują tokeny API lub robią cokolwiek innego, dla czego warto się ponownie uwierzytelnić, użyj dekoratora @login.fresh zamiast @login. Świeże logowanie to takie, w którym użytkownik wpisał hasło w tej sesji; zapamiętane logowanie nim nie jest. Panel nie ma niczego, co przekraczałoby ten próg, ale dekorator jest dostępny, gdy będzie potrzebny.
Panel wymaga teraz logowania, zanim odpowie którakolwiek z jego tras.