包含:登录失败锁定、90天密码有效期、30分钟会话超时、 强制改密、登录审计日志、屏幕水印、企业背景图、 备案信息固定底部、favicon、JS空集合保护、 会话过期体验优化(403 JSON)、display_errors 关闭、 固定 key 根治 Integrity check failed 等全部改动 注意:config.inc.php/.htaccess/.user.ini 含敏感信息, 通过 .gitignore 排除,勿推送到公开仓库。
226 lines
6.7 KiB
PHP
226 lines
6.7 KiB
PHP
<?php
|
|
declare(strict_types=1);
|
|
namespace MRBS\Auth;
|
|
|
|
use MRBS\DB\DB;
|
|
use MRBS\DB\DBExternalException;
|
|
use MRBS\DB\DBFactory;
|
|
use MRBS\User;
|
|
use ValueError;
|
|
|
|
class AuthDbExt extends AuthDbAbstract
|
|
{
|
|
protected $password_format;
|
|
protected $column_name_password;
|
|
protected $column_name_email;
|
|
protected $column_name_level;
|
|
|
|
public function __construct()
|
|
{
|
|
global $auth;
|
|
|
|
// Take our own copies of the settings
|
|
$vars = array(
|
|
'db_table',
|
|
'password_format',
|
|
'column_name_username',
|
|
'column_name_display_name',
|
|
'column_name_password',
|
|
'column_name_email',
|
|
'column_name_level'
|
|
);
|
|
|
|
foreach ($vars as $var)
|
|
{
|
|
$this->$var = $auth['db_ext'][$var] ?? null;
|
|
}
|
|
|
|
// Backwards compatibility setting
|
|
if (!isset($this->password_format) && !empty($auth['db_ext']['use_md5_passwords']))
|
|
{
|
|
$this->password_format = 'md5';
|
|
}
|
|
}
|
|
|
|
|
|
public function validateUser(
|
|
#[\SensitiveParameter]
|
|
?string $user,
|
|
#[\SensitiveParameter]
|
|
?string $pass)
|
|
{
|
|
// syntax_casesensitive_equals() modifies our SQL params array for us. We need an exact match -
|
|
// MySQL allows trailing spaces when using an '=' comparison, eg 'john' = 'john '
|
|
|
|
$sql_params = array();
|
|
|
|
$query = "SELECT " . $this->connection()->quote($this->column_name_password) .
|
|
"FROM " . $this->connection()->quote($this->db_table) .
|
|
"WHERE " . $this->connection()->syntax_casesensitive_equals($this->column_name_username,
|
|
$user,
|
|
$sql_params);
|
|
|
|
$stmt = $this->connection()->query($query, $sql_params);
|
|
|
|
// Check whether (a) there's just one result and (b) that password matches
|
|
return (($stmt->count() === 1) && $this->password_check($pass, $stmt->next_row()[0])) ? $user : false;
|
|
}
|
|
|
|
|
|
// Checks that a password matches a hash
|
|
protected function password_check(string $password, string $hash) : bool
|
|
{
|
|
switch ($this->password_format)
|
|
{
|
|
case 'crypt':
|
|
case 'password_hash':
|
|
// Don't call password_needs_rehash() as (a) we may not have UPDATE rights on the external
|
|
// database and (b) whether the password needs to be updated will depend on the PHP version
|
|
// on the external system, not this one.
|
|
return (password_verify($password, $hash));
|
|
break;
|
|
case 'plaintext':
|
|
return hash_equals($hash, $password);
|
|
break;
|
|
default:
|
|
// Check we've got a valid hashing algorithm. From PHP 8.0.0 hash() will do this.
|
|
if ((version_compare(PHP_VERSION, '8.0.0') < 0) &&
|
|
!in_array($this->password_format, hash_algos()))
|
|
{
|
|
throw new ValueError("'$this->password_format' is not a valid hashing algorithm");
|
|
}
|
|
return hash_equals($hash, hash($this->password_format, $password));
|
|
break;
|
|
}
|
|
}
|
|
|
|
|
|
protected function getUserFresh(string $username) : ?User
|
|
{
|
|
$sql_params = array();
|
|
|
|
// Only retrieve the columns we need (a) to minimise the query and (b) to avoid
|
|
// sending unnecessary information unencrypted over the internet (Remote SQL is
|
|
// usually unencrypted).
|
|
$columns = array();
|
|
|
|
$properties = array(
|
|
'column_name_display_name',
|
|
'column_name_email',
|
|
'column_name_level'
|
|
);
|
|
|
|
foreach ($properties as $property)
|
|
{
|
|
if (isset($this->$property))
|
|
{
|
|
$columns[] = $this->$property;
|
|
}
|
|
}
|
|
|
|
$sql = "SELECT " . implode(', ', array_map(array($this->connection(), 'quote'), $columns)) . "
|
|
FROM " . $this->connection()->quote($this->db_table) . "
|
|
WHERE " . $this->connection()->syntax_casesensitive_equals($this->column_name_username,
|
|
$username,
|
|
$sql_params) . "
|
|
LIMIT 1";
|
|
|
|
$stmt = $this->connection()->query($sql, $sql_params);
|
|
|
|
// The username doesn't exist - return NULL
|
|
if ($stmt->count() === 0)
|
|
{
|
|
return null;
|
|
}
|
|
|
|
// The username does exist - return a User object
|
|
$data = $stmt->next_row_keyed();
|
|
|
|
$user = new User($username);
|
|
|
|
// Set the email address
|
|
if (isset($this->column_name_email) && isset($data[$this->column_name_email]))
|
|
{
|
|
$user->email = $data[$this->column_name_email];
|
|
}
|
|
|
|
// Set the display name
|
|
if (isset($this->column_name_display_name) && isset($data[$this->column_name_display_name]))
|
|
{
|
|
$user->display_name = $data[$this->column_name_display_name];
|
|
}
|
|
|
|
// Set the level
|
|
// First get the default level. Any admins defined in the config
|
|
// file override settings in the external database.
|
|
$user->level = $this->getDefaultLevel($username);
|
|
|
|
// Then if they are not an admin get their level from the external db
|
|
if ($user->level < 2)
|
|
{
|
|
// If there's can entry in the db, then use that
|
|
if (isset($this->column_name_level) &&
|
|
($this->column_name_level !== '') &&
|
|
isset($data[$this->column_name_level]))
|
|
{
|
|
$user->level = $data[$this->column_name_level];
|
|
}
|
|
}
|
|
|
|
// Then set the remaining properties. (We don't set all the properties from
|
|
// $data initially because we want to preserve the default values if we don't
|
|
// have data for the four important properties.)
|
|
// (Note that normally there won't be any extra properties because we have
|
|
// specified above the columns that we want, but this code is here so that extra
|
|
// columns can be added if required.)
|
|
foreach ($data as $key => $value)
|
|
{
|
|
if (!property_exists($user, $key))
|
|
{
|
|
$user->$key = $value;
|
|
}
|
|
}
|
|
|
|
return $user;
|
|
}
|
|
|
|
|
|
protected function connection(): ?DB
|
|
{
|
|
global $auth;
|
|
|
|
static $connection = null;
|
|
|
|
if (!isset($connection))
|
|
{
|
|
if (empty($auth['db_ext']['db_system']))
|
|
{
|
|
$auth['db_ext']['db_system'] = 'mysql';
|
|
}
|
|
|
|
// Establish a connection
|
|
$port = isset($auth['db_ext']['db_port']) ? (int) $auth['db_ext']['db_port'] : null;
|
|
|
|
try
|
|
{
|
|
$connection = DBFactory::create(
|
|
$auth['db_ext']['db_system'],
|
|
$auth['db_ext']['db_host'],
|
|
$auth['db_ext']['db_username'],
|
|
$auth['db_ext']['db_password'],
|
|
$auth['db_ext']['db_name'],
|
|
false,
|
|
$port
|
|
);
|
|
}
|
|
catch (\PDOException $e)
|
|
{
|
|
// Differentiate between an error in the MRBS database and the external database
|
|
throw new DBExternalException($e->getMessage(), $e->getCode());
|
|
}
|
|
}
|
|
|
|
return $connection;
|
|
}
|
|
}
|