karelwintersky/arris.delight-php-auth

Arris µFramework - 基于 Delight-im PHP Auth 的身份验证库

0.99.4 2024-09-05 16:31 UTC

This package is auto-updated.

Last update: 2024-09-05 16:32:02 UTC


README

PHP 身份验证。简单、轻量级且安全。

一次编写,到处使用。

完全与框架无关和数据库无关。

我为什么需要这个?

  • 许多 网站 具有弱身份验证系统。不要建立这样的网站。
  • 为每个 PHP 项目重新实现新的身份验证系统是 不可取的
  • 逐个构建自己的身份验证类,并将它们复制到每个项目中,也是 不推荐的
  • 应该精心设计和计划易于使用的 API 的安全身份验证系统。
  • 对关键基础设施进行同行评审是 必需的

需求

  • PHP 5.6.0+
    • PDO (PHP 数据对象) 扩展 (pdo)
      • MySQL 本地驱动程序 (mysqlnd) 或 PostgreSQL 驱动程序 (pgsql) 或 SQLite 驱动程序 (sqlite)
    • OpenSSL 扩展 (openssl)
  • MySQL 5.5.3+ 或 MariaDB 5.5.23+ 或 PostgreSQL 9.5.10+ 或 SQLite 3.14.1+ 或 其他 SQL 数据库

安装

  1. 通过 Composer 包含库

    $ composer require karelwintersky/arris.delight-php-auth
    
  2. 包含 Composer 自动加载器

    require __DIR__ . '/vendor/autoload.php';
  3. 设置数据库并创建所需表

用法

创建新实例

// $db = new \PDO('mysql:dbname=my-database;host=localhost;charset=utf8mb4', 'my-username', 'my-password');
// or
// $db = new \PDO('pgsql:dbname=my-database;host=localhost;port=5432', 'my-username', 'my-password');
// or
// $db = new \PDO('sqlite:../Databases/my-database.sqlite');

// or

// $db = \Arris\DelightAuth\Db\PdoDatabase::fromDsn(new \Arris\DelightAuth\Db\PdoDsn('mysql:dbname=my-database;host=localhost;charset=utf8mb4', 'my-username', 'my-password'));
// or
// $db = \Arris\DelightAuth\Db\PdoDatabase::fromDsn(new \Arris\DelightAuth\Db\PdoDsn('pgsql:dbname=my-database;host=localhost;port=5432', 'my-username', 'my-password'));
// or
// $db = \Arris\DelightAuth\Db\PdoDatabase::fromDsn(new \Arris\DelightAuth\Db\PdoDsn('sqlite:../Databases/my-database.sqlite'));

$auth = new \Arris\DelightAuth\Auth\Auth($db);

如果您已经有了打开的 PDO 连接,只需重用它。数据库用户(例如 my-username)至少需要对此库使用的表(或其父数据库)具有 SELECTINSERTUPDATEDELETE 的权限。

如果您的Web服务器位于代理服务器后面,并且$_SERVER['REMOTE_ADDR']仅包含代理的IP地址,则必须在构造函数的第二个参数中传递用户的真实IP地址,该参数命名为$ipAddress。默认值为PHP接收到的常规远程IP地址。

如果这个库的数据库表需要公共前缀,例如my_users而不是users(以及其他表),则将前缀(例如my_)作为构造函数的第三个参数传递,该参数命名为$dbTablePrefix。这是可选的,并且默认情况下前缀为空。

在开发过程中,您可能想禁用此库执行的请求限制或节流。要这样做,请将false传递给构造函数作为第四个参数,该参数命名为$throttling。默认情况下,该功能是启用的。

在一个会话的生命周期内,某些用户数据可能会在远程被更改,无论是另一个会话中的客户端还是管理员。这意味着这些信息必须定期与其数据库中的权威源重新同步,这是此库自动完成的。默认情况下,这每五分钟发生一次。如果您想更改此间隔,请将自定义间隔(以秒为单位)作为构造函数的第五个参数传递,该参数命名为$sessionResyncInterval

如果您所有的数据库表都需要公共的数据库名称、模式名称或其他必须明确指定的限定符,您可以可选地将该限定符传递给构造函数作为第六个参数,该参数命名为$dbSchema

如果您还想独立使用PdoDatabase实例(例如$db),请参阅数据库库的文档

注册(注册)

try {
    $userId = $auth->register($_POST['email'], $_POST['password'], $_POST['username'], function ($selector, $token) {
        echo 'Send ' . $selector . ' and ' . $token . ' to the user (e.g. via email)';
    });

    echo 'We have signed up a new user with the ID ' . $userId;
}
catch (\Arris\DelightAuth\Auth\Exceptions\InvalidEmailException $e) {
    die('Invalid email address');
}
catch (\Arris\DelightAuth\Auth\Exceptions\InvalidPasswordException $e) {
    die('Invalid password');
}
catch (\Arris\DelightAuth\Auth\Exceptions\UserAlreadyExistsException $e) {
    die('User already exists');
}
catch (\Arris\DelightAuth\Auth\Exceptions\TooManyRequestsException $e) {
    die('Too many requests');
}

注意:匿名回调函数是一个闭包。因此,除了其自己的参数外,只有诸如$_GET$_POST$_COOKIE$_SERVER之类的超全局变量在内部可用。对于来自父作用域的任何其他变量,您需要显式地在内部提供一个副本,方法是在参数列表之后添加一个use子句。

第三个参数中的用户名是可选的。如果您不想管理用户名,可以在那里传递null

另一方面,如果您想强制唯一用户名,只需调用registerWithUniqueUsername而不是register,并准备好捕获DuplicateUsernameException

注意:在接收和管理用户名时,您可能希望排除非打印控制字符和某些可打印的特殊字符,例如字符类[\x00-\x1f\x7f\/:\\]。为了做到这一点,您可以在条件分支中包装对Auth#registerAuth#registerWithUniqueUsername的调用,例如,仅当以下条件满足时接受用户名

if (\preg_match('/[\x00-\x1f\x7f\/:\\\\]/', $username) === 0) {
    // ...
}

对于电子邮件验证,您应该使用选择器和令牌构建一个URL并将其发送给用户,例如

