Skip to content

33. Ćwiczenie praktyczne: wersja 13

Wersja 13 wprowadza następujące zmiany w stosunku do wersji 12:

  • przeprowadzono refaktoryzację niektórych fragmentów kodu;
  • sesja jest zarządzana w inny sposób za pomocą modułu [flask_session];
  • w pliku konfiguracyjnym stosuje się zaszyfrowane hasła;

Wersję 13 uzyskuje się początkowo poprzez skopiowanie wersji 12:

Image

Image

  • na [1], konfiguracja zostanie poddana refaktoryzacji. W szczególności zostanie ona przeniesiona z folderu [flask];
  • W [2] główny skrypt zostanie poddany refaktoryzacji. Zostanie on również przeniesiony z folderu [flask];
  • w [3] konfiguracja zostanie podzielona na kilka plików;
  • w wersji [4]: główny skrypt [main] zostanie uproszczony poprzez przeniesienie części kodu do innych plików;
  • w pliku [5] kontroler uwierzytelniania zostanie zmodyfikowany, ponieważ od tej pory hasła użytkowników będą szyfrowane;
  • w [6] główny kontroler przejmie kod, który wcześniej znajdował się w głównym skrypcie [main];

33.1. Refaktoryzacja konfiguracji aplikacji

Image

Pojawiają się trzy nowe pliki konfiguracyjne:

  • [mvc]: do konfiguracji architektury aplikacji MVC;
  • [parameters]: który będzie zawierał wszystkie stałe aplikacji;
  • [syspath]: który konfiguruje ścieżkę Python Path aplikacji;

Plik [syspath.py] ma następującą treść:


def configure(config: dict) -> dict:
    import os

    # katalog tego pliku
    script_dir = os.path.dirname(os.path.abspath(__file__))

    # ścieżka główna
    root_dir = "C:/Data/st-2020/dev/python/cours-2020/python3-flask-2020"

    # zależności
    absolute_dependencies = [
        # foldery projektu
        # BaseEntity, MyException
        f"{root_dir}/classes/02/entities",
        # InterfaceImpôtsDao, InterfaceImpôtsMétier, InterfaceImpôtsUi
        f"{root_dir}/impots/v04/interfaces",
        # AbstractImpôtsdao, ImpôtsConsole, ImpôtsMétier
        f"{root_dir}/impots/v04/services",
        # ImpotsDaoWithAdminDataInDatabase
        f"{root_dir}/impots/v05/services",
        # AdminData, ImpôtsError, TaxPayer
        f"{root_dir}/impots/v04/entities",
        # Stałe, przedziały
        f"{root_dir}/impots/v05/entities",
        # Rejestrator, SendAdminMail
        f"{root_dir}/impots/http-servers/02/utilities",
        # katalog głównego skryptu
        script_dir,
        # konfiguracje [database, layers, parameters, controllers, views]
        f"{script_dir}/../configs",
        # kontrolery
        f"{script_dir}/../controllers",
        # odpowiedzi HTTP
        f"{script_dir}/../responses",
        # szablony widoków
        f"{script_dir}/../models_for_views",
    ]

    # ustawiamy ścieżkę systemową
    from myutils import set_syspath
    set_syspath(absolute_dependencies)

    # udostępnianie konfiguracji
    return {
        "root_dir": root_dir,
        "script_dir": script_dir
    }
  • skrypt [syspath] służy do konfiguracji ścieżki Python Path aplikacji (wiersze 40–41);
  • dostarcza on dwóch informacji przydatnych dla innych skryptów konfiguracyjnych (wiersze 45–46);

Skrypt [mvc] konfiguruje architekturę MVC aplikacji internetowej jSON / XML / HTML:


