Grafikus C# – Windows Forms tanfolyam › 3.3 lecke

Windows Forms · 3.3 lecke · biztonságos adatkezelés

Bejelentkezés SQLite-tal – paraméterezett lekérdezés

Egy bejelentkező ablak az első olyan program, amelynél a biztonság is számít. A felhasználó által beírt szöveget soha nem szabad egyszerűen az SQL parancsba fűzni, mert az adatbázis a beírt szöveget is parancsnak veheti. Ebben a leckében megírjuk a bejelentkezést, és megtanuljuk a biztonságos megoldást: a paraméterezett lekérdezést.

  • kb. 90 perc
  • közepes szint
  • biztonság
Anime stílusú fiú és lány egy C# programozási feladatot beszél meg

A lecke végére

  • bejelentkező ablakot készítesz jelszómezővel;
  • érted, mi az SQL-befecskendezés (SQL injection), és miért veszélyes;
  • paraméterezett lekérdezést írsz (@nev, Parameters.AddWithValue);
  • az adatbázis-műveletet külön metódusba (DB.BelepesJo) teszed, amely bool értéket ad vissza;
  • nem árulod el a felhasználónak, hogy a név vagy a jelszó volt-e hibás;
  • felismered a bejelentkezés tipikus hibáit.

Mielőtt elkezded

Ehhez a leckéhez kell a 3.2 lecke (DB.cs, kapcsolat), az 1.9 lecke (jelszómező), az 1.10 lecke (visszajelzés) és az 1.11 lecke (bool visszatérési értékű metódus). A belepes.db adatbázisban legyen legalább egy felhasználó (3.1 lecke).

3.2 DB.cs és adatbázis-kapcsolat →

A felület

Új Windows Forms projektet használj a 3.2 leckéből (NuGet csomag, belepes.db, DB.cs), vagy bővítsd azt. Tegyél az ablakra:

txtFelhasznaloEgy TextBox a felhasználónévnek.
txtJelszoEgy TextBox a jelszónak, UseSystemPasswordChar: True (1.9 lecke).
btnBelepEgy gomb Belépés felirattal. Az ablak AcceptButton tulajdonsága legyen ez a gomb, hogy az Enter is működjön.
lblUzenetEgy Label a visszajelzéshez (1.10 lecke).
📷 Képernyőkép helye

Mit tegyél ide: a bejelentkező ablak tervezőnézete: két beviteli mező címkékkel (Felhasználónév, Jelszó), a Belépés gomb és az üzenet-felirat.Javasolt fájlnév: winforms-3-3-felulet.webp · Alt szöveg: A bejelentkező ablak felülete két beviteli mezővel és gombbal

A veszély: SQL-befecskendezés

A kézenfekvő, de veszélyes megoldás az, ha a beírt szöveget közvetlenül az SQL szövegébe fűzöd:

VESZÉLYES, így ne csináldC#
string sql = "SELECT COUNT(*) FROM belepesi_adatok WHERE felhnev = '" + txtFelhasznalo.Text
+ "' AND jelszo = '" + txtJelszo.Text + "'";

Normál bevitelnél ez jól működik. De mi történik, ha a felhasználó nem névvel és jelszóval, hanem az SQL nyelv egy darabjával tölti ki a mezőket? Az adatbázis nem tudja, hogy melyik rész a program parancsa, és melyik a felhasználó adata: az egész egyetlen szöveg. Így a beírt szöveg átalakíthatja a lekérdezés jelentését. Ezt nevezik SQL-befecskendezésnek (angolul SQL injection), és az egyik leggyakoribb és legsúlyosabb webes és alkalmazásbiztonsági hiba.

Jósolj!