$url = 'https://www.example.com/verify_email?selector=' . \urlencode($selector) . '&token=' . \urlencode($token);

如果您不想进行电子邮件验证,只需省略Auth#register的最后一个参数。然后,新用户将立即激活。

需要存储额外的用户信息?请继续阅读这里

注意:在向用户发送电子邮件时,请注意,此时(可选的)用户名尚未被确认为(新)电子邮件地址的所有者可以接受。它可能包含由地址的实际所有者以外的人选择的冒犯性或误导性语言。

登录(登录)

try {
    $auth->login($_POST['email'], $_POST['password']);

    echo 'User is logged in';
}
catch (\Arris\DelightAuth\Auth\Exceptions\InvalidEmailException $e) {
    die('Wrong email address');
}
catch (\Arris\DelightAuth\Auth\Exceptions\InvalidPasswordException $e) {
    die('Wrong password');
}
catch (\Arris\DelightAuth\Auth\Exceptions\EmailNotVerifiedException $e) {
    die('Email not verified');
}
catch (\Arris\DelightAuth\Auth\Exceptions\TooManyRequestsException $e) {
    die('Too many requests');
}

如果您想通过用户名登录,无论是作为电子邮件登录的补充还是替代,这也是可行的。只需调用方法 loginWithUsername 而不是方法 login。然后,在捕获 InvalidEmailException 时,请确保同时捕获 UnknownUsernameExceptionAmbiguousUsernameException。您还可以阅读有关如何 注册新用户 的部分中关于用户名唯一性的说明。

电子邮件验证

从用户在验证电子邮件中点击的URL中提取选择器和令牌。

try {
    $auth->confirmEmail($_GET['selector'], $_GET['token']);

    echo 'Email address has been verified';
}
catch (\Arris\DelightAuth\Auth\Exceptions\InvalidSelectorTokenPairException $e) {
    die('Invalid token');
}
catch (\Arris\DelightAuth\Auth\Exceptions\TokenExpiredException $e) {
    die('Token expired');
}
catch (\Arris\DelightAuth\Auth\Exceptions\UserAlreadyExistsException $e) {
    die('Email address already exists');
}
catch (\Arris\DelightAuth\Auth\Exceptions\TooManyRequestsException $e) {
    die('Too many requests');
}

如果您想在确认成功后自动让用户登录,只需调用 confirmEmailAndSignIn 而不是 confirmEmail。该替代方法还通过其可选的第三个参数支持 持久登录

在成功的情况下,两个方法 confirmEmailconfirmEmailAndSignIn 都返回一个数组,其中包含用户新验证的电子邮件地址,位于索引一。如果确认是为了地址变更而不是简单的地址验证,则用户的旧电子邮件地址将包含在数组中的索引零。

保持用户登录状态

Auth#loginAuth#confirmEmailAndSignIn 方法的第三个参数控制登录是否使用长生存期的cookie进行持久化。在这种持久登录中,用户可能会在很长时间内保持认证状态,即使浏览器会话已经关闭并且会话cookie已经过期。通常,您会希望使用此功能(称为“记住我”或“保持登录”)让用户保持几周或几个月的登录状态。许多用户会发现这更方便,但如果他们离开设备无人看管,这可能会更不安全。

if ($_POST['remember'] == 1) {
    // keep logged in for one year
    $rememberDuration = (int) (60 * 60 * 24 * 365.25);
}
else {
    // do not keep logged in after session ends
    $rememberDuration = null;
}

// ...

$auth->login($_POST['email'], $_POST['password'], $rememberDuration);

// ...

没有持久登录,这是 默认 行为,用户只能在关闭浏览器或根据PHP中的 session.cookie_lifetimesession.gc_maxlifetime 进行配置的时间范围内保持登录状态。

省略第三个参数或将其设置为 null 以禁用此功能。否则,您可能需要询问用户他们是否想启用“记住我”。这通常在用户界面中通过复选框完成。使用复选框的输入来决定在这里使用 null 和预定义的秒数之间的选择,例如 60 * 60 * 24 * 365.25 代表一年。

密码重置(忘记密码)

第 1 步/3 步:发起请求

try {
    $auth->forgotPassword($_POST['email'], function ($selector, $token) {
        echo 'Send ' . $selector . ' and ' . $token . ' to the user (e.g. via email)';
    });

    echo 'Request has been generated';
}
catch (\Arris\DelightAuth\Auth\Exceptions\InvalidEmailException $e) {
    die('Invalid email address');
}
catch (\Arris\DelightAuth\Auth\Exceptions\EmailNotVerifiedException $e) {
    die('Email not verified');
}
catch (\Arris\DelightAuth\Auth\Exceptions\ResetDisabledException $e) {
    die('Password reset is disabled');
}
catch (\Arris\DelightAuth\Auth\Exceptions\TooManyRequestsException $e) {
    die('Too many requests');
}

注意:匿名回调函数是一个闭包。因此,除了其自己的参数外,只有诸如$_GET$_POST$_COOKIE$_SERVER之类的超全局变量在内部可用。对于来自父作用域的任何其他变量,您需要显式地在内部提供一个副本,方法是在参数列表之后添加一个use子句。

您应该构建一个带有选择器和令牌的URL并将其发送给用户,例如。

$url = 'https://www.example.com/reset_password?selector=' . \urlencode($selector) . '&token=' . \urlencode($token);

如果默认的密码重置请求生存期不适用于您,您可以使用 Auth#forgotPassword 的第三个参数指定请求应在多少秒后过期的自定义间隔。

第 2 步/3 步:验证尝试

作为下一步,用户将点击他们收到的链接。从URL中提取选择器和令牌。

如果选择器/令牌对有效,让用户选择一个新密码。

