ugrás a tartalomhoz

Login rendszer felépítése

Anonymus04 · 2017. Már. 18. (Szo), 18.31
Sziasztok!
Régóta próbálkozok egy login rendszer létrehozásával. Olvastam sokat a biztonságról, elméletben ezen részek meg is vannak. A bajom a belépett felhasználó azonosításával, a session/cookie-val van. Sok cikket elolvastam(itt a weblaboron is) azzal kapcsolatban is, de vannak homályok. Reménykedek, hogy itt tudtok segíteni. (Mgj.: Nem vagyok profi, csak hobbi szinten programozgatok).
Én úgy gondoltam, hogy belép a felhasználó, elmentek egy SESSION[] változóban egy adatsort(session_id, ip, user_agent, hoszt...) Majd ezt ellenőrzöm(adatbázis alapján), hogy létezik-e és megfelelő-e az adatsor. Ez ugye akkor szűnik meg, ha kilép a felhasználó, vagy bezárja a böngészőt. Ehhez az kell, hogy minden oldal elején kell indítanom egy session_start()-ot. Ha eddig nagyjából jól gondolkodtam, akkor nem értem minek a cookie? Azt mire használja minden oldal?
 
1

Login

janoszen · 2017. Már. 18. (Szo), 23.50
Nemrég kellett egy jó adag fejlesztést végeznem egy login rendszeren, úgyhogy nézzük egy login rendszer anatómiáját, hátha ezzel lezárjuk végre ezt a kérdést.

1. Felhasználó azonosítása

Az első kérdés amit el kell dönteni az, hogy mi alapján azonosítjuk a felhasználót. Régebbi rendszerek jellemzően numerikus ID-kat (számokat) használnak erre. Ezzel azonban vigyázni kell, mert a számok növekedése árulkodhat arról, hogy milyen ütemben növekszik a felhasználók száma. Ehelyett újabb / nagyobb rendszerek UUID-ket használnak.

Ehhez az azonosítóhoz kötjük a felhasználó metaadatait, pl. usernév, e-mail cím, név, stb. Jellemzően újabb rendszerek többnyire e-mail címhez kötik bejelentkezést, mivel a felhasználónevek elég könnyen tudnak ütközni. Arra érdemes figyelni, hogy az e-mail cím ne "leakeljen", ne lehessen a felhasználó nyilvánosan hozzáférhető adataiból kikövetkeztetni az e-mail címet. Ilyen például a Gravatar használata, ahol az MD5 hash alapján hozzá lehet kötni a felhasználói accounthoz az e-mail címet.

2. Jelszavak

Ahhoz, hogy valaki bejelentkezzen, értelemszerűen szökség van jelszavakra. Itt részletesen leírtam, hogy miért rossz ötlet saját jelszó titkosítást implementálni. Vagyis akármit csinálsz, a jelszavak titkosítása vagy bcrypt, vagy PBKDF-2 legyen. PHP-ban erre vannak a jelszó függvények.

3. Bejelentkezés

Bejelentkezéskor a felhasználó megadja valamilyen egyedi azonosítóját (usernév vagy e-mail cím) és a jelszavát. Ilyenkor a rendszer a beütött jelszót azonos módon titkosítja, és összehasonlítja az eltárolt titkosított jelszóval. Ha azonos, akkor elhisszük a usernek, hogy az akinek mondja magát.

Na most, itt jön az ugró pont. Ha sessionöket használsz, a bejelentkezés hatására generálsz egy új session ID-t (ez fontos), eltárolhatod a user ID-ját sessionben, és ezt felhasználva a további lekérdezéseknél ez alapján azonosítód.

Ezzel azonban több probléma is van, többek között nem tudod a usernek megmondani, hány helyről van bejelentkezve, vagy kiütni a user többi sessionjét. (Mint ahogy a Facebook is csinálja.) Éppen ezért nagyobb projekteknél azt javaslom, hogy user azonosításra teljesen mellőzd a sessionöket. Helyette bejelentkezéskor generálsz egy tokent amit eltárolsz az adatbázisban, és ezt a felhasználónak sütiben adod oda. A további lekérdezéseknél ez alapján nézed meg, hogy milyen felhasználónévhez tartozik a token.

