
REST API (RESTful) działające na systemie klasy Enterprise cz. 2 – autentykacja użytkownika

W poprzednim artykule z tej serii zaprezentowaliśmy, jak przy pomocy webowego frameworka, jakim jest flask, zbudować prosty serwer. Dziś pokażemy, jak rozszerzyć ten projekt i usprawnić go o autentykację użytkownika za pomocą plików cookies.
W poprzednim artykule z tej serii zaprezentowaliśmy, jak przy pomocy webowego frameworka, jakim jest flask, zbudować prosty serwer. Dziś pokażemy, jak rozszerzyć ten projekt i usprawnić go o autentykację użytkownika za pomocą plików cookies.
Wszystkich, którzy nie czytali poprzedniego artykułu, serdecznie do tego zachęcamy – REST API (RESTful) działające na systemie klasy Enterprise cz. 1. Jest on wprowadzeniem do serii oraz wiąże się bezpośrednio z działaniami, które dziś podejmiemy.
Struktura, którą do tej pory stworzyliśmy, prezentuje się następująco:
.
├── app
│ ├── __init__.py
│ └── app.py
├── httpd.conf
├── run.py
└── wsgi.py
W katalogu projektu mogą pojawić się też pliki oraz katalogi takie jak np bin
, lib
, __pycache__
i inne. Są to pliki pozostawione przez uruchomioną aplikację oraz pliki tworzące wirtualne środowisko pythonowe. Do naszych działań będziemy potrzebować flask-sqlalchemy oraz flask-login, które instalujemy przy pomocy:
pip install flask flask-sqlalchemy flask-login
Flask-login dostarcza nam narzędzia pozwalające na prostą obsługę autentykacji użytkownika przez login i hasło z automatyczną obsługą plików cookes.
1. Model użytkownika
Pierwszym krokiem będzie stworzenie modelu użytkownika. W tym celu w pliku UserModel.py
tworzymy bazę danych przy pomocy metody SQLAlchemy()
wraz z klasą UserModel
dziedziczącą po UserMixin
oraz db.Model
. UserMixin jest klasą zawierającą metody podstawowej obsługi użytkownika, takie jak:
- is_authenticated() – wykorzystamy ją do rozróżnienia zalogowanych użytkowników
- get_id() – zwraca ID zalogowanego użytkownika
- is_active() – można ją wykorzystać w celu weryfikacji adresu e-mail lub blokowania użytkowników
- is_anonymous() – służy do identyfikowania użytkownika niezalogowanego lub anonimowego.
db.Model zajmuje się obsługą tabeli w bazie danych zawierającej informacje o użytkownikach. W ciele klasy definiujemy, jakie dane będzie przyjmował użytkownik:
class UserModel(UserMixin, db.Model):
id = db.Column(db.Integer, primary_key=True)
email = db.Column(db.String(80), unique=True)
username = db.Column(db.String(20))
password_hash = db.Column(db.String())
Następnie tworzymy metody przetwarzające i zapisujące hasło. W tym celu wykorzystamy wbudowane w pythona moduły oraz metody: hashlib.sha256
isecrets.compare_digest
. Stworzony w ten sposób plik powinien wyglądać w następujący sposób:
from hashlib import sha256
from secrets import compare_digest
from flask_login import UserMixin
from flask_sqlalchemy import SQLAlchemy
db = SQLAlchemy()
class UserModel(UserMixin, db.Model):
id = db.Column(db.Integer, primary_key=True)
email = db.Column(db.String(80), unique=True)
username = db.Column(db.String(20), unique=True)
password_hash = db.Column(db.String())
def set_password(self, password):
self.password_hash = sha256(password.encode()).digest()
def check_password(self, password):
return compare_digest(self.password_hash, sha256(password.encode()).digest())
2. Obsługa sesji użytkownika
Flask
wyposażony jest w proste narzędzia zapewniające obsługę sesji. W tym projekcie wykorzystamy LoginManager
zaimportowany z modułu flask_login
. W pliku o nazwie login.py
tworzymy obiekt klasy LoginManager
odpowiedzialny za obsługę sesji użytkownika:
from .UserModel import UserModel
from flask_login import LoginManager
lm = LoginManager()
@lm.user_loader
def load_user(id):
return UserModel.query.get(int(id))
Przy pomocy dekoratora user_loader
definiujemy funkcję odpowiedzialną za wczytanie użytkownika o danym ID z bazy danych.
3. Inicjowanie bazy danych oraz instancji klasy LoginManager
Teraz należy zainicjować bazę danych oraz stworzoną przez nas we wcześniejszym punkcie instancję.
Do przygotowanego w poprzednim artykule pliku app.py, który wygląda następująco:
#!/usr/bin/env python
from flask import Flask
app = Flask(__name__)
@app.route("/")
def hello():
return "Hello world!"
dodajemy konfigurację bazy danych:
app.config['SQLALCHEMY_TRACK_MODIFICATIONS'] = False
app.config['SQLALCHEMY_DATABASE_URI'] = 'sqlite:///db.sqlite'
app.config['SECRET_KEY'] = 'secret-key-goes-here'
oraz inicjujemy pustą tabelę przed otrzymaniem pierwszego zapytania do serwisu:
db.init_app(app)
@app.before_first_request
def create_table():
db.create_all()
Inicjujemy także obiekt login zaimportowany ze stworzonego przez nas pliku:
lm.init_app(app)
lm.login_view = 'login'
Przy pomocy lm.login_view
definiujemy adres strony, do której zostanie przekierowany niezalogowany użytkownik w przypadku próby otwarcia witryny, do której nie ma dostępu. Po tych operacjach plik app.py powinien wyglądać następująco:
from flask import Flask
from .UserModel import db
from .login import lm
def create_app():
app = Flask(__name__)
app.config['SQLALCHEMY_TRACK_MODIFICATIONS'] = False
app.config['SQLALCHEMY_DATABASE_URI'] = 'sqlite:///db.sqlite'
app.config['SECRET_KEY'] = 'secret-key-goes-here'
db.init_app(app)
@app.before_first_request
def create_table():
db.create_all()
lm.init_app(app)
lm.login_view = 'login'
return app
app = create_app()
@app.route("/")
def hello():
return "Hello world!"
4. Obsługa żądań
Teraz pozostało już tylko zdefiniować żądania i odpowiadające im strony html, które zostaną wyświetlone użytkownikowi.
index/hello
Zaczniemy od edycji funkcji hello
, która wyświetla użytkownikowi napis hello world
i jest naszą stroną startową. Umieścimy na niej przyciski przekierowujące do strony logowania oraz rejestracji. Na początku dodajmy kolejny dekorator route
z adresem “/hello” oraz zmieńmy return "Hello world!"
na return render_template('hello.html')
Żądanie wygląda teraz w następujący sposób:
@app.route("/")
@app.route("/hello")
def hello():
return render_template('hello.html')
Funkcja render_template
dostarczana przez moduł flask
pozwala nam na wyrenderowanie strony html z szablonu, który umieszczamy w katalogu templates
znajdującym się w folderze z aplikacją. Następnie tworzymy plik hello.html
, w którym umieszczamy przekierowania do strony z logowaniem i rejestracją (stworzymy je w kolejnych krokach). Plik wygląda tak:
<h3>Hello world!</h3>
<form action = "{{url_for(login')}}" method = "GET">
<input type = "submit" value = "Login">
</form>
<form action = "{{url_for(register')}}" method = "GET">
<input type = "submit" value = "Register">
</form>
Uwaga. Zdefiniowanie przekierowań do nieistniejącego endpointu spowoduje błąd wewnętrzny aplikacji. Należy mieć to na uwadze w trakcie testowania działania dodawanych do niej elementów.
hi
Żądanie /hi
będzie stroną wyświetlaną dla zalogowanego użytkownika. Definiujemy ją z dodaniem dekoratora login_required
w następujący sposób:
@app.route("/hi")
@login_required
def hi():
return render_template('hi.html')
Szablon html dla hi.html:
<h1>Hello {{ current_user.username }}!</h1>
<form action = "{{url_for('logout')}}" method = "GET">
<input type = "submit" value = "Logout">
</form>
Jak widać, w szablonach możemy korzystać z makr takich jak current_user.username
. Dzięki wykorzystaniu flask_login
możemy wyświetlać dowolne informacje powiązane z naszym użytkownikiem.
login
Żądanie /login
definiujemy z uwzględnieniem metody GET
oraz POST
. Żądanie GET będzie odpowiadać za wyświetlenie formularza, a POST za wysłanie znajdujących się w nim informacji z powrotem do serwisu. Rozpoczynamy od dekoratora z uwzględnieniem metod:
`@app.route('/login', methods = ['POST', 'GET'])`
Następnie sprawdzamy, czy użytkownik nie ma aktywnej sesji. Jeśli tak, przekierowujemy go do witryny użytkownika (w naszym przypadku będzie to /hi
).
if current_user.is_authenticated:
return redirect('/hi')
Teraz w zależności od tego, czy otrzymane żądanie jest POST, czy GET, zbieramy informacje z formularza i sprawdzamy, czy użytkownik oraz podane przez niego hasło pasują do tych znajdujących się w bazie:
username = request.form['username']
user = UserModel.query.filter_by(username = username).first()
if user is not None and user.check_password(request.form['password']):
login_user(user)
return redirect('/hi')
else:
return "wrong password\n" + render_template('login.html')
lub renderujemy stronę login.html
.
Jeżeli login i hasło są poprawne, to tworzymy sesję dla użytkownika przy pomocy polecenia login_user(user)
, po czym następuje przekierowanie go do witryny /hi
. W przypadku gdy login lub hasło są błędne, użytkownik zostanie przekierowany z powrotem do witryny login.html
z dopiskiem wrong password!
.
Cała funkcja login prezentuje się następująco:
@app.route('/login', methods = ['POST', 'GET'])
def login():
if current_user.is_authenticated:
return redirect('/hi')
if request.method == 'POST':
username = request.form['username']
user = UserModel.query.filter_by(username = username).first()
if user is not None and user.check_password(request.form['password']):
login_user(user)
return redirect('/hi')
else:
return "wrong password\n" + render_template('login.html')
return render_template('login.html')
Szablon dla strony login.html
wygląda tak:
<form action = "" method = "POST">
<label for = "username">Username:</label><br>
<input type = "text" id = "username" name = "username"><br>
<label for = "password">Password:</label><br>
<input type = "password" id = "password" name = "password"><br>
<input type = "submit" value = "Login">
</form>
<form action = "{{url_for('register') }}" method = "GET">
<input type = "submit" value = "Register">
</form>
Oprócz formularza logowania znajduje się w nim także przycisk przekierowujący do witryny rejestracji.
register
Funkcja register jest analogiczna do funkcji login, z drobnymi różnicami. Zamiast sprawdzania, czy dany użytkownik istnieje i czy hasło jest poprawne, weryfikuje, czy e-mail lub podana nazwa użytkownika nie są zajęte:
if UserModel.query.filter_by(email=new_email).all():
return ('Email already registered' + render_template('register.html'))
if UserModel.query.filter_by(username=new_username).all():
return ('Username already registered' + render_template('register.html'))
Jeżeli wprowadzone dane są wolne, tworzony jest nowy użytkownik i dodawany do bazy:
user = UserModel(email = new_email, username = new_username)
user.set_password(new_password)
db.session.add(user)
db.session.commit()
Następnie użytkownik zostaje przekierowany do witryny logowania. Funkcja register prezentuje się następująco:
@app.route('/register', methods=['POST', 'GET'])
def register():
if current_user.is_authenticated:
return redirect('/hi')
if request.method == 'POST':
new_email = request.form['email']
new_username = request.form['username']
new_password = request.form['password']
if UserModel.query.filter_by(email=new_email).all():
return ('Email already registered' + render_template('register.html'))
if UserModel.query.filter_by(username=new_username).all():
return ('Username already registered' + render_template('register.html'))
user = UserModel(email = new_email, username = new_username)
user.set_password(new_password)
db.session.add(user)
db.session.commit()
return redirect('/login')
return render_template('register.html')
Szablon html dla witryny register także jest analogiczny do szablonu login.html
:
<form action = "" method = "POST">
<label for = "email">Email:</label><br>
<input type = "email" id = "email" name = "email"><br>
<label for = "username">Username:</label><br>
<input type = "text" id = "username" name = "username"><br>
<label for = "password">Password:</label><br>
<input type = "password" id = "password" name = "password"><br>
<input type = "submit" value = "Register">
</form>
<form action = "{{url_for('login')}}" method = "GET">
<input type = "submit" value = "Login">
</form>
logout
Żądanie logout
odpowiada za wylogowanie użytkownika i jest trywialną do napisania funkcją. Wystarczy użyć w niej polecenia: logout_user()
dostarczonego przez moduł flask_login
oraz przekierować użytkownika do strony startowej lub strony logowania:
@app.route('/logout')
def logout():
logout_user()
return redirect('/hello')
Podsumowanie
Ostateczna wersja pliku app.py
wygląda tak:
#!/usr/bin/env python
from flask import Flask, request, render_template, redirect
from flask_login import current_user, login_user, login_required, logout_user
from .UserModel import UserModel, db
from .login import lm
def create_app():
app = Flask(__name__)
app.config['SQLALCHEMY_TRACK_MODIFICATIONS'] = False
app.config['SQLALCHEMY_DATABASE_URI'] = 'sqlite:///db.sqlite'
app.config['SECRET_KEY'] = 'secret-key-goes-here'
db.init_app(app)
@app.before_first_request
def create_table():
db.create_all()
lm.init_app(app)
lm.login_view = 'login'
return app
app = create_app()
@app.route("/")
@app.route("/hello")
def hello():
return render_template('hello.html')
@app.route("/hi")
@login_required
def hi():
return render_template('hi.html')
@app.route('/login', methods = ['POST', 'GET'])
def login():
if current_user.is_authenticated:
return redirect('/hi')
if request.method == 'POST':
username = request.form['username']
user = UserModel.query.filter_by(username = username).first()
if user is not None and user.check_password(request.form['password']):
login_user(user)
return redirect('/hi')
else:
return "wrong password\n" + render_template('login.html')
return render_template('login.html')
@app.route('/register', methods=['POST', 'GET'])
def register():
if current_user.is_authenticated:
return redirect('/hi')
if request.method == 'POST':
new_email = request.form['email']
new_username = request.form['username']
new_password = request.form['password']
if UserModel.query.filter_by(email=new_email).all():
return ('Email already registered' + render_template('register.html'))
if UserModel.query.filter_by(username=new_username).all():
return ('Username already registered' + render_template('register.html'))
user = UserModel(email = new_email, username = new_username)
user.set_password(new_password)
db.session.add(user)
db.session.commit()
return redirect('/login')
return render_template('register.html')
@app.route('/logout')
def logout():
logout_user()
return redirect('/hello')
Natomiast struktura plików gotowej aplikacji prezentuje się w ten sposób:
.
├── app
│ ├── app.py
│ ├── db.sqlite
│ ├── __init__.py
│ ├── login.py
│ ├── templates
│ │ ├── hello.html
│ │ ├── hi.html
│ │ ├── login.html
│ │ └── register.html
│ └── UserModel.py
├── httpd.conf
├── run.py
└── wsgi.py
Powyższy materiał pokazuje prosty sposób na autentykację użytkownika opartą o pliki cookies przy pomocy frameworka flask
z użyciem modułu flask_login
. Oczywiście możliwe jest dalsze rozszerzanie funkcjonalności szablonów, porządkowanie aplikacji czy przenoszenie definicji żądań http do innych plików wykorzystując dostarczoną przez flask
klasę Blueprint
. Jest to jednak temat na osobny artykuł.