try {
    $auth->canResetPasswordOrThrow($_GET['selector'], $_GET['token']);

    echo 'Put the selector into a "hidden" field (or keep it in the URL)';
    echo 'Put the token into a "hidden" field (or keep it in the URL)';

    echo 'Ask the user for their new password';
}
catch (\Arris\DelightAuth\Auth\Exceptions\InvalidSelectorTokenPairException $e) {
    die('Invalid token');
}
catch (\Arris\DelightAuth\Auth\Exceptions\TokenExpiredException $e) {
    die('Token expired');
}
catch (\Arris\DelightAuth\Auth\Exceptions\ResetDisabledException $e) {
    die('Password reset is disabled');
}
catch (\Arris\DelightAuth\Auth\Exceptions\TooManyRequestsException $e) {
    die('Too many requests');
}

或者,如果您不需要错误消息但只想检查有效性,可以使用稍微简单一点的版本。

if ($auth->canResetPassword($_GET['selector'], $_GET['token'])) {
    echo 'Put the selector into a "hidden" field (or keep it in the URL)';
    echo 'Put the token into a "hidden" field (or keep it in the URL)';

    echo 'Ask the user for their new password';
}

第 3 步/3 步:更新密码

现在,您已经有了用户的新密码(并且仍然拥有其他两份数据),您可以重置密码。

try {
    $auth->resetPassword($_POST['selector'], $_POST['token'], $_POST['password']);

    echo 'Password has been reset';
}
catch (\Arris\DelightAuth\Auth\Exceptions\InvalidSelectorTokenPairException $e) {
    die('Invalid token');
}
catch (\Arris\DelightAuth\Auth\Exceptions\TokenExpiredException $e) {
    die('Token expired');
}
catch (\Arris\DelightAuth\Auth\Exceptions\ResetDisabledException $e) {
    die('Password reset is disabled');
}
catch (\Arris\DelightAuth\Auth\Exceptions\InvalidPasswordException $e) {
    die('Invalid password');
}
catch (\Arris\DelightAuth\Auth\Exceptions\TooManyRequestsException $e) {
    die('Too many requests');
}

当密码重置成功时,您想自动让相应的用户登录吗?只需使用 Auth#resetPasswordAndSignIn 而不是 Auth#resetPassword 来立即登录用户。

如果您需要用户的ID或电子邮件地址,例如,向他们发送通知,告知他们密码已成功重置,只需使用 Auth#resetPassword 的返回值,该返回值是一个包含名为 idemail 的两个条目的数组。

更改当前用户的密码

如果用户当前已登录,他们可以更改密码。

try {
    $auth->changePassword($_POST['oldPassword'], $_POST['newPassword']);

    echo 'Password has been changed';
}
catch (\Arris\DelightAuth\Auth\Exceptions\NotLoggedInException $e) {
    die('Not logged in');
}
catch (\Arris\DelightAuth\Auth\Exceptions\InvalidPasswordException $e) {
    die('Invalid password(s)');
}
catch (\Arris\DelightAuth\Auth\Exceptions\TooManyRequestsException $e) {
    die('Too many requests');
}

建议通过要求用户输入当前的(以及即将变为旧的)密码并进行验证来处理密码更改。如上所示。

然而,如果您确信不需要确认,可以调用 changePasswordWithoutOldPassword 而不是 changePassword,并在该方法调用中省略第一个参数(否则将包含旧密码)。

在任何情况下,在用户更改密码后,您应向其账户的主要电子邮件地址发送电子邮件,作为带外通知,告知账户所有者这一重要更改。

更改当前用户的电子邮件地址

如果用户当前已登录,他们可以更改他们的电子邮件地址。

try {
    if ($auth->reconfirmPassword($_POST['password'])) {
        $auth->changeEmail($_POST['newEmail'], function ($selector, $token) {
            echo 'Send ' . $selector . ' and ' . $token . ' to the user (e.g. via email to the *new* address)';
        });

        echo 'The change will take effect as soon as the new email address has been confirmed';
    }
    else {
        echo 'We can\'t say if the user is who they claim to be';
    }
}
catch (\Arris\DelightAuth\Auth\Exceptions\InvalidEmailException $e) {
    die('Invalid email address');
}
catch (\Arris\DelightAuth\Auth\Exceptions\UserAlreadyExistsException $e) {
    die('Email address already exists');
}
catch (\Arris\DelightAuth\Auth\Exceptions\EmailNotVerifiedException $e) {
    die('Account not verified');
}
catch (\Arris\DelightAuth\Auth\Exceptions\NotLoggedInException $e) {
    die('Not logged in');
}
catch (\Arris\DelightAuth\Auth\Exceptions\TooManyRequestsException $e) {
    die('Too many requests');
}

注意:匿名回调函数是一个闭包。因此,除了其自己的参数外,只有诸如$_GET$_POST$_COOKIE$_SERVER之类的超全局变量在内部可用。对于来自父作用域的任何其他变量,您需要显式地在内部提供一个副本,方法是在参数列表之后添加一个use子句。

对于电子邮件验证,您应该使用选择器和令牌构建一个URL并将其发送给用户,例如

$url = 'https://www.example.com/verify_email?selector=' . \urlencode($selector) . '&token=' . \urlencode($token);

注意:在向用户发送电子邮件时,请注意,此时(可选的)用户名尚未被确认为(新)电子邮件地址的所有者可以接受。它可能包含由地址的实际所有者以外的人选择的冒犯性或误导性语言。

在提出更改电子邮件地址的请求之后,或者在用户确认更改之后,您应该向他们的账户的电子邮件地址发送电子邮件,作为带外通知,告知账户所有者这一重要更改。

注意:用户电子邮件地址的更改立即在本地会话中生效,正如预期的那样。在其他会话中(例如在其他设备上),更改可能需要多达五分钟才能生效,尽管这提高了性能,通常不会造成问题。如果您想更改此行为,可以简单地减小(或可能增加)传递给Auth 构造函数的名为 $sessionResyncInterval 的参数的值。

重新发送确认请求

如果之前的确认请求未能送达用户,或者用户错过了该请求,或者他们只是不想再等待,您可以重新发送之前的请求,如下所示:

try {
    $auth->resendConfirmationForEmail($_POST['email'], function ($selector, $token) {
        echo 'Send ' . $selector . ' and ' . $token . ' to the user (e.g. via email)';
    });

    echo 'The user may now respond to the confirmation request (usually by clicking a link)';
}
catch (\Arris\DelightAuth\Auth\Exceptions\ConfirmationRequestNotFound $e) {
    die('No earlier request found that could be re-sent');
}
catch (\Arris\DelightAuth\Auth\Exceptions\TooManyRequestsException $e) {
    die('There have been too many requests -- try again later');
}