4. Kijelentkezés

Ha sessionöket használsz, nagyon egyszerűen elpusztítod a sessiont és új session ID-t adsz a usernek. Ha tokent használod, törlöd a tokent.
2

Login Teszt

Anonymus04 · 2017. Már. 19. (V), 20.15
Nagyon szépen köszönöm! Nagyon szuper segítség!
El is készítettem egy nagyon alap login tesztet, ami nagyjából bemutatja, hogy hogyan működhetne ez a login rendszer: Ez így jónak tűnik? Min kellene javítani? (Mgj.: Ez csak logika)

// login.php
<?php
ob_start();	
echo "<h1>Login Rendszer működésének tesztje!</h1>";
if(isset($_POST['Belep']) && !empty($_POST['Belep'])){
	$MyIP 		= $_SERVER['REMOTE_ADDR'];
	$userAgent 	= $_SERVER['HTTP_USER_AGENT'];
	$host 		= gethostbyaddr($_SERVER['REMOTE_ADDR']);
	$UserToken	= "AdatbázisbólRegisztrációkorGeneráltToken"; //Generált karaktersorozat, egyedi, minden belépett felhasználónál, minden új bejelentkezéskor
	$token = $MyIP . $userAgent . $host . $UserToken;
	$tokenS = hash('sha256', $token);
	setcookie("kliens", $tokenS, time()+60);

	header("Location: Valahova");
}
if(isset($_POST['Kilep']) && !empty($_POST['Kilep'])){
	unset($_COOKIE['kliens']);
	setcookie ("kliens", "", time() - 3600);
	header("Location: Valahova");
}

if(isset($_COOKIE['kliens']) && !empty($_COOKIE['kliens'])){
	// Elenőrzöm, hogy van-e ilyen Token az adatbázisba - ha van, Be van lépve és tudom is, hogy melyik useré az.
	setcookie("kliens", $_COOKIE['kliens'], time()+60);

	echo "<h3>Azonosított kliens vagy! Token: " . $_COOKIE['kliens'] . "</h3>";
	echo "<a href='Session.php?page=link1'>Link 1 </a>";
}
else{
	echo "<h3>Nem azonosított kliens vagy! Nincsen Token</h3>";
}

switch( $_GET['page'] ) {
case 'link1': header("Location: Valami//link1.php"); break;
}
?>

<form method="post">
	<input type="submit" name="Belep" value="Belépés" />
	<input type="submit" name="Kilep" value="Kilépés" />
</form>

//link1.php - Védett oldal(ak)
if(isset($_COOKIE['kliens']) && !empty($_COOKIE['kliens'])){
	// Elenőrzöm, hogy van-e ilyen Token az adatbázisba - ha van, tudom is, hogy melyik useré az, Be van lépve...
	setcookie("kliens", $_COOKIE['kliens'], time()+60);
	echo "Be vagy lépve, Védet tartalom megjelenik <br> Token: " . $_COOKIE['kliens'];
}
else{
	echo "Hogy kerültél Te ide!? xD :)";
}
3

Hianyzik

janoszen · 2017. Már. 19. (V), 20.32
Ebbol mindenfele adatbazis muvelet hianyzik. A tokent ellenorizni kell, illetve ha kilepsz, torolni kell a DB-bol.
4

Persze

Anonymus04 · 2017. Már. 19. (V), 20.38
Persze, mert nekem ami nehézséget okozott, az a session/cookie rész, hogy hogyan kell azt beazonosítani, hogy a kliens, éppen be van-e jelentkezve, vagy csak egyszerű látogató. A fő kérdéses rész, az a belépés gomb algoritmus lenne:

	$MyIP 		= $_SERVER['REMOTE_ADDR'];
	$userAgent 	= $_SERVER['HTTP_USER_AGENT'];
	$host 		= gethostbyaddr($_SERVER['REMOTE_ADDR']);
	$UserToken	= "AdatbázisbólRegisztrációkorGeneráltToken";
	$token = $MyIP . $userAgent . $host . $UserToken;
	$tokenS = hash('sha256', $token);
	setcookie("kliens", $tokenS, time()+60);
