Flask/Grinberg/05 - Zalogování

Z Wikiverzity
Jak používat klasifikační nálepkuTato stránka je součástí projektu:
Příslušnost: všeobecná


05 - Zalogování[editovat]

Chapter 5: User Logins

Password Hashing[editovat]

Vytvoř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[editovat]

sudo 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[editovat]

Extense 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živatelem
    • True = 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[editovat]

user 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[editovat]

Nyní 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[editovat]

Na 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[editovat]

Ně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[editovat]

Namí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[editovat]

Zač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)