如果您想通过用户ID而不是电子邮件地址指定用户,这也是可能的。

try {
    $auth->resendConfirmationForUserId($_POST['userId'], function ($selector, $token) {
        echo 'Send ' . $selector . ' and ' . $token . ' to the user (e.g. via email)';
    });

    echo 'The user may now respond to the confirmation request (usually by clicking a link)';
}
catch (\Arris\DelightAuth\Auth\Exceptions\ConfirmationRequestNotFound $e) {
    die('No earlier request found that could be re-sent');
}
catch (\Arris\DelightAuth\Auth\Exceptions\TooManyRequestsException $e) {
    die('There have been too many requests -- try again later');
}

注意:匿名回调函数是一个闭包。因此,除了其自己的参数外,只有诸如$_GET$_POST$_COOKIE$_SERVER之类的超全局变量在内部可用。对于来自父作用域的任何其他变量,您需要显式地在内部提供一个副本,方法是在参数列表之后添加一个use子句。

通常,您应该使用选择器和令牌构建一个URL,并将其发送给用户,例如如下所示:

$url = 'https://www.example.com/verify_email?selector=' . \urlencode($selector) . '&token=' . \urlencode($token);

注意:在向用户发送电子邮件时,请注意,此时(可选的)用户名尚未被确认为(新)电子邮件地址的所有者可以接受。它可能包含由地址的实际所有者以外的人选择的冒犯性或误导性语言。

登出

$auth->logOut();

// or

try {
    $auth->logOutEverywhereElse();
}
catch (\Delight\Auth\NotLoggedInException $e) {
    die('Not logged in');
}

// or

try {
    $auth->logOutEverywhere();
}
catch (\Arris\DelightAuth\Auth\Exceptions\NotLoggedInException $e) {
    die('Not logged in');
}

此外,如果您还在会话中存储自定义信息,并且想删除该信息,可以通过调用第二个方法来销毁整个会话。

$auth->destroySession();

注意:全局登出立即在本地会话中生效,正如预期的那样。在其他会话中(例如在其他设备上),更改可能需要多达五分钟才能生效,尽管这提高了性能,通常不会造成问题。如果您想更改此行为,可以简单地减小(或可能增加)传递给Auth 构造函数的名为 $sessionResyncInterval 的参数的值。

访问用户信息

登录状态

if ($auth->isLoggedIn()) {
    echo 'User is signed in';
}
else {
    echo 'User is not signed in yet';
}

此方法的简写/别名是 $auth->check()

用户 ID

$id = $auth->getUserId();

如果用户当前未登录,则此方法返回 null

此方法的简写/别名是 $auth->id()

电子邮件地址

$email = $auth->getEmail();

如果用户当前未登录,则此方法返回 null

显示名称

$username = $auth->getUsername();

请记住,用户名是可选的,只有当您在注册时提供用户名时,才有用户名。

如果用户当前未登录,则此方法返回 null

状态信息

if ($auth->isNormal()) {
    echo 'User is in default state';
}

if ($auth->isArchived()) {
    echo 'User has been archived';
}

if ($auth->isBanned()) {
    echo 'User has been banned';
}

if ($auth->isLocked()) {
    echo 'User has been locked';
}

if ($auth->isPendingReview()) {
    echo 'User is pending review';
}

if ($auth->isSuspended()) {
    echo 'User has been suspended';
}

检查用户是否被“记住”

if ($auth->isRemembered()) {
    echo 'User did not sign in but was logged in through their long-lived cookie';
}
else {
    echo 'User signed in manually';
}

如果用户当前未登录,则此方法返回 null

IP 地址

$ip = $auth->getIpAddress();

其他用户信息

为了保持此库适用于所有目的以及其完全可重用性,它不附带额外的捆绑列用于用户信息。但您当然不需要没有额外的用户信息。

以下是使用此库与您自己的表以及自定义用户信息以可维护和可重用方式的方法。

  1. 添加任何数量的自定义数据库表,其中存储自定义用户信息,例如名为 profiles 的表。

  2. 每次调用 register 方法(该方法返回新用户的ID)时,请之后添加您自己的逻辑,以填充您的自定义数据库表。

  3. 如果您很少需要自定义用户信息,则可以按需检索它。但是,如果您需要更频繁地使用它,则可能希望将其包含在会话数据中。以下方法是如何以可靠的方式加载数据并访问数据的方法:

    function getUserInfo(\Delight\Auth\Auth $auth) {
        if (!$auth->isLoggedIn()) {
            return null;
        }
    
        if (!isset($_SESSION['_internal_user_info'])) {
            // TODO: load your custom user information and assign it to the session variable below
            // $_SESSION['_internal_user_info'] = ...
        }
    
        return $_SESSION['_internal_user_info'];
    }

重新确认用户的密码

当您想再次确认用户的身份时,例如在用户被允许执行某些“危险”操作之前,您应再次验证他们的密码,以确认他们确实是他们声称的那个人。

例如,当一个用户被长生命周期的cookie记住,并且Auth#isRemembered返回true时,这意味着用户可能已经很久没有输入他们的密码了。在这种情况下,您可能想要重新确认他们的密码。

try {
    if ($auth->reconfirmPassword($_POST['password'])) {
        echo 'The user really seems to be who they claim to be';
    }
    else {
        echo 'We can\'t say if the user is who they claim to be';
    }
}
catch (\Arris\DelightAuth\Auth\Exceptions\NotLoggedInException $e) {
    die('The user is not signed in');
}
catch (\Arris\DelightAuth\Auth\Exceptions\TooManyRequestsException $e) {
    die('Too many requests');
}

角色(或组)

每个用户都可以拥有任意数量的角色,您可以使用这些角色来实现授权和细化您的访问控制。

用户可能没有任何角色(这是默认行为),只有一个角色,或者任何任意组合的角色。

检查角色

if ($auth->hasRole(\Delight\Auth\Role::SUPER_MODERATOR)) {
    echo 'The user is a super moderator';
}

