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 вызывается при каждом защищённом запросе, чтобы превратить ID, хранящийся в сессии, в запись пользователя, которую увидит обработчик. Держите её лёгкой – она выполняется на горячем пути.

10.10.2. Форма входа, отправка входа, выход

Сама форма входа – это статическая страница, обслуживаемая из /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() очищает оба cookie, а последующий редирект возвращает браузер к форме.

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. Свежий вход – это вход, при котором пользователь ввёл пароль в текущей сессии; запомненный вход таким не является. На панели управления нет ничего, что дотягивало бы до этой планки, но декоратор есть на случай, когда он понадобится.

Теперь панель управления требует входа, прежде чем отвечать на любой из своих маршрутов.