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

v8.3.0 2021-04-21 14:39 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 delight-im/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,并从该方法调用中省略第一个参数(否则将包含旧密码)。

在任何情况下,用户更改密码后,您都应该向其账户的primary邮箱地址发送电子邮件,作为离线通知,告知账户所有者这一关键更改。

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

如果用户当前已登录,他们可以更改自己的邮箱地址。

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参数(请参阅$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_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 可用的目录时才应更改此属性,例如,为了在同一域名下并行托管多个应用程序,在不同目录中。

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

  • 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 = \Delight\Auth\Auth::createRandomString($length);

创建符合 RFC 4122 的 UUID v4

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

读取和写入会话数据

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

常见问题

密码哈希处理怎么办?

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