Reseni uzivatelu bez pouziti Nette/Security

Michwuanquana
Člen | 22
+
0
-

Zdravim,

nejak mi nesedi reseni uzivatelu v nette, pokusil jsem se tedy udelat vlastni reseni, ale citim, ze to neni nejlepsi. Jsem amater, velmi bych ocenil rady ohledne toho co delam spatne, co by slo zlepsit apod. Jakakoliv rada dobra. Prihlasovani, registrace i opravneni funguji, ale jak jsem rekl, moje reseni se mi tolik nelibi; necitim se tolik zkuseny abych to posoudil, nebo napsal pekne.
Jeste poznamka, ze jsem zatim neimplementoval overeni, ale to zde zatim nehraje roli, obzvlast, pokud jsem se vydal spatnou cestou.

App/Model/UserManager

<?php

namespace App\Model;

use Nette;
use Nette\Utils\DateTime;

class UserManager {

    use Nette\SmartObject;

    private Nette\Database\Explorer $database;

    const default_expiration = 60 * 60 * 48;

    public function __construct(Nette\Database\Explorer $database) {
        $this->database = $database;
    }

    public function login($email, $password, $stay = false) {
        $users = $this->database->table('users');

        if (!$user = $users->where('email', $email)->fetch())
            return false;

        $testPwd = sha1($user->salt . $password);

        if ($user->password != $testPwd)
            return false;

        $ses = sha1(time() . $testPwd);

        $count = $this->database->table('users')
                ->where('email', $email)
                ->update(['session' => $ses]);

        setcookie('login_session', $ses, $stay ? (time() + (10 * 365 * 24 * 60 * 60)) : (time() + self::default_expiration), '/', NULL, 0);

        return true;
    }

    public function logout() {
        if (!$this->isLoggined())
            return false;

        unset($_COOKIE['login_session']);
        setcookie('login_session', null, -1, '/');
    }

    public function register($email, $name, $password) {
        if (!filter_var($email, FILTER_VALIDATE_EMAIL) || $this->emailExists($email))
            throw new \Exception('Invalid email', 1);

        if (!preg_match("/^[0-9a-zA-Z-' ]*$/", $name) || strlen($name) > 64 || strlen($name) < 3)
            throw new \Exception('Invalid name. Name can contain only these characters: A-z, 0-9 and dash', 2);

        $salt = sha1(rand());
        $hashPass = sha1($salt . $password);

        $this->database->table('users')->insert([
            'name' => $name,
            'email' => $email,
            'password' => $hashPass,
            'salt' => $salt,
            'auth' => 0,
            'registered' => date('Y-m-d')
        ]);
        return true;
    }

    public function requireAccess($level): bool {
        if (!$this->isLoggined())
            return false;

        $userLevel = $this->getUser()->auth;
        if ($userLevel === 0) {
            return $level == 0;
        } elseif($userLevel > 0) {
            if ($userLevel == $level || $userLevel < $level)
            return true;
        } else
            return false;
    }

    public function emailExists($email): bool {
        return (bool) $this->database->table('users')->where('email', $email)->fetch();
    }

    public function sendVerificationEmail($id) {
        $user = $this->database->table('users')->where('id', $id)->fetch();
        $email = $user->email;
        // TODO mail send
    }

    public function getUser($id = 0) {
        $users = $this->database->table('users');

        if ($id == 0 && !isset($_COOKIE['login_session']))
            return false;

        if ($id == 0 && isset($_COOKIE['login_session'])) {
            if ($user = $users->where('session', $_COOKIE['login_session'])) {
                return $user->fetch();
            } else
                return false;
        }

        if ($id != 0) {
            if (!$user = $users->get($id))
                return false;
            else
                return $user;
        }

        return false;
    }

    public function isLoggined() {
        return (bool) $this->getUser();
    }

}

a presenter UserPresenter.php

<?php

declare(strict_types=1);

namespace App\Presenters;

use Nette;
use Nette\Application\UI\Form;
use App\Model\UserManager;

final class UserPresenter extends Nette\Application\UI\Presenter
{

    private UserManager $userManager;

    public function __construct(UserManager $userManager) {
            $this->userManager = $userManager;
    }

    public function renderDefault() {
        if($this->userManager->isLoggined())
            $this->redirect('Homepage:default');
        else
            $this->redirect('User:login');
    }