A logika, hogy ez a cookie elég biztonságos-e és megfelelő lehet-e azonosításra? Természetesen minden egyéb védelem, adatbázis ellenőrzés benne lesz.
5

Nem

janoszen · 2017. Már. 19. (V), 20.45
Nem, ne generalj user adatokbol tokeneket. Ezek csokkentik az entropiat, megjosolhatova teszik a generalt tokeneket. Gondolj bele, ha tudom az IP-det, tudom milyen bongeszot hasznalsz, akkor mar csomo mindent tudok.

Ehelyett probalj meg rendes random generatort hasznalni, pl. mt_rand vagy openssl_random_pseudo_bytes.

Ezen felul a tokent nem 60 masodpercre allitanam, mert az eleg gyorsan automatikusan kilepteti a usert.

Valami ilyesmi:

<?php

function user_exists($username) {
  //ellenorzi hogy letezik-e a user
}

function user_get_encrypted_password($username) {
  //hashelt jelszo betoltese
}

function store_token($username, $token) {
  //token eltarolasa
}

function validate_token($token) {
  //Token ellenorzese es usernev visszaadasa
}

function delete_token($token) {
  //Token torlese a DB-bol
}

function check_password($username, $password) {
  if (!user_exists($username)) {
    return false;
  }
  $hash = user_get_encrypted_password($username);
  return password_verify($password, $hash);
}

function generate_token() {
  return hash('sha256', openssl_random_pseudo_bytes(192));
}

switch (isset($_GET['action'])) {
  case 'login';
    if (!check_password($_POST['username'], $_POST['password'])) {
      //Hibas belepes
    } else {
      $token = generate_token();
      store_token($_POST['username'], $token);
      setcookie('authtoken', $token);
      //User belepett
    }
    break;
  case 'logout':
    if (
      !isset($_COOKIE['authtoken']) ||
      !validate_token($_COOKIE['authtoken'])
    ) {
      //User nincs belepve, hibaoldal
    } else {
      //User kileptetese
      delete_token($_COOKIE['authtoken']);
    }
    break;
  default:
    if (
      !isset($_COOKIE['authtoken']) ||
      !validate_token($_COOKIE['authtoken'])
    ) {
      //User nincs belepve, hibaoldal vagy redirect a login oldalra
    } else {
      //Betoltjuk az oldalt amihez jelszo szukseges
    }
    break;
}
Nem probaltam ki, de a lenyeg lathato belole. Ami hianyzik, az a check_password fuggvenyben egy ujrakodolas ha a jelszo hash tul gyenge. Lasd: password_needs_rehash()
6

Rendben

Anonymus04 · 2017. Már. 19. (V), 21.12
Rendben! Akkor azt hiszem most már értem ezt a login/cookie részt. Nagyon szépen köszönöm az érthető megfogalmazást és hasznos leírást!

Ez lenne az a bizonyos kódolás?

<?php
$bytes = openssl_random_pseudo_bytes('64', $cstrong);
$hex   = bin2hex($bytes);
var_dump($hex);
var_dump($cstrong);
?>
7

igen

janoszen · 2017. Már. 19. (V), 21.35
Igen, pl.
8

setcookie

Hidvégi Gábor · 2017. Már. 20. (H), 15.10
Ebben a formában ez így szerintem kevés, mert javascriptből el lehet érni a sütemény tartalmát, a pecsét ismeretében pedig bárhonnan be lehet jelentkezni.

setcookie('authtoken', $token, 0, '', '', (isset($_SERVER['HTTPS']) and $_SERVER['HTTPS'] === 'on'), true);
9

Akar...

janoszen · 2017. Már. 20. (H), 21.54
Reszben jogos, de annak sincs akadalya hogy JS-bol is hasznald API lekeresekre a tokent.
10

Nem értem

Hidvégi Gábor · 2017. Már. 20. (H), 22.49
János!

Miben másabb az, amit leírtál, mint egy saját munkamenetkezelő?

