10.10. Вхід для панелі керування

Веб-панелі керування потрібна форма входу – сторонні користувачі в локальній мережі не повинні бачити двір. Саме для цього призначені сесії на основі cookie та декоратор входу.

Сесія – це невеликий словник, який камера записує в cookie. Cookie підписується секретом JWT, завантаженим раніше в цьому розділі, тому браузер може зберігати його, але не може змінити вміст, не порушивши підпис.

10.10.1. Налаштування об’єктів сесії та входу

microdot.session.Session встановлює механізм сесій для app. microdot.login.Login додає декоратор у стилі login_required та допоміжні функції 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 не дозволяє JavaScript на сторінці читати cookie – це багаторівневий захист проти викрадення сесії XSS-зловмисником. secure=False є заповнювачем до впровадження HTTPS; змініть на True, коли сервер працюватиме через TLS, щоб cookie ніколи не передавалося через незахищений HTTP.

Примітка

Міжсайтовий скриптинг (XSS) – це клас атак, при яких зловмисник виконує JavaScript у браузері користувача на довіреній сторінці – зазвичай через неекранізоване поле форми, HTML-коментар або вразливий сторонній віджет. Прапорець cookie http_only не запобігає XSS; він просто приховує сесійний cookie від впровадженого скрипту, щоб успішний XSS не міг легко перетворитися на викрадення сесії.

user_loader викликається під час кожного захищеного запиту, щоб перетворити ідентифікатор, збережений у сесії, на запис користувача, який побачить обробник. Тримайте його легким – він виконується на гарячому шляху.

10.10.2. Форма входу, POST входу, вихід

Сама форма входу – це статична сторінка, що обслуговується з /sdcard/static/, як і панель керування. Форма надсилає POSTна /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() записує сесійний cookie і повертає перенаправлення 302 на сторінку, яку клієнт спочатку намагався відкрити (аргумент запиту next=, або / за замовчуванням). remember=True також записує довготривалий cookie _remember, щоб сесія зберігалася після перезапуску браузера.

microdot.login.Login.logout_user() очищає обидва cookies, а подальше перенаправлення повертає браузер до форми.

10.10.3. Захист панелі керування

Декорувати кожен маршрут панелі керування за допомогою @login, щоб неавтентифікований запит отримував перенаправлення 302 на /login замість вмісту:

@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):
    ...

Кінцеві точки API на основі токенів (/api/login, /api/ack) залишаються токен-орієнтованими – вони призначені для мобільного додатку, а не для браузера. Аутентифікація за токеном і аутентифікація за сесією чудово співіснують на одному app.

10.10.4. Свіжі та запам’ятовані сесії

Cookie _remember зберігає вхід користувача після перезапуску браузера, але це слабша форма аутентифікації – браузер міг бути залишений у кав’ярні. Для маршрутів, що змінюють паролі, реєструють токени API або виконують будь-що інше, що вимагає повторної аутентифікації, декоруйте за допомогою @login.fresh замість @login. Свіжий вхід – це той, де користувач ввів свій пароль у цій сесії; запам’ятований вхід – ні. На панелі керування немає нічого, що відповідало б цьому критерію, але декоратор є, коли він потрібний.

Тепер панель керування вимагає входу перед тим, як будь-який маршрут відповідатиме.