pinga/auth

PHP认证。简单、轻量级且安全。

维护者

详细信息

github.com/getpinga/auth

主页

源码

安装: 241

依赖: 0

推荐者: 0

安全: 0

星标: 1

关注者: 0

分支: 234

v0.3.4 2024-01-15 11:17 UTC

This package is auto-updated.

Last update: 2024-09-15 12:55:51 UTC


README

PHP认证。简单、轻量级且安全。

基于 estento/PHP-Auth-Argon 和 delight-im/PHP-Auth

我为什么需要这个?

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

要求

  • PHP 8.1.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 pinga/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 = \Delight\Db\PdoDatabase::fromDsn(new \Delight\Db\PdoDsn('mysql:dbname=my-database;host=localhost;charset=utf8mb4', 'my-username', 'my-password'));
// or
// $db = \Delight\Db\PdoDatabase::fromDsn(new \Delight\Db\PdoDsn('pgsql:dbname=my-database;host=localhost;port=5432', 'my-username', 'my-password'));
// or
// $db = \Delight\Db\PdoDatabase::fromDsn(new \Delight\Db\PdoDsn('sqlite:../Databases/my-database.sqlite'));

$auth = new \Delight\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 '  For emails, consider using the mail(...) function, Symfony Mailer, Swiftmailer, PHPMailer, etc.';
        echo '  For SMS, consider using a third-party service and a compatible SDK';
    });

    echo 'We have signed up a new user with the ID ' . $userId;
}
catch (\Delight\Auth\InvalidEmailException $e) {
    die('Invalid email address');
}
catch (\Delight\Auth\InvalidPasswordException $e) {
    die('Invalid password');
}
catch (\Delight\Auth\UserAlreadyExistsException $e) {
    die('User already exists');
}
catch (\Delight\Auth\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 (\Delight\Auth\InvalidEmailException $e) {
    die('Wrong email address');
}
catch (\Delight\Auth\InvalidPasswordException $e) {
    die('Wrong password');
}
catch (\Delight\Auth\EmailNotVerifiedException $e) {
    die('Email not verified');
}
catch (\Delight\Auth\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 (\Delight\Auth\InvalidSelectorTokenPairException $e) {
    die('Invalid token');
}
catch (\Delight\Auth\TokenExpiredException $e) {
    die('Token expired');
}
catch (\Delight\Auth\UserAlreadyExistsException $e) {
    die('Email address already exists');
}
catch (\Delight\Auth\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 '  For emails, consider using the mail(...) function, Symfony Mailer, Swiftmailer, PHPMailer, etc.';
        echo '  For SMS, consider using a third-party service and a compatible SDK';
    });

    echo 'Request has been generated';
}
catch (\Delight\Auth\InvalidEmailException $e) {
    die('Invalid email address');
}
catch (\Delight\Auth\EmailNotVerifiedException $e) {
    die('Email not verified');
}
catch (\Delight\Auth\ResetDisabledException $e) {
    die('Password reset is disabled');
}
catch (\Delight\Auth\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 (\Delight\Auth\InvalidSelectorTokenPairException $e) {
    die('Invalid token');
}
catch (\Delight\Auth\TokenExpiredException $e) {
    die('Token expired');
}
catch (\Delight\Auth\ResetDisabledException $e) {
    die('Password reset is disabled');
}
catch (\Delight\Auth\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 (\Delight\Auth\InvalidSelectorTokenPairException $e) {
    die('Invalid token');
}
catch (\Delight\Auth\TokenExpiredException $e) {
    die('Token expired');
}
catch (\Delight\Auth\ResetDisabledException $e) {
    die('Password reset is disabled');
}
catch (\Delight\Auth\InvalidPasswordException $e) {
    die('Invalid password');
}
catch (\Delight\Auth\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 (\Delight\Auth\NotLoggedInException $e) {
    die('Not logged in');
}
catch (\Delight\Auth\InvalidPasswordException $e) {
    die('Invalid password(s)');
}
catch (\Delight\Auth\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 '  For emails, consider using the mail(...) function, Symfony Mailer, Swiftmailer, PHPMailer, etc.';
            echo '  For SMS, consider using a third-party service and a compatible SDK';
        });

        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 (\Delight\Auth\InvalidEmailException $e) {
    die('Invalid email address');
}
catch (\Delight\Auth\UserAlreadyExistsException $e) {
    die('Email address already exists');
}
catch (\Delight\Auth\EmailNotVerifiedException $e) {
    die('Account not verified');
}
catch (\Delight\Auth\NotLoggedInException $e) {
    die('Not logged in');
}
catch (\Delight\Auth\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 '  For emails, consider using the mail(...) function, Symfony Mailer, Swiftmailer, PHPMailer, etc.';
        echo '  For SMS, consider using a third-party service and a compatible SDK';
    });

    echo 'The user may now respond to the confirmation request (usually by clicking a link)';
}
catch (\Delight\Auth\ConfirmationRequestNotFound $e) {
    die('No earlier request found that could be re-sent');
}
catch (\Delight\Auth\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 '  For emails, consider using the mail(...) function, Symfony Mailer, Swiftmailer, PHPMailer, etc.';
        echo '  For SMS, consider using a third-party service and a compatible SDK';
    });

    echo 'The user may now respond to the confirmation request (usually by clicking a link)';
}
catch (\Delight\Auth\ConfirmationRequestNotFound $e) {
    die('No earlier request found that could be re-sent');
}
catch (\Delight\Auth\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 (\Delight\Auth\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 (\Delight\Auth\NotLoggedInException $e) {
    die('The user is not signed in');
}
catch (\Delight\Auth\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();

可用角色

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

您可以使用这些角色中的任何一个,忽略那些您不需要的角色。上面的列表也可以以三种格式之一编程获取

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

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

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

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

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

    private function __construct() {}

}

上面的例子将允许您使用

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

而不是

\Delight\Auth\Role::REVIEWER;
// and
\Delight\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 (\Delight\Auth\NotLoggedInException $e) {
    die('The user is not signed in');
}
catch (\Delight\Auth\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 (\Delight\Auth\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 (\Delight\Auth\InvalidEmailException $e) {
    die('Invalid email address');
}
catch (\Delight\Auth\InvalidPasswordException $e) {
    die('Invalid password');
}
catch (\Delight\Auth\UserAlreadyExistsException $e) {
    die('User already exists');
}

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

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

删除用户

通过用户ID删除用户

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

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

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

通过用户名删除用户

try {
    $auth->admin()->deleteUserByUsername($_POST['username']);
}
catch (\Delight\Auth\UnknownUsernameException $e) {
    die('Unknown username');
}
catch (\Delight\Auth\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 (\Delight\Auth\UnknownIdException $e) {
    die('Unknown user ID');
}

// or

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

// or

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

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

从用户那里撤回角色

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

// or

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

// or

try {
    $auth->admin()->removeRoleForUserByUsername($username, \Delight\Auth\Role::ADMIN);
}
catch (\Delight\Auth\UnknownUsernameException $e) {
    die('Unknown username');
}
catch (\Delight\Auth\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 (\Delight\Auth\UnknownIdException $e) {
    die('Unknown user ID');
}

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

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

模拟用户(登录为用户)

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

// or

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

// or

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

更改用户的密码

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

// or

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

Cookie

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

\session_name();

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

\Delight\Auth\Auth::createRememberCookieName();

重命名库的Cookie

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

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

    session.name = session_v1
    
  • 在您的应用程序中尽可能早地调用,在创建Auth实例之前,调用\ini_set以将session.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。

无论您选择哪个子域名集,您都应该将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)中发送cookies,或者是否需要安全的连接,即HTTPS(带有SSL/TLS)。前一种(不安全)模式可以通过将属性设置为0来选择,后一种(更安全)模式可以通过将属性设置为1来选择。

显然,这完全取决于您是否能够仅通过HTTPS提供所有页面。如果您能这样做,您应该将属性设置为1,并且可能还需要结合使用将HTTP重定向到安全协议的HTTP Strict Transport Security (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 = \Delight\Auth\Auth::createRandomString($length);

根据RFC 4122创建UUID v4

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

读取和写入会话数据

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

常见问题

密码散列怎么办?

任何密码或身份验证令牌都会自动使用基于“Blowfish”密码的“bcrypt”函数进行散列,这是当今被认为是最强的密码散列函数之一。使用1,024次迭代,即“成本”因素为10。同时也会自动应用一个随机的“salt”

您可以通过查看数据库表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>元素中,您必须禁用默认的clickjacking防护。

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

异常

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

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

一般建议

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

贡献

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

许可证

本项目根据MIT许可证的条款许可。