def configure(config: dict) -> dict:
    # konfiguracja aplikacji MVC

    # kontrolery
    from AfficherCalculImpotController import AfficherCalculImpotController
    from AuthentifierUtilisateurController import AuthentifierUtilisateurController
    from CalculerImpotController import CalculerImpotController
    from CalculerImpotsController import CalculerImpotsController
    from FinSessionController import FinSessionController
    from GetAdminDataController import GetAdminDataController
    from InitSessionController import InitSessionController
    from ListerSimulationsController import ListerSimulationsController
    from MainController import MainController
    from SupprimerSimulationController import SupprimerSimulationController
    # odpowiedzi HTTP
    from HtmlResponse import HtmlResponse
    from JsonResponse import JsonResponse
    from XmlResponse import XmlResponse
    # szablony widoków
    from ModelForAuthentificationView import ModelForAuthentificationView
    from ModelForCalculImpotView import ModelForCalculImpotView
    from ModelForErreursView import ModelForErreursView
    from ModelForListeSimulationsView import ModelForListeSimulationsView

    # dozwolone akcje i ich kontrolery
    controllers = {
        # inicjowanie sesji obliczeniowej
        "init-session": InitSessionController(),
        # uwierzytelnianie użytkownika
        "authentifier-utilisateur": AuthentifierUtilisateurController(),
        # obliczanie podatku w trybie indywidualnym
        "calculer-impot": CalculerImpotController(),
        # obliczenie podatku w trybie zbiorczym
        "calculer-impots": CalculerImpotsController(),
        # lista symulacji
        "lister-simulations": ListerSimulationsController(),
        # usunięcie symulacji
        "supprimer-simulation": SupprimerSimulationController(),
        # zakończenie sesji obliczeniowej
        "fin-session": FinSessionController(),
        # wyświetlenie widoku obliczeń podatku
        "afficher-calcul-impot": AfficherCalculImpotController(),
        # pobieranie danych z urzędu skarbowego
        "get-admindata": GetAdminDataController(),
        # główny kontroler
        "main-controller": MainController()
    }
    # różne typy odpowiedzi (json, xml, html)
    responses = {
        "json": JsonResponse(),
        "html": HtmlResponse(),
        "xml": XmlResponse()
    }
    # widoki HTML i ich szablony zależą od stanu zwracanego przez kontroler
    views = [
        {
            # widok uwierzytelniania
            "états": [
                700,    # /init-session – sukces
                201,    # /autentyfikacja-użytkownika – niepowodzenie
            ],
            "view_name""views/vue-authentification.html",
            "model_for_view": ModelForAuthentificationView()
        },
        {
            # widok obliczeń podatku
            "états"[
                200,    # /uwierzytelnianie-użytkownika – pomyślnie
                300,    # /obliczyć-podatek pomyślnie
                301,    # /obliczyć-podatek – niepowodzenie
                800,    # /wyświetlenie-obliczeń-podatku pomyślnie
            ],
            "view_name""views/vue-calcul-impot.html",
            "model_for_view": ModelForCalculImpotView()
        },
        {
            # wyświetlenie listy symulacji
            "états"[
                500,    # /wyświetlić-listę-symulacji – pomyślnie
                600,    # /usuń-symulację – pomyślnie
            ],
            "view_name""views/vue-liste-simulations.html",
            "model_for_view": ModelForListeSimulationsView()
        },

    ]
    # widok nieoczekiwanych błędów
    view_erreurs = {
        "view_name""views/vue-erreurs.html",
        "model_for_view": ModelForErreursView()
    }
    # przekierowania
    redirections = [
        {
            "états": [
                400,  # /pomyślne zakończenie sesji
            ],
            # przekierowanie do URL
            "to""/init-session/html",
        }
    ]


    # przywracamy konfigurację MVC
    return {
        # kontrolery
        "controllers": controllers,
        # odpowiedzi HTTP
        "responses": responses,
        # widoki i szablony
        "views": views,
        # lista przekierowań
        "redirections": redirections,
        # widok nieoczekiwanych błędów
        "view_erreurs": view_erreurs
    }
  • wiersze 1–101: ten kod jest znany;
  • wiersze 105–116: przywracamy konfigurację aplikacji MVC;