$token = generate_token();
store_token($_POST['username'], $token);
setcookie('authtoken', $token);
versus
session_regenerate_id();
store_id($_POST['username'], session_id());
setcookie('PHPSESSID', session_id());

Miért fájdítsa ezzel a fejét egy kezdő? Világos-e mindenki számára, hogy a pecsét ismeretében bárhonnan, akár többszörösen be lehet jelentkezni? A PHP a munkamenethez kínál biztonsági beállításokat, a fenti megoldásodnál viszont nekünk kell ezeket lefejleszteni.
11

Nem értem..

Anonymus04 · 2017. Már. 21. (K), 08.52
Nem értem, hogy most akkor mi van? Mi az a pecsét? Most akkor a cookie nem biztonságos? (Tudom, nem sokat dob a biztonságon, de én mindenféleképpen le akarom ellenőrizni a bejelentkezett felhasználók esetében a kliens adatokat is [usag, ip, host]. Tudom ezek hamisíthatóak, ezért nem sokat ér.) Akkor maradjak a session-nél? Bejelentkezéskor a tokent session-be mentsem, akkor minden oldal elején kell a session_start, és ezt ellenőrizzem, ez biztonságosabb?
12

Türelem

Hidvégi Gábor · 2017. Már. 21. (K), 09.00
Várjuk meg janoszen válaszát.
20

Bocsanat

janoszen · 2017. Már. 25. (Szo), 14.07
Bocsanat a keslekedesert, kicsit suruek a napjaim.

Szoval, alapvetoen en ugy gondolom, hogy aki sajat login rendszert ir, az nezzen utana hogy mit hogyan csinal. A PHP adta biztonsagi (?) megoldasokra alapozni szerintem nem olyan nyero.

Az, hogy HTTPonly sutiket hasznalsz-e, vagy megengeded-e a login tokenek kozlekedeset IP, bongeszo, stb. kozott az egyedi dontes. Siman lehet olyan helyzet ahol JS-bol is kell matatni a tokeneket.

Ha nekem kellene biztonsagi megoldast epitenem, akkor valoszinuleg a bongeszo tipusat neznem (pl. Chrome, Firefox, stb) es a belepesi orszagot (GeoIP alapon). Ezen felul beallitanam a secure flaget a sutin, mert LetsEncrypt ota nem bocsanatos bun SSL nelkul weboldalt futtatni.
13

mi biztonságos?

Pepita · 2017. Már. 21. (K), 10.09
Most akkor a cookie nem biztonságos?

Erre akkor lehet válaszolni, ha meghatározod, hogy számodra mit jelent a biztonság.
Ha azt, hogy "sohasemlehetkijátszani/feltörni/átverni/stb", akkor mondj le a szoftverfejlesztésről, mert olyan nincs. :)

Ha azt szeretnéd, hogy a sütiben tárolt adatodat (token, sessionid, kisnyuszifarka, bármi) a böngésző - jellemzően javascript - ne tudja felhasználni, állítsd be a httponly flaget is rajta.

én mindenféleképpen le akarom ellenőrizni a bejelentkezett felhasználók esetében a kliens adatokat is

Ha pl. IP változáskor "kidobod" a user-t, akkor számíthatsz némi reklamációra a mobileszközzel utazóktól (is)...
Mindig alaposan meg kell gondolni az ilyesmit, általában ami biztonsági szempontból kicsit jobb, az user happiness téren sokkal rosszabb.
Nyilván olyat nem szabad megengedni, hogy a Vér Pistike is 2 perc alatt be tudjon jelentkezni a Mariska helyett, de nem is szabad látszólag random (IP changed) okból kidobálni.

User agent: átlagosan havonta 1-2 alkalommal frissülnek a modern böngészők. Ilyenkor ez a string is változik, mégis ugyanaz a böngésző. Emiatt megint (havi 1-2 esetben) indokolatlanul ki tudod dobni user-t, ha teljes egyezőséget vizsgálsz. Nem rossz dolog vizsgálni / ellenőrizni, de pl verzió felfelé növekedést tudni kell kezelni. (Itt feltételeztem, hogy van / lesz "remember me" feature is, a böngészők újraindulnak update-kor.)