    public function renderLogin() {
        if($this->userManager->isLoggined())
            $this->redirect('Homepage:default');
    }

    public function renderLogout() {
        if(!$this->userManager->isLoggined())
            $this->redirect('User:login');
        else {
            $this->userManager->logout();
            $this->flashMessage('Succesfully logged off!', 'success');
            $this->redirect('Homepage:default');
        }
    }

    public function renderRegister() {
        if($this->userManager->isLoggined())
            $this->redirect('Homepage:default');
    }

    public function renderProfile($id = 0) {
        if(!$this->template->user =$this->userManager->getUser($id))
            $this->redirect('Homepage:default');
    }

    /**************************************************************************/

    protected function createComponentRegisterForm(): Form {
        $form = new Form;
        $form->addText('name');
        $form->addEmail('email');
        $form->addPassword('pass');
        $form->addPassword('passver');
        $form->addSubmit('send');

        $form->onSuccess[] = [$this, 'registerSucceeded'];
        return $form;
    }

    protected function createComponentLoginForm(): Form {
        $form = new Form;
        $form->addEmail('email');
        $form->addPassword('pass');
        $form->addCheckbox("stay");
        $form->addSubmit('send');
        $form->onSuccess[] = [$this, 'loginSucceeded'];
        return $form;
    }


    public function loginSucceeded(\stdClass $values): void {

        $status = $this->userManager->login($values->email, $values->pass, (bool)$values->stay);

        if( $status === true ) {
            $this->flashMessage('Logged in!', 'success');
            $this->redirect('Homepage:default');
        } elseif( $status === -1 ) {
            $this->flashMessage('Account not verified, please check email', 'danger');
            $this->redirect('User:login');
        } else {
            $this->flashMessage('Wrong email or password', 'danger');
            $this->redirect('User:login');
        }
    }

    public function registerSucceeded(\stdClass $values): void {
        if($this->userManager->emailExists($values->email)) {
            $this->flashMessage('Account with this email exists', 'danger');
            $this->redirect('User:register');
        }

        if( $values->pass != $values->passver) {
            $this->flashMessage('Passwords do not match', 'danger');
            $this->redirect('User:register');
        }

        $success = false;
        try {
            $this->userManager->register($values->email, $values->name, $values->pass);
            $this->flashMessage('Registration was successful, please check your email for veriffication.', 'success');
            $success = true;
        } catch (\Exception $ex) {
            $this->flashMessage($ex->getMessage(), 'danger');
            $this->redirect('User:register');
        }

        if($success)
            $this->redirect('User:login');
    }
}

Editoval Michwuanquana (13. 3. 2022 16:10)

Pepino
Člen | 247
+
+3
-

Nette reseni ti nesedi, tvoje se ti nelibi. Mozna by bylo lepsi napsat co ti na reseni nette nesedi a proc. V tom kodu co si poslal je to same co v reseni nette.

Michwuanquana
Člen | 22
+
0
-

Zkratka nejsem si jisty, jestli to, jak to resim je idealni… Nette implementaci prilis nerozumim, nevim, jak to s jeho pomoci napsat tak, aby se to chovalo stejne.

Pepino
Člen | 247
+
0
-

Cetl jsi dokumentaci? https://doc.nette.org/…thentication

Jen ve zkratce. Tvojemu usermanageru naimplementuj Nette\Security\Authenticator, metodu login prepis na authenticate a vracej v ni identitu. Metodu logout a isLoggedIn muzes smazat. V prezenteru je pristup k uzivatelovi pres $this->getUser() takze prihlaseni bude $this->getUser()->login(user, pw, ..), odhlaseni $this->getUser()->logout(), jestli je prihlaseny $this->getUser()->isLoggedIn() atd..

Jestli ti jde o pouziti cookies tak https://doc.nette.org/…thentication#…

nightfish
Člen | 470
+
+8
-

Nikdy nepoužívej SHA1 pro hashování hesel. Hashování hesel a ověřování jejich správnosti by měly mít na starosti funkce password_hash() a password_verify(), případně jejich Nette obálky.

Michwuanquana
Člen | 22
+
0
-

Dekuji vam mnohokrat, nakopli jste me na lepsi cestu. Upravil jsem kod takto a je funkcni a libi se mi vice, trochou treninku casem udelam i pekny kod :D

