Files
mrbs-equbao-2026/lib/MRBS/Auth/Auth.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

474 lines
13 KiB
PHP

<?php
declare(strict_types=1);
namespace MRBS\Auth;
use MRBS\Language;
use MRBS\User;
use function MRBS\format_compound_name;
use function MRBS\get_registrants;
use function MRBS\get_sortable_name;
use function MRBS\get_vocab;
use function MRBS\in_arrayi;
use function MRBS\session;
abstract class Auth
{
protected $getDisplayNamesAtOnce = true;
/**
* Checks if the specified username/password pair are valid.
*
* @return false|string the validated username or false
*/
abstract public function validateUser(
#[\SensitiveParameter]
?string $user,
#[\SensitiveParameter]
?string $pass);
protected function getUserFresh(string $username) : ?User
{
$user = new User($username);
$user->display_name = $username;
$user->level = $this->getDefaultLevel($username);
$user->email = $this->getDefaultEmail($username);
return $user;
}
public function getUser(string $username) : ?User
{
// Cache results for performance as getting user details in
// most authentication types is expensive.
static $users = array();
// Use array_key_exists() rather than isset() in case the value is NULL
if (!array_key_exists($username, $users))
{
// Check to see if this is the current user. If it is, then we
// can save ourselves a potentially expensive operation.
// But we can only do this if we are not being called by getCurrentUser(), which
// some session schemes do, as otherwise we'll end up with an infinite recursion.
// TODO: is there a better way of handling this??
$backtrace = debug_backtrace(DEBUG_BACKTRACE_IGNORE_ARGS);
$backtrace_functions = array_column($backtrace, 'function');
if (!in_array('getCurrentUser', $backtrace_functions))
{
$mrbs_user = session()->getCurrentUser();
}
if (isset($mrbs_user) && ($mrbs_user->username === $username))
{
$user = $mrbs_user;
}
else
{
$user = $this->getUserFresh($username);
// Make sure we've got a sensible display name
if (isset($user) &&
(!isset($user->display_name) || ($user->display_name === '')))
{
$user->display_name = $user->username;
}
}
$users[$username] = $user;
}
return $users[$username];
}
/**
* Returns a username given an email address.
*
* @return string|null The username (or the first found if two or more users share the same email address), or
* NULL if no user is found.
*/
public function getUsernameByEmail(string $email) : ?string
{
global $mail_settings;
// Default: return the email address, unless we're constructing email
// addresses by adding a domain name onto the username. In which case,
// strip off the domain name and then add on any necessary suffix.
// This should be the inverse of getDefaultEmail().
$result = $email;
if (isset($mail_settings['domain']) && ($mail_settings['domain'] !== ''))
{
$at_domain = '@' . self::trimDomain($mail_settings['domain']);
if (str_ends_with($email, $at_domain))
{
// Strip the @domain
$result = str_replace($at_domain, '', $result);
// And add on the suffix if there is one
if (isset($mail_settings['username_suffix']))
{
$result .= $mail_settings['username_suffix'];
}
}
}
return $result;
}
public function getDisplayName(?string $username) : ?string
{
global $get_display_names_all_at_once;
static $display_names = null; // Cache for performance
// Easy case 1: $username is null
if (!isset($username))
{
return null;
}
// Easy case 2: it's the current user
$mrbs_user = session()->getCurrentUser();
if (isset($mrbs_user) && ($mrbs_user->username === $username))
{
return $mrbs_user->display_name;
}
// If we can (and want to) then get all the usernames at the same time. It's
// much faster than getting them one at a time when they are stored externally.
// Check to see if $display_names is set before getting the usernames, so that
// if getUsernames returns false we don't keep on trying it for every username
// (the else block will set $display_names).
if ($get_display_names_all_at_once &&
method_exists($this, 'getUsernames') &&
!isset($display_names) &&
(false !== ($usernames = $this->getUsernames())))
{
$display_names = array_column($usernames, 'display_name', 'username');
}
// Otherwise just get them one at a time
else
{
if (!isset($display_names[$username]))
{
$user = $this->getUser($username);
$display_names[$username] = (isset($user)) ? $user->display_name : $username;
}
}
if (isset($display_names[$username]) && ($display_names[$username] !== ''))
{
return $display_names[$username];
}
return $username;
}
/**
* Checks whether the authentication type allows the creation of new users.
*
* This will normally return false if users are managed elsewhere (e.g. on
* an external database, or on an LDAP server).
*/
public function canCreateUsers() : bool
{
return false;
}
/**
* Checks whether validation of a user by email address is possible and allowed.
*/
public function canValidateByEmail() : bool
{
return false;
}
/**
* Checks whether validation of a user by username is possible and allowed.
*/
public function canValidateByUsername() : bool
{
return true;
}
/**
* Checks whether the method has a password reset facility
*/
public function canResetPassword() : bool
{
return false;
}
/**
* Checks whether the password can be reset by supplying an email address
*/
public function canResetByEmail() : bool
{
return false;
}
/**
* Validates that the password conforms to the password policy.
*
* Ideally this function should also be matched by client-side
* validation, but unfortunately JavaScript's native support for Unicode
* pattern matching is very limited. Would need to be implemented using
* an add-in library.
*/
public function validatePassword(
#[\SensitiveParameter]
string $password) : bool
{
global $pwd_policy;
if (isset($pwd_policy))
{
// Set up regular expressions. Use p{Ll} instead of [a-z] etc.
// to make sure accented characters are included
$pattern = array('alpha' => '/\p{L}/',
'lower' => '/\p{Ll}/',
'upper' => '/\p{Lu}/',
'numeric' => '/\p{N}/',
'special' => '/[^\p{L}|\p{N}]/');
// Check for conformance to each rule
foreach($pwd_policy as $rule => $value)
{
switch($rule)
{
case 'length':
if (mb_strlen($password) < $value)
{
return false;
}
break;
default:
// turn on Unicode matching
$pattern[$rule] .= 'u';
$n = preg_match_all($pattern[$rule], $password, $matches);
if (($n === false) || ($n < $value))
{
return false;
}
break;
}
}
}
// Everything is OK
return true;
}
/**
* Returns an array of registrants' display names
*
* @return string[]
*/
public function getRegistrantsDisplayNames (array $entry, bool $with_registered_by=false, bool $with_registrant_username=false) : array
{
$display_names = array();
// Only bother getting the names if we don't already know how many there are,
// or if we know there are definitely some
if (!isset($entry['n_registered']) || ($entry['n_registered'] > 0))
{
$display_names = $this->getRegistrantsDisplayNamesUnsorted($entry['id'], $with_registered_by, $with_registrant_username);
usort($display_names, 'MRBS\compare_display_names');
}
return $display_names;
}
/**
* @return string[]
*/
protected function getRegistrantsDisplayNamesUnsorted(int $id, bool $with_registered_by, bool $with_registrant_username) : array
{
$display_names = array();
$registrants = get_registrants($id, false);
foreach ($registrants as $registrant)
{
$display_name = $this->getDisplayName($registrant['username']);
// Add in the name of the person who registered this user, if required and if different.
if ($with_registered_by &&
isset($registrant['create_by']) &&
($registrant['create_by'] !== $registrant['username']))
{
if ($with_registrant_username)
{
$display_names[] = get_vocab("registrant_username_and_registered_by",
$registrant['username'],
$display_name,
$this->getDisplayName($registrant['create_by']));
}
else
{
$display_names[] = get_vocab("registrant_registered_by",
$display_name,
$this->getDisplayName($registrant['create_by']));
}
}
else
{
$display_names[] = ($with_registrant_username) ? format_compound_name($registrant['username'], $display_name) : $display_name;
}
}
return $display_names;
}
/**
* Gets the level from the $auth['admin'] array in the config file
*/
protected function getDefaultLevel(?string $username) : int
{
global $auth, $max_level;
// User not logged in, user level '0'
if(!isset($username))
{
return 0;
}
// Check whether the user is an admin; if not they are level 1.
return (isset($auth['admin']) && in_arrayi($username, $auth['admin'])) ? $max_level : 1;
}
/**
* Gets the default email address using config file settings
*/
protected function getDefaultEmail(?string $username) : string
{
global $mail_settings;
if (!isset($username) || $username === '')
{
return '';
}
$email = $username;
// Remove the suffix, if there is one
if (isset($mail_settings['username_suffix']) && ($mail_settings['username_suffix'] !== ''))
{
$suffix = $mail_settings['username_suffix'];
if (substr($email, -strlen($suffix)) === $suffix)
{
$email = substr($email, 0, -strlen($suffix));
}
}
// Add on the domain, if there is one
if (isset($mail_settings['domain']) && ($mail_settings['domain'] !== ''))
{
$email .= '@' . self::trimDomain($mail_settings['domain']);
}
return $email;
}
/**
* Trim any leading '@' character.
*
* Older versions of MRBS required the '@' character to be included in $mail_settings['domain'],
* and we still allow this for backwards compatibility.
*/
private static function trimDomain(string $domain) : string
{
return ltrim($domain, '@');
}
/**
* Callback function for comparing two users.
*
* Compares first by 'display_name' and then by 'username'.
*
* @param array{username: string, display_name: string} $user1
* @param array{username: string, display_name: string} $user2
*/
private static function compareUsers(array $user1, array $user2) : int
{
$display_name1 = get_sortable_name($user1['display_name']);
$display_name2 = get_sortable_name($user2['display_name']);
// Provide fallbacks just in case the display names are NULL or empty
$display_name1 = (isset($display_name1) && ($display_name1 !== '')) ? $display_name1 : $user1['username'];
$display_name2 = (isset($display_name2) && ($display_name2 !== '')) ? $display_name2 : $user2['username'];
$collator = new \Collator(Language::getInstance()->getWebLocale());
$collator->setStrength(\Collator::SECONDARY); // Case-insensitive, but accent-sensitive
$display_name_comparison = $collator->compare($display_name1, $display_name2);
if ($display_name_comparison === 0)
{
return $collator->compare($user1['username'], $user2['username']);
}
return $display_name_comparison;
}
/**
* Sorts an array of users indexed by 'username' and 'display_name', eg the
* output of getUsernames().
*
* Sorts by display_name then username.
*
* @param array{array{username: string, display_name: string}} $users
* @return void
*/
protected static function sortUsers(array &$users) : void
{
usort($users, [__CLASS__, 'compareUsers']);
}
/**
* Check we've got the right session scheme for the type.
*
* To be called for those authentication types which require the same session scheme.
*
* @return void|never
*/
protected function checkSessionMatchesType()
{
global $auth;
if ($auth['session'] !== $auth['type'])
{
$class = get_called_class();
$message = "MRBS configuration error: $class needs \$auth['session'] set to '" . $auth['type'] . "'";
die($message);
}
}
/**
* Writes the debug message to the error log together with the calling method and line number.
* It assumes that it has been called by a debug method.
*/
protected static function logDebugMessage(string $message) : void
{
// Need to go three levels back to get the real calling method.
list( , $called, $caller) = debug_backtrace(DEBUG_BACKTRACE_IGNORE_ARGS, 3);
error_log(
"[MRBS DEBUG] " .
$caller['class'] . $caller['type'] . $caller['function'] . '(' . $called['line'] . ')' .
": $message"
);
}
}