gotzmann/auth

PHP 的 PSR 兼容认证

v1.0.0 2022-01-07 17:02 UTC

This package is auto-updated.

Last update: 2024-09-07 22:50:52 UTC


README

PHP 的 PSR 兼容认证。 基于 PHP-Auth

一次编写,到处使用。完全框架无关和数据库无关。

最好与 Comet PHP 框架一起使用

我为什么需要这个?

  • 很多 网站 使用弱认证系统。不要建立这样的网站。
  • 为每个 PHP 项目重新实现一个新的认证系统不是一个好主意。
  • 逐个构建自己的认证类,并将其复制到每个项目中,也是不推荐的。
  • 应该彻底设计和规划一个易于使用的 API 的安全认证系统。
  • 对你的关键基础设施进行同行评审是 必须的

需求

  • PHP 7.2.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 gotzmann/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');

$auth = new \Comet\Auth($db, $session, $params);

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

您必须将用户的真实 IP 地址作为第三个参数传递给构造函数,使用 $params['REMOTE_ADDR'] 来使用节流和速率限制功能。

如果您的数据库表需要公共前缀,例如 my_users 而不是 users(以及其他表),请在 $params 中传递前缀(例如 my_),作为 DB_TABLE_PREFIX 键。这是可选的,默认情况下前缀为空。

在开发过程中,您可能想禁用该库执行的请求限制或节流。为此,将 $params 中的 THROTTLING 键作为 false 传递给构造函数。该功能默认启用。

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

如果您的所有数据库表都需要一个公共的数据库名称、模式名称或其他必须明确指定的限定符,您可以可选地传递该限定符,该限定符名为 DB_SCHEMA。

