Files
mrbs-equbao-2026/lib/MRBS/Auth/AuthDbExt.php
T
人事系统开发 1ba6efd8ed MRBS 1.12.2 等保2.0二级整改完整提交
包含:登录失败锁定、90天密码有效期、30分钟会话超时、
强制改密、登录审计日志、屏幕水印、企业背景图、
备案信息固定底部、favicon、JS空集合保护、
会话过期体验优化(403 JSON)、display_errors 关闭、
固定 key 根治 Integrity check failed 等全部改动

注意:config.inc.php/.htaccess/.user.ini 含敏感信息,
通过 .gitignore 排除,勿推送到公开仓库。
2026-09-09 16:55:02 +08:00

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