Bicycle/Services/Auth/FileAuthDriver.php
2026-08-11 22:35:26 +03:00

216 lines
6.5 KiB
PHP
Raw Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

<?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);
}
}