注册(注册)

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 (\Comet\InvalidEmailException $e) {
    die('Invalid email address');
}
catch (\Comet\InvalidPasswordException $e) {
    die('Invalid password');
}
catch (\Comet\UserAlreadyExistsException $e) {
    die('User already exists');
}
catch (\Comet\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 (\Comet\InvalidEmailException $e) {
    die('Wrong email address');
}
catch (\Comet\InvalidPasswordException $e) {
    die('Wrong password');
}
catch (\Comet\EmailNotVerifiedException $e) {
    die('Email not verified');
}
catch (\Comet\TooManyRequestsException $e) {
    die('Too many requests');
}

另一方面,如果您想在其他地方使用用户名登录,无论是作为通过电子邮件地址登录的补充还是替代,这也是可能的。只需调用方法 loginWithUsername 而不是方法 login。然后,确保捕获 UnknownUsernameExceptionAmbiguousUsernameException,而不是捕获 InvalidEmailException。您还可能想阅读有关用户名唯一性的说明,这部分内容解释了如何 注册新用户

电子邮件验证

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

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

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

如果希望在验证成功后用户自动登录,只需调用confirmEmailAndSignIn而不是confirmEmail。该方法也支持通过可选的第三个参数实现持久登录

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

密码重置(“忘记密码”)

第3步中的第1步:发起请求

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 (\Comet\InvalidEmailException $e) {
    die('Invalid email address');
}
catch (\Comet\EmailNotVerifiedException $e) {
    die('Email not verified');
}
catch (\Comet\ResetDisabledException $e) {
    die('Password reset is disabled');
}
catch (\Comet\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的第三个参数来指定一个自定义的秒数间隔,在该间隔后请求应过期。

第3步中的第2步:验证尝试

作为下一步,用户将点击他们收到的链接。从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 (\Comet\InvalidSelectorTokenPairException $e) {
    die('Invalid token');
}
catch (\Comet\TokenExpiredException $e) {
    die('Token expired');
}
catch (\Comet\ResetDisabledException $e) {
    die('Password reset is disabled');
}
catch (\Comet\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 (\Comet\InvalidSelectorTokenPairException $e) {
    die('Invalid token');
}
catch (\Comet\TokenExpiredException $e) {
    die('Token expired');
}
catch (\Comet\ResetDisabledException $e) {
    die('Password reset is disabled');
}
catch (\Comet\InvalidPasswordException $e) {
    die('Invalid password');
}
catch (\Comet\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 (\Comet\NotLoggedInException $e) {
    die('Not logged in');
}
catch (\Comet\InvalidPasswordException $e) {
    die('Invalid password(s)');
}
catch (\Comet\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 (\Comet\InvalidEmailException $e) {
    die('Invalid email address');
}
catch (\Comet\UserAlreadyExistsException $e) {
    die('Email address already exists');
}
catch (\Comet\EmailNotVerifiedException $e) {
    die('Account not verified');
}
catch (\Comet\NotLoggedInException $e) {
    die('Not logged in');
}
catch (\Comet\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 (\Comet\ConfirmationRequestNotFound $e) {
    die('No earlier request found that could be re-sent');
}
catch (\Comet\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 (\Comet\ConfirmationRequestNotFound $e) {
    die('No earlier request found that could be re-sent');
}
catch (\Comet\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 (\Comet\NotLoggedInException $e) {
    die('Not logged in');
}

// or

try {
    $auth->logOutEverywhere();
}
catch (\Comet\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(\Comet\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 (\Comet\NotLoggedInException $e) {
    die('The user is not signed in');
}
catch (\Comet\TooManyRequestsException $e) {
    die('Too many requests');
}

角色(或组)

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

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

检查角色

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

// or

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

// or

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

虽然hasRole方法恰好需要一个角色作为其参数,但hasAnyRolehasAllRoles方法可以检查您想要检查的任意数量的角色。

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

$auth->getRoles();

可用的角色

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

您可以使用这些角色并忽略那些不需要的角色。上述列表也可以以三种格式之一程序化地检索

\Comet\Role::getMap();
// or
\Comet\Role::getNames();
// or
\Comet\Role::getValues();

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

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

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

function canEditArticle(\Comet\Auth\Auth $auth) {
    return $auth->hasAnyRole(
        \Comet\Role::MODERATOR,
        \Comet\Role::SUPER_MODERATOR,
        \Comet\Role::ADMIN,
        \Comet\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 = \Comet\Role::REVIEWER;
    const FINANCIAL_DIRECTOR = \Comet\Role::COORDINATOR;

    private function __construct() {}

}

上面的示例将允许您使用

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

而不是

\Comet\Role::REVIEWER;
// and
\Comet\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 (\Comet\NotLoggedInException $e) {
    die('The user is not signed in');
}
catch (\Comet\TooManyRequestsException $e) {
    die('Too many requests');
}

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

$auth->isPasswordResetEnabled();

从您的用户界面的正确默认选项返回值。您不需要检查此值以限制功能的限制,这些限制会自动执行。

节流或速率限制

此库提供的所有方法都自动针对来自客户端的大量请求进行保护。

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

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 (\Comet\TooManyRequestsException $e) {
    // operation cancelled

    \http_response_code(429);
    exit;
}

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

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

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

在某些情况下,您可能只想模拟节流或速率限制。这可以让您检查某个操作是否被允许,而无需实际修改活动跟踪器。要这样做,只需将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 (\Comet\InvalidEmailException $e) {
    die('Invalid email address');
}
catch (\Comet\InvalidPasswordException $e) {
    die('Invalid password');
}
catch (\Comet\UserAlreadyExistsException $e) {
    die('User already exists');
}

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

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

删除用户

通过ID删除用户

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

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

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

通过用户名删除用户

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

检索注册用户列表

当获取所有用户的列表时,项目和使用案例之间的要求差异很大,并且定制很常见。例如,您可能想获取不同的列,连接相关表,根据某些标准进行过滤,更改结果排序的方式(以及不同的方向),以及限制结果数量(同时提供偏移量)。

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

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

将角色分配给用户

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

// or

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

// or

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

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

从用户中移除角色

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

// or

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

// or

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

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

检查角色

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

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

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

模拟用户(作为用户登录)

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

// or

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

// or

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

更改用户密码

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

// or

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

Cookie

此库使用两个cookie来在客户端保持状态:第一个,您可以使用

\session_name();

是通用(必需)会话cookie。第二个(可选)cookie仅用于持久登录,其名称如下

\Comet\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时才应使用不同的值。通常,您希望在不同子域(例如,裸域和www子域)之间共享cookie,但您可能还希望在其他任何一组子域之间共享它们。

无论您选择哪组子域,您都应将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属性,您可以控制是否应通过任何连接(包括纯HTTP)发送cookie,或者是否需要安全连接,即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 = \Comet\Auth::createRandomString($length);

根据 RFC 4122 创建 UUID v4

$uuid = \Comet\Auth::createUuid();

读取和写入会话数据

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

常见问题解答

关于密码散列呢?

任何密码或身份验证令牌都会自动使用基于“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许可证的条款进行许可。