UserManager.php

<?php

namespace App\Model;

use Nette;
use Nette\Security\SimpleIdentity;
use Nette\Security\Passwords;
use Nette\Database\Explorer;

class UserManager implements Nette\Security\Authenticator {

    use Nette\SmartObject;

    private Passwords $passwords;
    private Explorer $database;

    const default_expiration = 60 * 60 * 48;

    public function __construct(Explorer $database, Passwords $passwords) {
        $this->database = $database;
        $this->passwords = $passwords;
    }

    public function authenticate($email, $password, $stay = false): SimpleIdentity {
        $users = $this->database->table('users');

        if (!$row = $users->where('email', $email)->fetch())
            throw new Nette\Security\AuthenticationException('User not found.');

        if (!$this->passwords->verify($password, $row->password))
            throw new Nette\Security\AuthenticationException('Invalid password.');

        return new SimpleIdentity(
			$row->id,
			$row->role,
			[   'name' => $row->name,
                            'email' => $row->email,
                            'registered' => $row->registered
                        ]
		);
    }

    public function register($email, $name, $password) {
        if (!filter_var($email, FILTER_VALIDATE_EMAIL) || $this->emailExists($email))
            throw new Nette\Security\AuthenticationException('Invalid email or email already exists.', 1);

        if (!preg_match("/^[0-9a-zA-Z-' ]*$/", $name) || strlen($name) > 64 || strlen($name) < 3)
            throw new Nette\Security\AuthenticationException('Invalid name. Name can contain only these characters: A-z, 0-9 and dash', 2);

        if ($this->nameExists($name))
            throw new Nette\Security\AuthenticationException('Name is already registered.', 3);

        $res = $this->database->table('users')->insert([
            'name' => $name,
            'email' => $email,
            'password' => $this->passwords->hash($password),
            'role' => 0,
            'registered' => date('Y-m-d'),
        ]);

        if(!$res instanceof \Nette\Database\Table\ActiveRow)
            throw new \Exception('Internal register error');

        return $res->id;
    }

    public function nameExists($name): bool {
        return (bool) $this->database->table('users')->where('name', $name)->fetch();
    }

    public function emailExists($email): bool {
        return (bool) $this->database->table('users')->where('email', $email)->fetch();
    }
}

UserPresenter.php

<?php

declare(strict_types=1);

namespace App\Presenters;

use Nette;
use Nette\Application\UI\Form;
use Nette\Security\User;


final class UserPresenter extends Nette\Application\UI\Presenter
{
    private User $user;

    public function __construct(User $user) {
            $this->user = $user;
    }

    public function renderDefault() {
        if($this->user->isLoggedIn())
            $this->redirect('User:profile');
        else
            $this->redirect('User:login');
    }

    public function renderLogin() {
        if($this->user->isLoggedIn())
            $this->redirect('User:profile');
    }

    public function renderLogout() {
        if($this->user->isLoggedIn()) {
            $this->user->logout();
            $this->flashMessage('Succesfully logged off!', 'success');
        }

        $this->redirect('User:login');
    }

    public function renderRegister() {
        if($this->user->isLoggedIn())
            $this->redirect('User:profile');
    }

    public function renderProfile($id = 0) {
        if($this->user->isLoggedIn()) {
            $this->template->user = $this->user->getIdentity();
        } else
            $this->redirect('User:login');
    }

    /**************************************************************************/

    protected function createComponentRegisterForm(): Form {
        $form = new Form;
        $form->addText('name');
        $form->addEmail('email');
        $form->addPassword('password');
        $form->addPassword('passver');
        $form->addSubmit('send');

        $form->onSuccess[] = [$this, 'registerSucceeded'];
        return $form;
    }

    protected function createComponentLoginForm(): Form {
        $form = new Form;
        $form->addEmail('email');
        $form->addPassword('password');
        $form->addCheckbox("stay");
        $form->addSubmit('send');
        $form->onSuccess[] = [$this, 'loginSucceeded'];
        return $form;
    }


    public function loginSucceeded(\stdClass $values): void {
        try {
            $this->user->login($values->email, $values->password);
            $this->flashMessage('Logged in!', 'success');
            $this->redirect('User:profile');
        } catch(Nette\Security\AuthenticationException $e) {
            $this->flashMessage($e->getMessage(), 'danger');
        }
    }