Skrypt [parameters] gromadzi stałe aplikacji:


def configure(config: dict) -> dict:
    # konfiguracja aplikacji

    # script_dir
    script_dir = config['syspath']['script_dir']

    # konfiguracja aplikacji
    parameters = {
        # użytkownicy uprawnieni do korzystania z aplikacji
        "users"[
            {
                "login""admin",
                "password""$pbkdf2-sha256$29000$mPM.h3COkTIGYOzde68VIg$7LH5Q7rN/1hW.Xa.6rcmR6h52PntvVqd5.na7EtgQNw"
            }
        ],
        # plik logów
        "logsFilename"f"{script_dir}/../data/logs/logs.txt",
        # konfiguracja serwera SMTP
        "adminMail": {
            # serwer SMTP
            "smtp-server""localhost",
            # port serwera SMTP
            "smtp-port""25",
            # administrator
            "from""guest@localhost.com",
            "to""guest@localhost.com",
            # temat wiadomości e-mail
            "subject""plantage du serveur de calcul d'impôts",
            # tls ustawione na True, jeśli serwer SMTP wymaga uwierzytelnienia, w przeciwnym razie na False
            "tls"False
        },
        # czas przerwy wątku w sekundach
        "sleep_time"0,
        # serwer Redis
        "redis": {
            "host""127.0.0.1",
            "port"6379
        },
    }

    # ustawienia aplikacji
    return parameters
  • wiersz 13: od tej pory hasła użytkowników będą szyfrowane;
  • wiersze 34–38: konfiguracja serwera [Redis], do której jeszcze wrócimy;

Dzięki tym nowym plikom konfiguracyjnym skrypt [config] wygląda następująco:


def configure(config: dict) -> dict:
    # konfiguracja ścieżki systemowej
    import syspath
    config['syspath'] = syspath.configure(config)

    # konfiguracja aplikacji
    import parameters
    config['parameters'] = parameters.configure(config)

    # konfiguracja bazy danych
    import database
    config["database"] = database.configure(config)

    # instancjonowanie warstw aplikacji
    import layers
    config['layers'] = layers.configure(config)

    # konfiguracja warstwy MVC
    import mvc
    config['mvc'] = mvc.configure(config)

    # zwracamy konfigurację
    return config

33.2. Refaktoryzacja głównego skryptu [main]

Image

Główny skrypt [main.py] ogranicza się do uruchomienia serwera:


# oczekuje się na parametr mysql lub pgres
import sys

syntaxe = f"{sys.argv[0]} mysql / pgres"
erreur = len(sys.argv) != 2
if not erreur:
    sgbd = sys.argv[1].lower()
    erreur = sgbd != "mysql" and sgbd != "pgres"
if erreur:
    print(f"syntaxe : {syntaxe}")
    sys.exit()

# konfigurujemy aplikację
import config
config = config.configure({'sgbd': sgbd})

# zależności
from SendAdminMail import SendAdminMail
from Logger import Logger
from ImpôtsError import ImpôtsError
import redis

# wysyłanie wiadomości e-mail do administratora
def send_adminmail(config: dict, message: str):
    # wysyłanie wiadomości e-mail do administratora aplikacji
    config_mail = config['parameters']['adminMail']
    config_mail["logger"] = config['logger']
    SendAdminMail.send(config_mail, message)

# sprawdzanie pliku logów
logger = None
erreur = False
message_erreur = None
try:
    # moduł rejestrujący
    logger = Logger(config['parameters']['logsFilename'])
except BaseException as exception:
    # log konsoli
    print(f"L'erreur suivante s'est produite : {exception}")
    # odnotowuje się błąd
    erreur = True
    message_erreur = f"{exception}"
# zapisujemy logger w konfiguracji
config['logger'] = logger
# obsługa błędu
if erreur:
    # wiadomość e-mail do administratora
    send_adminmail(config, message_erreur)
    # zakończenie działania aplikacji
    sys.exit(1)

