Menu

  • Home
  • Flask Tutorial
  • WebGL
  • Kontakt

Alicja | Theme by Theme in Progress | Proudly powered by WordPress

Alicja & IT
  • Home
  • Flask Tutorial
  • WebGL
  • Kontakt

Aplikacja webowa w Pythonie – Flask – Logowanie – #8

January 10, 2017Flask Standard

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.

15

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ć.

17
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:

18

Gdy jednak wprowadzisz poprawne nowe dane, operacja zakończy się sukcesem:

19

I po zalogowaniu uda Ci się otworzyć podstronę z dodawaniem postów:

20

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 :).

21

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.

  • Zevi7

    Przerobiłem wszystko od pierwszego tutoriala i chyba czegoś mi tutaj brakuje. Gdzie jest odniesienie do przekierowania na stronę logowania po kliknięciu w “Dodaj post” ? U mnie po kliknięciu pokazuje się od razu strona “Dodaj post”. Natomiast jak ręcznie wpiszę adres …/login to wszystko niby działa (po zalogowaniu przekierowuje automatycznie na “Dodaj post”, ale bez logowania ta strona tez działa). Brakuje też jednego wyjątku. W momencie aktywnego zalogowania ponowne przejście na stronę …/login powoduje błąd.

    • Alicja

      Cześć!
      Tak jak napisałam w ostatnim zdaniu: “wystarczy pamiętać, aby na każdej “ukrytej” stronie sprawdzać, czy użytkownik znajduje się w aktywnej sesji” – czyli wystarczy, że na początku routingu dla strony dodawania posta dopiszesz coś w stylu:

      `if !’username’ in session:
      return redirect(url_for(‘login’))’
      i powinno zadziałać. Możesz wtedy zaimplementować dodatkowy feature, aby funkcja obsługująca login wiedziała, skąd przychodzi użytkownik, i po poprawnym wprowadzeniu danych przekierowywała go właśnie tam :).

Flask:

  • Aplikacja webowa w Pythonie – Flask – Hello World – #1
  • Aplikacja webowa w Pythonie – Flask – Template’y – #2
  • Aplikacja webowa w Pythonie – Flask na Windowsie
  • Aplikacja webowa w Pythonie – Flask – Routing – #3
  • Aplikacja webowa w Pythonie – Flask – Static Files – #4
  • Aplikacja webowa w Pythonie – Flask – Łączenie z bazą danych – #5
  • Aplikacja webowa w Pythonie – Flask – Formularze – #6
  • Aplikacja webowa w Pythonie – Flask – Struktura większej aplikacji, czyli Blueprint – #7
  • Aplikacja webowa w Pythonie – Flask – Logowanie – #8
  • Aplikacja webowa w Pythonie – Flask – Obsługa błędów – #9
  • Aplikacja webowa w Pythonie – Flask – Migracja z localhosta na serwer, czyli co może pójść nie tak z bazą danych – #10
  • Aplikacja webowa w Pythonie – Flask – Deploy na Azure’a – #11

Facebook

Kontakt

kontakt[at]alicja.it

Kategorie

  • Flask (12)
  • Luźne (2)
  • PyGame (5)
  • Relacje z wydarzeń (3)
  • WebGL (3)