A felhasználónév mezőbe egyetlen aposztrófot (') írsz, a jelszó mezőbe bármit. Mi történik a fenti, összefűzött SQL-lel?

Jóslat ellenőrzése

Az aposztróf az SQL-ben a szöveg határolója. Az összefűzött SQL-ben a határolók felborulnak, és az SQL szintaktikailag hibás lesz: a program hibaüzenetet ad, vagy összeomlik. Már ez is jelzi, hogy a felhasználó beavatkozik a parancs szerkezetébe. Egy rosszindulatú felhasználó pedig úgy írja meg a szöveget, hogy a parancs érvényes marad, de más lesz a jelentése, például a feltétel mindig igaz lesz, és jelszó nélkül is beengedi.

Ez nem csak a bejelentkezésnél baj

Ugyanaz a hiba fenyeget minden olyan helyen, ahol a felhasználó szövege az SQL-be kerül: keresőmezőben, regisztrációnál, szerkesztésnél. Az ilyen hibán keresztül adatokat lehet kiolvasni, módosítani és törölni is. Ezért tanuljuk meg az egyetlen helyes megoldást: soha ne fűzz felhasználói szöveget az SQL-be.

A megoldás: paraméterezett lekérdezés

A biztonságos megoldásban az SQL szövegében helyfoglaló jelek (paraméterek) állnak, amelyek @ jellel kezdődnek (@nev, @jelszo). A tényleges értékeket külön adjuk át az SQL parancsnak. Az adatbázis így tudja, hogy a parancs szerkezete rögzített, a paraméterek pedig csak adatok, amelyeket sosem értelmez parancsként, bármi áll is bennük (akár aposztróf is):

MintaC#
string sql = "SELECT COUNT(*) FROM belepesi_adatok WHERE felhnev = @nev AND jelszo = @jelszo";

using (SQLiteCommand parancs = new SQLiteCommand(sql, kapcsolat))
{
parancs.Parameters.AddWithValue("@nev", nev);
parancs.Parameters.AddWithValue("@jelszo", jelszo);
// ... ExecuteScalar ...
}
@nev, @jelszoParaméterek: az SQL szövegében helyet foglalnak a későbbi értékeknek. Nem kell köréjük aposztróf.
Parameters.AddWithValue("@nev", nev)A paraméternek értéket ad: az @nev helyére a nev változó tartalma kerül, adatként.
Az SQL szövege rögzítettA sql változó tartalma sosem függ a felhasználó bevitelétől.
Plusz előnyAz aposztrófos nevek (például O'Brien) is hibátlanul működnek, mert a paraméter értékét nem kell külön védeni.
Összefűzéssel: a bevitel része lesz a parancsnak→Paraméterrel: a bevitel mindig csak adat marad

Jósolj!

A felhasználónévnek az O'Brien nevet adod meg paraméterként. Szintaktikai hibát okoz az aposztróf a névben?

Jóslat ellenőrzése

Nem. A paraméter értéke soha nem kerül az SQL szövegébe, hanem külön, adatként jut el az adatbázishoz. Az aposztróf így egyszerű karakter, és a keresés az O'Brien névre pontosan úgy működik, mint bármelyik másikra.

A teljes kód: DB.BelepesJo és a gomb

Az adatbázis-műveletet a DB osztályba tesszük egy metódusként, amely igaz/hamis választ ad: jó-e a belépési adatpár. Az ablak csak ezt a választ használja fel (1.11 lecke: bool visszatérési érték). Így az adatbázis-kód és a felület külön marad:

DB.cs (a DB osztályba, az UjKapcsolat() mellé)C#
public static bool BelepesJo(string nev, string jelszo)
{
using (SQLiteConnection kapcsolat = UjKapcsolat())
{
kapcsolat.Open();

string sql = "SELECT COUNT(*) FROM belepesi_adatok WHERE felhnev = @nev AND jelszo = @jelszo";

using (SQLiteCommand parancs = new SQLiteCommand(sql, kapcsolat))
{
parancs.Parameters.AddWithValue("@nev", nev);
parancs.Parameters.AddWithValue("@jelszo", jelszo);

int darab = Convert.ToInt32(parancs.ExecuteScalar());
return darab > 0;
}
}
}
Form1.csC#
private void Uzenet(string szoveg, bool hiba)
{
lblUzenet.Text = szoveg;
lblUzenet.ForeColor = hiba ? Color.Firebrick : Color.SeaGreen;
}

private void btnBelep_Click(object sender, EventArgs e)
{
string nev = txtFelhasznalo.Text.Trim();
string jelszo = txtJelszo.Text;

if (nev.Length == 0 || jelszo.Length == 0)
{
Uzenet("Mindkét mezőt töltsd ki!", true);
return;
}

try
{
if (DB.BelepesJo(nev, jelszo))
{
Uzenet($"Sikeres belépés, {nev}!", false);
}
else
{
Uzenet("Hibás felhasználónév vagy jelszó.", true);
}
}
catch (Exception ex)
{
MessageBox.Show("Adatbázis-hiba: " + ex.Message);
}
}
BelepesJo(string nev, string jelszo)A DB osztály metódusa: két szöveget kap, és igaz/hamis választ ad vissza.
darab > 0Ha a COUNT(*) legalább 1, létezik ilyen név-jelszó pár: a belépés jó. Mivel a név egyedi (UNIQUE, 3.1 lecke), az eredmény 0 vagy 1.
nev.Length == 0 || jelszo.Length == 0Az üres mezőket már a lekérdezés előtt kiszűrjük. A return kilép a metódusból.
try–catchAz adatbázis-hibákat (hiányzó fájl, zárolt adatbázis) a gomb kezeli, és érthető üzenetet mutat.
Miért két hibaüzenet-mód?A hibás belépés várt helyzet (Label), az adatbázis-hiba váratlan (MessageBox, 1.10 lecke).

Jósolj!

Az adatbázisban van egy kata felhasználó almafa jelszóval. Mit ír ki a program, ha a nevet kata, a jelszót Almafa (nagy A-val) írod be?

Jóslat ellenőrzése

Hibás belépést: az SQLite szövegek összehasonlításánál az = alapból megkülönbözteti a kis- és nagybetűt, ezért az Almafa és az almafa nem azonos. A jelszó így helyes viselkedés. A felhasználónévnél viszont kényelmes lehet a nagybetű-érzéketlenség (felhnev = @nev COLLATE NOCASE), amelyet a feladatok között kipróbálhatsz.

📷 Képernyőkép helye

Mit tegyél ide: két kép egymás mellett a futó programról: zöld „Sikeres belépés, kata!” üzenet, és piros „Hibás felhasználónév vagy jelszó.” üzenet.Javasolt fájlnév: winforms-3-3-belepes.webp · Alt szöveg: A bejelentkező ablak sikeres és sikertelen belépés után

További biztonsági szabályok

Ne áruld el, mi volt hibásA „Hibás felhasználónév vagy jelszó” üzenet szándékosan nem mondja meg, melyik adat rossz. A „Nincs ilyen felhasználó” üzenetből egy támadó megtudná, hogy mely nevek léteznek.
Ne tárolj nyílt jelszótEbben a tanfolyamban a jelszó egyszerű szövegként van az adatbázisban, hogy az SQL és a C# működését érthesd. Valódi programban a jelszót hash-eléssel (egyirányú kódolással) tárolják, és a belépéskor a beírt jelszó hash-ét hasonlítják össze. A hash-elés a haladó biztonsági ismeretek témaköre.
Korlátozd a próbálkozásokatEgy igazi rendszer néhány sikertelen kísérlet után késlelteti vagy tiltja a belépést. A feladatok között ezt is kipróbálod.
Csak gyakorló adatot használjA gyakorló adatbázisban kitalált nevek és jelszavak legyenek. Soha ne írd be a saját igazi jelszavad.

Hibakeresés: te vagy a hibadetektív

Mi a baj, és hogyan javítod?

Gondold végig az okot, és csak utána nyisd le a megoldást.

1. Összefűzött SQL

string sql = "SELECT COUNT(*) FROM belepesi_adatok WHERE felhnev = '" + nev + "'";
Hiba és javítás

Biztonsági hiba: a felhasználó szövege az SQL részévé válik (SQL-befecskendezés), és egy aposztrófos név hibát is okoz. Használj paramétert: WHERE felhnev = @nev és parancs.Parameters.AddWithValue("@nev", nev);

2. Hiányzó paraméterérték

parancs.Parameters.AddWithValue("@nev", nev);
// az @jelszo paraméternek nem adtunk értéket
Hiba és javítás

Az SQL szövegében szereplő minden paraméternek értéket kell adni. Ha az @jelszo kimarad, a program hibával áll meg, vagy hibásan működik. Ellenőrizd, hogy minden @ jeles névhez van AddWithValue sor, és a nevek betűre pontosan egyeznek.

3. A paraméter neve nem egyezik

string sql = "... WHERE felhnev = @nev";
parancs.Parameters.AddWithValue("@felhasznalo", nev);
Hiba és javítás

A paraméter neve az SQL-ben (@nev) és az AddWithValue hívásban (@felhasznalo) különbözik, ezért az SQL-ben szereplő paraméter nem kap értéket. A két névnek pontosan egyeznie kell.

4. Mindig sikeres a belépés

if (DB.BelepesJo(nev, jelszo) == false)
{
Uzenet("Sikeres belépés!", false);
}
Hiba és javítás

Fordított feltétel: a == false miatt a sikeres üzenet a hibás belépésnél jelenik meg. Írd így: if (DB.BelepesJo(nev, jelszo)), és a hibás ág legyen az else.

5. Kis- és nagybetű

A Tanulo névvel nem lehet belépni, pedig tanulo néven van a felhasználó az adatbázisban.

Ok és javítás

Az SQLite az = összehasonlításnál megkülönbözteti a kis- és nagybetűt. Ha a név esetében ezt nem szeretnéd, írd az SQL-be: felhnev = @nev COLLATE NOCASE. A jelszónál maradjon betűérzékeny.

Mini projekt: biztonságos bejelentkező ablak

Állítsd össze a teljes bejelentkezést:

  1. Az ablak: txtFelhasznalo, txtJelszo (rejtett), btnBelep (AcceptButton), lblUzenet.
  2. A DB.BelepesJo metódus paraméterezett lekérdezéssel (a 4. szakasz szerint).
  3. A gomb ellenőrzi az üres mezőket, hívja a metódust, és színes üzenetet ad.
  4. Próbáld ki: helyes pár, hibás jelszó, ismeretlen név, üres mezők, aposztrófos név, egyetlen aposztróf. Egyik se okozzon összeomlást.
  5. Zárd be a DB Browsert vagy írd ki a változtatásokat (Ctrl+S), hogy ne legyen zárolt az adatbázis.
📷 Képernyőkép helye

Mit tegyél ide: a kész bejelentkező ablak futás közben, hibás belépés után a piros üzenettel.Javasolt fájlnév: winforms-3-3-kesz.webp · Alt szöveg: A kész bejelentkező ablak hibaüzenettel

Három önálló gyakorlófeladat

Egyszerre csak egyet csinálj: tervezd meg, írd meg, futtasd, ellenőrizd, és csak utána nyisd ki a megoldást.

1. Három próbálkozás

Három sikertelen belépés után tiltsd le a Belépés gombot, és írd ki, hogy túl sok sikertelen próbálkozás történt.

Egy lehetséges megoldás
private int sikertelen = 0;

// a hibás ágban (else):
sikertelen = sikertelen + 1;

if (sikertelen >= 3)
{
btnBelep.Enabled = false;
Uzenet("Túl sok sikertelen próbálkozás.", true);
}
else
{
Uzenet("Hibás felhasználónév vagy jelszó.", true);
}

A sikertelen egy mező (1.12 lecke), amely megjegyzi a próbálkozások számát a kattintások között. A sikeres belépésnél érdemes nullázni.

2. Nagybetű-érzéketlen felhasználónév

Úgy módosítsd a lekérdezést, hogy a felhasználónév nagybetűtől függetlenül egyezzen, a jelszó viszont továbbra is betűérzékeny.

Egy lehetséges megoldás
string sql = "SELECT COUNT(*) FROM belepesi_adatok WHERE felhnev = @nev COLLATE NOCASE AND jelszo = @jelszo";

3. Üdvözlés a tárolt névvel

A sikeres belépés után az üzenet az adatbázisban tárolt pontos felhasználónevet mutassa (például Üdv, Kata!), nem azt, amit a felhasználó beírt.

Egy lehetséges megoldás
// a DB osztályban: a név lekérdezése, vagy üres szöveg, ha nincs találat
public static string TaroltNev(string nev, string jelszo)
{
using (SQLiteConnection kapcsolat = UjKapcsolat())
{
kapcsolat.Open();

string sql = "SELECT felhnev FROM belepesi_adatok WHERE felhnev = @nev COLLATE NOCASE AND jelszo = @jelszo";

using (SQLiteCommand parancs = new SQLiteCommand(sql, kapcsolat))
{
parancs.Parameters.AddWithValue("@nev", nev);
parancs.Parameters.AddWithValue("@jelszo", jelszo);

object eredmeny = parancs.ExecuteScalar();

if (eredmeny == null)
{
return "";
}

return eredmeny.ToString();
}
}
}

Találat nélkül az ExecuteScalar() null értéket ad vissza, ezért kell az ellenőrzés. A gombban a DB.TaroltNev(...) eredménye üres szöveg esetén a hibás belépést jelenti.

Gyakorlótár: 6 további feladat

Három szinten gyakorolhatsz: alap, közepes és kihívás. A feladatokhoz a 3.3 lecke bejelentkező ablaka kell (txtFelhasznalo, txtJelszo, btnBelep, lblUzenet, DB.BelepesJo). Csak a próbálkozás után nyisd le a megoldást.

Alap szint

1. Enter a belépéshez

A felhasználó a jelszó beírása után az Enter billentyűvel is beléphessen, ne csak a gombbal. Mit állítasz be?

Megoldás megnyitása

A Form AcceptButton tulajdonságát állítsd a btnBelep gombra (Properties ablak, legördülő lista). Az Enter ekkor a gomb Click eseményét hívja, kódot nem kell írnod.

2. Jelszó megjelenítése

Egy chkMutat jelölőnégyzet kapcsolja a jelszó láthatóságát: bepipálva a jelszó látszik, egyébként pontok.

Megoldás megnyitása

A UseSystemPasswordChar igaz értéknél rejti a karaktereket, ezért a jelölőnégyzet értékének ellentettjét kell megadni (!).

Form1.csC#
private void chkMutat_CheckedChanged(object sender, EventArgs e)
{
txtJelszo.UseSystemPasswordChar = !chkMutat.Checked;
}

Közepes szint

3. Belépés után

Sikeres belépés után a címsor mutassa: Belépve: kata, a Belépés gomb és a két mező legyen letiltva.

Megoldás megnyitása

Az Enabled tulajdonság hamisra állítása letiltja a vezérlőt. A jelszót érdemes a mezőből is törölni, hogy ne maradjon a képernyőn.

Form1.csC#
if (DB.BelepesJo(nev, jelszo))
{
this.Text = "Belépve: " + nev;
Uzenet($"Sikeres belépés, {nev}!", false);

txtJelszo.Clear();
txtFelhasznalo.Enabled = false;
txtJelszo.Enabled = false;
btnBelep.Enabled = false;
}

4. Kijelentkezés

Egy btnKilep gomb állítsa vissza a belépés előtti állapotot: mezők engedélyezve és üresek, címsor az eredeti, üzenet törölve.

Megoldás megnyitása

A visszaállítást külön metódusba tesszük, amelyet később máshonnan is hívhatunk. A kurzort a felhasználónév mezőre tesszük.

Form1.csC#
private void btnKilep_Click(object sender, EventArgs e)
{
this.Text = "Bejelentkezés";
lblUzenet.Text = "";

txtFelhasznalo.Clear();
txtJelszo.Clear();
txtFelhasznalo.Enabled = true;
txtJelszo.Enabled = true;
btnBelep.Enabled = true;

txtFelhasznalo.Focus();
}

Kihívás szint

5. Biztonsági ellenőrzőlista

Készíts ötpontos ellenőrzőlistát egy bejelentkező ablak biztonságához, amelyet egy kód átnézésekor végigkérdeznél.

Megoldás megnyitása
#KérdésJó válasz
1A felhasználó szövege közvetlenül az SQL-be kerül?Nem: minden érték paraméterként (@nev) megy át.
2A jelszómező rejtett?Igen: UseSystemPasswordChar bekapcsolva.
3A hibaüzenet elárulja, melyik adat rossz?Nem: ugyanaz az üzenet név- és jelszóhibára.
4Korlátozott a próbálkozások száma?Igen: néhány hiba után tiltás vagy késleltetés.
5A jelszót tárolják vagy naplózzák nyílt szövegként?Valódi programban nem: hash-elt tárolás, és a jelszó soha nem kerül naplóba.

6. Belépési napló

Minden belépési kísérlet kerüljön egy lstNaplo ListBoxba az idővel és az eredménnyel (14:32:05 – siker: kata, 14:32:40 – sikertelen). A jelszót soha ne írd bele.

Megoldás megnyitása

Az időt a DateTime.Now.ToString("HH:mm:ss") adja. A naplóba csak a felhasználónév és az eredmény kerül, a jelszó soha. Sikertelen kísérletnél a megadott nevet sem naplózzuk, mert a felhasználók néha véletlenül a jelszavukat írják a névmezőbe.

Form1.csC#
private void Naplo(string szoveg)
{
lstNaplo.Items.Insert(0, DateTime.Now.ToString("HH:mm:ss") + " – " + szoveg);
}

// a gombban:
if (DB.BelepesJo(nev, jelszo))
{
Naplo("siker: " + nev);
}
else
{
Naplo("sikertelen");
}

Gyors önellenőrzés

Válaszolj fejben, és csak utána nyisd le a választ.

1. Mi az SQL-befecskendezés?

Amikor a felhasználó beírt szövege az SQL parancs részévé válik, és megváltoztatja a parancs jelentését.

2. Hogyan védekezel ellene?

Paraméterezett lekérdezéssel: az SQL szövegében @ jeles helyfoglalók állnak, az értékeket pedig a Parameters.AddWithValue adja át adatként.

3. Miért nem mondjuk meg, hogy a név vagy a jelszó volt-e rossz?

Mert a pontos üzenetből egy támadó megtudná, mely felhasználónevek léteznek.

4. Miért kell az ExecuteScalar eredményét átalakítani?

Mert object típusú értéket ad vissza, ezért a Convert.ToInt32 metódussal alakítjuk számmá.

5. Mi a különbség a várt és a váratlan hiba kezelése között?

A várt hibát (hibás jelszó) a program az üzenet-felirattal kezeli. A váratlan (adatbázis-)hibát try–catch kapja el, és MessageBox mutatja.

6. Miért nem jó a nyílt jelszótárolás valódi programban?

Mert ha az adatbázishoz hozzáfér valaki, az összes jelszót elolvashatja. Valódi programban hash-elt jelszót tárolnak.

Mit tudsz már?

  • Bejelentkező ablakot készítesz jelszómezővel.
  • Érted az SQL-befecskendezést.
  • Paraméterezett lekérdezéssel védekezel ellene.
  • A műveletet külön, bool értékű metódusba teszed.
  • Ismered a bejelentkezés további biztonsági szabályait.

Következő lecke: regisztráció és DataGridView

A bejelentkezés kész. A következő leckében új felhasználót regisztrálunk, és táblázatban megjelenítjük az adatokat.