# log uruchomienia
log = "[serveur] démarrage du serveur"
logger.write(f"{log}\n")

# sprawdzanie dostępności serwera Redis
redis_client = redis.Redis(host=config["parameters"]["redis"]["host"],
                           port=config["parameters"]["redis"]["port"])
# wykonywanie polecenia ping do serwera Redis
try:
    redis_client.ping()
except BaseException as exception:
    # Redis niedostępny
    log = f"[serveur] Le serveur Redis n'est pas disponible : {exception}"
    # konsola
    print(log)
    # log
    logger.write(f"{log}\n")
    # koniec
    sys.exit(1)

# zapisujemy klienta [redis] w konfiguracji
config['redis_client'] = redis_client

# pobieranie danych z urzędu skarbowego
erreur = False
try:
    # admindata będzie danymi o zasięgu aplikacji, dostępnymi tylko do odczytu
    config["admindata"] = config["layers"]["dao"].get_admindata()
    # log pomyślnego wykonania
    logger.write("[serveur] connexion à la base de données réussie\n")
except ImpôtsError as ex:
    # odnotowano błąd
    erreur = True
    # log błędu
    log = f"L'erreur suivante s'est produite : {ex}"
    # konsola
    print(log)
    # plik logów
    logger.write(f"{log}\n")
    # wiadomość e-mail do administratora
    send_adminmail(config, log)

# główny wątek nie potrzebuje już modułu logującego
logger.close()

# w razie błędu następuje zatrzymanie
if erreur:
    sys.exit(2)

# import tras aplikacji internetowej
import routes
routes.config=config
routes.execute(__name__)
  • wiersze 56–73: dodano serwer Redis i jego klienta;
  • wiersze 102–104: trasy aplikacji zostały przeniesione do skryptu [routes];

33.2.1. Moduły [flask_session] i [redis]

Serwer [Redis] będzie służył do przechowywania sesji użytkowników. Do zarządzania tymi sesjami wykorzystamy moduł [flask_session]. Moduł ten może przechowywać sesje użytkowników w kilku lokalizacjach. Jedną z nich jest Redis i to właśnie z niego będziemy korzystać.

Moduł [flask_session] należy zainstalować w terminalu PyCharm:


(venv) C:\Data\st-2020\dev\python\cours-2020\python3-flask-2020\packages>pip install flask-session
Collecting flask-session

Aby komunikować się z serwerem Redis, potrzebujemy klienta Redis. Zapewni nam go moduł [redis], który również instalujemy:


(venv) C:\Data\st-2020\dev\python\cours-2020\python3-flask-2020\packages>pip install redis
Collecting redis

33.2.2. Serwer Redis

Serwer Redis będzie służył do przechowywania sesji użytkowników. Moduł [flask_session] działa w następujący sposób:

  • każdy użytkownik posiada identyfikator sesji i to właśnie on jest wysyłany do klienta – i tylko on. Klient otrzymuje plik cookie sesji tylko raz, po wysłaniu pierwszego żądania. Plik ten zawiera identyfikator sesji użytkownika, który nie ulegnie zmianie w trakcie kolejnych żądań wysyłanych przez klienta. Dlatego serwer nie musi ponownie wysyłać pliku cookie sesji;
  • wcześniej zawartość sesji była wysyłana do klienta. Teraz już tak nie będzie. Zawartość sesji użytkownika będzie przechowywana na serwerze Redis;

Laragon jest dostarczany z serwerem Redis, który domyślnie nie jest włączony. Należy więc najpierw go włączyć:

Image

  • w [3] włączyć serwer [Redis];
  • w [4] pozostawić port [6379], z którego domyślnie korzystają klienci Redis;

Usługi Laragon są automatycznie uruchamiane ponownie po aktywacji Redis:

Image