Az, hogy session-t használsz-e, vagy mást, a te döntésed kell legyen, mellette és ellene is vannak érvek.
Én azt javaslom, hogy ha már Janoszen ennyi időt áldozott rá(d), hallgass rá. :)
14

Alap biztonság

Anonymus04 · 2017. Már. 21. (K), 10.44
Alap biztonságot akarok. Nem akarok titkosított csatornát fizetni, nem hiszem, hogy szükségem lesz olyan biztonságra. De ahogy írta, azt sem akarom, hogy egy cookie létrehozással, bárki, bárhol, bármilyen néven beléphessen.
Tehát ha van egy ilyen generált token-em. amit elmentek adatbázisba, a megfelelő user mellé, elmentem kliens gépére sütibe. Beállítok pl 30 perc lejárati időt a sütinek(amit minden oldalfrissítéskor újra beállítok), akkor azzal a token-nel, onnantól bárki beléphet. Vagy valamit nagyon rosszul értelmezek? Ha nem, akkor ez ellen mi a teendő?
15

HTTP

Poetro · 2017. Már. 21. (K), 12.02
A HTTP állapotmentes. Azaz ha valakinek megvan a süti, az azt jelenti, hogy be van lépve. Ha nem akarsz sütiket használni, akkor ne használj HTTP-t, hanem mondjuk WebSocket-et, vagy bármi más kapcsolatot, ami folyamatosan nyitva van. Manapság a HTTPS nem kerül semmibe, ha a szolgáltatód tudja. Ekkor nem lesz egyszerű elfogni a sütit az adatforgalom során.
21

Problema

janoszen · 2017. Már. 25. (Szo), 14.09
Ha az a problema hogy nincs ertelmes szolgaltatod es nem tudsz HTTPS oldalt csinalni, keress meg, szoritok egy kis helyet az egyik szerveremen.

Egyebkent szerintem a Te celjaidra ez a megoldas tokeletesen elegendo.
16

User agent: átlagosan havonta

Hidvégi Gábor · 2017. Már. 21. (K), 13.10
User agent: átlagosan havonta 1-2 alkalommal frissülnek a modern böngészők.
Itt az egy munkameneten belüli vizsgálat, ami számít, és bár elvileg frissítheti közben a szoftvert, nem biztos, hogy érdemes ezzel foglalkozni. Szóval szerintem nyugodtan lehet teljes User Agent stringet hasonlítani, és ha ebből probléma akad, akkor találni egy szofisztikáltabb megoldást.

Amit én még ellenőriznék, azok az egyéb HTTP fejlécek, amik ugyancsak a böngésző ujjlenyomatai: Accept, Accept-Encoding, Accept-Language
18

nem

Pepita · 2017. Már. 21. (K), 18.59
Oda írtam, hogy "ha van remember feature".
Nem tudom te mennyit foglalkoztál ezzel a gyakorlatban, de nekem userenként havi 1-2 hiba nagyon sok.
19

Nem értem

Hidvégi Gábor · 2017. Már. 21. (K), 20.16
Nem tudom, nekem állandó bejelentkezésnél eszembe nem jutna a böngésző típusát vizsgálni.
22

Miert?

janoszen · 2017. Már. 25. (Szo), 14.10
Miert nem? Hogyan maszna at az allando bejelentkezesi suti egyik bongeszo tipusbol a masikba? Nyilvan nem a user agentet kell matchelni, hanem a markat.
17

Visszatérve

Anonymus04 · 2017. Már. 21. (K), 14.51
Akkor visszatérve a session-re ebből is megpróbáltam egy nagyon alap kódot összerakni. Így működne a session-ös login rendszer? http://www73.zippyshare.com/v/pcNa9iEM/file.html
Persze a sok-sok ellenőrzés, adatbázis művelet ebben sincs benne, csak maga a session-user azonosítás. Remélem ez már egy fokkal jobb.
23

Github

janoszen · 2017. Már. 25. (Szo), 14.11
Tedd fel Githubra pls, ugy sokkal konnyebb lesz mindenkinek az elete.