216 lines
6.5 KiB
PHP
216 lines
6.5 KiB
PHP
<?php
|
||
/**
|
||
* @package Bicycle
|
||
* @author Egor Isaev
|
||
* @description FileAuthDriver.php
|
||
* @copyright (c) 06/08/2026
|
||
*/
|
||
|
||
namespace Services\Auth;
|
||
|
||
use System\Classes\Config;
|
||
use System\Classes\Cookie;
|
||
use System\Classes\Session;
|
||
|
||
/**
|
||
* Авторизация по списку пользователей из PHP-файла:
|
||
* return ['login' => ['password' => '<password_hash>', ...доп. поля], ...];
|
||
*
|
||
* Путь к файлу — Config::get('auth', 'users_file').
|
||
* Состояние авторизации хранится в сессии (логин текущего пользователя),
|
||
* ключ сессии — Config::get('auth', 'session_key') (по умолчанию 'auth_user').
|
||
*/
|
||
class FileAuthDriver implements AuthDriver
|
||
{
|
||
/** @var string Имя cookie «запомнить меня» */
|
||
protected const REMEMBER_COOKIE = 'remember_me';
|
||
|
||
/** @var int Время жизни cookie «запомнить меня», сек (90 дней) */
|
||
protected const REMEMBER_LIFETIME = 90 * 24 * 3600;
|
||
|
||
/** @var string|null Путь к файлу пользователей (если не передан — берётся из конфига) */
|
||
protected ?string $_users_file;
|
||
|
||
/** @var string Ключ сессии, под которым хранится логин авторизованного пользователя */
|
||
protected string $_session_key;
|
||
|
||
/**
|
||
* @param string|null $users_file Путь к файлу пользователей (для тестов/переопределения)
|
||
* @param string|null $session_key Ключ сессии (для тестов/переопределения)
|
||
*/
|
||
public function __construct(?string $users_file = null, ?string $session_key = null)
|
||
{
|
||
$this->_users_file = $users_file ?? Config::get('auth', 'users_file') ?? null;
|
||
$this->_session_key = $session_key ?? Config::get('auth', 'session_key') ?? 'auth_user';
|
||
}
|
||
|
||
/**
|
||
* Загружает список пользователей из файла (с полем 'password').
|
||
*
|
||
* @return array<string,array>
|
||
*/
|
||
protected function users(): array
|
||
{
|
||
if ($this->_users_file === null || !file_exists($this->_users_file)) {
|
||
return [];
|
||
}
|
||
|
||
return (array)(include $this->_users_file);
|
||
}
|
||
|
||
/**
|
||
* Логин текущего авторизованного пользователя: из сессии, либо (если сессии
|
||
* нет) восстанавливается из cookie «запомнить меня» — тогда же выставляется
|
||
* обратно и в сессию, чтобы не проверять cookie на каждом обращении.
|
||
*
|
||
* @return string|null
|
||
*/
|
||
protected function currentLogin(): ?string
|
||
{
|
||
$login = Session::instance()->get($this->_session_key);
|
||
|
||
if ($login !== null) {
|
||
return $login;
|
||
}
|
||
|
||
return $this->restoreFromRememberCookie();
|
||
}
|
||
|
||
/**
|
||
* @inheritDoc
|
||
*/
|
||
public function login(string $login, string $password): bool
|
||
{
|
||
if (!$this->verifyCredentials($login, $password)) {
|
||
return false;
|
||
}
|
||
|
||
Session::instance()->set($this->_session_key, $login);
|
||
|
||
return true;
|
||
}
|
||
|
||
/**
|
||
* @inheritDoc
|
||
*/
|
||
public function verifyCredentials(string $login, string $password): bool
|
||
{
|
||
$user = $this->users()[$login] ?? null;
|
||
|
||
return $user !== null && password_verify($password, $user['password'] ?? '');
|
||
}
|
||
|
||
/**
|
||
* @inheritDoc
|
||
*/
|
||
public function forceLogin(string $login, bool $remember = false): void
|
||
{
|
||
Session::instance()->set($this->_session_key, $login);
|
||
|
||
if ($remember) {
|
||
$this->setRememberCookie($login);
|
||
}
|
||
}
|
||
|
||
/**
|
||
* @inheritDoc
|
||
*/
|
||
public function logout(): void
|
||
{
|
||
Session::instance()->delete($this->_session_key);
|
||
Cookie::delete(self::REMEMBER_COOKIE);
|
||
}
|
||
|
||
/**
|
||
* Ставит cookie «запомнить меня»: логин + HMAC-подпись на хэше пароля
|
||
* пользователя (не отдельный секрет — при смене пароля хэш меняется,
|
||
* старые cookie сами становятся недействительными). Без серверного
|
||
* хранилища токенов — не нужно для файлового драйвера.
|
||
*
|
||
* @param string $login
|
||
* @return void
|
||
*/
|
||
protected function setRememberCookie(string $login): void
|
||
{
|
||
$hash = $this->users()[$login]['password'] ?? null;
|
||
|
||
if ($hash === null) {
|
||
return;
|
||
}
|
||
|
||
Cookie::set(self::REMEMBER_COOKIE, $login . '|' . hash_hmac('sha256', $login, $hash), self::REMEMBER_LIFETIME);
|
||
}
|
||
|
||
/**
|
||
* Проверяет cookie «запомнить меня» и, если подпись верна, восстанавливает
|
||
* сессию для этого логина.
|
||
*
|
||
* @return string|null Логин, если cookie валиден
|
||
*/
|
||
protected function restoreFromRememberCookie(): ?string
|
||
{
|
||
$cookie = Cookie::get(self::REMEMBER_COOKIE);
|
||
|
||
if ($cookie === null || !str_contains($cookie, '|')) {
|
||
return null;
|
||
}
|
||
|
||
[$login, $signature] = explode('|', $cookie, 2);
|
||
$hash = $this->users()[$login]['password'] ?? null;
|
||
|
||
if ($hash === null || !hash_equals(hash_hmac('sha256', $login, $hash), $signature)) {
|
||
return null;
|
||
}
|
||
|
||
Session::instance()->set($this->_session_key, $login);
|
||
|
||
return $login;
|
||
}
|
||
|
||
/**
|
||
* @inheritDoc
|
||
*/
|
||
public function loggedIn(): bool
|
||
{
|
||
return $this->getUser() !== null;
|
||
}
|
||
|
||
/**
|
||
* @inheritDoc
|
||
*/
|
||
public function getUser(): ?array
|
||
{
|
||
$login = $this->currentLogin();
|
||
|
||
if ($login === null) {
|
||
return null;
|
||
}
|
||
|
||
$user = $this->users()[$login] ?? null;
|
||
|
||
if ($user === null) {
|
||
return null;
|
||
}
|
||
|
||
unset($user['password']);
|
||
|
||
return ['login' => $login] + $user;
|
||
}
|
||
|
||
/**
|
||
* @inheritDoc
|
||
*/
|
||
public function checkPassword(string $password): bool
|
||
{
|
||
$login = $this->currentLogin();
|
||
|
||
if ($login === null) {
|
||
return false;
|
||
}
|
||
|
||
$hash = $this->users()[$login]['password'] ?? null;
|
||
|
||
return $hash !== null && password_verify($password, $hash);
|
||
}
|
||
}
|