Z serwerem Redis można komunikować się w trybie poleceń. Otwieramy terminal Laragon [6]:

Image

  • w [1] polecenie [redis-cli] uruchamia klienta w trybie poleceń serwera Redis;

W lipcu 2019 r. klient Redis może korzystać ze 172 poleceń do komunikacji z serwerem [https://redis.io/commands#list]. Jedno z nich, [command count] [2], wyświetla tę liczbę: [3].

Zapis w [Redis] odbywa się za pomocą polecenia Redis [set attribut valeur] [4]. Wartość tę można następnie odczytać za pomocą polecenia [get attribut] [5].

Konieczne może być wyczyszczenie pamięci Redis. Odbywa się to za pomocą polecenia [flushdb] [6]. Następnie, jeśli zapytamy o wartość atrybutu za pomocą polecenia [titre] [7], otrzymamy odwołanie [nil] [8] wskazujące, że atrybut nie został znaleziony. Można również użyć polecenia [exists] [9-10], aby sprawdzić, czy dany atrybut istnieje.

Aby zamknąć klienta Redis, należy wpisać polecenie [quit] [11].

Można również skorzystać z interfejsu internetowego do zarządzania kluczami na serwerze Redis. W tym celu należy uruchomić serwer Apache w Laragonie:

Image

Pojawi się następujący interfejs:

Image

  • w [1-4], jedna z sesji przechowywanych na serwerze Redis;

33.2.3. Zarządzanie serwerem Redis w skrypcie głównym [main]

Skrypt [main] sprawdza obecność serwera Redis w następujący sposób:


import redis

# sprawdzamy dostępność serwera Redis
redis_client = redis.Redis(host=config["parameters"]["redis"]["host"],
                           port=config["parameters"]["redis"]["port"])
# wykonywany jest ping do serwera Redis
try:
    redis_client.ping()
except BaseException as exception:
    # Redis niedostępny
    log = f"[serveur] Le serveur Redis n'est pas disponible : {exception}"
    # konsola
    print(log)
    # log
    logger.write(f"{log}\n")
    # koniec
    sys.exit(1)

# zapisujemy klienta [redis] w konfiguracji
config['redis_client'] = redis_client
  • wiersz 4: konstruktor klasy [redis.Redis] tworzy klienta serwera Redis. Jego parametry (adres, port) znajdują się w skrypcie [parameters];
  • wiersz 8: metoda [ping] pozwala sprawdzić obecność serwera Redis;
  • wiersze 9–17: jeśli polecenie ping nie zakończy się powodzeniem, rejestruje się błąd i zatrzymuje serwer;
  • wiersz 20: identyfikator klienta Redis jest umieszczany w konfiguracji;

33.2.4. Zarządzanie trasami w głównym skrypcie [main]

Zarządzanie trasami w skrypcie [main] ogranicza się do następujących wierszy:


# import tras z aplikacji internetowej
import routes
routes.config=config
routes.execute(__name__)
  • wiersz 1: trasy zostały przeniesione do modułu [routes];
  • wiersz 3: trasy muszą znać konfigurację uruchomienia;
  • wiersz 4: uruchamia się aplikację Flask, przekazując jej nazwę wykonywanego skryptu (__main__);

Skrypt tras wygląda następująco:


# zależności
import os

from flask import Flask, redirect, request, session, url_for
from flask_api import status
from flask_session import Session

# aplikacja Flask
app = Flask(__name__, template_folder="../flask/templates", static_folder="../flask/static")

# konfiguracja aplikacji
config = {}

# kontroler frontowy
def front_controller() -> tuple:
    # przekierowujemy żądanie do głównego kontrolera
    main_controller = config['mvc']['controllers']['main-controller']
    return main_controller.execute(request, session, config)

@app.route('/', methods=['GET'])
def index() -> tuple:
    # przekierowanie do /init-session/html
    return redirect(url_for("init_session", type_response="html"), status.HTTP_302_FOUND)

# init-session
@app.route('/init-session/<string:type_response>', methods=['GET'])
def init_session(type_response: str) -> tuple:
    # uruchamiany jest kontroler powiązany z akcją
    return front_controller()

# uwierzytelnianie użytkownika
@app.route('/authentifier-utilisateur', methods=['POST'])
def authentifier_utilisateur() -> tuple:
    # uruchamiany jest kontroler powiązany z akcją
    return front_controller()

# oblicz-podatek
@app.route('/calculer-impot', methods=['POST'])
def calculer_impot() -> tuple:
    # uruchamiany jest kontroler powiązany z akcją
    return front_controller()

# obliczanie podatku partiami
@app.route('/calculer-impots', methods=['POST'])
def calculer_impots():
    # uruchamia się kontroler powiązany z akcją
    return front_controller()

# wyświetlanie listy symulacji
@app.route('/lister-simulations', methods=['GET'])
def lister_simulations() -> tuple:
    # uruchamiany jest kontroler powiązany z akcją
    return front_controller()

# usuń-symulację
@app.route('/supprimer-simulation/<int:numero>', methods=['GET'])
def supprimer_simulation(numero: int) -> tuple:
    # uruchamia się kontroler powiązany z akcją
    return front_controller()

# koniec sesji
@app.route('/fin-session', methods=['GET'])
def fin_session() -> tuple:
    # uruchamia się kontroler powiązany z akcją
    return front_controller()

# wyświetl-obliczenia-podatku
@app.route('/afficher-calcul-impot', methods=['GET'])
def afficher_calcul_impot() -> tuple:
    # uruchamiany jest kontroler powiązany z akcją
    return front_controller()

# pobierz-dane-administracyjne
@app.route('/get-admindata', methods=['GET'])
def get_admindata() -> tuple:
    # uruchamia się kontroler powiązany z akcją
    return front_controller()

def execute(name: str):
    # tajny klucz sesji
    app.secret_key = os.urandom(12).hex()
    # Flask-Session
    app.config.update(SESSION_TYPE='redis',
                      SESSION_REDIS=config['redis_client'])
    Session(app)
    # przypadek, w którym aplikacja Flask jest uruchamiana za pomocą skryptu konsolowego
    if name == '__main__':
        app.config.update(ENV="development", DEBUG=True)
        app.run(threaded=True)
  • wiersz 9: tworzona jest instancja aplikacji Flask;
  • wiersz 12: konfiguracja aplikacji nie jest jeszcze znana w momencie pisania skryptu. Staje się znana dopiero w momencie jego wykonania;
  • wiersze 20–77: trasy aplikacji zgodnie z definicjami z poprzedniej wersji. Nie ulegają one zmianie;
  • wiersze 14–18: wszystkie procedury ograniczają się do wywołania funkcji [front_controller]. Usunęliśmy z niej pierwotny kod. Obecnie funkcja ta ogranicza się do wywołania głównego kontrolera aplikacji internetowej;
  • wiersze 79–89: [execute] to funkcja wywoływana przez skrypt [main] w celu uruchomienia aplikacji internetowej;
  • wiersz 81: moduł [flask_session] wykorzystuje tajny klucz Flaska;
  • wiersze 82–84: konfiguracja modułu [flask_session]. Polega ona na dodaniu kluczy [SESSION_TYPE, SESSION_REDIS] do konfiguracji [app.config] aplikacji Flask [app]:
  • [SESSION_TYPE]: typ sesji. Istnieje kilka typów. Typ [redis] oznacza, że [flask_session] korzysta z serwera [redis] do przechowywania sesji użytkowników. Z tego powodu należy zdefiniować klucz [SESSION_REDIS], który powinien odnosić się do klienta Redis;
  • wiersz 85: sesja [Flask-Session] jest powiązana z aplikacją Flask;
  • wiersze 86–89: jeśli parametr [name] z wiersza 79 ma wartość ciągu znaków [__main__], wówczas uruchamiana jest aplikacja Flask;

33.3. Refaktoryzacja głównego kontrolera

Kod, który wcześniej znajdował się w funkcji [front_controller] skryptu [main], został przeniesiony do głównego kontrolera:

Image


# importowanie zależności
import threading
import time
from random import randint

from flask_api import status
from werkzeug.local import LocalProxy

from InterfaceController import InterfaceController
from Logger import Logger
from SendAdminMail import SendAdminMail

def send_adminmail(config: dict, message: str):
    # wysyłamy wiadomość e-mail do administratora aplikacji
    config_mail = config['parameters']['adminMail']
    config_mail["logger"] = config['logger']
    SendAdminMail.send(config_mail, message)

# główny kontroler aplikacji
class MainController(InterfaceController):
    def execute(self, request: LocalProxy, session: LocalProxy, config: dict) -> (dict, int):
        # przetwarzanie żądania
        logger = None
        action = None
        type_response1 = None
        try:
            # pobierane są elementy ścieżki
            params = request.path.split('/')

            # akcja jest pierwszym elementem
            action = params[1]

            # na początku nie ma błędów
            erreur = False

            # typ sesji musi być znany przed wykonaniem niektórych akcji
            type_response1 = session.get('typeResponse')
            if type_response1 is None and action != "init-session":
                # odnotowujemy błąd
                résultat = {"action": action, "état"101,
                            "réponse"["pas de session en cours. Commencer par action [init-session]"]}
                erreur = True

            # rejestrowanie
            logger = Logger(config['parameters']['logsFilename'])

            # zapisujemy go w konfiguracji powiązanej z wątkiem
            thread_config = {"logger": logger}
            thread_name = threading.current_thread().name
            config[thread_name] = {"config": thread_config}

            # rejestruje się żądanie
            logger.write(f"[MainController] requête : {request}\n")

            # przerywamy wątek, jeśli zostało to zlecone
            sleep_time = config['parameters']['sleep_time']
            if sleep_time != 0:
                # przerwa jest losowa, aby niektóre wątki zostały przerwane, a inne nie
                aléa = randint(01)
                if aléa == 1:
                    # logowanie przed przerwą
                    logger.write(f"[front_controller] mis en pause du thread pendant {sleep_time} seconde(s)\n")
                    # przerwa
                    time.sleep(sleep_time)

            # w przypadku niektórych działań wymagane jest uwierzytelnienie
            user = session.get('user')
            if not erreur and user is None and action not in ["init-session""authentifier-utilisateur"]:
                # odnotowujemy błąd
                résultat = {"action": action, "état"101,
                            "réponse"[f"action [{action}] demandée par utilisateur non authentifié"]}
                erreur = True

            # czy występują błędy?
            if erreur:
                # żądanie jest nieprawidłowe
                status_code = status.HTTP_400_BAD_REQUEST
            else:
                # uruchamiany jest kontroler powiązany z akcją
                controller = config['mvc']['controllers'][action]
                résultat, status_code = controller.execute(request, session, config)

        except BaseException as exception:
            # inne (nieoczekiwane) wyjątki
            résultat = {"action": action, "état"131"réponse"[f"{exception}"]}
            status_code = status.HTTP_400_BAD_REQUEST

        finally:
            pass

        # rejestruje się wynik wysłany do klienta
        log = f"[MainController] {résultat}\n"
        logger.write(log)

        # czy wystąpił błąd krytyczny?
        if status_code == status.HTTP_500_INTERNAL_SERVER_ERROR:
            # wysyłamy wiadomość e-mail do administratora aplikacji
            send_adminmail(config, log)

        # określa się żądany typ odpowiedzi
        type_response2 = session.get('typeResponse')
        if type_response2 is None and type_response1 is None:
            # typ sesji nie został jeszcze ustalony – będzie to jSON
            type_response = 'json'
        elif type_response2 is not None:
            # typ odpowiedzi jest znany i znajduje się w sesji
            type_response = type_response2
        else:
            # w przeciwnym razie nadal używa się type_response1
            type_response = type_response1

        # tworzy się odpowiedź do wysłania
        response_builder = config['mvc']['responses'][type_response]
        response, status_code = response_builder \
            .build_http_response(request, session, config, status_code, résultat)

        # zamyka się plik logów, jeśli był otwarty
        if logger:
            logger.close()

        # wysyłamy odpowiedź HTTP
        return response, status_code

Cały ten kod pojawiał się już wcześniej w różnych momentach.

33.4. Obsługa zaszyfrowanych haseł

Do zarządzania zaszyfrowanymi hasłami wykorzystamy moduł [passlib], który instalujemy z poziomu terminala PyCharm:


(venv) C:\Data\st-2020\dev\python\cours-2020\python3-flask-2020\packages>pip install passlib
Collecting passlib

Oto przykład skryptu szyfrującego hasło przekazane mu jako parametr:

Image

Skrypt [create_hashed_password] wygląda następująco (https://passlib.readthedocs.io/en/stable/):


import sys

# funkcja szyfrowania
from passlib.hash import pbkdf2_sha256

# oczekuje się na hasło do zaszyfrowania
syntaxe = f"{sys.argv[0]} password"
erreur = len(sys.argv) != 2
if erreur:
    print(f"syntaxe : {syntaxe}")
    sys.exit()
else:
    password = sys.argv[1]

# szyfruje się hasło
hash = pbkdf2_sha256.hash(password)
print(f"version cryptée de [{password}] = {hash}")

# weryfikacja
correct = pbkdf2_sha256.verify(password, hash)
print(correct)
  • wiersz 16: szyfruje się hasło przekazane jako parametr;
  • wiersz 20: porównuje się hasło [password] przekazane jako parametr z jego zaszyfrowaną wersją [hash]. Funkcja [verify] szyfruje hasło [password] i porównuje uzyskany zaszyfrowany ciąg znaków z [hash]. Zwraca wartość True, jeśli oba ciągi znaków są równe;

Powyższy skrypt pozwala nam uzyskać zaszyfrowaną wersję hasła [admin]:


C:\Data\st-2020\dev\python\cours-2020\python3-flask-2020\venv\Scripts\python.exe C:/Data/st-2020/dev/python/cours-2020/python3-flask-2020/impots/http-servers/08/passlib/create_hashed_password.py admin
version cryptée de [admin] = $pbkdf2-sha256$29000$fU9pTendO6c0ZoyR8r5Xqg$5ZXywIUnbMfN2hPnBaefiuqWjEbmAY.Lu06i4dwcnek
True

Wiersz 2, wartość, którą wprowadzamy do skryptu [parameters]:


        "users"[
            {
                "login""admin",
                "password""$pbkdf2-sha256$29000$fU9pTendO6c0ZoyR8r5Xqg$5ZXywIUnbMfN2hPnBaefiuqWjEbmAY.Lu06i4dwcnek"
            }
        ],

Kontroler uwierzytelniający [AuthentifierUtilisateurController] zmienia się w następujący sposób:


from passlib.handlers.pbkdf2 import pbkdf2_sha256

            # sprawdzana jest poprawność pary (użytkownik, hasło)
            users = config['parameters']['users']
            i = 0
            nbusers = len(users)
            trouvé = False
            while not trouvé and i < nbusers:
                trouvé = user == users[i]["login"] and pbkdf2_sha256.verify(password, users[i]["password"])
                i += 1
            # znaleziono?
            if not trouvé:

33.5. Testy

Oprócz testów w przeglądarce można również skorzystać z klientów z folderu [http-servers/07] napisanych dla wersji 12. Powinny one działać również w wersji 13:

Image

  • w [1] wszystkie trzy klienckie programy powinny działać;
  • w [2] oba testy powinny działać;