// or

if ($auth->hasAnyRole(\Delight\Auth\Role::DEVELOPER, \Delight\Auth\Role::MANAGER)) {
    echo 'The user is either a developer, or a manager, or both';
}

// or

if ($auth->hasAllRoles(\Delight\Auth\Role::DEVELOPER, \Delight\Auth\Role::MANAGER)) {
    echo 'The user is both a developer and a manager';
}

虽然hasRole方法正好接受一个角色作为其参数,但hasAnyRolehasAllRoles方法可以接受您希望检查的任意数量的角色。

或者,您可以获取分配给用户的所有角色的列表

$auth->getRoles();

可用角色

\Arris\DelightAuth\Auth\Role::ADMIN;
\Arris\DelightAuth\Auth\Role::AUTHOR;
\Arris\DelightAuth\Auth\Role::COLLABORATOR;
\Arris\DelightAuth\Auth\Role::CONSULTANT;
\Arris\DelightAuth\Auth\Role::CONSUMER;
\Arris\DelightAuth\Auth\Role::CONTRIBUTOR;
\Arris\DelightAuth\Auth\Role::COORDINATOR;
\Arris\DelightAuth\Auth\Role::CREATOR;
\Arris\DelightAuth\Auth\Role::DEVELOPER;
\Arris\DelightAuth\Auth\Role::DIRECTOR;
\Arris\DelightAuth\Auth\Role::EDITOR;
\Arris\DelightAuth\Auth\Role::EMPLOYEE;
\Arris\DelightAuth\Auth\Role::MAINTAINER;
\Arris\DelightAuth\Auth\Role::MANAGER;
\Arris\DelightAuth\Auth\Role::MODERATOR;
\Arris\DelightAuth\Auth\Role::PUBLISHER;
\Arris\DelightAuth\Auth\Role::REVIEWER;
\Arris\DelightAuth\Auth\Role::SUBSCRIBER;
\Arris\DelightAuth\Auth\Role::SUPER_ADMIN;
\Arris\DelightAuth\Auth\Role::SUPER_EDITOR;
\Arris\DelightAuth\Auth\Role::SUPER_MODERATOR;
\Arris\DelightAuth\Auth\Role::TRANSLATOR;

您可以使用这些角色中的任何一个,忽略那些您不需要的角色。上述列表也可以以三种格式之一通过编程方式检索

\Arris\DelightAuth\Auth\Role::getMap();
// or
\Arris\DelightAuth\Auth\Role::getNames();
// or
\Arris\DelightAuth\Auth\Role::getValues();

权限(或访问权限、特权或能力)

每个用户的权限以您在整个代码库中指定角色要求的方式编码。如果使用特定用户的角色集评估这些要求,则隐式检查的权限是结果。

对于较大的项目,通常建议在一个地方维护权限的定义。这样,您就不需要在业务逻辑中检查角色,而是检查单个权限。您可以按照以下方式实现这个概念

function canEditArticle(\Arris\DelightAuth\Auth\Auth $auth) {
    return $auth->hasAnyRole(
        \Arris\DelightAuth\Auth\Role::MODERATOR,
        \Arris\DelightAuth\Auth\Role::SUPER_MODERATOR,
        \Arris\DelightAuth\Auth\Role::ADMIN,
        \Arris\DelightAuth\Auth\Role::SUPER_ADMIN
    );
}

// ...

if (canEditArticle($auth)) {
    echo 'The user can edit articles here';
}

// ...

if (canEditArticle($auth)) {
    echo '... and here';
}

// ...

if (canEditArticle($auth)) {
    echo '... and here';
}

如您所见,是否允许特定用户编辑文章的权限被存储在中央位置。这种实现有两个主要优点

如果您想了解哪些用户可以编辑文章,您不必检查业务逻辑的各个地方,只需查看特定权限的定义即可。如果您想更改可以编辑文章的人,也只需在一个地方操作即可,而无需在整个代码库中进行更改。

但这也意味着在首次实现访问限制时会有稍微更多的开销,这可能或可能不值得您的项目。

自定义角色名称

如果包含的角色名称对您不起作用,您可以使用自己的标识符为任意数量的角色创建别名,例如这样

namespace My\Namespace;

final class MyRole {

    const CUSTOMER_SERVICE_AGENT = \Arris\DelightAuth\Auth\Role::REVIEWER;
    const FINANCIAL_DIRECTOR = \Arris\DelightAuth\Auth\Role::COORDINATOR;

    private function __construct() {}

}

上面的示例将允许您使用

\My\Namespace\MyRole::CUSTOMER_SERVICE_AGENT;
// and
\My\Namespace\MyRole::FINANCIAL_DIRECTOR;

而不是

\Arris\DelightAuth\Auth\Role::REVIEWER;
// and
\Arris\DelightAuth\Auth\Role::COORDINATOR;

请记住不要将一个包含的单个角色别名到多个具有自定义名称的角色。

启用或禁用密码重置

虽然通过电子邮件重置密码是一个方便的功能,大多数用户时不时都会觉得很有帮助,但这个功能的可用性意味着您的服务上的账户安全性只能与用户相关联的电子邮件账户的安全性相匹配。

您可以为对安全意识(以及经验)较强的用户提供禁用账户密码重置(并在以后再次启用)的功能,以增强安全性

try {
    if ($auth->reconfirmPassword($_POST['password'])) {
        $auth->setPasswordResetEnabled($_POST['enabled'] == 1);

        echo 'The setting has been changed';
    }
    else {
        echo 'We can\'t say if the user is who they claim to be';
    }
}
catch (\Arris\DelightAuth\Auth\Exceptions\NotLoggedInException $e) {
    die('The user is not signed in');
}
catch (\Arris\DelightAuth\Auth\Exceptions\TooManyRequestsException $e) {
    die('Too many requests');
}

为了检查此设置的当前值,请使用从

$auth->isPasswordResetEnabled();

返回的值,在您的用户界面中正确选择默认选项。您不需要为此功能的限制检查此值,因为它们会自动执行。

节流或速率限制

