MRBS 1.12.2 等保2.0二级整改完整提交
Docker image / push (push) Canceled after 0s

包含:登录失败锁定、90天密码有效期、30分钟会话超时、
强制改密、登录审计日志、屏幕水印、企业背景图、
备案信息固定底部、favicon、登录页JS修复等全部改动
This commit is contained in:
人事系统开发
2026-09-08 21:19:47 +08:00
commit 48092cab42
2221 changed files with 659586 additions and 0 deletions
+246
View File
@@ -0,0 +1,246 @@
<?php
declare(strict_types=1);
namespace MRBS\Session;
use MRBS\SessionHandler\SessionHandlerDb;
use MRBS\SessionHandler\SessionHandlerDbException;
use MRBS\User;
use SessionHandler;
use function MRBS\db;
use function MRBS\db_schema_version;
use function MRBS\get_cookie_path;
use function MRBS\is_https;
abstract class Session
{
protected const SAMESITE_NONE = 'None';
protected const SAMESITE_LAX = 'Lax';
protected const SAMESITE_STRICT = 'Strict';
protected $lifetime;
protected $samesite = null;
public function __construct()
{
global $auth, $cookie_samesite_lax;
// Child classes can set $this->samesite
if (!isset($this->samesite))
{
$this->samesite = ($cookie_samesite_lax) ? self::SAMESITE_LAX : self::SAMESITE_STRICT;
}
// Set the session lifetime
if (!isset($this->lifetime))
{
$this->lifetime = $auth['session_php']['session_expire_time'] ?? 0;
}
// Start up sessions
$this->init($this->lifetime);
}
/**
* Get the session handler to use.
*
* If the database session handler is not available, use the ordinary PHP session handler.
*
* @return SessionHandlerDb|SessionHandler
*/
protected function getSessionHandler()
{
// The sessions table was only created in Upgrade 56. We test for the schema version rather than the existence of
// the table, because the table is renamed in later upgrades.
if (db_schema_version(db()) < 56)
{
return new SessionHandler();
}
// The DB session handler uses locks, and because we use locks elsewhere, this means we need support for multiple
// locks. We need to test now, rather than catching an exception later, because resetting the session handler
// will reset the session id causing us to lose session data.
if (!db()->supportsMultipleLocks())
{
$message = "The database server does not support multiple locks, so the database session handler " .
"cannot be used. Using ordinary PHP sessions instead.";
trigger_error($message);
return new SessionHandler();
}
// Otherwise use the DB session handler.
return new SessionHandlerDb();
}
// Normally there's no need to call init() from outside the Session classes.
// It only needs to be called to restart sessions, after, for example, a user
// has been logged off, and you need to use session variables.
public function init(int $lifetime) : void
{
global $auth;
if (session_status() === PHP_SESSION_ACTIVE)
{
// We've already started sessions
return;
}
// Session settings, for security
// ini_set() only accepts string values prior to PHP 8.1.0
ini_set('session.cookie_httponly', '1');
if (version_compare(PHP_VERSION, '7.3', '>='))
{
// Only introduced in PHP Version 7.3
ini_set('session.cookie_samesite', $this->samesite);
}
ini_set('session.cookie_secure', (is_https()) ? '1' : '0');
// More settings, as a defence against session fixation.
ini_set('session.use_only_cookies', '1');
ini_set('session.use_strict_mode', '1');
ini_set('session.use_trans_sid', '0');
$cookie_path = get_cookie_path();
// We don't want the session garbage collector to delete the session before it has expired
if ($lifetime !== 0)
{
assert(version_compare(MRBS_MIN_PHP_VERSION, '8.1') < 0, 'The strval() in the line below is no longer required.');
ini_set('session.gc_maxlifetime', strval(max(ini_get('session.gc_maxlifetime'), $lifetime)));
}
if (isset($auth['session_php']['session_name']))
{
// call before session_set_cookie_params() - see PHP manual
session_name($auth['session_php']['session_name']);
}
session_set_cookie_params($lifetime, $cookie_path);
// Set the session handler and start up sessions
try
{
session_set_save_handler($this->getSessionHandler(), true);
if (false === session_start())
{
throw new \Exception("session_start() failed");
}
}
catch (\Exception $e)
{
$message = "Could not start sessions ('" . $e->getMessage() . "').";
$message .= " Trying ordinary PHP sessions.";
trigger_error($message, E_USER_WARNING);
session_set_save_handler(new SessionHandler(), true);
if (false === session_start())
{
throw new \Exception("MRBS: could not start sessions");
}
}
}
protected function destroy() : void
{
// Delete the session data encryption key cookie. If we don't do this then, when
// a new session is created, unless the expiry is set to 0 (ie on browser close),
// it will have a longer lifetime than the key cookie, which when it was created
// was given the same lifetime as the session cookie. Once the key cookie expires,
// the session handler will create a new cookie with a new key. So when the session
// handler comes to decrypt the session data it will be doing so with the new key,
// and not the key used to encrypt it. This will result in the Crypto library
// throwing a WrongKeyOrModifiedCiphertextException with the message "Integrity
// check failed".
//
// This needs to be done before the session is destroyed, otherwise the
// deleteKeyCookie method won't be able to get the session name (which it needs
// in order to delete the key cookie).
SessionHandlerDb::deleteKeyCookie();
// Unset the session variables
// Note that session_unset() only works if a session is active.
$_SESSION = [];
// Check whether a session is active before destroying it in order to avoid a
// "Trying to destroy uninitialized session" warning.
if (session_status() === PHP_SESSION_ACTIVE)
{
session_destroy();
}
// Problems have been reported on Windows IIS with session data not being
// written out without a call to session_write_close(). [Is this necessary
// after session_destroy() ??]
session_write_close();
}
protected function regenerate() : void
{
// Regenerate the session id
session_regenerate_id(true);
// Change the lifetime of the key cookie to match the new expiry - see the
// comment in destroy().
SessionHandlerDb::regenerateKeyCookie();
}
public function get(string $name)
{
return $_SESSION[$name] ?? null;
}
public function isset(string $name) : bool
{
return isset($_SESSION[$name]);
}
public function set(string $name, $value) : void
{
$_SESSION[$name] = $value;
}
public function unset(string $name) : void
{
unset($_SESSION[$name]);
}
// Returns the currently logged-in user
// This method provides the fallback user for un-logged in users.
// Subclasses are expected to override this method, calling it as the parent
// if they cannot find a current user.
public function getCurrentUser() : ?User
{
global $auth;
if (empty($auth['allow_anonymous_booking']))
{
return null;
}
// Use an empty string for anonymous bookings
return new User('');
}
// Allows this to be extended with strategies for getting the referer when
// HTTP_REFERER is going to be unreliable, eg when the Referrer-Policy is
// set to strict-origin.
public function getReferrer() : ?string
{
global $server;
return $server['HTTP_REFERER'] ?? null;
}
// Updates the current and previous pages
public function updatePage(string $url) : void
{
}
}
+91
View File
@@ -0,0 +1,91 @@
<?php
declare(strict_types=1);
namespace MRBS\Session;
use MRBS\Form\Form;
use MRBS\User;
use phpCAS;
use function MRBS\auth;
use function MRBS\location_header;
use function MRBS\this_page;
class SessionCas extends SessionWithLogin
{
public function __construct()
{
$this->checkTypeMatchesSession();
$this->samesite = self::SAMESITE_LAX;
auth()->init(); // Initialise CAS
parent::__construct();
}
public function init(int $lifetime) : void
{
// phpCAS does its own session initialisation and handling
}
public function authGet(?string $target_url=null, ?string $returl=null, ?string $error=null, bool $raw=false) : void
{
if (!phpCAS::isAuthenticated())
{
phpCAS::forceAuthentication();
}
}
public function getCurrentUser() : ?User
{
return (phpCAS::isAuthenticated()) ? auth()->getUser(phpCAS::getUser()) : parent::getCurrentUser();
}
public function getLogonFormParams() : ?array
{
$target_url = this_page(true);
return array(
'action' => $target_url,
'method' => Form::METHOD_POST,
'hidden_inputs' => array('target_url' => $target_url,
'action' => 'QueryName')
);
}
public function processForm() : void
{
if (isset($this->form['action']))
{
// Target of the form with sets the URL argument "action=QueryName".
if ($this->form['action'] == 'QueryName')
{
phpCAS::forceAuthentication();
}
// Target of the form with sets the URL argument "action=SetName".
// Will eventually return to URL argument "target_url=whatever".
if ($this->form['action'] == 'SetName')
{
// If we're going to do something then check the CSRF token first
Form::checkToken();
// You should only get here using CAS authentication after clicking the logoff
// link, no matter what the value of the form parameters.
$this->logoffUser();
location_header($this->form['target_url']); // Redirect browser to initial page
}
}
}
public function logoffUser() : void
{
phpCAS::logout();
}
}
+70
View File
@@ -0,0 +1,70 @@
<?php
declare(strict_types=1);
namespace MRBS\Session;
use MRBS\SessionHandler\Cookie;
use MRBS\SessionHandler\SessionHandlerCookie;
use SessionHandler;
use function MRBS\get_cookie_path;
/**
* Manage sessions via cookies stored in the client browser.
*/
class SessionCookie extends SessionPhp
{
public function __construct()
{
global $auth;
// We have to use output buffering to ensure that the cookies are set before any other output is sent.
ob_start();
// Delete old-style cookies
if (!empty($_COOKIE) && isset($_COOKIE["UserName"]))
{
setcookie('UserName', '', time()-42000, get_cookie_path());
}
// Set the session lifetime
$this->lifetime = $auth['session_cookie']['session_expire_time'] ?? 0;
parent::__construct();
}
protected function getSessionHandler() : SessionHandlerCookie
{
global $auth;
// Set the session handler
return new SessionHandlerCookie(
$auth['session_cookie']['secret'],
$auth['session_cookie']['hash_algorithm'],
$auth['session_cookie']['include_ip']
);
}
public function init(int $lifetime) : void
{
$old_session_id = session_id();
parent::init($lifetime);
$new_session_id = session_id();
SessionHandlerCookie::updateExpiry($new_session_id, ($lifetime === 0) ? 0 : time() + $lifetime);
// Not entirely sure why this is necessary, but the old session data cookie is sometimes not deleted.
if (($old_session_id !== '') && ($old_session_id !== $new_session_id))
{
Cookie::delete($old_session_id);
}
}
public function logoffUser() : void
{
unset($_SESSION['user']);
session_regenerate_id(true);
session_write_close();
}
}
+18
View File
@@ -0,0 +1,18 @@
<?php
declare(strict_types=1);
namespace MRBS\Session;
class SessionFactory
{
public static function create(string $type)
{
// Transform the session type from lowercase_separated to LowercaseSeparated
$parts = explode('_', $type);
$parts = array_map('ucfirst', $parts);
$class = __NAMESPACE__ . "\\Session" . implode('', $parts);
return new $class;
}
}
+41
View File
@@ -0,0 +1,41 @@
<?php
declare(strict_types=1);
namespace MRBS\Session;
use MRBS\User;
use function MRBS\auth;
/**
* This is a slight variant of session_ip.
* Session management scheme that uses the DNS name of the computer to identify users and administrators.
* Anyone who can access the server can make bookings etc.
*
* To use this authentication scheme set the following things in config.inc.php:
*
* $auth['type'] = 'none';
* $auth['session'] = 'host';
*
* Then, you may configure admin users:
*
* $auth['admin'][] = 'DNSname1';
* $auth['admin'][] = 'DNSname2';
*/
class SessionHost extends Session
{
// No need to prompt for a name: if no DNSname is returned, the IP address is used
public function getCurrentUser() : ?User
{
global $server;
if ((!isset($server['REMOTE_ADDR'])) ||
(!is_string($server['REMOTE_ADDR'])) ||
(($server['REMOTE_ADDR'] === '')))
{
return parent::getCurrentUser();
}
return auth()->getUser(gethostbyaddr($server['REMOTE_ADDR']));
}
}
+63
View File
@@ -0,0 +1,63 @@
<?php
declare(strict_types=1);
namespace MRBS\Session;
use MRBS\User;
use function MRBS\auth;
/**
* Get user identity using the HTTP basic authentication.
*/
class SessionHttp extends SessionWithLogin
{
public function authGet(?string $target_url=null, ?string $returl=null, ?string $error=null, bool $raw=false) : void
{
global $auth;
header("WWW-Authenticate: Basic realm=\"$auth[realm]\"");
header("HTTP/1.0 401 Unauthorized");
}
public function getCurrentUser() : ?User
{
global $server;
if (!isset($server['PHP_AUTH_USER']))
{
return parent::getCurrentUser();
}
// Trim any whitespace because PHP_AUTH_USER can contain it.
$php_auth_user = trim($server['PHP_AUTH_USER']);
if ($php_auth_user === '')
{
return parent::getCurrentUser();
}
if (auth()->validateUser($php_auth_user, self::getAuthPassword()) === false)
{
return parent::getCurrentUser();
}
return auth()->getUser($php_auth_user);
}
public function getLogoffFormParams() : ?array
{
// Just return null - you can't log off
// (well, there are ways of achieving a logoff but we haven't implemented them)
return null;
}
private static function getAuthPassword() : ?string
{
global $server;
return (isset($server['PHP_AUTH_PW'])) ? $server['PHP_AUTH_PW'] : null;
}
}
+41
View File
@@ -0,0 +1,41 @@
<?php
declare(strict_types=1);
namespace MRBS\Session;
use MRBS\User;
use function MRBS\auth;
/**
* Session management scheme that uses IP addresses to identify users.
*
* Anyone who can access the server can make bookings. Administrators are also identified by their IP address.
*
* To use this authentication scheme, set the following things in config.inc.php:
*
* $auth['type'] = 'none';
* $auth['session'] = 'ip';
*
* Then, you may configure admin users:
*
* $auth['admin'][] = '127.0.0.1'; // Local host = the server you're running on
* $auth['admin'][] = '192.168.0.1';
*/
class SessionIp extends Session
{
// No need to prompt for a name - IP address always there
public function getCurrentUser() : ?User
{
global $server;
if ((!isset($server['REMOTE_ADDR'])) ||
(!is_string($server['REMOTE_ADDR'])) ||
(($server['REMOTE_ADDR'] === '')))
{
return parent::getCurrentUser();
}
return auth()->getUser($server['REMOTE_ADDR']);
}
}
+129
View File
@@ -0,0 +1,129 @@
<?php
declare(strict_types=1);
namespace MRBS\Session;
use Joomla\CMS\Factory;
use Joomla\CMS\Language\Language;
use MRBS\Joomla\JFactory;
use MRBS\User;
use function MRBS\auth;
require_once MRBS_ROOT . '/auth/cms/joomla.inc';
class SessionJoomla extends SessionWithLogin
{
private const NAMESPACE = 'MRBS';
private $app;
private $session;
public function __construct()
{
$this->checkTypeMatchesSession();
if (!defined('JVERSION'))
{
throw new \Exception("Joomla! version not known");
}
if (version_compare(JVERSION, '4.0', '<'))
{
$this->app = JFactory::getApplication('site');
$this->app->initialise();
}
else
{
// Thanks to Alex Chartier and Emmanuel Ingelaere.
// See https://groups.google.com/g/joomla-dev-general/c/55J2s9hhMxA
// Boot the DI container
$container = Factory::getContainer();
// Alias the session service keys to the web session service as that is the primary session backend for this application.
// In addition to aliasing "common" service keys, we also create aliases for the PHP classes to ensure autowiring objects
// is supported. This includes aliases for aliased class names, and the keys for aliased class names should be considered
// deprecated to be removed when the class name alias is removed as well.
$container->alias('session.web', 'session.web.site')
->alias('session', 'session.web.site')
->alias('JSession', 'session.web.site')
->alias(\Joomla\CMS\Session\Session::class, 'session.web.site')
->alias(\Joomla\Session\Session::class, 'session.web.site')
->alias(\Joomla\Session\SessionInterface::class, 'session.web.site');
// Instantiate the application.
$this->app = $container->get(\Joomla\CMS\Application\SiteApplication::class);
// Build the namespace map and load the language (necessary from Joomla 4.3.0 onwards - see
// https://groups.google.com/g/joomla-dev-general/c/55J2s9hhMxA/m/IpBrs3HZAgAJ?utm_medium=email&utm_source=footer&pli=1
// and https://joomla.stackexchange.com/questions/32145/joomla-4-error-when-i-use-getarticleroute/32146#32146)
if (version_compare(JVERSION, '4.3.0', '>='))
{
$this->app->createExtensionNamespaceMap();
$lang = Language::getInstance('en'); // doesn't matter which language as we never use it
$this->app->loadLanguage($lang);
}
// Set the application as global app
Factory::$application = $this->app;
}
if (version_compare(JVERSION, '5.0', '<'))
{
$this->session = JFactory::getSession();
}
else
{
$this->session = Factory::getSession();
}
parent::__construct();
}
public function init(int $lifetime) : void
{
}
public function get(string $name)
{
return $this->session->get($name, null, self::NAMESPACE);
}
public function isset(string $name) : bool
{
return ($this->get($name) !== null);
}
public function set(string $name, $value) : void
{
$this->session->set($name, $value, self::NAMESPACE);
}
public function unset(string $name) : void
{
$this->session->clear($name, self::NAMESPACE);
}
public function getCurrentUser() : ?User
{
return auth()->getCurrentUser() ?? parent::getCurrentUser();
}
protected function logonUser(string $username) : void
{
// Don't need to do anything: the user will have been logged on when the
// username and password were validated.
}
public function logoffUser() : void
{
$this->app->logout();
}
}
+33
View File
@@ -0,0 +1,33 @@
<?php
declare(strict_types=1);
namespace MRBS\Session;
use MRBS\User;
use function MRBS\auth;
/**
* Session management scheme that uses Windows NT domain users and Internet
* Information Server as the source for user authentication.
*
* To use this authentication scheme, set the following things in config.inc.php:
*
* $auth['type'] = 'none';
* $auth['session'] = 'nt';
*
* Then, you may configure admin users:
*
* $auth['admin'][] = 'nt_username1';
* $auth['admin'][] = 'nt_username2';
*
* See AUTHENTICATION for more information.
*/
class SessionNt extends Session
{
// For this scheme no need to prompt for a name - NT User always there.
public function getCurrentUser() : ?User
{
return auth()->getUser(get_current_user()) ?? parent::getCurrentUser();
}
}
+57
View File
@@ -0,0 +1,57 @@
<?php
declare(strict_types=1);
namespace MRBS\Session;
use MRBS\User;
use function MRBS\auth;
/**
* Session management scheme that relies on OmniHttpd security for user
* authentication. This is suitable for few users because we have to create all
* users connecting to MRBS, since they will have to log in.
*
* To use this authentication scheme set the following things :
* - Edit your virtual server hosting MRBS.
* - Select security tab.
* - If not yet set, choose "User and Directory" security type.
* - Select "Users and groups" tab.
* - Here, select "New User" and create as many users (Username/passwords) as you have users using MRBS.
* - Select "New Group".
* - Type "MRBS" as group name and add all users you just created to this group.
* - Now select "Access Control list" tab.
* - Select New. ENTER the relative path to MRBS. FOR example, if you created
* the MRBS folder on the root web folder, you should type /MRBS/.
* - Now go to the" user permission "tab, select " * ",
* - Select Properties", and type MRBS (remove the star) and select "Is group".
*
* That's all! Confirm all windows. Now it is the web server that authenticates each user.
*
* In config.inc.php:
*
* $auth['type'] = 'none';
* $auth['session'] = 'omni';
*
* Then, you may configure admin users:
*
* $auth['admin'][] = 'user1';
* $auth['admin'][] = 'user2';
*/
class SessionOmni extends Session
{
// No need to prompt for a name - this is done by the server.
public function getCurrentUser() : ?User
{
global $server;
if ((!isset($server['REMOTE_USER'])) ||
(!is_string($server['REMOTE_USER'])) ||
(($server['REMOTE_USER'] === '')))
{
return parent::getCurrentUser();
}
return auth()->getUser($server['REMOTE_USER']);
}
}
+159
View File
@@ -0,0 +1,159 @@
<?php
declare(strict_types=1);
namespace MRBS\Session;
use MRBS\SessionHandler\Cookie;
use MRBS\User;
use function MRBS\auth;
use function MRBS\get_form_var;
use function MRBS\is_ajax;
use function MRBS\str_ends_with_array;
use function MRBS\url_base;
/**
* Uses PHP's built-in session handling.
*/
class SessionPhp extends SessionWithLogin
{
public function __construct()
{
global $auth, $server;
parent::__construct();
// Check to see if we've been inactive for longer than allowed and if so log out the user.
// Don't log out the user if we're in kiosk mode because the kiosk will normally be inactive.
// Note that we cannot use is_kiosk_mode() here as that will create an infinite loop calling session().
if (!empty($auth['session_php']['inactivity_expire_time']) && !isset($_SESSION['kiosk_password_hash']))
{
if (isset($_SESSION['LastActivity']) &&
((time() - $_SESSION['LastActivity']) > $auth['session_php']['inactivity_expire_time']))
{
$this->logoffUser();
}
// Ajax requests don't count as activity, unless it's the special Ajax request used
// to record client side activity.
$activity = get_form_var('activity', 'int');
if ($activity || !is_ajax() || !isset($_SESSION['LastActivity'])) {
$_SESSION['LastActivity'] = time();
}
}
// Move the current page to the last page, so it can be used as a referrer, and store the new current page -
// but only if (a) we are at the top level of the MRBS web directory (eg index.php) so as to eliminate all
// the ./js, ./ajax and ./css pages and (b) this is not otherwise an Ajax request, eg one of the prefetch
// calls to index.php.
if (isset($server['SCRIPT_FILENAME']) && (MRBS_ROOT === dirname($server['SCRIPT_FILENAME'])) &&
!(isset($server['HTTP_X_REQUESTED_WITH']) && ($server['HTTP_X_REQUESTED_WITH'] === 'XMLHttpRequest')))
{
$this->updatePage($server['REQUEST_URI'] ?? $server['PHP_SELF'] ?? null);
}
}
// If the server has a Referrer-Policy of strict-origin then HTTP_REFERER will be unreliable
// and it is better to use the last page that we have stored in the $_SESSION variable.
public function getReferrer(): ?string
{
$result = parent::getReferrer();
// If the last page is known, and it's well-formed (parse_url() doesn't return false), and it's
// not already a fully-qualified URL (ie the scheme is empty), and we've got a url base that
// isn't the empty string, then return everything after the last '/' from the last page, prefixed
// by the url base, in case there's a proxy in use.
if (isset($_SESSION['last_page']))
{
$last_page = $_SESSION['last_page'];
$scheme = parse_url($last_page, PHP_URL_SCHEME);
if (($scheme === null) && ('' !== ($base = url_base())))
{
// Get everything after the last '/'
$array = explode('/', $last_page);
$result = $base . array_pop($array);
}
}
return $result;
}
public function updatePage(?string $url): void
{
// Don't update the page if the URL as the same as the one we've already got
// stored for this page. This will be the case if the user has refreshed the
// browser, and if we update the page then we'll lose the last page, which is
// sometimes needed for MRBS to know where to go back to.
if (isset($_SESSION['this_page']) && ($url === $_SESSION['this_page']))
{
return;
}
$_SESSION['last_page'] = $_SESSION['this_page'] ?? null;
$_SESSION['this_page'] = $url;
}
public function getCurrentUser() : ?User
{
$result = $_SESSION['user'] ?? null;
// For some unknown reason the integer value 0 is sometimes stored in the session
// variable. It's not clear how this can happen.
if (isset($result) && !(is_object($result) && is_a($result, 'MRBS\User')))
{
trigger_error('$_SESSION["user"] is expected to be a User object, not ' . json_encode($result), E_USER_WARNING);
$result = null;
}
return $result ?? parent::getCurrentUser();
}
protected function logonUser(string $username) : void
{
$user = auth()->getUser($username);
// As a defence against session fixation, regenerate
// the session id and delete the old session.
$this->regenerate();
$_SESSION['user'] = $user;
// Problems have been reported on Windows IIS with session data not being
// written out without a call to session_write_close()
session_write_close();
}
public function logoffUser() : void
{
global $cookie_path_override;
if (ini_get("session.use_cookies"))
{
// Delete the session cookie
Cookie::delete(session_name());
// Delete any cookies which may have previously been set, incorrectly, before the fixes to get_cookie_path (see
// https://github.com/meeting-room-booking-system/mrbs-code/commit/90ceeb8a0bc5f4850065695a3e085114c5ecae8e and
// https://github.com/meeting-room-booking-system/mrbs-code/commit/cb74320048149c4199281b88d50fb69988a41312).
// Note that the problem didn't occur if $cookie_path_override was set.
// In time, once all the incorrect cookies have expired naturally, this block can be deleted.
if (!isset($cookie_path_override))
{
$params = session_get_cookie_params();
$suffixes = array('ajax/', 'js/');
// If the path ends with one of the suffixes we'll already have deleted it above
if (!str_ends_with_array($params['path'], $suffixes))
{
foreach ($suffixes as $suffix)
{
setcookie(session_name(), '', time() - 42000, $params['path'] . $suffix, $params['domain'], $params['secure'], isset($params['httponly']));
}
}
}
}
$this->destroy();
}
}
@@ -0,0 +1,84 @@
<?php
declare(strict_types=1);
namespace MRBS\Session;
use MRBS\Form\Form;
use MRBS\User;
use function MRBS\auth;
/**
* Get user identity/password using the REMOTE_USER environment variable. Both identity and password
* equal the value of REMOTE_USER.
*
* To use this session scheme, set in config.inc.php:
*
* $auth['session'] = 'remote_user';
* $auth['type'] = 'none';
*
* If you want to display a login link, set in config.inc.php:
*
* $auth['remote_user']['login_link'] = '/login/link.html';
*
* If you want to display a logout link, set in config.inc.php:
*
* $auth['remote_user']['logout_link'] = '/logout/link.html';
*/
class SessionRemoteUser extends SessionWithLogin
{
// User is expected to already be authenticated by the web server, so do nothing
public function authGet(?string $target_url=null, ?string $returl=null, ?string $error=null, bool $raw=false) : void
{
}
public function getCurrentUser() : ?User
{
global $server;
if ((!isset($server['REMOTE_USER'])) ||
(!is_string($server['REMOTE_USER'])) ||
(($server['REMOTE_USER'] === '')))
{
return parent::getCurrentUser();
}
return auth()->getUser($server['REMOTE_USER']);
}
public function getLogonFormParams() : ?array
{
global $auth;
if (isset($auth['remote_user']['login_link']))
{
return array(
'action' => $auth['remote_user']['login_link'],
'method' => Form::METHOD_GET,
);
}
else
{
return null;
}
}
public function getLogoffFormParams() : ?array
{
global $auth;
if (isset($auth['remote_user']['logout_link']))
{
return array(
'action' => $auth['remote_user']['logout_link'],
'method' => Form::METHOD_GET
);
}
else
{
return null;
}
}
}
+165
View File
@@ -0,0 +1,165 @@
<?php
declare(strict_types=1);
namespace MRBS\Session;
use MRBS\Form\Form;
use MRBS\User;
use SimpleSAML\Auth\Simple;
use function MRBS\auth;
use function MRBS\this_page;
use function MRBS\url_base;
/**
* Session management scheme that delegates everything to a ready configured
* SimpleSamlPhp instance. You should use this scheme, along with the
* authentication scheme with the same name, if you want your users to
* authenticate using SAML Single Sign-on.
*
* In config.inc.php (assuming Active Directory attributes):
*
* $auth['type'] = 'saml';
* $auth['session'] = 'saml';
* $auth['saml']['ssp_path'] = '/opt/simplesamlphp';
* $auth['saml']['authsource'] = 'default-sp';
* $auth['saml']['attr']['username'] = 'sAMAccountName';
* $auth['saml']['attr']['mail'] = 'mail';
* $auth['saml']['attr']['givenName'] = 'givenname';
* $auth['saml']['attr']['surname'] = 'sn'
* $auth['saml']['admin']['memberOf'] = ['CN=Domain Admins,CN=Users,DC=example,DC=com'];
*
* This scheme assumes that you've already configured SimpleSamlPhp,
* and that you have set up aliases in your webserver so that SimpleSamlPhp
* can handle incoming assertions. Refer to the SimpleSamlPhp documentation
* for more information on how to do that.
*
* @see https://simplesamlphp.org/docs/stable/simplesamlphp-install
* @see https://simplesamlphp.org/docs/stable/simplesamlphp-sp
*/
class SessionSaml extends SessionWithLogin
{
public $ssp;
public function __construct()
{
global $auth;
$this->checkTypeMatchesSession();
// Check that the config variables have been set
if (!isset($auth['saml']['ssp_path']))
{
throw new \Exception('$auth["saml"]["ssp_path"] must be set in the config file.');
}
if (!isset($auth['saml']['attr']['username']))
{
throw new \Exception('$auth["saml"]["attr"]["username"] must be set in the config file.');
}
// Include the SimpleSamlPhp autoloader
require_once $auth['saml']['ssp_path'] . '/lib/_autoload.php';
// Get the SimpleSamlPhp instance for the configured auth source
$authSource = $auth['saml']['authsource'] ?? 'default-sp';
$this->ssp = new \SimpleSAML\Auth\Simple($authSource);
$this->samesite = self::SAMESITE_LAX;
parent::__construct();
}
public function init(int $lifetime) : void
{
global $auth;
if ($auth['saml']['disable_mrbs_session_init'])
{
// If we're using SAML then initialising sessions here can interfere with
// session handling in some SAML libraries
return;
}
parent::init($lifetime);
}
// No need to prompt for a name - this is done by SimpleSamlPhp
public function authGet(?string $target_url=null, ?string $returl=null, ?string $error=null, bool $raw=false) : void
{
$this->ssp->requireAuth();
}
public function getCurrentUser() : ?User
{
$current_username = $this->getUsername();
return (isset($current_username)) ? auth()->getUser($current_username) : parent::getCurrentUser();
}
public function getUsername() : ?string
{
global $auth;
if (!$this->ssp->isAuthenticated())
{
return null;
}
$userData = $this->ssp->getAttributes();
$userNameAttr = $auth['saml']['attr']['username'];
return array_key_exists($userNameAttr, $userData) ? $userData[$userNameAttr][0] : null;
}
public function getLogonFormParams() : ?array
{
$target_url = url_base() . this_page(true);
$url = $this->ssp->getLoginURL($target_url);
$baseURL = strstr($url, '?', true);
parse_str(substr(strstr($url, '?'), 1), $params);
$result = array(
'action' => $baseURL,
'method' => Form::METHOD_GET
);
if (!empty($params))
{
$result['hidden_inputs'] = $params;
}
return $result;
}
public function getLogoffFormParams() : ?array
{
$target_url = url_base() . this_page(true);
$url = $this->ssp->getLogoutURL($target_url);
$baseURL = strstr($url, '?', true);
parse_str(substr(strstr($url, '?'), 1), $params);
$result = array(
'action' => $baseURL,
'method' => Form::METHOD_GET
);
if (!empty($params))
{
$result['hidden_inputs'] = $params;
}
return $result;
}
public function processForm() : void
{
// No need to do anything - all handled by SAML
}
}
+324
View File
@@ -0,0 +1,324 @@
<?php
declare(strict_types=1);
namespace MRBS\Session;
use MRBS\Form\ElementA;
use MRBS\Form\ElementFieldset;
use MRBS\Form\ElementP;
use MRBS\Form\FieldDiv;
use MRBS\Form\FieldInputPassword;
use MRBS\Form\FieldInputSubmit;
use MRBS\Form\FieldInputText;
use MRBS\Form\Form;
use MRBS\Audit;
use function MRBS\auth;
use function MRBS\get_form_var;
use function MRBS\get_vocab;
use function MRBS\location_header;
use function MRBS\multisite;
use function MRBS\print_footer;
use function MRBS\print_header;
use function MRBS\this_page;
/**
* An abstract class for those session schemes that implement a login form.
*/
abstract class SessionWithLogin extends Session
{
protected $form = array();
public function __construct()
{
parent::__construct();
// Get the non-standard form variables
$vars = [
'action' => 'string',
'username' => 'string',
'password' => 'string',
'returl' => 'url_local'
];
foreach ($vars as $var => $type)
{
$this->form[$var] = get_form_var($var, $type, null, INPUT_POST);
}
// Allow the target_url to be a GET or POST value to help password managers (the target_url can
// be stored as a query string parameter in the password manager).
$this->form['target_url'] = get_form_var('target_url', 'url_local');
if (isset($this->form['username']))
{
// It's easy for extra spaces to appear, especially on a mobile device
$this->form['username'] = trim($this->form['username']);
}
}
// Gets the username and password. Returns: Nothing
//
// $target_url The URL to go to after successful login
// $returl The URL to return to eventually
public function authGet(?string $target_url=null, ?string $returl=null, ?string $error=null, bool $raw=false) : void
{
if (!isset($target_url))
{
$target_url = $this->form['target_url'] ?? this_page(true);
}
// Omit the Login link in the header when we're on the login page itself
print_header(null, false, true);
$action = multisite(this_page());
$this->printLoginForm($action, $target_url, $returl, $error, $raw);
exit;
}
// Returns the parameters ('method', 'action' and 'hidden_inputs') for a
// Logon form. Returns an array.
public function getLogonFormParams() : ?array
{
return array(
'action' => multisite('admin.php'),
'method' => Form::METHOD_POST,
'hidden_inputs' => array('target_url' => this_page(true),
'action' => 'QueryName')
);
}
// Returns the parameters ('method', 'action' and 'hidden_inputs') for a
// logoff form. Returns an array of parameters, or null if no form is to be
// shown.
public function getLogoffFormParams() : ?array
{
return array(
'action' => multisite('admin.php'),
'method' => Form::METHOD_POST,
'hidden_inputs' => array('target_url' => this_page(true),
'action' => 'SetName',
'username' => '',
'password' => '')
);
}
public function processForm() : void
{
if (isset($this->form['action']))
{
// Target of the form with sets the URL argument "action=QueryName".
// Will eventually return to URL argument "target_url=whatever".
if ($this->form['action'] == 'QueryName')
{
$this->authGet($this->form['target_url']);
exit(); // unnecessary because authGet() exits, but just included for clarity
}
// Target of the form with sets the URL argument "action=SetName".
// Will eventually return to URL argument "target_url=whatever".
if ($this->form['action'] == 'SetName')
{
// First make sure the password is valid
if (!isset($this->form['username']) || ($this->form['username'] == ''))
{
$this->logoffUser();
}
else
{
// If we're going to do something then check the CSRF token first.
// (Don't check the token before logging off the user because if the session has
// expired due to inactivity, the token will be invalid, but that won't matter because
// the result will be the same anyway - logging off the user - and we avoid
// generating an unnecessary CSRF error message.)
Form::checkToken();
// Get a valid user
$valid_username = $this->getValidUser($this->form['username'], $this->form['password']);
// Successful login. You can't get out of getValidUser() without a valid username and password
// ===== 等保整改:登录成功审计 =====
Audit::log('LOGIN_OK', $valid_username);
// ===== 等保整改:口令到期 / 存量账号首登 → 强制改密 =====
// 必须在 logonUser()(其内部调用 session_write_close())之前写入会话,否则会丢失
$auth_obj = auth();
if (method_exists($auth_obj, 'needsPasswordChange') &&
$auth_obj->needsPasswordChange($valid_username))
{
$_SESSION['mrbs_force_pwd_change'] = 1;
}
$this->logonUser($valid_username);
if (!empty($this->form['returl']))
{
// check to see whether there's a query string already
$this->form['target_url'] .= (mb_strpos($this->form['target_url'], '?') === false) ? '?' : '&';
$this->form['target_url'] .= 'returl=' . urlencode($this->form['returl']);
}
}
location_header($this->form['target_url']); // Redirect browser to initial page
}
}
}
// Can only return a valid username. If the username and password are not valid it will ask for new ones.
protected function getValidUser(
#[\SensitiveParameter]
?string $username,
#[\SensitiveParameter]
?string $password) : string
{
if (!isset($this->form['password']) ||
(($valid_username = auth()->validateUser($this->form['username'], $this->form['password'])) === false))
{
// ===== 等保整改:登录失败 / 账号锁定 审计与提示区分 =====
$login_name = $this->form['username'] ?? '';
$auth_obj = auth();
$error = get_vocab('unknown_user');
if (method_exists($auth_obj, 'getLoginBlocked') && $auth_obj->getLoginBlocked())
{
global $login_lock_duration;
$minutes = (int)ceil(($login_lock_duration ?? 900) / 60);
$error = get_vocab('account_locked', $minutes);
Audit::log('LOGIN_BLOCKED', $login_name);
}
else
{
Audit::log('LOGIN_FAIL', $login_name);
}
$this->authGet($this->form['target_url'], $this->form['returl'], $error);
exit(); // unnecessary because authGet() exits, but just included for clarity
}
return $valid_username;
}
protected function logonUser(string $username) : void
{
}
public function logoffUser() : void
{
}
// Displays the login form.
// Will eventually return to $target_url with query string returl=$returl
// If $error is set then an $error is printed.
// If $raw is true then the message is not HTML escaped
private function printLoginForm(string $action, ?string $target_url, ?string $returl, ?string $error=null, bool $raw=false) : void
{
$form = new Form(Form::METHOD_POST);
$form->setAttributes(array('class' => 'standard',
'id' => 'logon',
'action' => $action));
// Hidden inputs
$hidden_inputs = array('returl' => $returl,
'target_url' => $target_url,
'action' => 'SetName');
$form->addHiddenInputs($hidden_inputs);
// Now for the visible fields
if (isset($error))
{
$p = new ElementP();
$p->setText($error, false, $raw);
$form->addElement($p);
}
$fieldset = new ElementFieldset();
$fieldset->addLegend(get_vocab('please_login'));
// The username field
if (auth()->canValidateByEmail() && auth()->canValidateByUsername())
{
$tag = 'username_or_email';
}
elseif (auth()->canValidateByUsername())
{
$tag = 'users.name';
}
else
{
$tag = 'users.email';
}
$placeholder = get_vocab($tag);
$field = new FieldInputText();
$field->setLabel(get_vocab('user'))
->setLabelAttributes(array('title' => $placeholder))
->setControlAttributes(array('id' => 'username',
'name' => 'username',
'placeholder' => $placeholder,
'required' => true,
'autofocus' => true,
'autocomplete' => 'username'));
$fieldset->addElement($field);
// The password field
$field = new FieldInputPassword();
$field->setLabel(get_vocab('users.password'))
->setControlAttributes(array('id' => 'password',
'name' => 'password',
'autocomplete' => 'current-password'));
$fieldset->addElement($field);
$form->addElement($fieldset);
// The submit button
$fieldset = new ElementFieldset();
$field = new FieldInputSubmit();
$field->setControlAttributes(array('value' => get_vocab('login')));
$fieldset->addElement($field);
$form->addElement($fieldset);
if (auth()->canResetPassword())
{
$fieldset = new ElementFieldset();
$field = new FieldDiv();
$a = new ElementA();
$a->setAttribute('href', multisite('reset_password.php'))
->setText(get_vocab('lost_password'));
$field->addControl($a);
$fieldset->addElement($field);
$form->addElement($fieldset);
}
$form->render();
// Print footer and exit
print_footer(true);
}
// Check we've got the right authentication type for the session scheme.
// To be called for those session schemes which require the same
// authentication type
protected function checkTypeMatchesSession() : void
{
global $auth;
if ($auth['type'] !== $auth['session'])
{
$class = get_called_class();
$message = "MRBS configuration error: $class needs \$auth['type'] set to '" . $auth['session'] . "'";
die($message);
}
}
}
+74
View File
@@ -0,0 +1,74 @@
<?php
declare(strict_types=1);
namespace MRBS\Session;
use MRBS\User;
use function MRBS\auth;
require_once MRBS_ROOT . '/auth/cms/wordpress.inc';
class SessionWordpress extends SessionWithLogin
{
public function __construct()
{
$this->checkTypeMatchesSession();
parent::__construct();
}
public function getCurrentUser() : ?User
{
if (!is_user_logged_in())
{
return parent::getCurrentUser();
}
$mrbs_user = wp_get_current_user();
return auth()->getUser($mrbs_user->user_login);
}
// Can only return a valid username. If the username and password are not valid it will ask for new ones.
protected function getValidUser(
#[\SensitiveParameter]
?string $username,
#[\SensitiveParameter]
?string $password) : string
{
global $errors; // $errors is a WordPress global
$credentials = array();
$credentials['user_login'] = $username;
$credentials['user_password'] = $password;
$credentials['remember'] = false;
$wp_user = wp_signon($credentials);
if (is_wp_error($wp_user))
{
$errors = $wp_user;
$error_message = apply_filters('login_errors', $wp_user->get_error_message());
// The WordPress error message contains HTML so don't escape it.
$this->authGet($this->form['target_url'], $this->form['returl'], $error_message, true);
exit(); // unnecessary because authGet() exits, but just included for clarity
}
return $username;
}
protected function logonUser(string $username) : void
{
// Don't need to do anything: the user will have been logged on when the
// username and password were validated.
}
public function logoffUser() : void
{
wp_logout();
}
}