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 中实现安全用户身份验证系统的方式!上面信息中有一些要点被一笔带过,还有一些要点完全被省略了(例如,如何仅在必要时启动会话)。如果你正在实现自己的用户登录系统,并且试图遵循此处给出的建议,那么在进行过程中,你一定会发现一些事情,并且希望这里能解释清楚,因此请大胆地编辑此页面以添加你认为缺少的任何内容。
如果你认为需要更改某些内容,请不要犹豫!维基教科书是一个维基,这意味着每个人(包括你)都可以通过点击编辑此页面来编辑任何页面。你甚至不需要登录!新来者总是欢迎来到维基教科书,所以不要害怕犯错。如果你不知道如何编辑页面,请查看帮助:编辑或在沙盒中进行实验。 |