horizom / auth
PHP 的认证。简单、轻量级且安全。
Requires
- php: ^8.0
- ext-openssl: *
- delight-im/base64: ^1.0
- delight-im/cookie: ^3.1
- delight-im/db: ^1.3
README
Horizom\Auth
PHP 的认证。简单、轻量级且安全。一次编写,到处可用。完全框架无关和数据库无关。
我为什么需要这个?
- 有很多 网站 使用弱认证系统。不要建立这样的网站。
- 为每个 PHP 项目重新实现新的认证系统是 不 好的。
- 逐步构建自己的认证类并将它们复制到每个项目中,也是 不 推荐的。
- 一个具有易于使用 API 的安全认证系统应该经过彻底的设计和规划。
- 对关键基础设施进行同行评审是 必需的。
要求
- PHP 5.6.0+
- PDO (PHP 数据对象) 扩展 (
pdo
)- MySQL 原生驱动 (
mysqlnd
) 或 PostgreSQL 驱动 (pgsql
) 或 SQLite 驱动 (sqlite
)
- MySQL 原生驱动 (
- OpenSSL 扩展 (
openssl
)
- PDO (PHP 数据对象) 扩展 (
- MySQL 5.5.3+ 或 MariaDB 5.5.23+ 或 PostgreSQL 9.5.10+ 或 SQLite 3.14.1+ 或 其他 SQL 数据库
安装
-
通过 Composer 包含库
composer require horizom/auth
-
包含 Composer 自动加载器
require __DIR__ . '/vendor/autoload.php';
-
设置数据库并创建所需的表
升级
从此项目的早期版本迁移?请参阅我们的升级指南 以获取帮助。
用法
- Horizom\Auth
创建新实例
$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 \Horizom\Auth\Auth($db); // Set password hashing algorithm $auth->setPasswordHashAlgorithm(PASSWORD_DEFAULT);
如果您已经有了打开的 PDO
连接,只需重复使用它。数据库用户(例如 my-username
)至少需要此库所使用的表(或其父数据库)的 SELECT
、INSERT
、UPDATE
和 DELETE
权限。
如果您的 Web 服务器位于代理服务器后面,并且 $_SERVER['REMOTE_ADDR']
只包含代理的 IP 地址,您必须将用户的真实 IP 地址作为第二个参数传递给构造函数,该参数名为 $ipAddress
。默认值为 PHP 收到的常规远程 IP 地址。
如果此库的数据库表需要一个共同的表前缀,例如 my_users
而不是 users
(以及其他表),请将前缀(例如 my_
)作为第三个参数传递给构造函数,该参数名为 $dbTablePrefix
。这是可选的,并且默认情况下前缀为空。
在开发过程中,您可能想禁用此库执行的需求限制或节流。要这样做,将 false
作为第四个参数传递给构造函数,该参数名为 $throttling
。默认情况下,此功能是启用的。
在会话生命周期内,一些用户数据可能被远程更改,无论是来自另一个会话的客户还是管理员。这意味着这些信息必须定期与数据库中的权威来源重新同步,这是此库自动执行的。默认情况下,每五分钟发生一次。如果您想更改此间隔,请将自定义间隔(以秒为单位)作为第五个参数传递给构造函数,该参数名为 $sessionResyncInterval
。
如果您的所有数据库表都需要一个共同的数据库名称、模式名称或其他必须明确指定的限定符,您可以可选地将该限定符作为第六个参数传递给构造函数,该参数名为 $dbSchema
。
注册(注册)
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 (\Horizom\Auth\Exception\InvalidEmailException $e) { die('Invalid email address'); } catch (\Horizom\Auth\Exception\InvalidPasswordException $e) { die('Invalid password'); } catch (\Horizom\Auth\Exception\UserAlreadyExistsException $e) { die('User already exists'); } catch (\Horizom\Auth\Exception\TooManyRequestsException $e) { die('Too many requests'); }
注意:匿名回调函数是一个 闭包。因此,除了其自己的参数外,仅当存在 超全局变量(如 $_GET
、$_POST
、$_COOKIE
和 $_SERVER
)时,内部才可用。对于父作用域中的任何其他变量,您需要通过在参数列表之后添加一个 use
子句来显式提供一个副本。
第三个参数中的用户名是可选的。如果您不希望管理用户名,可以将其传递为 null
。
另一方面,如果您想强制唯一用户名,只需调用 registerWithUniqueUsername
而不是 register
,并准备好捕获 DuplicateUsernameException
。
注意:在接收和管理用户名时,您可能想排除非打印控制字符和某些可打印的特殊字符,例如字符类 [\x00-\x1f\x7f\/:\\]
。为此,您可以在条件分支中包裹对 Auth#register
或 Auth#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 (\Horizom\Auth\Exception\InvalidEmailException $e) { die('Wrong email address'); } catch (\Horizom\Auth\Exception\InvalidPasswordException $e) { die('Wrong password'); } catch (\Horizom\Auth\Exception\EmailNotVerifiedException $e) { die('Email not verified'); } catch (\Horizom\Auth\Exception\TooManyRequestsException $e) { die('Too many requests'); }
如果您想通过用户名登录,无论是作为电子邮件地址登录的补充还是替代,也是可以实现的。只需调用 loginWithUsername
方法而不是 login
方法。然后,不要捕获 InvalidEmailException
,而要确保捕获 UnknownUsernameException
和 AmbiguousUsernameException
。您还可能想阅读有关用户名唯一性的说明,这部分内容解释了如何 注册新用户。
电子邮件验证
从用户在验证电子邮件中点击的URL中提取选择器和令牌。
try { $auth->confirmEmail($_GET['selector'], $_GET['token']); echo 'Email address has been verified'; } catch (\Horizom\Auth\Exception\InvalidSelectorTokenPairException $e) { die('Invalid token'); } catch (\Horizom\Auth\Exception\TokenExpiredException $e) { die('Token expired'); } catch (\Horizom\Auth\Exception\UserAlreadyExistsException $e) { die('Email address already exists'); } catch (\Horizom\Auth\Exception\TooManyRequestsException $e) { die('Too many requests'); }
如果您希望用户在成功确认后自动登录,只需调用 confirmEmailAndSignIn
而不是 confirmEmail
。该替代方法还通过可选的第三个参数支持 持久登录。
在成功的情况下,两个方法 confirmEmail
和 confirmEmailAndSignIn
都会返回一个数组,其中索引为1的位置是刚刚验证的新电子邮件地址。如果确认是为了地址变更而不是简单的地址验证,则用户的旧电子邮件地址将包含在索引为0的位置的数组中。
保持用户登录
Auth#login
和 Auth#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); // ...
没有持久登录(这是默认行为),用户将只在关闭浏览器或配置的 session.cookie_lifetime
和 session.gc_maxlifetime
在PHP中配置的时间范围内保持登录状态。
省略第三个参数或将它设置为 null
以禁用此功能。否则,您可以询问用户是否想启用“记住我”。这通常是通过用户界面中的复选框来完成的。使用该复选框的输入来决定在这里使用 null
或预定义的秒数,例如,使用 60 * 60 * 24 * 365.25
为一年。
密码重置(“忘记密码”)
第 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 (\Horizom\Auth\Exception\InvalidEmailException $e) { die('Invalid email address'); } catch (\Horizom\Auth\Exception\EmailNotVerifiedException $e) { die('Email not verified'); } catch (\Horizom\Auth\Exception\ResetDisabledException $e) { die('Password reset is disabled'); } catch (\Horizom\Auth\Exception\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 (\Horizom\Auth\Exception\InvalidSelectorTokenPairException $e) { die('Invalid token'); } catch (\Horizom\Auth\Exception\TokenExpiredException $e) { die('Token expired'); } catch (\Horizom\Auth\Exception\ResetDisabledException $e) { die('Password reset is disabled'); } catch (\Horizom\Auth\Exception\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 (\Horizom\Auth\Exception\InvalidSelectorTokenPairException $e) { die('Invalid token'); } catch (\Horizom\Auth\Exception\TokenExpiredException $e) { die('Token expired'); } catch (\Horizom\Auth\Exception\ResetDisabledException $e) { die('Password reset is disabled'); } catch (\Horizom\Auth\Exception\InvalidPasswordException $e) { die('Invalid password'); } catch (\Horizom\Auth\Exception\TooManyRequestsException $e) { die('Too many requests'); }
您想在密码重置成功时自动让相应的用户登录吗?只需使用 Auth#resetPasswordAndSignIn
而不是 Auth#resetPassword
来立即登录用户。
如果您需要用户的ID或电子邮件地址,例如,向他们发送通知,告知他们密码已成功重置,只需使用 Auth#resetPassword
的返回值,该返回值是一个包含名为 id
和 email
的两个条目的数组。
更改当前用户的密码
如果用户目前登录,他们可以更改密码。
try { $auth->changePassword($_POST['oldPassword'], $_POST['newPassword']); echo 'Password has been changed'; } catch (\Horizom\Auth\Exception\NotLoggedInException $e) { die('Not logged in'); } catch (\Horizom\Auth\Exception\InvalidPasswordException $e) { die('Invalid password(s)'); } catch (\Horizom\Auth\Exception\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 (\Horizom\Auth\Exception\InvalidEmailException $e) { die('Invalid email address'); } catch (\Horizom\Auth\Exception\UserAlreadyExistsException $e) { die('Email address already exists'); } catch (\Horizom\Auth\Exception\EmailNotVerifiedException $e) { die('Account not verified'); } catch (\Horizom\Auth\Exception\NotLoggedInException $e) { die('Not logged in'); } catch (\Horizom\Auth\Exception\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 (\Horizom\Auth\Exception\ConfirmationRequestNotFound $e) { die('No earlier request found that could be re-sent'); } catch (\Horizom\Auth\Exception\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 (\Horizom\Auth\Exception\ConfirmationRequestNotFound $e) { die('No earlier request found that could be re-sent'); } catch (\Horizom\Auth\Exception\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 (\Horizom\Auth\Exception\NotLoggedInException $e) { die('Not logged in'); } // or try { $auth->logOutEverywhere(); } catch (\Horizom\Auth\Exception\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();
其他用户信息
为了保持此库适用于所有目的以及其完全可重用性,它不附带额外的捆绑列用于用户信息。但您当然不需要没有额外的用户信息
以下是使用此库与您自己的表结合自定义用户信息的方式,以可维护和可重用的方式
-
添加任何数量的自定义数据库表,其中存储自定义用户信息,例如名为
profiles
的表。 -
每当您调用
register
方法(它返回新用户的ID)时,之后添加您自己的逻辑来填充您的自定义数据库表。 -
如果您很少需要自定义用户信息,您可以只需按需检索它。但是,如果您需要更频繁地使用它,您可能希望将其存储在会话数据中。以下方法是如何以可靠的方式加载数据和访问数据的方法
function getUserInfo(\Horizom\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 (\Horizom\Auth\Exception\NotLoggedInException $e) { die('The user is not signed in'); } catch (\Horizom\Auth\Exception\TooManyRequestsException $e) { die('Too many requests'); }
角色(或组)
每个用户都可以拥有任意数量的角色,您可以使用这些角色来实现授权和细化您的访问控制。
用户可能没有任何角色(这是他们的默认设置),一个角色,或者任何任意组合的角色。
检查角色
if ($auth->hasRole(\Horizom\Auth\Role::SUPER_MODERATOR)) { echo 'The user is a super moderator'; } // or if ($auth->hasAnyRole(\Horizom\Auth\Role::DEVELOPER, \Horizom\Auth\Role::MANAGER)) { echo 'The user is either a developer, or a manager, or both'; } // or if ($auth->hasAllRoles(\Horizom\Auth\Role::DEVELOPER, \Horizom\Auth\Role::MANAGER)) { echo 'The user is both a developer and a manager'; }
虽然hasRole
方法正好接受一个角色作为参数,但hasAnyRole
和hasAllRoles
方法可以接受您要检查的任意数量的角色。
或者,您可以获取分配给用户的所有角色的列表
$auth->getRoles();
可用的角色
\Horizom\Auth\Role::ADMIN; \Horizom\Auth\Role::AUTHOR; \Horizom\Auth\Role::COLLABORATOR; \Horizom\Auth\Role::CONSULTANT; \Horizom\Auth\Role::CONSUMER; \Horizom\Auth\Role::CONTRIBUTOR; \Horizom\Auth\Role::COORDINATOR; \Horizom\Auth\Role::CREATOR; \Horizom\Auth\Role::DEVELOPER; \Horizom\Auth\Role::DIRECTOR; \Horizom\Auth\Role::EDITOR; \Horizom\Auth\Role::EMPLOYEE; \Horizom\Auth\Role::MAINTAINER; \Horizom\Auth\Role::MANAGER; \Horizom\Auth\Role::MODERATOR; \Horizom\Auth\Role::PUBLISHER; \Horizom\Auth\Role::REVIEWER; \Horizom\Auth\Role::SUBSCRIBER; \Horizom\Auth\Role::SUPER_ADMIN; \Horizom\Auth\Role::SUPER_EDITOR; \Horizom\Auth\Role::SUPER_MODERATOR; \Horizom\Auth\Role::TRANSLATOR;
您可以使用这些角色中的任何一个,忽略那些您不需要的角色。上述列表也可以以三种格式之一编程检索
\Horizom\Auth\Role::getMap(); // or \Horizom\Auth\Role::getNames(); // or \Horizom\Auth\Role::getValues();
权限(或访问权限、特权或能力)
每个用户的权限都是按照在代码库中指定角色要求的编码方式来编码的。如果使用特定用户的角色集评估这些要求,则隐式检查的权限是结果。
对于较大的项目,通常建议在单个位置维护权限的定义。这样,您就不需要在业务逻辑中检查角色,而是检查单个权限。您可以如下实现这一概念
function canEditArticle(\Horizom\Auth\Auth $auth) { return $auth->hasAnyRole( \Horizom\Auth\Role::MODERATOR, \Horizom\Auth\Role::SUPER_MODERATOR, \Horizom\Auth\Role::ADMIN, \Horizom\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 = \Horizom\Auth\Role::REVIEWER; const FINANCIAL_DIRECTOR = \Horizom\Auth\Role::COORDINATOR; private function __construct() {} }
上面的示例将允许您使用
\My\Namespace\MyRole::CUSTOMER_SERVICE_AGENT; // and \My\Namespace\MyRole::FINANCIAL_DIRECTOR;
代替
\Horizom\Auth\Role::REVIEWER; // and \Horizom\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 (\Horizom\Auth\NotLoggedInException $e) { die('The user is not signed in'); } catch (\Horizom\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 (\Horizom\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 (\Horizom\Auth\InvalidEmailException $e) { die('Invalid email address'); } catch (\Horizom\Auth\InvalidPasswordException $e) { die('Invalid password'); } catch (\Horizom\Auth\UserAlreadyExistsException $e) { die('User already exists'); }
第三个参数中的用户名是可选的。如果您不希望管理用户名,可以将其传递为 null
。
另一方面,如果你想强制唯一的用户名,只需调用createUserWithUniqueUsername
而不是createUser
,并准备好捕获DuplicateUsernameException
。
删除用户
通过用户ID删除用户
try { $auth->admin()->deleteUserById($_POST['id']); } catch (\Horizom\Auth\UnknownIdException $e) { die('Unknown ID'); }
通过电子邮件地址删除用户
try { $auth->admin()->deleteUserByEmail($_POST['email']); } catch (\Horizom\Auth\InvalidEmailException $e) { die('Unknown email address'); }
通过用户名删除用户
try { $auth->admin()->deleteUserByUsername($_POST['username']); } catch (\Horizom\Auth\UnknownUsernameException $e) { die('Unknown username'); } catch (\Horizom\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, \Horizom\Auth\Role::ADMIN); } catch (\Horizom\Auth\UnknownIdException $e) { die('Unknown user ID'); } // or try { $auth->admin()->addRoleForUserByEmail($userEmail, \Horizom\Auth\Role::ADMIN); } catch (\Horizom\Auth\InvalidEmailException $e) { die('Unknown email address'); } // or try { $auth->admin()->addRoleForUserByUsername($username, \Horizom\Auth\Role::ADMIN); } catch (\Horizom\Auth\UnknownUsernameException $e) { die('Unknown username'); } catch (\Horizom\Auth\AmbiguousUsernameException $e) { die('Ambiguous username'); }
注意:用户角色集的变化可能需要最多五分钟才能生效。这提高了性能,通常不会造成问题。如果你希望更改这种行为,只需简单地将你传递给Auth
构造函数中名为$sessionResyncInterval
的参数值减小(或可能增加)。
从用户那里撤回角色
try { $auth->admin()->removeRoleForUserById($userId, \Horizom\Auth\Role::ADMIN); } catch (\Horizom\Auth\UnknownIdException $e) { die('Unknown user ID'); } // or try { $auth->admin()->removeRoleForUserByEmail($userEmail, \Horizom\Auth\Role::ADMIN); } catch (\Horizom\Auth\InvalidEmailException $e) { die('Unknown email address'); } // or try { $auth->admin()->removeRoleForUserByUsername($username, \Horizom\Auth\Role::ADMIN); } catch (\Horizom\Auth\UnknownUsernameException $e) { die('Unknown username'); } catch (\Horizom\Auth\AmbiguousUsernameException $e) { die('Ambiguous username'); }
注意:用户角色集的变化可能需要最多五分钟才能生效。这提高了性能,通常不会造成问题。如果你希望更改这种行为,只需简单地将你传递给Auth
构造函数中名为$sessionResyncInterval
的参数值减小(或可能增加)。
检查角色
try { if ($auth->admin()->doesUserHaveRole($userId, \Horizom\Auth\Role::ADMIN)) { echo 'The specified user is an administrator'; } else { echo 'The specified user is not an administrator'; } } catch (\Horizom\Auth\UnknownIdException $e) { die('Unknown user ID'); }
或者,您可以获取分配给用户的所有角色的列表
$auth->admin()->getRolesForUserById($userId);
模拟用户(以用户身份登录)
try { $auth->admin()->logInAsUserById($_POST['id']); } catch (\Horizom\Auth\UnknownIdException $e) { die('Unknown ID'); } catch (\Horizom\Auth\EmailNotVerifiedException $e) { die('Email address not verified'); } // or try { $auth->admin()->logInAsUserByEmail($_POST['email']); } catch (\Horizom\Auth\InvalidEmailException $e) { die('Unknown email address'); } catch (\Horizom\Auth\EmailNotVerifiedException $e) { die('Email address not verified'); } // or try { $auth->admin()->logInAsUserByUsername($_POST['username']); } catch (\Horizom\Auth\UnknownUsernameException $e) { die('Unknown username'); } catch (\Horizom\Auth\AmbiguousUsernameException $e) { die('Ambiguous username'); } catch (\Horizom\Auth\EmailNotVerifiedException $e) { die('Email address not verified'); }
更改用户的密码
try { $auth->admin()->changePasswordForUserById($_POST['id'], $_POST['newPassword']); } catch (\Horizom\Auth\UnknownIdException $e) { die('Unknown ID'); } catch (\Horizom\Auth\InvalidPasswordException $e) { die('Invalid password'); } // or try { $auth->admin()->changePasswordForUserByUsername($_POST['username'], $_POST['newPassword']); } catch (\Horizom\Auth\UnknownUsernameException $e) { die('Unknown username'); } catch (\Horizom\Auth\AmbiguousUsernameException $e) { die('Ambiguous username'); } catch (\Horizom\Auth\InvalidPasswordException $e) { die('Invalid password'); }
Cookie
此库使用两个cookie在客户端保持状态:第一个,你可以使用以下方法检索其名称:
\session_name();
是通用的(必需的)会话cookie。第二个(可选的)cookie仅用于持久登录,其名称可以通过以下方式检索
\Horizom\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的属性设置为最具体的域名,该域名仍然包括所有必要的子域。例如,为了在example.com
和www.example.com
之间共享cookie,您应该将属性设置为example.com
。但是,如果您想在不同子域sub1.app.example.com
和sub2.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 = \Horizom\Auth\Auth::createRandomString($length);
根据 RFC 4122 创建 UUID v4
$uuid = \Horizom\Auth\Auth::createUuid();
读取和写入会话数据
有关如何方便地读取和写入会话数据的详细信息,请参阅会话库的文档,该文档是默认包含的。
常见问题解答
关于密码散列怎么办?
任何密码或认证令牌都会自动使用基于“Blowfish”加密算法的“bcrypt”函数进行哈希处理,该算法被认为是当今最强大的密码哈希函数之一。 “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个字符。此外,你不应该限制允许的字符集。
- 每当用户通过在登录期间启用或禁用“记住我”功能而被记住时,这意味着他们不是通过输入密码来登录的,你应该要求对关键功能进行重新认证。
- 鼓励用户使用密码短语,即单词组合甚至完整的句子,而不是单个密码。
- 不要阻止用户的密码管理器正常工作。因此,仅使用标准表单字段,并且不要阻止复制和粘贴。
- 在执行敏感账户操作之前(例如更改用户的电子邮件地址,删除用户的账户),您应该始终要求重新认证,即要求用户再次验证他们的登录凭据。
- 对于高安全性的应用,您不应提供在线密码重置功能(“忘记密码”)。
- 对于高安全性应用,您不应使用电子邮件地址作为标识符。相反,选择与应用程序特定且保密的标识符,例如内部客户编号。
贡献
所有贡献都受欢迎!如果您希望贡献,请首先创建一个问题,以便讨论您的功能、问题或疑问。
许可证
Horizom框架是开源软件,根据MIT许可证授权。