mirror of
https://github.com/avecms/AVE.cms.git
synced 2026-08-01 08:45:44 +00:00
568 lines
17 KiB
PHP
568 lines
17 KiB
PHP
<?php
|
|
|
|
/*
|
|
|--------------------------------------------------------------------------------------
|
|
| AVE.cms
|
|
|--------------------------------------------------------------------------------------
|
|
| @package AVE.cms
|
|
| @file system/App/Common/Auth.php
|
|
| @author AVE.cms <support@ave-cms.ru>
|
|
| @copyright 2007-2026 (c) AVE.cms
|
|
| @link https://ave-cms.ru
|
|
| @version 3.3
|
|
*/
|
|
|
|
namespace App\Common;
|
|
|
|
defined("BASEPATH") || die('Direct access to this location is not allowed.');
|
|
|
|
use DB;
|
|
use App\Helpers\Request;
|
|
use App\Common\Auth\IdentityLinker;
|
|
use App\Common\Auth\PublicUserProvider;
|
|
|
|
class Auth
|
|
{
|
|
const DUMMY_PASSWORD_HASH = '$2y$12$CPSYMCBc.us9zmv1TxUrmuc6r8fEQzxQwT2f93rwzGGyZpW92RKBq';
|
|
/**
|
|
* Кеш пользователей нового table-based API в рамках запроса.
|
|
*
|
|
* @var array
|
|
*/
|
|
protected static $currentUsers = [];
|
|
protected static $systemRolePermissions = [];
|
|
|
|
protected function __construct()
|
|
{
|
|
//
|
|
}
|
|
|
|
// ------------------------------------------------------------------ //
|
|
// Системная авторизация публичного сайта
|
|
// ------------------------------------------------------------------ //
|
|
|
|
public static function publicCheck()
|
|
{
|
|
return PublicUserProvider::check();
|
|
}
|
|
|
|
public static function publicBootstrap()
|
|
{
|
|
if (!PublicUserProvider::check()) {
|
|
$systemUser = self::systemUser();
|
|
if (!$systemUser) {
|
|
$systemUser = self::user();
|
|
}
|
|
|
|
$publicUser = $systemUser ? IdentityLinker::publicForSystem($systemUser) : null;
|
|
if ($systemUser && !$publicUser) {
|
|
$publicId = IdentityLinker::ensurePublicForSystem((int) $systemUser['id']);
|
|
if ($publicId > 0) {
|
|
PublicUserProvider::syncById($publicId);
|
|
}
|
|
}
|
|
|
|
if ($publicUser && (string) $publicUser['status'] === '1') {
|
|
PublicUserProvider::syncById((int) $publicUser['Id']);
|
|
}
|
|
}
|
|
|
|
return PublicUserProvider::bootstrap();
|
|
}
|
|
|
|
public static function publicUser()
|
|
{
|
|
return PublicUserProvider::user();
|
|
}
|
|
|
|
public static function publicAttempt($identifier, $password, $remember = false)
|
|
{
|
|
return PublicUserProvider::attempt($identifier, $password, $remember);
|
|
}
|
|
|
|
public static function publicLoginById($userId, $remember = false)
|
|
{
|
|
return PublicUserProvider::loginById($userId, $remember);
|
|
}
|
|
|
|
public static function publicLogout()
|
|
{
|
|
PublicUserProvider::logout();
|
|
}
|
|
|
|
public static function publicRegistrationEnabled()
|
|
{
|
|
return PublicUserProvider::registrationEnabled();
|
|
}
|
|
|
|
// ------------------------------------------------------------------ //
|
|
// Новый универсальный Auth API для схемы:
|
|
// {{prefix}}_users + {{prefix}}_users_session
|
|
// ------------------------------------------------------------------ //
|
|
|
|
/**
|
|
* Попытка входа по логину/email + пароль.
|
|
*
|
|
* Не зависит от Marketplace: таблицы, поля, cookie и session key задаются
|
|
* через options. По умолчанию рассчитано на актуальную схему:
|
|
* users.email/password_hash/role/is_active и users_session.token_hash.
|
|
*
|
|
* Возвращает массив пользователя при успехе, иначе:
|
|
* 1 — пустой логин/пароль
|
|
* 2 — неверная пара
|
|
* 3 — пользователь отключён
|
|
*/
|
|
public static function attempt($identifier, $password, array $options = [])
|
|
{
|
|
$options = self::modernAuthOptions($options);
|
|
$identifier = trim((string) $identifier);
|
|
|
|
if ($identifier === '' || $password === '') {
|
|
return 1;
|
|
}
|
|
|
|
$where = [];
|
|
$args = [];
|
|
foreach ($options['identifier_fields'] as $field) {
|
|
$where[] = $field . ' = %s';
|
|
$args[] = $identifier;
|
|
}
|
|
|
|
$sql = "SELECT * FROM " . $options['users_table'] . " WHERE (" . implode(' OR ', $where) . ") LIMIT 1";
|
|
$rows = call_user_func_array([DB::class, 'query'], array_merge([$sql], $args))->getAll();
|
|
$user = $rows ? (array) $rows[0] : null;
|
|
|
|
$storedHash = $user && !empty($user[$options['password_field']])
|
|
? (string) $user[$options['password_field']]
|
|
: self::DUMMY_PASSWORD_HASH;
|
|
$passwordValid = password_verify($password, $storedHash);
|
|
if (!$user || empty($user[$options['password_field']]) || !$passwordValid) {
|
|
return 2;
|
|
}
|
|
|
|
if ($options['active_field'] !== '' && (int) $user[$options['active_field']] !== (int) $options['active_value']) {
|
|
return 3;
|
|
}
|
|
|
|
if (password_needs_rehash($user[$options['password_field']], PASSWORD_BCRYPT, ['cost' => $options['bcrypt_cost']])) {
|
|
$newHash = password_hash($password, PASSWORD_BCRYPT, ['cost' => $options['bcrypt_cost']]);
|
|
DB::Update($options['users_table'], [$options['password_field'] => $newHash], "id = %i", $user['id']);
|
|
$user[$options['password_field']] = $newHash;
|
|
}
|
|
|
|
self::login($user, $options);
|
|
return $user;
|
|
}
|
|
|
|
/**
|
|
* Установить пользователя как авторизованного для нового Auth API.
|
|
*/
|
|
public static function login(array $user, array $options = [])
|
|
{
|
|
$options = self::modernAuthOptions($options);
|
|
$cacheKey = self::modernAuthCacheKey($options);
|
|
|
|
if (!empty($options['regenerate_session'])) {
|
|
Session::regenerateId(!empty($options['delete_old_session']));
|
|
}
|
|
|
|
Session::set($options['session_key'], (int) $user['id']);
|
|
self::$currentUsers[$cacheKey] = $user;
|
|
|
|
if (!empty($options['load_permissions']) && !empty($options['role_field']) && isset($user[$options['role_field']])) {
|
|
Permission::setForRole($user[$options['role_field']]);
|
|
}
|
|
|
|
if (!empty($options['remember']) || !empty($options['issue_session_token'])) {
|
|
self::modernIssueRememberToken((int) $user['id'], $options, !empty($options['remember']));
|
|
}
|
|
}
|
|
|
|
/**
|
|
* Гарантирует общий browser-token для уже открытой системной сессии.
|
|
* Нужен, когда разные части приложения используют разные PHP session handlers.
|
|
*/
|
|
public static function ensureBrowserToken(array $options = [])
|
|
{
|
|
$options = self::modernAuthOptions($options);
|
|
$user = self::user($options);
|
|
if (!$user) {
|
|
return false;
|
|
}
|
|
|
|
$token = isset($_COOKIE[$options['cookie_name']]) ? (string) $_COOKIE[$options['cookie_name']] : '';
|
|
if (preg_match('/^[a-f0-9]{64}$/', $token)) {
|
|
$ownerId = (int) DB::query(
|
|
"SELECT user_id FROM " . $options['session_table'] . " WHERE token_hash = %s LIMIT 1",
|
|
hash('sha256', $token)
|
|
)->getOne();
|
|
if ($ownerId === (int) $user['id']) {
|
|
return true;
|
|
}
|
|
}
|
|
|
|
self::modernIssueRememberToken((int) $user['id'], $options, false);
|
|
return true;
|
|
}
|
|
|
|
/** Системная личность, доступная и в публичном runtime через общий token. */
|
|
public static function systemUser()
|
|
{
|
|
$options = self::modernAuthOptions(array(
|
|
'users_table' => SystemTables::table('users'),
|
|
'session_table' => SystemTables::table('users_session'),
|
|
'load_permissions' => false,
|
|
'regenerate_session' => false,
|
|
'regenerate_session_on_logout' => false,
|
|
));
|
|
$cacheKey = 'system-token|' . $options['cookie_name'];
|
|
|
|
if (array_key_exists($cacheKey, self::$currentUsers)) {
|
|
return self::$currentUsers[$cacheKey] ?: null;
|
|
}
|
|
|
|
$id = self::modernRestoreFromCookie($options, false);
|
|
if ($id <= 0) {
|
|
$linkedUser = self::linkedPublicSystemUser($options);
|
|
if (!$linkedUser) {
|
|
self::$currentUsers[$cacheKey] = false;
|
|
return null;
|
|
}
|
|
|
|
self::modernIssueRememberToken((int) $linkedUser['id'], $options, false);
|
|
self::$currentUsers[$cacheKey] = $linkedUser;
|
|
return $linkedUser;
|
|
}
|
|
|
|
$rows = DB::query(
|
|
"SELECT * FROM " . $options['users_table']
|
|
. " WHERE id = %i AND " . $options['active_field'] . " = %i LIMIT 1",
|
|
$id,
|
|
(int) $options['active_value']
|
|
)->getAll();
|
|
self::$currentUsers[$cacheKey] = $rows ? (array) $rows[0] : false;
|
|
|
|
return self::$currentUsers[$cacheKey] ?: null;
|
|
}
|
|
|
|
protected static function linkedPublicSystemUser(array $options)
|
|
{
|
|
$publicUser = self::publicUser();
|
|
if (!$publicUser || empty($publicUser['email'])) {
|
|
return null;
|
|
}
|
|
|
|
$linked = IdentityLinker::systemForPublic((int) $publicUser['id'], (string) $publicUser['email']);
|
|
if ($linked && (int) $linked[$options['active_field']] === (int) $options['active_value']) {
|
|
return $linked;
|
|
}
|
|
|
|
$config = PublicConfiguration::all();
|
|
$groups = isset($config['system_identity_public_groups']) && is_array($config['system_identity_public_groups'])
|
|
? array_map('intval', $config['system_identity_public_groups'])
|
|
: array();
|
|
if (!in_array((int) $publicUser['group'], $groups, true)) {
|
|
return null;
|
|
}
|
|
|
|
$rows = DB::query(
|
|
'SELECT * FROM ' . $options['users_table']
|
|
. ' WHERE email = %s AND ' . $options['active_field'] . ' = %i LIMIT 1',
|
|
(string) $publicUser['email'],
|
|
(int) $options['active_value']
|
|
)->getAll();
|
|
|
|
return $rows ? (array) $rows[0] : null;
|
|
}
|
|
|
|
public static function systemUserCan($permission)
|
|
{
|
|
$user = self::systemUser();
|
|
if (!$user || empty($user['role'])) {
|
|
return false;
|
|
}
|
|
|
|
$role = (string) $user['role'];
|
|
if ($role === 'admin') {
|
|
return true;
|
|
}
|
|
|
|
return in_array((string) $permission, self::systemPermissionsForRole($role), true);
|
|
}
|
|
|
|
protected static function systemPermissionsForRole($role)
|
|
{
|
|
$role = trim((string) $role);
|
|
if ($role === '') {
|
|
return array();
|
|
}
|
|
|
|
if (isset(self::$systemRolePermissions[$role])) {
|
|
return self::$systemRolePermissions[$role];
|
|
}
|
|
|
|
try {
|
|
$rows = DB::query(
|
|
'SELECT rp.permission_code FROM ' . SystemTables::table('roles') . ' r'
|
|
. ' JOIN ' . SystemTables::table('role_permissions') . ' rp ON rp.role_id = r.id'
|
|
. ' WHERE r.code = %s ORDER BY rp.permission_code ASC',
|
|
$role
|
|
)->getAll();
|
|
} catch (\Throwable $e) {
|
|
$rows = array();
|
|
}
|
|
|
|
self::$systemRolePermissions[$role] = array();
|
|
foreach ($rows ?: array() as $row) {
|
|
$row = (array) $row;
|
|
if (!empty($row['permission_code'])) {
|
|
self::$systemRolePermissions[$role][] = (string) $row['permission_code'];
|
|
}
|
|
}
|
|
|
|
return self::$systemRolePermissions[$role];
|
|
}
|
|
|
|
/**
|
|
* Завершить вход для нового Auth API.
|
|
*/
|
|
public static function logout(array $options = [])
|
|
{
|
|
$options = self::modernAuthOptions($options);
|
|
$cacheKey = self::modernAuthCacheKey($options);
|
|
|
|
if (!empty($_COOKIE[$options['cookie_name']])) {
|
|
DB::Delete(
|
|
$options['session_table'],
|
|
"token_hash = %s",
|
|
hash('sha256', (string) $_COOKIE[$options['cookie_name']])
|
|
);
|
|
}
|
|
|
|
self::modernForgetCookie($options);
|
|
Session::del($options['session_key']);
|
|
if (!empty($options['load_permissions'])) {
|
|
Session::del('permissions');
|
|
}
|
|
|
|
self::$currentUsers[$cacheKey] = false;
|
|
|
|
if (!empty($options['regenerate_session_on_logout'])) {
|
|
Session::regenerateId(!empty($options['delete_old_session']));
|
|
}
|
|
}
|
|
|
|
public static function check(array $options = [])
|
|
{
|
|
return self::user($options) !== null;
|
|
}
|
|
|
|
/**
|
|
* Текущий пользователь нового Auth API как массив или null.
|
|
*/
|
|
public static function user(array $options = [])
|
|
{
|
|
$options = self::modernAuthOptions($options);
|
|
$cacheKey = self::modernAuthCacheKey($options);
|
|
|
|
if (array_key_exists($cacheKey, self::$currentUsers)) {
|
|
return self::$currentUsers[$cacheKey] ?: null;
|
|
}
|
|
|
|
$id = (int) Session::get($options['session_key']);
|
|
if ($id <= 0) {
|
|
$id = self::modernRestoreFromCookie($options);
|
|
}
|
|
|
|
if ($id <= 0) {
|
|
self::$currentUsers[$cacheKey] = false;
|
|
return null;
|
|
}
|
|
|
|
$sql = "SELECT * FROM " . $options['users_table'] . " WHERE id = %i";
|
|
$args = [$sql, $id];
|
|
if ($options['active_field'] !== '') {
|
|
$sql .= " AND " . $options['active_field'] . " = %i";
|
|
$args = [$sql, $id, (int) $options['active_value']];
|
|
}
|
|
|
|
$sql .= " LIMIT 1";
|
|
$args[0] = $sql;
|
|
|
|
$rows = call_user_func_array([DB::class, 'query'], $args)->getAll();
|
|
self::$currentUsers[$cacheKey] = $rows ? (array) $rows[0] : false;
|
|
|
|
if (self::$currentUsers[$cacheKey] && !empty($options['load_permissions']) && !empty($options['role_field'])) {
|
|
$user = self::$currentUsers[$cacheKey];
|
|
if (isset($user[$options['role_field']])) {
|
|
Permission::setForRole($user[$options['role_field']]);
|
|
}
|
|
}
|
|
|
|
return self::$currentUsers[$cacheKey] ?: null;
|
|
}
|
|
|
|
public static function id(array $options = [])
|
|
{
|
|
$user = self::user($options);
|
|
return $user ? (int) $user['id'] : 0;
|
|
}
|
|
|
|
public static function role(array $options = [])
|
|
{
|
|
$options = self::modernAuthOptions($options);
|
|
$user = self::user($options);
|
|
return ($user && isset($user[$options['role_field']])) ? (string) $user[$options['role_field']] : '';
|
|
}
|
|
|
|
public static function is($role, array $options = [])
|
|
{
|
|
return self::role($options) === (string) $role;
|
|
}
|
|
|
|
protected static function modernAuthOptions(array $options)
|
|
{
|
|
return array_merge([
|
|
'users_table' => SystemTables::table('users'),
|
|
'session_table' => SystemTables::table('users_session'),
|
|
'session_key' => 'auth_user_id',
|
|
'cookie_name' => 'auth_token',
|
|
'cookie_path' => defined('ABS_PATH') && ABS_PATH !== '' ? ABS_PATH : '/',
|
|
'cookie_domain' => '',
|
|
'token_ttl' => defined('COOKIE_LIFETIME') ? COOKIE_LIFETIME : 1209600,
|
|
'identifier_fields' => ['email'],
|
|
'password_field' => 'password_hash',
|
|
'active_field' => 'is_active',
|
|
'active_value' => 1,
|
|
'role_field' => 'role',
|
|
'bcrypt_cost' => 12,
|
|
'remember' => true,
|
|
'issue_session_token' => false,
|
|
'load_permissions' => true,
|
|
'regenerate_session' => true,
|
|
'regenerate_session_on_logout' => true,
|
|
'delete_old_session' => true,
|
|
], $options);
|
|
}
|
|
|
|
protected static function modernAuthCacheKey(array $options)
|
|
{
|
|
return $options['session_key'] . '|' . $options['cookie_name'];
|
|
}
|
|
|
|
protected static function modernIssueRememberToken($userId, array $options, $persistent = true)
|
|
{
|
|
if (headers_sent()) {
|
|
return;
|
|
}
|
|
|
|
$token = bin2hex(random_bytes(32));
|
|
$now = date('Y-m-d H:i:s');
|
|
|
|
DB::Insert($options['session_table'], [
|
|
'user_id' => (int) $userId,
|
|
'token_hash' => hash('sha256', $token),
|
|
'agent' => substr(Request::userAgent(), 0, 255),
|
|
'ip' => self::modernClientIp(),
|
|
'created_at' => $now,
|
|
'last_active' => $now,
|
|
]);
|
|
|
|
self::modernSendCookie($token, $persistent ? time() + (int) $options['token_ttl'] : 0, $options);
|
|
$_COOKIE[$options['cookie_name']] = $token;
|
|
|
|
if (random_int(1, 20) === 1) {
|
|
DB::Delete(
|
|
$options['session_table'],
|
|
"last_active < %s",
|
|
date('Y-m-d H:i:s', time() - (int) $options['token_ttl'])
|
|
);
|
|
}
|
|
}
|
|
|
|
protected static function modernRestoreFromCookie(array $options, $persistSession = true)
|
|
{
|
|
if (empty($_COOKIE[$options['cookie_name']])) {
|
|
return 0;
|
|
}
|
|
|
|
$token = (string) $_COOKIE[$options['cookie_name']];
|
|
if (!preg_match('/^[a-f0-9]{64}$/', $token)) {
|
|
return 0;
|
|
}
|
|
|
|
$hash = hash('sha256', $token);
|
|
$userId = (int) DB::query(
|
|
"SELECT user_id FROM " . $options['session_table'] . " WHERE token_hash = %s LIMIT 1",
|
|
$hash
|
|
)->getOne();
|
|
|
|
if ($userId <= 0) {
|
|
self::modernForgetCookie($options);
|
|
return 0;
|
|
}
|
|
|
|
$sql = "SELECT id FROM " . $options['users_table'] . " WHERE id = %i";
|
|
$args = [$sql, $userId];
|
|
if ($options['active_field'] !== '') {
|
|
$sql .= " AND " . $options['active_field'] . " = %i";
|
|
$args = [$sql, $userId, (int) $options['active_value']];
|
|
}
|
|
|
|
$sql .= " LIMIT 1";
|
|
$args[0] = $sql;
|
|
$ok = (int) call_user_func_array([DB::class, 'query'], $args)->getOne();
|
|
|
|
if ($ok <= 0) {
|
|
DB::Delete($options['session_table'], "token_hash = %s", $hash);
|
|
self::modernForgetCookie($options);
|
|
return 0;
|
|
}
|
|
|
|
if ($persistSession) {
|
|
Session::set($options['session_key'], $userId);
|
|
}
|
|
|
|
$touchKey = '_auth_token_touch_' . substr($hash, 0, 16);
|
|
$lastTouch = (int) Session::get($touchKey);
|
|
if ($lastTouch <= time() - 60) {
|
|
DB::Update(
|
|
$options['session_table'],
|
|
['last_active' => date('Y-m-d H:i:s'), 'ip' => self::modernClientIp()],
|
|
"token_hash = %s",
|
|
$hash
|
|
);
|
|
Session::set($touchKey, time());
|
|
}
|
|
|
|
return $userId;
|
|
}
|
|
|
|
protected static function modernClientIp()
|
|
{
|
|
return substr((string) Request::ip(), 0, 45);
|
|
}
|
|
|
|
protected static function modernForgetCookie(array $options)
|
|
{
|
|
unset($_COOKIE[$options['cookie_name']]);
|
|
if (!headers_sent()) {
|
|
self::modernSendCookie('', 1, $options);
|
|
}
|
|
}
|
|
|
|
protected static function modernSendCookie($value, $expires, array $options)
|
|
{
|
|
$secure = Request::isHttps();
|
|
|
|
setcookie($options['cookie_name'], $value, [
|
|
'expires' => $expires,
|
|
'path' => $options['cookie_path'],
|
|
'domain' => $options['cookie_domain'],
|
|
'secure' => $secure,
|
|
'httponly' => true,
|
|
'samesite' => 'Lax',
|
|
]);
|
|
}
|
|
}
|