Bases de données
Interaction Python - PostgreSQL avec Psycopg
Nadime Francis
Université Gustave Eiffel
LIGM - 4B130 Copernic
[email protected] 1 / 12
Psycopg
Psycopg
Psycopg : database adapter de PostgreSQL vers Python
Envoie les requêtes à PostgreSQL
Traduit les résultats dans un format exploitable par Python
Remarque : ce cours utilise Psycopg 2
Psycopg 3 est en cours de développement (version 3.0 le 13/10/2021)
2 / 12
Connexion à PostgreSQL
Fonction connect : création d’une nouvelle connexion à Postgres
⇒ requiert les mêmes informations de connexion que psql
import psycopg2
import psycopg2
conn = psycopg2.connect(
host = "sqletud.u-pem.fr", conn = psycopg2.connect(
dbname = "zelia_db", dbname = "DemoPsyco",
password = "miaou18", )
)
# connexion à la base de données
# connexion à la base de données # locale 'DemoPsyco' sans mdp
# étudiante 'zelia_db'
Fermeture de la connexion :
conn.close()
3 / 12
Curseurs
La connexion communique avec la base à l’aide de curseurs
Une connexion peut avoir plusieurs curseurs
# creation du curseur
cur = conn.cursor()
# on peut utiliser le curseur ici
cur.execute('SELECT ∗ FROM players')
# fermeture du curseur
cur.close()
4 / 12
Curseurs
La connexion communique avec la base à l’aide de curseurs
Une connexion peut avoir plusieurs curseurs
# creation du curseur
cur = conn.cursor()
# on peut utiliser le curseur ici
cur.execute('SELECT ∗ FROM players')
# fermeture du curseur
cur.close()
Les curseurs peuvent être utilisés comme context managers
(fonctionne aussi avec les connexions !)
# creation du curseur dans un contexte
with conn.cursor() as cur:
cur.execute('SELECT ∗ FROM players')
# cur.close() est implicitement appelé à la sortie du contexte
4 / 12
Requêtes SELECT
Les requêtes sont transmises à PostgreSQL par la méthode execute :
cur.execute('SELECT ∗ FROM players')
On peut récupérer les résultats :
1 En itérant sur le curseur :
for result in cur:
print(result)
2 Avec les méthodes fetchone(), fetchmany(nb) et fetchall() :
# récupération du premier resultat (ou None s'il n'y en a pas)
first_result = cur.fetchone()
# récupération des résutlats restants dans une liste
# le premier resultat a déjà été récupéré par fetchone
lst_results = cur.fetchall()
Remarques :
Par défaut, les résultats sont des tuples Python
Le curseur se vide au fur et à mesure qu’il renvoie les résultats
5 / 12
Tuples nommés
Par défaut, les résultats sont des tuples Python
psycopg2.extras fournit plusieurs alternatives, dont les tuples nommés
cur = conn.cursor(cursor_factory = psycopg2.extras.NamedTupleCursor)
cur.execute('SELECT nick, score FROM players')
for result in cur:
print(result.nick, result.score) # même résultat que print(result[0], result[1])
6 / 12
Tuples nommés
Par défaut, les résultats sont des tuples Python
psycopg2.extras fournit plusieurs alternatives, dont les tuples nommés
cur = conn.cursor(cursor_factory = psycopg2.extras.NamedTupleCursor)
cur.execute('SELECT nick, score FROM players')
for result in cur:
print(result.nick, result.score) # même résultat que print(result[0], result[1])
Remarques :
Accès aux champs du tuple par indice ou par nom
Attention, tous les champs doivent avoir un nom différent
cur.execute('SELECT max(level), max(score) FROM players')
# erreur : par défaut, les deux champs s'appellent 'max'
cur.execute('SELECT max(level) AS mLevel, max(score) AS mScore FROM players')
# version corrigée
6 / 12
Requêtes INSERT, UPDATE et DELETE
Les mises-à-jour ne sont pas immédiatement visibles dans la base !
But : éviter les exécutions partielles et interférences d’autres connexions
cur.execute('UPDATE players SET level = level + 1 WHERE score >= 1000')
# Et si le serveur tombe en panne ici ? Ou si un joueur gagne des points ?
cur.execute('UPDATE players SET score = score - 1000 WHERE score >= 1000')
7 / 12
Requêtes INSERT, UPDATE et DELETE
Les mises-à-jour ne sont pas immédiatement visibles dans la base !
But : éviter les exécutions partielles et interférences d’autres connexions
cur.execute('UPDATE players SET level = level + 1 WHERE score >= 1000')
# Et si le serveur tombe en panne ici ? Ou si un joueur gagne des points ?
cur.execute('UPDATE players SET score = score - 1000 WHERE score >= 1000')
La connexion doit valider les modifications :
conn.commit() : rend visible les modifications en attente
conn.rollback() : annule et revient à l’état du dernier commit
7 / 12
Requêtes INSERT, UPDATE et DELETE
Les mises-à-jour ne sont pas immédiatement visibles dans la base !
But : éviter les exécutions partielles et interférences d’autres connexions
cur.execute('UPDATE players SET level = level + 1 WHERE score >= 1000')
# Et si le serveur tombe en panne ici ? Ou si un joueur gagne des points ?
cur.execute('UPDATE players SET score = score - 1000 WHERE score >= 1000')
La connexion doit valider les modifications :
conn.commit() : rend visible les modifications en attente
conn.rollback() : annule et revient à l’état du dernier commit
Remarque : la gestion de la concurrence est complexe et délicate
Pour ce cours, on va utiliser le mode autocommit
⇒ validation immédiate et automatique des requêtes
(C’est dommage, mais chaque chose en son temps... Rendez-vous en L3 pour la suite !)
7 / 12
Intégration au site web
Une connexion à la base par requête HTTP
⇒ deux utilisateurs ne partagent pas la même connexion !
Un curseur par requête SQL
⇒ deux fonctions Python ne partagent pas le même curseur !
8 / 12
Intégration au site web
Une connexion à la base par requête HTTP
⇒ deux utilisateurs ne partagent pas la même connexion !
Un curseur par requête SQL
⇒ deux fonctions Python ne partagent pas le même curseur !
Pour le projet : fichier db.py obligatoirement paramétré comme suit :
import psycopg2
import psycopg2.extras
def connect():
conn = psycopg2.connect(
host = 'sqletud.u-pem.fr',
dbname = 'zelia_db', # nom de votre base de données
password = 'miaou18', # mot de passe de la base
cursor_factory = psycopg2.extras.NamedTupleCursor,
)
conn.autocommit = True
return conn
conn = db.connect() à chaque requête HTTP utilisant la base de données
conn.close() avant le rendu du template
8 / 12
Exemple : liste des joueurs et page de profil
La fonction playerList :
@app.route("/playerList")
def playerList():
with db.connect() as conn:
with conn.cursor() as cur:
cur.execute("select pid, nick, avatar, level from players order by level desc")
result = cur.fetchall()
return render_template("player_list.html", plist = result)
Et la fonction profile :
@app.route("/profile/<int:pid>")
def profile(pid):
with db.connect() as conn:
with conn.cursor() as cur:
cur.execute("select ∗ from players where pid = %s", (pid,))
player = cur.fetchone()
if not player:
return render_template("profile_error.html")
return render_template("profile.html", player = player)
9 / 12
Injection SQL
On ne fait JAMAIS confiance à une donnée rentrée par l’utilisateur :
Erreurs involontaires : fautes de frappe, oublis, mauvais format...
Exploitation de failles de sécurité, en particulier injection SQL
10 / 12
Injection SQL
On ne fait JAMAIS confiance à une donnée rentrée par l’utilisateur :
Erreurs involontaires : fautes de frappe, oublis, mauvais format...
Exploitation de failles de sécurité, en particulier injection SQL
Extrait de la documentation de Psycopg
Never, never, NEVER use Python string concatenation (+) or string parameters
interpolation (%) to pass variables to a SQL query string. Not even at gunpoint.
10 / 12
Injection SQL
On ne fait JAMAIS confiance à une donnée rentrée par l’utilisateur :
Erreurs involontaires : fautes de frappe, oublis, mauvais format...
Exploitation de failles de sécurité, en particulier injection SQL
Extrait de la documentation de Psycopg
Never, never, NEVER use Python string concatenation (+) or string parameters
interpolation (%) to pass variables to a SQL query string. Not even at gunpoint.
source : xkcd.com
10 / 12
Injection SQL
On ne fait JAMAIS confiance à une donnée rentrée par l’utilisateur :
Erreurs involontaires : fautes de frappe, oublis, mauvais format...
Exploitation de failles de sécurité, en particulier injection SQL
Extrait de la documentation de Psycopg
Never, never, NEVER use Python string concatenation (+) or string parameters
interpolation (%) to pass variables to a SQL query string. Not even at gunpoint.
Ce qu’il ne faut pas faire :
with conn.cursor() as cur:
cur.execute(
"insert into players(nick, message) values ('"+form_nick+"','"+form_message+"')"
)
10 / 12
Injection SQL
On ne fait JAMAIS confiance à une donnée rentrée par l’utilisateur :
Erreurs involontaires : fautes de frappe, oublis, mauvais format...
Exploitation de failles de sécurité, en particulier injection SQL
Extrait de la documentation de Psycopg
Never, never, NEVER use Python string concatenation (+) or string parameters
interpolation (%) to pass variables to a SQL query string. Not even at gunpoint.
Ce qu’il ne faut pas faire :
with conn.cursor() as cur:
cur.execute(
"insert into players(nick, message) values ('"+form_nick+"','"+form_message+"')"
)
execute permet d’encapsuler correctement les paramètres de la requête :
with conn.cursor() as cur:
cur.execute(
"insert into players(nick, message) values (%s,%s)", (form_nick, form_message)
)
10 / 12
Gestion des mots de passe
On ne stocke JAMAIS de mots de passe en clair !
En cas de fuite, l’attaquant peut se faire passer pour l’utilisateur
En cas de réutilisation, les autres comptes sont aussi vulnérables
11 / 12
Gestion des mots de passe
On ne stocke JAMAIS de mots de passe en clair !
En cas de fuite, l’attaquant peut se faire passer pour l’utilisateur
En cas de réutilisation, les autres comptes sont aussi vulnérables
Idée : on stocke une empreinte du mot de passe, appelée hash
Le calcul mot de passe → hash est très simple
Le calcul hash → mot de passe est presque impossible
Il est très rare que deux mots de passe différents aient le même hash
11 / 12
Gestion des mots de passe
On ne stocke JAMAIS de mots de passe en clair !
En cas de fuite, l’attaquant peut se faire passer pour l’utilisateur
En cas de réutilisation, les autres comptes sont aussi vulnérables
Idée : on stocke une empreinte du mot de passe, appelée hash
Le calcul mot de passe → hash est très simple
Le calcul hash → mot de passe est presque impossible
Il est très rare que deux mots de passe différents aient le même hash
L’étude du hachage sort du cadre de ce cours (C’est de la crypto !)
Nous allons utiliser une bibliothèque Python qui fait le travail pour nous :
from passlib.context import CryptContext
password_ctx = CryptContext(schemes=['bcrypt']) # configuration de la bibliothèque
hash_pw = password_ctx.hash("miaou18") # calcul du hash, à stocker dans la base
password_ctx.verify("miaou18", hash_pw) # test à effectuer au login de l'utilisateur
11 / 12
Pour aller plus loin...
Ce cours n’aborde pas de nombreuses fonctionnalités avancées, dont :
Gestion de la concurrence et des transactions (Rendez-vous en L3)
L’accès à la structure de la base et des réponses
⇒ schéma, type de données, nombre de résultats, temps de calcul...
Les curseurs côté serveur
Les contextes des requêtes HTTP dans Flask
⇒ Comment ne pas ouvrir et fermer la connexion à chaque fonction ?
Le fonctionnement et les paramètres de passlib
Et bien d’autres encore...
Pour aller plus loin, consulter les documentations officielles :
https://www.psycopg.org/docs/
https://passlib.readthedocs.io/
12 / 12