10.10. 대시보드용 로그인¶
웹 대시보드에는 로그인 폼이 필요합니다 – LAN에 있는 아무나 마당을 봐서는 안 됩니다. 쿠키 기반 세션과 로그인 데코레이터가 바로 그 역할을 합니다.
세션은 카메라가 쿠키에 기록하는 작은 dict입니다. 쿠키는 이 장 앞부분에서 로드한 JWT 서명 비밀로 서명되므로, 브라우저는 이를 가지고 다닐 수 있지만 서명을 무효화하지 않고는 내용을 변조할 수 없습니다.
10.10.1. session 객체와 login 객체 설정하기¶
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가 쿠키를 읽지 못하게 합니다 – XSS 공격자가 세션을 탈취하는 것에 대한 계층적 방어입니다. secure=False 는 HTTPS가 적용될 때까지의 임시값입니다. 서버가 TLS 위에서 동작하게 되면 True 로 바꿔서 쿠키가 일반 HTTP로는 절대 전송되지 않도록 하세요.
참고
크로스 사이트 스크립팅(Cross-site scripting, XSS)은 공격자가 신뢰된 페이지에 대한 사용자의 화면 안에서 JavaScript를 실행시키는 부류의 공격입니다 – 일반적으로 이스케이프되지 않은 폼 필드, HTML로 렌더링되는 댓글, 또는 취약한 서드파티 위젯을 통해 일어납니다. http_only 쿠키 플래그는 XSS 자체를 막지는 않습니다. 다만 주입된 스크립트가 세션 쿠키에 접근하지 못하게 하므로, XSS가 성공하더라도 그것을 손쉽게 세션 탈취로 전환할 수는 없게 됩니다.
user_loader 는 보호된 모든 요청마다 호출되어 세션에 저장된 ID를 핸들러가 보게 될 사용자 레코드로 변환합니다. 가볍게 유지하세요 – 핫 패스에서 실행됩니다.
10.10.2. 로그인 폼, 로그인 POST, 로그아웃¶
로그인 폼 자체는 대시보드와 마찬가지로 /sdcard/static/ 에서 제공되는 정적 페이지입니다. 폼은 /login 으로 POST됩니다:
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() 는 세션 쿠키를 기록하고, 클라이언트가 원래 도달하려던 페이지로(next= 쿼리 인수, 없으면 / 로 폴백) 302 리다이렉트를 반환합니다. remember=True 는 수명이 더 긴 _remember 쿠키도 기록하여 세션이 브라우저 재시작 후에도 유지되게 합니다.
microdot.login.Login.logout_user() 는 두 쿠키를 모두 지우며, 이어지는 리다이렉트가 브라우저를 폼으로 되돌려 보냅니다.
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. 새로 로그인한 세션 vs 기억된 세션¶
_remember 쿠키는 브라우저 재시작에 걸쳐 사용자를 로그인 상태로 유지하지만, 더 약한 형태의 인증입니다 – 브라우저가 카페에 방치되어 있었을 수도 있습니다. 비밀번호를 변경하거나, API 토큰을 등록하거나, 그 외에 재인증할 가치가 있는 작업을 하는 라우트에는 @login 대신 @login.fresh 을 붙이세요. 새로 로그인한 상태(fresh)란 사용자가 이번 세션에서 비밀번호를 입력한 경우이며, 기억된 로그인은 그렇지 않습니다. 대시보드에는 그 정도 수준에 해당하는 것이 없지만, 데코레이터는 필요할 때를 위해 마련되어 있습니다.
이제 대시보드는 어떤 라우트라도 응답하기 전에 로그인을 요구합니다.