PHP 编程/构建安全的用户登录系统
许多初学者 PHP 程序员开始构建具有用户登录系统的网站,但并不知道等待着的陷阱。下面是一个逐步指南,介绍了用户身份验证系统和用户授权系统的必要组件。前者是关于确定用户是否是他们自称的人,而后者则关注用户是否被允许做他们试图做的事情(例如访问特定页面或执行特定查询)。
建议读者将下面概述的概念集成到他们的脚本中,而不仅仅是复制粘贴代码示例。登录系统安全性取决于开发软件的个人的信任。由于任何人都可以编辑这些页面,包括那些怀有恶意意图的人,因此你不应该信任代码示例。 |
身份验证过程有两个部分
- 登录表单。用户被提供了一些输入其凭据的方式;系统会将这些凭据与已知用户的列表进行比对;如果找到匹配项,则用户将被身份验证。该系统的一部分通常还会启动某种方式来记住用户已通过身份验证(例如通过设置 cookie),以便此过程不必为每个请求重复。
- 每次请求检查(为了便于理解!)。这与登录表单过程的第二部分相同,用户凭据是从对用户更方便的来源获取的,例如 cookie。
下面给出的代码可能需要根据你的脚本架构进行调整,无论是面向对象还是过程式的,无论是只有一个入口点还是十几个脚本分别调用。但是,无论登录系统的组件如何执行,其基本原理都是相同的。同样,当提到“数据库”时,它不一定意味着 MySQL 或任何其他 RDBMS;用户信息可以存储在平面文件中、LDAP 服务器上或以其他方式存储。
这是系统中最简单的一部分,也是最容易上手的地方。简单来说:一个 HTML 表单被呈现给用户,用户输入其凭据,表单内容被提交到登录系统的下一部分进行处理。
用户的凭据通常是用户名和密码,但也有其他可能性(例如来自硬件令牌生成器的随机数)。许多网站现在使用用户的电子邮件地址而不是用户名。这样做的好处是电子邮件地址对每个用户都是唯一的,并且允许人们拥有始终如一的用户名,因为具有常见用户名的用户可能无法在他们注册的每个地方都获得相同的用户名。
<form action="/login" method="post">
<p>
<label for="email">Email address:</label>
<input id="email" type="text" name="email" />
</p>
<p>
<label for="password">Password:</label>
<input id="password" type="password" name="password" />
</p>
<p>
<input type="submit" name="login" value="Login" />
</p>
</form>
登录表单中的数据可以提交给处理身份验证和登录过程的任何脚本,而真正的登录过程就是在该脚本中进行的。
在登录时,我们不需要像创建或更改帐户时那样担心检查输入,因为我们只是将输入的内容与数据库中的内容进行匹配。任何非法的输入(例如没有 @ 符号的电子邮件地址)都将无法匹配,登录将失败。所有用户提交的数据在传递到数据库时都会被安全地转义,因此,在这一点上,我们无需担心SQL 注入攻击,并且限制用户名或密码的长度或格式没有任何安全优势。
登录处理脚本本身需要做几件事。首先,它启动一个会话(这通常会构成“每次请求身份验证”的一部分,如下所述)。其次,它查询数据库以查找匹配的用户;如果没有,则登录尝试失败,用户将返回登录表单。最后(假设用户确实存在),其标识符(用户名或电子邮件地址)将被保存为会话变量。
session_start();
$db = new Database(); // Database abstraction class.
$email_address = $db->esc($_POST['email']);
$password = $db->esc($_POST['password']);
$matching_users = $db->get_num_rows("SELECT 1 FROM `users` WHERE email_address='$email_address' AND password=crypt('$password', password) LIMIT 1");
if ($matching_users) {
// User exists; log user in.
$_SESSION['email_address'] = $email_address;
echo "You are now logged in.";
} else {
// Login failed; re-display login form.
}
Database
数据库抽象类用于隐藏数据库实现细节,以实现本示例的目的,不应被认为代表任何实际存在的库类。- 其中
Database
类的get_num_rows($sql)
方法返回$sql
查询产生的行的计数。上面的LIMIT 1
表示它将只返回 0 或 1。
脚本现在可以显示欢迎消息,并且身份验证将与用户保持一致,因此他无需在每次加载页面时都登录。现在让我们来看看如何做到这一点。
必须在每个 HTTP 请求(即对于页面、图像或任何其他内容)上验证用户的真实性。这表面上很简单,只要看看相关的会话变量是否已设置即可
session_start();
if (!isset($_SESSION['email_address']))
{
// User is not logged in, so send user away.
header("Location:/login");
die();
}
// User is logged in; private code goes here.
这在很多方面都足够了,但它容易受到多种攻击。它完全依赖于会话与正确用户绑定。这不是一件好事,因为会话可能会被劫持(会话密钥可能被第三方窃取)或固定(第三方可以迫使用户使用第三方知道的会话密钥)。阅读这本维基教科书的会话页面,以了解更多相关信息以及如何避免这种情况。
$timeout = 60 * 30; // In seconds, i.e. 30 minutes.
$fingerprint = hash_hmac('sha256', $_SERVER['HTTP_USER_AGENT'], hash('sha256', $_SERVER['REMOTE_ADDR'], true));
session_start();
if ( (isset($_SESSION['last_active']) && $_SESSION['last_active']<(time()-$timeout))
|| (isset($_SESSION['fingerprint']) && $_SESSION['fingerprint']!=$fingerprint)
|| isset($_GET['logout'])
)
{
setcookie(session_name(), '', time()-3600, '/');
session_destroy();
}
session_regenerate_id();
$_SESSION['last_active'] = time();
$_SESSION['fingerprint'] = $fingerprint;
// User authenticated at this point (i.e. $_SESSION['email_address'] can be trusted).
如果要使用 $_SESSION['last_active']
和 $_SESSION['fingerprint']
变量,则也需要在初始登录点(处理登录表单的位置)设置它们;只需将以下几行插入设置 email_address
变量的位置上方即可
$fingerprint = hash_hmac('sha256', $_SERVER['HTTP_USER_AGENT'], hash('sha256', $_SERVER['REMOTE_ADDR'], true));
$_SESSION['last_active'] = time();
$_SESSION['fingerprint'] = $fingerprint;
但是,在使用浏览器指纹时需要注意的一点是,尽管它们确实为你的应用程序增加了一些安全性,但它们并不是万能的。许多ISP提供动态 IP 地址,这些地址是定期更改的 IP 地址。如果这种情况发生在用户浏览你的页面时,他将被踢出帐户。此外,检查浏览器是否相同的代码片段可以通过修改请求页面的标头的 Firefox 扩展程序进行修改。
这就是在 PHP5 中实现安全的用户身份验证系统的方式!在上面的信息中,有一些点被一笔带过,也有一些点完全被省略了(例如,如何只在必要时启动会话)。如果你正在实现自己的用户登录系统并尝试遵循这里给出的建议,你肯定会遇到一些在进行过程中会发现并希望在这里解释的事情,因此请大胆地编辑此页面,添加你认为缺失的任何内容。
如果你认为需要更改某些内容,请不要犹豫!维基教科书是一个维基,这意味着每个人(包括你)都可以通过点击编辑此页面来编辑任何页面。你甚至不需要登录!维基教科书总是欢迎新手,所以不要害怕犯错。如果你不知道如何编辑页面,请查看帮助:编辑或在沙盒中尝试。 |