Ga naar hoofdinhoud

Wachtwoorden: een server met accounts

Hier bouw je op verder: Tuples uitpakken

Weet je dit niet meer zeker? Lees het terug voordat je verdergaat.

Waar je werkt

Maak voor deze reeks een nieuwe map veiligheid-wachtwoorden, naast je eigen project, en open die in VS Code. De server, de scripts en de database van deze reeks komen daarin. Zo blijft je eigen gastenboek heel. Aan het eind van de reeks pas je het geleerde toe op je eigen project. Maak in die map een virtual environment en installeer de packages; hoe dat gaat staat bij Een map per reeks.

Je zet de server van deze reeks neer en maakt met een script drie accounts aan.

Je gastenboek heeft accounts sinds Registreren: een account opslaan, en controleert het wachtwoord sinds Inloggen: een wachtwoord controleren. Het bewaart dat wachtwoord precies zoals de gebruiker het intypt; in Registreren zag je het al staan. Deze reeks laat zien waarom dat gevaarlijk is, en maakt het stap voor stap veilig.

Het startbestand​

Je werkt met dezelfde twee stappen, als een kleine server die JSON teruggeeft, zodat een script elk antwoord kan lezen: /registreer uit de basis, en /inloggen met de controle uit je gastenboek, als eigen endpoint zoals in Sessies, maar nog zonder sessie. Een fout wachtwoord geeft hier geen 401 maar een melding: het gaat in deze reeks om wat er in de database staat. Zet dit in main.py:

from fastapi import FastAPI, Form, HTTPException
from sqlitedict import SqliteDict

app = FastAPI()

@app.post("/registreer")
async def registreer(naam: str = Form(...), wachtwoord: str = Form(...)):
with SqliteDict("gebruikers.db") as db:
if naam in db:
raise HTTPException(status_code=400, detail="Deze naam is al bezet")
db[naam] = wachtwoord
db.commit()
return {"bericht": f"Welkom, {naam}"}

@app.post("/inloggen")
async def inloggen(naam: str = Form(...), wachtwoord: str = Form(...)):
with SqliteDict("gebruikers.db") as db:
if db.get(naam) == wachtwoord:
return {"bericht": "Ingelogd"}
return {"bericht": "Naam of wachtwoord klopt niet"}
  1. Regel 9-10:

    Bestaat de naam al, dan weigert de server hem met een 400. Zonder die controle overschrijft iemand die zich als sara registreert het wachtwoord van Sara, en kan hij daarna als haar inloggen.

  2. Regel 11:

    Het wachtwoord gaat letterlijk de database in, onder de naam van de gebruiker.

  3. Regel 18:

    Inloggen vergelijkt wat de gebruiker intypt met wat er in de database staat. Staat de naam er niet in, dan geeft db.get None, en dat is nooit gelijk aan een wachtwoord.

Start de server met fastapi dev main.py.

Drie accounts aanmaken​

Alleen je eigen database

Doe dit uitsluitend met je eigen server op 127.0.0.1 en je eigen database. De wachtwoorden van een ander bekijken of gebruiken is strafbaar.

Maak naast main.py een los scriptje registreer.py dat drie gebruikers aanmaakt, net zoals een formulier dat zou doen. Het gebruikt httpx, zoals in Een script als bezoeker.

import httpx

adres = "http://127.0.0.1:8000/registreer"

gebruikers = [("sara", "welkom123"), ("alex", "Zonnebloem-Fiets-42"), ("noor", "welkom123")]
for naam, wachtwoord in gebruikers:
antwoord = httpx.post(adres, data={"naam": naam, "wachtwoord": wachtwoord})
print(antwoord.json())
  1. Regel 5:

    Drie gebruikers, elk een tuple van naam en wachtwoord. Sara en Noor kozen hetzelfde wachtwoord.

  2. Regel 7:

    httpx.post stuurt een formulier: data zijn de velden, net als name="naam" en name="wachtwoord" in een HTML-formulier.

Draai het in een tweede terminal, in dezelfde map.

Zo ziet je main.py er nu uit
from fastapi import FastAPI, Form, HTTPException
from sqlitedict import SqliteDict

app = FastAPI()

@app.post("/registreer")
async def registreer(naam: str = Form(...), wachtwoord: str = Form(...)):
with SqliteDict("gebruikers.db") as db:
if naam in db:
raise HTTPException(status_code=400, detail="Deze naam is al bezet")
db[naam] = wachtwoord
db.commit()
return {"bericht": f"Welkom, {naam}"}

@app.post("/inloggen")
async def inloggen(naam: str = Form(...), wachtwoord: str = Form(...)):
with SqliteDict("gebruikers.db") as db:
if db.get(naam) == wachtwoord:
return {"bericht": "Ingelogd"}
return {"bericht": "Naam of wachtwoord klopt niet"}

Dit is de beginstand. In de volgende lessen verandert steeds één stuk.

Controlevraag
Je zet in de lijst van registreer.py een vierde gebruiker achter de drie: tim, met het wachtwoord geheim. Daarna draai je het script nog een keer. Wat print het?
Kies wat jij denkt. Bij het goede antwoord zie je de uitleg.

Er gaat iets mis​

Deze naam is al bezet​

Je draait registreer.py een tweede keer, en krijgt:

{'detail': 'Deze naam is al bezet'}
{'detail': 'Deze naam is al bezet'}
{'detail': 'Deze naam is al bezet'}

Oorzaak: de eerste keer heeft je server de drie accounts al opgeslagen in gebruikers.db. Een tweede registratie met dezelfde naam weigert hij met een 400.

Oplossing: niets, je accounts staan er al. Wil je opnieuw beginnen, gooi dan gebruikers.db weg en draai het script opnieuw. Je server mag blijven draaien: hij opent het bestand alleen tijdens een verzoek, en maakt bij het eerste verzoek een nieuw, leeg bestand.

Opdrachten​

Opdracht 1: Modify - Inloggen met een script​

Maak een kopie van registreer.py die inlogt in plaats van registreert: één keer als Sara met welkom123, en één keer als Sara met welkom124.

Tip

Verander het adres naar /inloggen, en zet in de lijst twee pogingen voor Sara.

Antwoord
import httpx

adres = "http://127.0.0.1:8000/inloggen"

pogingen = [("sara", "welkom123"), ("sara", "welkom124")]
for naam, wachtwoord in pogingen:
antwoord = httpx.post(adres, data={"naam": naam, "wachtwoord": wachtwoord})
print(antwoord.json())

Uitvoer:

{'bericht': 'Ingelogd'}
{'bericht': 'Naam of wachtwoord klopt niet'}

Inloggen verandert niets in de database, dus je kunt dit script zo vaak draaien als je wilt.

Door naar les 2: waarom geen gewone tekst.