此库提供的所有方法都自动保护免受来自客户端的大量请求。如果需要,您可以使用构造函数传入的$throttling参数(请参阅创建新实例)来(暂时)禁用此保护。

如果您还想对外部功能或方法进行节流或速率限制,例如您自己的代码中的那些,可以使用内置的节流和速率限制辅助方法。

try {
    // throttle the specified resource or feature to *3* requests per *60* seconds
    $auth->throttle([ 'my-resource-name' ], 3, 60);

    echo 'Do something with the resource or feature';
}
catch (\Arris\DelightAuth\Auth\Exceptions\TooManyRequestsException $e) {
    // operation cancelled

    \http_response_code(429);
    exit;
}

如果资源或功能的保护还应依赖于另一个属性,例如按IP地址分别跟踪某些内容,只需向资源描述中添加更多数据即可,例如

[ 'my-resource-name', $_SERVER['REMOTE_ADDR'] ]
// instead of
// [ 'my-resource-name' ]

通过指定第四个参数为突发因子,可以在高峰需求期间允许短时间内的活动。例如,值为5,与通常接受的水平相比,将允许五倍的活动临时爆发。

在某些情况下,您可能只是想模拟节流或速率限制。这允许您在不实际修改活动跟踪器的情况下检查操作是否被允许。要这样做,只需将true作为第五个参数传递即可。

注意:当您在实例上禁用节流(通过构造函数中传递的$throttling参数)时,这将关闭自动内部保护和您自己应用程序代码中对Auth#throttle的任何调用的效果——除非您还在特定的Auth#throttle调用中将可选的$force参数设置为true

管理(用户管理)

管理界面可通过$auth->admin()访问。您可以调用以下文档中描述的该接口上的各种方法。

在公开此接口之前,不要忘记实现安全的访问控制。例如,您可能只允许具有管理员角色的已登录用户访问此接口,或者仅在私有脚本中使用此接口。

创建新用户

try {
    $userId = $auth->admin()->createUser($_POST['email'], $_POST['password'], $_POST['username']);

    echo 'We have signed up a new user with the ID ' . $userId;
}
catch (\Arris\DelightAuth\Auth\Exceptions\InvalidEmailException $e) {
    die('Invalid email address');
}
catch (\Arris\DelightAuth\Auth\Exceptions\InvalidPasswordException $e) {
    die('Invalid password');
}
catch (\Arris\DelightAuth\Auth\Exceptions\UserAlreadyExistsException $e) {
    die('User already exists');
}

第三个参数中的用户名是可选的。如果您不想管理用户名,可以在那里传递null

另一方面,如果您想强制唯一的用户名,只需调用createUserWithUniqueUsername而不是createUser,并准备好捕获DuplicateUsernameException

删除用户

通过ID删除用户

try {
    $auth->admin()->deleteUserById($_POST['id']);
}
catch (\Arris\DelightAuth\Auth\Exceptions\UnknownIdException $e) {
    die('Unknown ID');
}

通过电子邮件地址删除用户

try {
    $auth->admin()->deleteUserByEmail($_POST['email']);
}
catch (\Arris\DelightAuth\Auth\Exceptions\InvalidEmailException $e) {
    die('Unknown email address');
}

通过用户名删除用户

try {
    $auth->admin()->deleteUserByUsername($_POST['username']);
}
catch (\Arris\DelightAuth\Auth\Exceptions\UnknownUsernameException $e) {
    die('Unknown username');
}
catch (\Arris\DelightAuth\Auth\Exceptions\AmbiguousUsernameException $e) {
    die('Ambiguous username');
}

检索注册用户列表

当获取所有用户的列表时,由于项目和使用案例的不同,要求差异很大,并且定制是常见的。例如,您可能想要获取不同的列、连接相关表、按某些标准筛选、更改结果排序的方式(在相反的方向上),以及限制结果的数量(同时提供偏移量)。

这就是为什么使用单个自定义SQL查询更容易。从以下内容开始

SELECT id, email, username, status, verified, roles_mask, registered, last_login FROM users;

分配角色给用户

try {
    $auth->admin()->addRoleForUserById($userId, \Delight\Auth\Role::ADMIN);
}
catch (\Arris\DelightAuth\Auth\Exceptions\UnknownIdException $e) {
    die('Unknown user ID');
}

// or

try {
    $auth->admin()->addRoleForUserByEmail($userEmail, \Delight\Auth\Role::ADMIN);
}
catch (\Arris\DelightAuth\Auth\Exceptions\InvalidEmailException $e) {
    die('Unknown email address');
}

// or

try {
    $auth->admin()->addRoleForUserByUsername($username, \Delight\Auth\Role::ADMIN);
}
catch (\Arris\DelightAuth\Auth\Exceptions\UnknownUsernameException $e) {
    die('Unknown username');
}
catch (\Arris\DelightAuth\Auth\Exceptions\AmbiguousUsernameException $e) {
    die('Ambiguous username');
}

注意:更改用户角色集可能需要最多五分钟才能生效。这提高了性能,通常不会造成问题。如果您仍然想更改此行为,只需简单地将传递给Auth构造函数的名为$sessionResyncInterval的参数值减小(或许可能增加)。

从用户那里撤回角色

try {
    $auth->admin()->removeRoleForUserById($userId, \Delight\Auth\Role::ADMIN);
}
catch (\Arris\DelightAuth\Auth\Exceptions\UnknownIdException $e) {
    die('Unknown user ID');
}

// or

try {
    $auth->admin()->removeRoleForUserByEmail($userEmail, \Delight\Auth\Role::ADMIN);
}
catch (\Arris\DelightAuth\Auth\Exceptions\InvalidEmailException $e) {
    die('Unknown email address');
}

// or

try {
    $auth->admin()->removeRoleForUserByUsername($username, \Delight\Auth\Role::ADMIN);
}
catch (\Arris\DelightAuth\Auth\Exceptions\UnknownUsernameException $e) {
    die('Unknown username');
}
catch (\Arris\DelightAuth\Auth\Exceptions\AmbiguousUsernameException $e) {
    die('Ambiguous username');
}