    public function registerSucceeded(\stdClass $values): void {
        if( $values->password != $values->passver) {
            $this->flashMessage('Passwords do not match', 'danger');
            $this->redirect('User:register');
        }

        $success = false;
        try {
            $this->user->getAuthenticator()->register($values->email, $values->name, $values->password);
            //$this->userManager
            $this->flashMessage('Registration was successful, please check your email for veriffication.', 'success');
            $success = true;
        } catch (\Exception $ex) {
            $this->flashMessage($ex->getMessage(), 'danger');
        }

        if($success)
            $this->redirect('User:login');
    }
}
petr.pavel
Člen | 535
+
0
-

Ještě ti tam vypadlo zpracování ‚stay‘. Potřebuješ něco jako

        if (!$values['stay']) {
          $this->user->setExpiration('+ 2 days', true);
        }

Nemá moc smysl nastavovat těch 10 let, pokud současně nenastavuješ i session.gc_maxlifetime (cookie ti vydrží, ale ne soubor na serveru). K tomu slouží nastavení v configu (např. local.neon):

session:
	expiration: 10 years	# sets session.gc_maxlifetime and cookie.lifetime

https://doc.nette.org/…onfiguration#…

Marek Bartoš
Nette Blogger | 1155
+
+2
-

petr.pavel napsal(a):

Ještě ti tam vypadlo zpracování ‚stay‘. Potřebuješ něco jako

        if (!$values['stay']) {
          $this->user->setExpiration('+ 2 days', true);
        }

Nemá moc smysl nastavovat těch 10 let, pokud současně nenastavuješ i session.gc_maxlifetime (cookie ti vydrží, ale ne soubor na serveru). K tomu slouží nastavení v configu (např. local.neon):

session:
	expiration: 10 years	# sets session.gc_maxlifetime and cookie.lifetime

https://doc.nette.org/…onfiguration#…

Ono je nastavení stay signed přežitek, jednodušší je vyhodit jej a mít dlouhé přihlášení vždy.

Na to nastavení expirace v neonu pozor, cron co na serveru maže soubory se sessions to nebude respektovat.

petr.pavel
Člen | 535
+
0
-

Ono je nastavení stay signed přežitek, jednodušší je vyhodit jej a mít dlouhé přihlášení vždy.

Nesouhlasím. Uživatel by měl mít možnost zvolit „nezapamatovat“, pokud se přihlašuje ze sdíleného počítače. Pro případ, že by se zapomněl odhlásit.

Polki
Člen | 553
+
+1
-

@petrpavel to záleží, jaký veřejný PC používáš. Například různé kiosky mají pořád přihlášeného uživatale, kde se jen loguje návštěva a je tam natvrdo spuštěný prohlížeč, takže když se zapomeneš odhlásit, tak prohlížeč pořád nechá spuštěnou session i když máš zavřenou záložku zrovna…
Proto souhlasím spíše s Markem, že to je přežitek.

Pokud chceš něco takového udělat, tak to dělej jako internetové bankovnictví a to tak, že uživateli nastavíš expiraci session na 2 minuty a poté uživatele odhlásíš, nebo při každé akci, co uživatel vyvolá tu session prodloužíš… K tomu můžeš ještě checkovat to jestli uživatel kouká na stránku tak, že si budeš dělat nějaký ajax request na pozadí co 5 vteřin a když request nepřijde, tak je stránka zavřená a proběhne invalidace session, nebo mít spuštěný natvrdo nějaký tunel, který se zavře po zavření tabu, například pomocí websocketu a jak se zavře spojení, tak se uživatel odhlásí atp.

rumcais1
Člen | 80
+
0
-

V novem nette už třetí parametr neprojde. Neřešil jste už někdo.

public function authenticate($email, $password, $stay = false): SimpleIdentity
Marek Bartoš
Nette Blogger | 1155
+
0
-

rumcais1 napsal(a):

V novem nette už třetí parametr neprojde. Neřešil jste už někdo.

public function authenticate($email, $password, $stay = false): SimpleIdentity

Předávej si do login() metody už vytvořenou Identity. Můžeš v podstatě dělat, že interface Authenticator neexistuje, nepotřebuješ ho.