Tato stránka je součástí projektu: | |
Příslušnost: všeobecná |
05 - Zalogování
editovat- https://github.com/miguelgrinberg/microblog/tree/v0.5
- https://github.com/miguelgrinberg/microblog/releases/tag/v0.5
- https://github.com/miguelgrinberg/microblog/compare/v0.4...v0.5
Password Hashing
editovatVytvoření a verifikace hashe – zkusíme si to nejdříve v pythonovském shellu:
>>> from werkzeug.security import generate_password_hash, check_password_hash
>>> hash = generate_password_hash('brekeke')
>>> hash
>>> check_password_hash (hash, 'brekeke')
>>> check_password_hash (hash, 'brekek')
Introduction to Flask-Login
editovatsudo pip3 install flask-login
(Naštěstí na pythonanywhere je nainstalován: pythonanywhere.com/batteries_included/)
Tato flasková extense obhospodařuje stav uživatelů, kteří jsou zalogováni, včetně funkce remember me, která umožňuje udržovat uživatele zalogovaného i když si zavře okno browseru.
Stejně jako jiné extense ji inicializujeme v souboru app/__init__.py:
# …
from flask_login import LoginManager
# …
app = Flask(__name__)
# …
login = LoginManager(app)
Preparing The User Model for Flask-Login
editovatExtense Flask-Login potřebuje tři vlastnosti (property) a jednu metodu:
is_authenticated
: property je:True
= uživatel se zalogoval se správnými kredenciály (loginname, password)False
= nikoli
is_active
: property je:True
= uživatelský účet (account) je aktivníFalse
= nikoli
is_anonymous
: property je:False
= uživatel je řádným uživatelemTrue
= speciální anonymní uživatel
get_id()
: metoda, která vrací jednoznačný identifikátor uživatele jako string
Tyto čtyři věci se implementují celkem snadno, nicméně Flask-Login poskytuje třídu mixin, která v sobě už zahrnuje generickou implementaci, vyhovující většině běžných požadavků – takže stačí do souboru app/models.py přidat:
# …
from flask_login import UserMixin
class User(UserMixin, db.Model):
# …
User Loader Function
editovatuser session = prostor, kam Flask-Login ukládá stopu zalogovaných uživatelů (jejich jedinečný identifikátor) – jaké stránky ten uživatel navštíví.
Protože Flask-Login neví nic o (použitých) databázích a modelech, tak si aplikace musí nakonfigurovat funkci user_loader, a to v modulu app/models.py:
from app import login
# …
@login.user_loader
def load_user(id):
return User.query.get(int(id))
Funkci user_loader jsme zaregistrovali dekorátorem @login.user_loader
.
V databázi je uživatel uložený jako numerické ID a my jej chceme konvertovat na jméno uživatele.
Logging Users In
editovatNyní již máme přístup k databázi a tím pádem můžeme zkompletovat naši view-funkci v souboru app/routes.py:
# app/routes.py: Login view function logic
# …
from flask_login import current_user, login_user
from app.models import User
# …
@app.route('/login', methods=['GET', 'POST'])
def login():
if current_user.is_authenticated: # kdyby zalogovaný user šel znovu na login,
return redirect(url_for('index')) # pošleme ho na index
form = LoginForm()
if form.validate_on_submit():
user = User.query.filter_by(username=form.username.data).first() # filtrem vyhledám usera dle username; first() vrací None jestliže neexistuje
if user is None or not user.check_password(form.password.data): # Když se mu nepovedlo zalogovat …
flash('Invalid username or password')
return redirect(url_for('login')) # … znovu na login
login_user(user, remember=form.remember_me.data) # login_user() je fce z Flask-Login: zaregistruje zalogovaného usera, nastaví current_user
return redirect(url_for('index')) # vrátíme se na index
return render_template('login.html', title='Sign In', form=form)
Logging Users Out
editovatNa to má Flask-Login funkci logout_user()
, kterou velmi jednoduše použijeme opět v souboru app/routes.py:
# app/routes.py: Logout view function
# …
from flask_login import logout_user
# …
@app.route('/logout')
def logout():
logout_user()
return redirect(url_for('index'))
Aby uživatel ten link viděl, tak mu ho přidáme do navigačního řádku hned po tom, co se zaloguje.
Soubor app/templates/base.html:
<div>
Microblog:
<a href="{{ url_for('index') }}">Home</a>
{% if current_user.is_anonymous %}
<a href="{{ url_for('login') }}">Login</a>
{% else %}
<a href="{{ url_for('logout') }}">Logout</a>
{% endif %}
</div>
Vlastnost (property) is_anonymous
je jedním z atributů, které Flask-Login přidává objektu user skrzevá třídu UserMixin
.
Výraz current_user.is_anonymous
je True
pouze v případě, že uživatel není zalogován.
Requiring Users To Login
editovatNěkteré stránky (protected pages) můžeme ochránit heslem a Flask-Login podporuje možnost, že pro jejich prohlížení pořádá uživatele o přihlášení.
Flask-Login potřebuje vědět, jaká view-funkce zařizuje login, tak mu to řekneme hned na začátku v souboru app/__init__.py:
# …
login = LoginManager(app)
login.login_view = 'login' # 'login' = jméno té funkce, která dělá login_view neboli to samé jméno, které také použijeme jako argument url_for()
Flask-Login ochrání stránky před anonymním přístupem pomocí dekorátoru @login_required
.
Když ten dekorátor umístíme pod jiné dekorátory, tak ta následná view-funkce bude ochráněna a příslušné stránky budou vyžadovat zalogování.
V následujícím příkladu ochráníme již hlavní stránku aplikace v souboru app/routes.py:
#app/routes.py: @login_required decorator
from flask_login import login_required
@app.route('/')
@app.route('/index')
@login_required
def index():
# …
Po úspěšném zalogování ještě zbývá nasměrovat uživatele na tu stránku, ke které se předtím pokoušel přistoupit. To se udělá tak, že se do URL přidá query string (tj. string s otazníkem), takže v URL se nám pak ukáže:
…/login?next=/puvodni_stranka_na_kterou_chtel_uzivatel_jit
To lomítko ale bude zaencodované %2F, takže ve skutečnosti uvidíme:
…/login?next=%2Fpuvodni_stranka_na_kterou_chtel_uzivatel_jit
app/routes.py pak bude vypadat:
# app/routes.py: Redirect to "next" page
from flask import request # z flasku importujeme request
from werkzeug.urls import url_parse # z werkzeugu importujeme url_parse: podle netloc nám určí, jestli je URL absolutní nebo relativní
@app.route('/login', methods=['GET', 'POST'])
def login():
# …
if form.validate_on_submit(): # atd … už jsme vysvětlovali v sekci 'Logging Users In'
user = User.query.filter_by(username=form.username.data).first()
if user is None or not user.check_password(form.password.data):
flash('Invalid username or password')
return redirect(url_for('login'))
login_user(user, remember=form.remember_me.data) # user se zaloguje
next_page = request.args.get('next') # var. request obsahuje request clienta, request.args je slovník. next_page = další stránka za 'next'
if not next_page or url_parse(next_page).netloc != '': # next_page není anebo obsahuje kompletní URL včetně domain name (= může být pokus o attack!)
next_page = url_for('index') # … jdi normálně na hlavní stránku
return redirect(next_page) # jdi na next_page
# …
Showing The Logged In User in Templates
editovatNamísto fake-usera (kapitola 02) už můžeme v šabloně app/templates/index.html použít skutečného uživatele:
{% extends "base.html" %}
{% block content %}
<h1>Hi, {{ current_user.username }}!</h1>
{% for post in posts %}
<div><p>{{ post.author.username }} says: <b>{{ post.body }}</b></p></div>
{% endfor %}
{% endblock %}
A naopak vyndáme user template argument (tj. argument user=user) z view-funkce v souboru app/routes.py:
# app/routes.py: Do not pass user to template anymore
@app.route('/')
@app.route('/index')
def index():
# …
return render_template("index.html", title='Home Page', posts=posts)
Teď si můžeme vyzkoušet, jak nám to funguje. Protože ještě nemáme udělanou registraci uživatele, vyzkoušíme si to z flask shell
:
>>> u = User(username='Kychot', email='kychot@example.com')
>>> u.set_password('kyky')
>>> db.session.add(u)
>>> db.session.commit()
Anebo si takový testovací kousek programu, který nám bude vkládat nějaký záznam do databáze, můžeme dočasně strčit na konec souboru models.py
# TEST:
password_hash = generate_password_hash('kyky')
u = User(username='Kychot', email='kychot@example.com', password_hash = password_hash)
db.session.add(u)
db.session.commit()
User Registration
editovatZačneme tím, že si vytvoříme třídu RegistrationForm
v souboru app/forms.py:
# app/forms.py: User registration form
from flask_wtf import FlaskForm
from wtforms import StringField, PasswordField, BooleanField, SubmitField
from wtforms.validators import ValidationError, DataRequired, Email, EqualTo
from app.models import User
# …
class RegistrationForm(FlaskForm):
username = StringField('Username', validators=[DataRequired()])
email = StringField('Email', validators=[DataRequired(), Email()]) # nový validátor Email() ⇐ WTForms
password = PasswordField('Password', validators=[DataRequired()])
password2 = PasswordField( # heslo napsat dvakrát – pro kontrolu
'Repeat Password', validators=[DataRequired(), EqualTo('password')]) # nový validátor EqualTo()
submit = SubmitField('Register')
def validate_username(self, username):
user = User.query.filter_by(username=username.data).first()
if user is not None:
raise ValidationError('Please use a different username.')
def validate_email(self, email):
user = User.query.filter_by(email=email.data).first()
if user is not None:
raise ValidationError('Please use a different email address.')
Těm validátorům, které použijeme z WTForms, se říká stock, jako že jsou na skladě.
Mimo nich si ale můžeme vytvořit vlastní validátory – zkrátka když si vytvoříme nějakou metodu, kterou nazveme validate_<field_name>
,
tak to WTForms bude brát jako uživatelský validátor vyvolá ho stejně, jako ostatní validátory, které má „skladem”.
Aby se nám ten registrační formulář zobrazil, musíme si pro něj vytvořit registrační šablonu app/templates/register.html:
{% extends "base.html" %}
{% block content %}
<h1>Register</h1>
<form action="" method="post">
{{ form.hidden_tag() }}
<p>
{{ form.username.label }}<br>
{{ form.username(size=32) }}<br>
{% for error in form.username.errors %}
<span style="color: red;">[{{ error }}]</span>
{% endfor %}
</p>
<p>
{{ form.email.label }}<br>
{{ form.email(size=64) }}<br>
{% for error in form.email.errors %}
<span style="color: red;">[{{ error }}]</span>
{% endfor %}
</p>
<p>
{{ form.password.label }}<br>
{{ form.password(size=32) }}<br>
{% for error in form.password.errors %}
<span style="color: red;">[{{ error }}]</span>
{% endfor %}
</p>
<p>
{{ form.password2.label }}<br>
{{ form.password2(size=32) }}<br>
{% for error in form.password2.errors %}
<span style="color: red;">[{{ error }}]</span>
{% endfor %}
</p>
<p>{{ form.submit() }}</p>
</form>
{% endblock %}
Do souboru app/templates/login.html potřebujeme ještě doplnit link, který pošle uživatele na registrační formulář:
<p>Nový uživatel? <a href="{{ url_for('register') }}">Zaregistruj se!</a></p>
A nakonec musíme napsat view-funkci, která bude tu registraci uživatele dělat (tj. nakonec ho přidá do databáze) – dáme ji do našeho souboru app/routes.py:
from app import db
from app.forms import RegistrationForm
# …
@app.route('/register', methods=['GET', 'POST'])
def register():
if current_user.is_authenticated: # pokud je už uživatel zalogovaný, nebude se registrovat
return redirect(url_for('index'))
form = RegistrationForm()
if form.validate_on_submit(): # vše v pořádku, uživatele uložíme do databáze
user = User(username=form.username.data, email=form.email.data)
user.set_password(form.password.data)
db.session.add(user)
db.session.commit()
flash('Gratulujeme, jsi naším registrovaným uživatelem!')
return redirect(url_for('login')) # Nově vytvořený uživatel se může rovnou zalogovat
return render_template('register.html', title='Register', form=form)