注意:更改用户角色集可能需要最多五分钟才能生效。这提高了性能,通常不会造成问题。如果您仍然想更改此行为,只需简单地将传递给Auth构造函数的名为$sessionResyncInterval的参数值减小(或许可能增加)。

检查角色

try {
    if ($auth->admin()->doesUserHaveRole($userId, \Delight\Auth\Role::ADMIN)) {
        echo 'The specified user is an administrator';
    }
    else {
        echo 'The specified user is not an administrator';
    }
}
catch (\Arris\DelightAuth\Auth\Exceptions\UnknownIdException $e) {
    die('Unknown user ID');
}

或者,您可以获取分配给用户的所有角色的列表

$auth->admin()->getRolesForUserById($userId);

模拟用户(以用户身份登录)

try {
    $auth->admin()->logInAsUserById($_POST['id']);
}
catch (\Arris\DelightAuth\Auth\Exceptions\UnknownIdException $e) {
    die('Unknown ID');
}
catch (\Arris\DelightAuth\Auth\Exceptions\EmailNotVerifiedException $e) {
    die('Email address not verified');
}

// or

try {
    $auth->admin()->logInAsUserByEmail($_POST['email']);
}
catch (\Arris\DelightAuth\Auth\Exceptions\InvalidEmailException $e) {
    die('Unknown email address');
}
catch (\Arris\DelightAuth\Auth\Exceptions\EmailNotVerifiedException $e) {
    die('Email address not verified');
}

// or

try {
    $auth->admin()->logInAsUserByUsername($_POST['username']);
}
catch (\Arris\DelightAuth\Auth\Exceptions\UnknownUsernameException $e) {
    die('Unknown username');
}
catch (\Arris\DelightAuth\Auth\Exceptions\AmbiguousUsernameException $e) {
    die('Ambiguous username');
}
catch (\Arris\DelightAuth\Auth\Exceptions\EmailNotVerifiedException $e) {
    die('Email address not verified');
}

更改用户的密码

try {
    $auth->admin()->changePasswordForUserById($_POST['id'], $_POST['newPassword']);
}
catch (\Arris\DelightAuth\Auth\Exceptions\UnknownIdException $e) {
    die('Unknown ID');
}
catch (\Arris\DelightAuth\Auth\Exceptions\InvalidPasswordException $e) {
    die('Invalid password');
}

// or

try {
    $auth->admin()->changePasswordForUserByUsername($_POST['username'], $_POST['newPassword']);
}
catch (\Arris\DelightAuth\Auth\Exceptions\UnknownUsernameException $e) {
    die('Unknown username');
}
catch (\Arris\DelightAuth\Auth\Exceptions\AmbiguousUsernameException $e) {
    die('Ambiguous username');
}
catch (\Arris\DelightAuth\Auth\Exceptions\InvalidPasswordException $e) {
    die('Invalid password');
}

Cookie

此库使用两个cookie来在客户端保持状态:第一个,您可以使用以下方式检索其名称

\session_name();

是通用的(必需的)会话cookie。第二个(可选)cookie仅用于持久登录,其名称可以按以下方式检索

\Arris\DelightAuth\Auth\Auth::createRememberCookieName();

重命名库的 Cookie

您可以通过以下方式之一重命名此库使用的会话cookie,推荐顺序如下

  • PHP配置php.ini)中,找到包含session.name指令的行,并将其值更改为类似session_v1的内容,如下所示

    session.name = session_v1
    
  • 在您的应用程序中尽可能早地,并在创建Auth实例之前,调用\ini_setsession.name更改为类似session_v1的内容,如下所示

    \ini_set('session.name', 'session_v1');

    要使此功能生效,必须在PHP配置php.ini)中将session.auto_start设置为0

  • 在您的应用程序中尽可能早地调用,在创建Auth实例之前,使用类似于session_v1的参数调用\session_name,如下所示:

    \session_name('session_v1');

    要使此功能生效,必须在PHP配置php.ini)中将session.auto_start设置为0

用于持久登录的cookie名称也将相应更改,即根据您更改会话cookie名称后自动更改。

定义 Cookie 的域名范围

cookie的domain属性控制cookie有效的域名(以及子域名),从而确定用户会话和认证状态可用的地方。

建议的默认值为空字符串,这意味着cookie仅对确切的当前主机有效,不包括任何可能存在的子域名。您只有在需要在不同子域名之间共享cookie时才应使用不同的值。通常,您可能希望将cookie在基域名和www子域名之间共享,但您也可能希望将它们共享在任何其他子域名集合之间。

无论您选择哪组子域名,都应将cookie的属性设置为包含所有必需子域名的最具体的域名。例如,要共享example.comwww.example.com之间的cookie,您应将属性设置为example.com。但是,如果您想共享sub1.app.example.comsub2.app.example.com之间的cookie,则应将属性设置为app.example.com。任何明确指定的域名都将始终包含所有可能存在的子域名。

您可以通过以下方法之一更改此属性,按照推荐顺序:

  • PHP配置php.ini)中,找到包含session.cookie_domain指令的行,并将其值更改为所需的值,例如:

    session.cookie_domain = example.com
    
  • 在您的应用程序中尽可能早地调用,在创建Auth实例之前,调用\ini_set以更改session.cookie_domain指令的值,例如:

    \ini_set('session.cookie_domain', 'example.com');

    要使此功能生效,必须在PHP配置php.ini)中将session.auto_start设置为0

限制 Cookie 可用的路径

cookie的path属性控制cookie有效的目录(以及子目录),从而确定用户会话和认证状态可用的地方。

在大多数情况下,您希望使cookie对所有路径可用,即从根目录开始的任何目录和文件。这就是为什么将属性值设置为/,这也是推荐的默认值。除非您需要将cookie限制在特定的目录中,例如,在相同域名下并排托管多个应用程序,否则您不应更改此属性,例如,将其更改为/path/to/subfolder

您可以通过以下方法之一更改此属性,按照推荐顺序:

  • PHP配置php.ini)中,找到包含session.cookie_path指令的行,并将其值更改为所需的值,例如:

    session.cookie_path = /
    
  • 在您的应用程序中尽可能早地调用,在创建Auth实例之前,调用\ini_set以更改session.cookie_path指令的值,例如:

    \ini_set('session.cookie_path', '/');

    要使此功能生效,必须在PHP配置php.ini)中将session.auto_start设置为0

