Aplikacja webowa w Pythonie – Flask – Logowanie – #8
W poprzednich odcinkach tutorialu udało nam się stworzyć coś w rodzaju panelu administratora, z którego możemy dodawać wpisy z bazy danych. Jak stworzyć mechanizm logowania, aby mogli się do niego dostać jedynie “wybrańcy”? Dowiecie się już dziś :)
Na początek – poprawmy nieco strukturę, czyli podzielmy “Posty z bazy danych” na dwie podstrony – jedna pozostanie w osobnym blueprincie (ta do dodawania wpisów), a ta wyświetlająca posty powróci do reszty aplikacji.
Poprawienie struktury
W base.html oraz w blueprintowym index.html poprawmy nawigację:
1 2 |
<li><a href="{{url_for('db_posts_blueprint.database_posts')}}">Dodaj post</a></li> <li><a href="{{url_for('all_posts')}}">Posty z bazy danych</a></li> |
Zgodnie ze skryptem, w views.py utwórzmy routing dla all_posts(), który zaprowadzi nas na dostępną ogólnie podstronę:
1 2 3 4 5 6 7 |
@app.route('/wszystkie-posty') def all_posts(): cursor = mysql.connect().cursor() cursor.execute("SELECT * from posty") data = cursor.fetchall() cursor.close() return render_template('all_posts.html', data = data) |
I zedytujmy plik __init__.py dla blueprintu:
1 |
@db_posts_blueprint.route("/dodaj-post", methods=['POST', 'GET']) |
Tak naprawdę to tylko taka kosmetyczna poprawka, żeby całość miała jakiś semantyczny sens:). Wyświetlanie postów póki co zostawimy również na stronie edycji, aby łatwiej było je usuwać. Pozostało jeszcze tylko stworzenie pliku wyświetlającego wpisy dla osób niezalogowanych, czyli all_posts.html:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 |
{% extends "base.html" %} {% block title %}Posty{% endblock %} {% block content %} <h2>Posty</h2> <p> Wszystkie posty: {% for row in data %} <article> <h3>{{ row[0] }}. {{ row[1] }}</h3> <p>{{ row[2] }}</p> <p><i>Autor: {{ row[3] }}</i></p> </article> {% endfor %} </p> {% endblock %} |
Całość powinna teraz działać poprawnie, z tą różnicą, że na podstronie “Posty z bazy danych” wyświetlają się wszystkie pobrane z bazy wpisy, a na “Dodaj wpis” (z szarym tłem) formularze do dodawania i lista z możliwością usuwania.
Logowanie
Czas na zabezpieczenie podstrony “Dodaj wpis” przed niepożądanym dostępem. Najpierw – baza danych. Utwórz tabelę users, składającą się z czterech kolumn: ID (z automatyczną inkrementacją i jako primary key), login, password i name.
Wstaw do niej jakieś przykładowe dane.
Kolejnym krokiem będzie uzupełnienie rdzenia naszej aplikacji. Aby móc uruchomić autoryzację, będziemy potrzebować tzw. sekretnego klucza – skomplikowanego i unikatowego (system “uderz głową w klawiaturę” sprawdzi się doskonale;)) . Dodamy go w głównym pliku __init__.py zaraz pod konfiguracją:
1 2 3 4 5 6 7 8 9 10 11 12 13 |
from flask import Flask from flaskext.mysql import MySQL app = Flask(__name__) mysql = MySQL() app.config['MYSQL_DATABASE_USER'] = 'root' app.config['MYSQL_DATABASE_PASSWORD'] = '' app.config['MYSQL_DATABASE_DB'] = 'flask' app.config['MYSQL_DATABASE_HOST'] = 'localhost' app.config['MYSQL_CHARSET'] = 'utf8' app.secret_key = '329743bjshads93982472463246sas' mysql.init_app(app) from hello_world import views |
Do zrealizowania mechanizmu logowania będziemy potrzebować jeszcze kilku importów, które umieścimy na początku views.py:
1 2 |
from flask import url_for, session, redirect, escape from hashlib import md5 |
Oraz w __init__.py w blueprincie:
1 2 |
from flask import url_for, session, redirect, escape from hashlib import md5 |
MD5 to sposób szyfrowania, dzięki którym hasła zostaną zakodowane przed zapisem do bazy danych. Koniecznie z tego skorzystajmy.
W views.py dodajmy jeszcze tymczasowy routing dla login oraz logout:
1 2 3 4 5 6 7 8 |
@app.route('/login') def login(): return render_template('login.html') @app.route('/logout') def logout(): session.pop('username', None) return redirect(url_for('index')) |
I utwórzmy login.html:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 |
{% extends "base.html" %} {% block title %}Zaloguj się{% endblock %} {% block content %} <h2>Zaloguj się</h2> <div class="row"> <div class="col s10 offset-s1"> <form action="" method="POST" class="top-margin"> {% if error %} <p class="error"><strong>Error:</strong> {{ error }} {% endif %} <div class="input-group"> <span class="input-group-addon">Login</span> <input type="text" class="form-control" name="username" id="username"> </div> <br> <div class="input-group"> <span class="input-group-addon">Hasło</span> <input type="text" class="form-control" id="password" name="password"> </div> <br> <button class="btn waves-effect waves-light custom-green" type="submit" name="action" value="login">Zaloguj się</button> </form> </div> </div> {% endblock %} |
Uruchom aplikację i spróbuj wejść na podstronę “Dodaj post”. Co się dzieje? Zostajesz przekierowany na podstronę logowania – dokładnie tak, jak powinno być.
Kolejny krok to przetworzenie danych wysłanych przez formularz i sprawdzenie, czy użytkownik znajduje się w bazie.
Uwierzytelnianie i autoryzacja
Zabierzmy się za funkcję wyświetlającą login.html. Oczywiście musi ona obsługiwać metodę POST (w końcu prześlemy w jej kierunku formularz). Jak będzie działać? Na początku sprawdzi, czy przypadkiem nie jesteśmy już zalogowani, aby wtedy przekierować nas na /dodaj-post. Jeżeli nie, połączy się z bazą i wyśle zapytanie o nazwę użytkownika, którą wprowadzimy. Dla nieistniejącej zwróci błąd, a dla istniejącej porówna hasła – gdy okaże się niepoprawne, zwróci inny błąd. Dopiero, gdy oba pola zostaną uzupełnione prawidłowo, przekieruje nas na wymarzoną podstronę.
Nie brzmi skomplikowanie, więc zabierzmy się za kod:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 |
@app.route('/login', methods=['GET', 'POST']) def login(): if 'username' in session: return redirect(url_for('profile')) error = None class ServerError(Exception):pass if request.method == 'POST': if request.form["action"] == "login": try: conn = mysql.connect() cur = conn.cursor() username_form = request.form['username'] cur.execute("SELECT COUNT(1) FROM users WHERE login = '" + username_form +"'") if not cur.fetchone()[0]: raise ServerError('Błędna nazwa użytkownika') password_form = request.form['password'] cur.execute("SELECT password FROM users WHERE login = '" + username_form +"'") for row in cur.fetchall(): if md5(password_form.encode('utf-8')).hexdigest() == row[0]: session['username'] = request.form['username'] return redirect(url_for('db_posts_blueprint.database_posts')) raise ServerError('Błędne hasło') except ServerError as e: error = str(e) return render_template('login.html', error=error) |
Zmienna error jest graficzną reprezentacją błędu, który wyświetlimy na stronie login.html, a ServerError(Exception) to pythonowy wyjątek, który zostanie zwrócony, jeśli cokolwiek będzie nieprawidłowe.
Jeżeli teraz spróbujesz zalogować się nazwą użytkownika i hasłem, które wprowadziłeś wcześniej do bazy, na 100% dostaniesz błąd “Błędne hasło”. To wszystko przez fragment if md5(password_form.encode('utf-8')).hexdigest() == row[0]. Jak możesz się domyślić md5 szyfruje podane hasło i dopiero porównuje z tym zapisanym w bazie – w końcu w tabelach ma ono właśnie postać ukrytą. To, które wprowadziłeś do bazy jest po prostu w swojej normalnej postaci.
Aby móc w końcu się zalogować, stworzymy w takim razie jeszcze prosty mechanizm rejestracji.
Do login.html dodaj kod:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 |
<h3 class="text-dark-grey">Zarejestruj się</h3> <div class="row"> <div class="col s10 offset-s1"> <form action="" method="POST" class="top-margin"> {% if success_register %} <p class="success"><strong>Success:</strong> {{ success_register }} {% endif %} {% if error_register %} <p class="error"><strong>Error:</strong> {{ error_register }} {% endif %} <div class="input-group"> <span class="input-group-addon">Imię</span> <input type="text" class="form-control" name="name" id="name"> </div> <div class="input-group"> <span class="input-group-addon">Login</span> <input type="text" class="form-control" name="user" id="user"> </div> <br> <div class="input-group"> <span class="input-group-addon">Hasło</span> <input type="text" class="form-control" id="pass" name="pass"> </div> <br> <button class="btn waves-effect waves-light custom-green" type="submit" name="action" value="register">Zarejestruj się</button> </form> </div> </div> |
A do views.py w routingu dla login() skrypt obsługujący samą rejestrację:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 |
@app.route('/login', methods=['GET', 'POST']) def login(): if 'username' in session: return redirect(url_for('profile')) error = None error_register = None success_register = None class ServerError(Exception):pass if request.method == 'POST': if request.form["action"] == "login": try: conn = mysql.connect() cur = conn.cursor() username_form = request.form['username'] cur.execute("SELECT COUNT(1) FROM users WHERE login = '" + username_form +"'") if not cur.fetchone()[0]: raise ServerError('Błędna nazwa użytkownika') password_form = request.form['password'] cur.execute("SELECT password FROM users WHERE login = '" + username_form +"'") for row in cur.fetchall(): if md5(password_form.encode('utf-8')).hexdigest() == row[0]: session['username'] = request.form['username'] return redirect(url_for('db_posts_blueprint.database_posts')) raise ServerError('Błędne hasło') except ServerError as e: error = str(e) if request.form["action"] == "register": try: conn = mysql.connect() cur = conn.cursor() name_form = request.form['name'] username_form = request.form['user'] password_form = request.form['pass'] hash_password = md5(password_form.encode('utf-8')).hexdigest() to_db = ("", username_form, hash_password, name_form) cur.execute("SELECT COUNT(1) FROM users WHERE login = '" + username_form +"'") if cur.fetchone()[0]: raise ServerError('Nazwa uzytkownika zajęta') else: cur.execute("INSERT INTO users VALUES (%s, %s, %s, %s)", to_db) conn.commit() success_register = "Zarejestrowałeś się!" except ServerError as e: error_register = str(e) return render_template('login.html', error=error, error_register=error_register, success_register=success_register) |
Na koniec dodaj jeszcze w menu przycisk do wylogowywania (w base.html oraz blueprintowym index.html):
1 |
<li><a href="{{url_for('logout')}}">Wyloguj się</a></li> |
I spróbuj utworzyć nowe konto. Jeżeli użyjesz loginu, który już został wykorzystany, powinien pojawić się błąd:
Gdy jednak wprowadzisz poprawne nowe dane, operacja zakończy się sukcesem:
I po zalogowaniu uda Ci się otworzyć podstronę z dodawaniem postów:
Przycisk “Wyloguj się” zakończy sesję i umożliwi ponowne logowanie. Jeśli teraz zajrzysz do bazy, znajdziesz w niej loginy oraz zakodowane hasła :).
Tak naprawdę to by było na tyle – mechanizm logowania we Flasku to nic skomplikowanego – wystarczy pamiętać, aby na każdej “ukrytej” stronie sprawdzać, czy użytkownik znajduje się w aktywnej sesji.