控制客户端脚本对 Cookie 的访问

使用httponly属性,您可以控制客户端脚本,即JavaScript,是否能够访问您的cookie。出于安全原因,最好拒绝脚本访问您的cookie,这可以减少针对您的应用程序的成功的XSS攻击可能造成的损害。

因此,您应始终将httponly设置为1,除非在极少数情况下您确实需要从JavaScript访问cookie且找不到更好的解决方案。在这些情况下,将属性设置为0,但要注意其后果。

您可以通过以下方法之一更改此属性,按照推荐顺序:

  • PHP配置php.ini)中,找到包含session.cookie_httponly指令的行,并将其值更改为所需的值,例如:

    session.cookie_httponly = 1
    
  • 在您的应用程序中尽早调用,在您创建 Auth 实例之前,使用 \ini_set 修改 session.cookie_httponly 指令的值,例如:

    \ini_set('session.cookie_httponly', 1);

    要使此功能生效,必须在PHP配置php.ini)中将session.auto_start设置为0

配置 Cookie 的传输安全

使用 secure 属性,您可以控制是否应该通过 任何 连接发送cookie,包括普通HTTP,或者是否需要安全连接,即HTTPS(带有SSL/TLS)。前一种(较不安全)模式可以通过将属性设置为 0 来选择,而后一种(更安全)模式可以通过将属性设置为 1 来选择。

显然,这完全取决于您是否能够通过HTTPS独家提供 所有 页面。如果您可以,应将属性设置为 1,并可能结合使用将HTTP重定向到安全协议和HTTP严格传输安全(HSTS)。否则,您可能需要将属性保持为 0

您可以通过以下方法之一更改此属性,按照推荐顺序:

  • PHP配置文件php.ini)中,找到带有 session.cookie_secure 指令的行,并修改其值,例如:

    session.cookie_secure = 1
    
  • 在您的应用程序中尽早调用,在您创建 Auth 实例之前,使用 \ini_set 修改 session.cookie_secure 指令的值,例如:

    \ini_set('session.cookie_secure', 1);

    要使此功能生效,必须在PHP配置php.ini)中将session.auto_start设置为0

实用工具

创建随机字符串

$length = 24;
$randomStr = \Arris\DelightAuth\Auth\Auth::createRandomString($length);

创建符合 RFC 4122 的 UUID v4

$uuid = \Arris\DelightAuth\Auth\Auth::createUuid();

读取和写入会话数据

有关如何方便地读取和写入会话数据的详细信息,请参阅包含在默认配置中的 会话库文档

常见问题

关于密码散列呢?

任何密码或认证令牌都会自动使用基于 “bcrypt” 函数进行散列,该函数基于 “Blowfish” 密码,并且目前被认为是最好的密码散列函数之一。“bcrypt” 使用 1,024 次迭代,即“成本”因子为 10。还会自动应用随机的 “盐”

您可以通过查看数据库表 users 中的散列来验证此配置。如果上述配置对您的设置正确,您的 users 表中的所有密码散列都应该以前缀 $2$10$$2a$10$$2y$10$ 开头。

当未来可能引入新的算法(例如 Argon2)时,此库将自动在用户登录或更改密码时“升级”您现有的密码散列。

如何实现自定义密码要求?

强制密码的最小长度通常是好主意。除此之外,您可能还想检查潜在密码是否在某些黑名单中,这些黑名单您可以在数据库或文件中管理,以防止字典单词或常用密码在您的应用程序中使用。

为了提供最大灵活性和易用性,此库已经设计成不包含对密码要求的任何进一步检查,而是允许您在库方法的相应调用周围包装自己的检查。示例

function isPasswordAllowed($password) {
    if (\strlen($password) < 8) {
        return false;
    }

    $blacklist = [ 'password1', '123456', 'qwerty' ];

    if (\in_array($password, $blacklist)) {
        return false;
    }

    return true;
}

if (isPasswordAllowed($password)) {
    $auth->register($email, $password);
}

为什么使用其他与会话一起工作的库时会存在问题?

您可能尝试首先加载此库,并首先创建 Auth 实例,然后再 加载其他库。除此之外,我们可能在这里无能为力。

为什么其他网站无法框架或嵌入我的网站?

如果您想允许他人将您的网站包含在 <frame><iframe><object><embed><applet> 元素中,您必须禁用默认的点击劫持防护。

\header_remove('X-Frame-Options');

异常

此库抛出两种类型的异常来指示问题

  • AuthException及其子类会在方法未能成功完成时抛出。你应该始终捕获这些异常,因为它们携带了你必须响应的正常错误响应。
  • AuthError及其子类会在出现内部问题或库安装不正确时抛出。你应该不要捕获这些异常。

一般建议

  • 仅通过HTTPS提供所有页面,即使用SSL/TLS处理每个请求。
  • 你应该强制密码的最小长度,例如10个字符,但永远不要限制最大长度,至少不要低于100个字符。此外,你也不应该限制允许的字符集。
  • 每当用户通过在登录时启用或禁用“记住我”功能而被记住时,这意味着他们没有通过输入密码来登录,你应该要求对关键功能进行重新认证。
  • 鼓励用户使用密码短语,即单词的组合,甚至是完整的句子,而不是单个密码
  • 不要阻止用户的密码管理器正常工作。因此,只使用标准表单字段,并且不要阻止复制和粘贴。
  • 在执行敏感的账户操作(例如,更改用户的电子邮件地址,删除用户的账户)之前,你应该始终要求重新认证,即要求用户再次验证他们的登录凭证。
  • 对于高度安全的应用,不应提供在线密码重置功能(“忘记密码”)。
  • 对于高度安全的应用,不应使用电子邮件地址作为标识符。相反,选择与应用程序特定且保密的标识符,例如内部客户编号。

贡献

所有贡献都受欢迎!如果您希望贡献,请首先创建一个问题,以便讨论您的功能、问题或疑问。

许可协议

本项目按照MIT许可协议的条款进行许可。