操练开始
在我们做出测试代码之前,首先要创建一个用户数据表。运行如下语句:
create database myapp;
use myapp;
create table users (user varchar(60), pass varchar(60));
create database myapp;
use myapp;
create table users (user varchar(60), pass varchar(60));
其中user用来储存用户名,pass用来储存密码的hash值。目前,phpass生成的密码hash值最大长度为60。
创建新用户
首先,我们从phpass项目网站把PasswordHash.php下载到网站目录中,并设置能让php加载的权限(Unix系统下一般为600或者644)。然后在网站目录中创建两个文件:user-man.html (644权限), and user-man.php (权限同asswordHash.php)。
下面,把下面的内容写在user-man.html中:
用户名:
密码:
用户名:
密码:
这个文件获取用户名和密码,然后提交到user-man.php。下面是user-man.php的代码:
header('Content-Type: text/plain');
// 本例只是简单的输出文本的hash值,所以开头要声明下,不让浏览器当作html解析。
require '../PasswordHash.php';
// Base-2 logarithm of the iteration count used for password stretching
$hash_cost_log2 = 8;
// Do we require the hashes to be portable to older systems (less secure)?
$hash_portable = FALSE;
//在实际应用中,上面两行最好写在配置文件中,比如config.inc.php
//下面开始获取提交的用户名和密码,实际应用中需要验证有效性,不再赘述。
$user = $_POST['user'];
$pass = $_POST['pass'];
//下面开始计算密码hash值
$hasher = new PasswordHash($hash_cost_log2, $hash_portable);
$hash = $hasher->HashPassword($pass);
if (strlen($hash) < 20)//这里用的CRYPT_EXT_DES方法,其它加密算法得到结果会更长。
fail('Failed to hash new password');
unset($hasher);
function fail($pub, $pvt = '')
{
$msg = $pub;
if ($pvt !== '')
$msg .= ": $pvt";
exit("An error occurred ($msg).\n");
}
//下面开始把用户信息存入到数据库中
$db_host = 'localhost';
$db_port = 3306;
$db_user = ‘dbuser’;
$db_pass = 'dbpass';
$db_name = 'dbname';
//数据库信息也最好存储在配置文件中。下面开始连接数据库,并注意弹出失败信息。
$db = new mysqli($db_host, $db_user, $db_pass, $db_name, $db_port);
if (mysqli_connect_errno())
fail('MySQL connect', mysqli_connect_error());
//下面用预备语句插入用户信息
($stmt = $db->prepare('insert into users (user, pass) values (?, ?)'))
|| fail('MySQL prepare', $db->error);
$stmt->bind_param('ss', $user, $hash)
|| fail('MySQL bind_param', $db->error);
$stmt->execute()
|| fail('MySQL execute', $db->error);
//最后数据库连接
$stmt->close();
$db->close();
header('Content-Type: text/plain');
// 本例只是简单的输出文本的hash值,所以开头要声明下,不让浏览器当作html解析。
require '../PasswordHash.php';
// Base-2 logarithm of the iteration count used for password stretching
$hash_cost_log2 = 8;
// Do we require the hashes to be portable to older systems (less secure)?
$hash_portable = FALSE;
//在实际应用中,上面两行最好写在配置文件中,比如config.inc.php
//下面开始获取提交的用户名和密码,实际应用中需要验证有效性,不再赘述。
$user = $_POST['user'];
$pass = $_POST['pass'];
//下面开始计算密码hash值
$hasher = new PasswordHash($hash_cost_log2, $hash_portable);
$hash = $hasher->HashPassword($pass);
if (strlen($hash) < 20)//这里用的CRYPT_EXT_DES方法,其它加密算法得到结果会更长。
fail('Failed to hash new password');
unset($hasher);
function fail($pub, $pvt = '')
{
$msg = $pub;
if ($pvt !== '')
$msg .= ": $pvt";
exit("An error occurred ($msg).\n");
}
//下面开始把用户信息存入到数据库中
$db_host = 'localhost';
$db_port = 3306;
$db_user = ‘dbuser’;
$db_pass = 'dbpass';
$db_name = 'dbname';
//数据库信息也最好存储在配置文件中。下面开始连接数据库,并注意弹出失败信息。
$db = new mysqli($db_host, $db_user, $db_pass, $db_name, $db_port);
if (mysqli_connect_errno())
fail('MySQL connect', mysqli_connect_error());
//下面用预备语句插入用户信息
($stmt = $db->prepare('insert into users (user, pass) values (?, ?)'))
|| fail('MySQL prepare', $db->error);
$stmt->bind_param('ss', $user, $hash)
|| fail('MySQL bind_param', $db->error);
$stmt->execute()
|| fail('MySQL execute', $db->error);
//最后数据库连接
$stmt->close();
$db->close();
好了,把左右文件保存好,放在web server下测试下。输入用户名和密码,提交后,到数据库中看下:
mysql> select * from users;
+——–+————————————————————–+
| user |pass |
+——–+————————————————————–+
| myuser | $3b$08$Lg5XF1Tr.X5TGyfb43vBBeEFZm4GTRQhKQ6SY6emkcnhAGT8KfxFS |
+——–+————————————————————–+
1 row in set (0.00 sec)
至此,用户插入成功。
用户已经存在
下面,我们用上面的方法插入一个相同的用户,同时,用相同的密码。然后查看数据库:
mysql> select * from users;
+——–+————————————————————–+
| user |pass |
+——–+————————————————————–+
| myuser | $3b$08$Lg5XF1Tr.X5TGyfb43vBBeEFZm4GTRQhKQ6SY6emkcnhAGT8KfxFS |
| myuser | $1a$08$7lM07FwQMm5/C8G/urT4z..MudfsS227e8oUEu6T51bNWk/RGb/qe |
+——–+————————————————————–+
2 rows in set (0.00 sec)
我们得到了用户名相同的两条记录,但是密码hash值不相同,虽然我们使用了相同的密码。
为了解决这个问题,我们可以在执行插入前先执行一个select语句,查询下该用户名是否已经存在了。但是,这对程序的效率来说不是最优化的。好的做法是让为用户名建立唯一索引,禁止用户用户名的出现:
DROP TABLE users;
CREATE TABLE users (user varchar(60), pass varchar(60), UNIQUE (user));
DROP TABLE users;
CREATE TABLE users (user varchar(60), pass varchar(60), UNIQUE (user));
当我们插入相同的用户名时,程序就会报错:
An error occurred (MySQL execute: Duplicate entry ‘myuser’ for key 1).
如此,系统效率会得到提高。虽然,这是纯技术性的错误提示, 我们将稍侯予以解决。
避免泄漏过多服务器细节
上面出现的报错多是mysql服务器报错,可能会泄漏一些敏感信息,如数据库名,数据库地址,甚至数据表文件的存储地址都会被显示,这是很危险的。因此,这些信息我们并不希望被显示,除非我们就是用户,或者是在调试。如此,我们可以修改fail()函数,把错误信息显示为用户可见的内容。
// 是否为debug模式,如果是,会显示敏感信息。
$debug = TRUE;
function fail($pub, $pvt = '')
{
global $debug;
$msg = $pub;
if ($debug && $pvt !== '')
$msg .= ": $pvt";
/* $pvt 可能会含有敏感信息,比如需要隐藏掉,或者需要编码才能被html正确显示的内容。*/
exit("An error occurred ($msg).\n");
}
// 是否为debug模式,如果是,会显示敏感信息。
$debug = TRUE;
function fail($pub, $pvt = '')
{
global $debug;
$msg = $pub;
if ($debug && $pvt !== '')
$msg .= ": $pvt";
/* $pvt 可能会含有敏感信息,比如需要隐藏掉,或者需要编码才能被html正确显示的内容。*/
exit("An error occurred ($msg).\n");
}
需要注意的,不管是apache还是php,默认情况下是会显示所有调试信息的。所以,作为一个程序员,我们的职责是防止这些信息被泄漏,就跟我们设置了debug模式一样,这对程序员或者服务器运维人员来说至关重要。默认情况下,要把$debug值设置为false,但我们的例子作为测试来说,将继续使用true.
如何区分mysql报错
我们需要去辨别mysql报错,以确定用户是否已经存在于数据库中,如果已经存在,需要输出一个友好的错误提示。因为当我们插入用户的时候,不只是会有一种错误,当出现其它错误的时候,我们不能傻不愣瞪的提示相同的错误(用户已经存在)吧?
一种解决方法是在出现报错后执行一个针对该用户名的select查询,如果能够返回一行数据,说明用户确实一定存在了。实现方法如下:
if (!$stmt->execute()) {
$save_error = $db->error;
$stmt->close();
// 用户已经存在了?
($stmt = $db->prepare('select user from users where user=?'))
|| fail('MySQL prepare', $db->error);
$stmt->bind_param('s', $user)
|| fail('MySQL bind_param', $db->error);
$stmt->execute()
|| fail('MySQL execute', $db->error);
$stmt->store_result()
|| fail('MySQL store_result', $db->error);
if ($stmt->num_rows === 1)
fail('This username is already taken');
else
fail('MySQL execute', $save_error);
}
if (!$stmt->execute()) {
$save_error = $db->error;
$stmt->close();
// 用户已经存在了?
($stmt = $db->prepare('select user from users where user=?'))
|| fail('MySQL prepare', $db->error);
$stmt->bind_param('s', $user)
|| fail('MySQL bind_param', $db->error);
$stmt->execute()
|| fail('MySQL execute', $db->error);
$stmt->store_result()
|| fail('MySQL store_result', $db->error);
if ($stmt->num_rows === 1)
fail('This username is already taken');
else
fail('MySQL execute', $save_error);
}
这个方法确实奏效,而且也很可靠。但是,我们还有更简捷的实现方法,那就是使用mysql错误码:
if (!$stmt->execute()) {
if ($db->errno === 1062 /* ER_DUP_ENTRY */)
fail('额滴神,该用户已经存在了');
else
fail('MySQL execute', $db->error);
}
if (!$stmt->execute()) {
if ($db->errno === 1062 /* ER_DUP_ENTRY */)
fail('额滴神,该用户已经存在了');
else
fail('MySQL execute', $db->error);
}
在接下来的例子中,我们将使用这种简单的方法做演示。
魔法引号的处理
Magic quotes 开启后会自动转义输入的数据。其中,所有的单引号(’)、双引号(”)、反斜线、和 NULL 字符都会被转义(增加个反斜线),其实这操作本质上调用的是 addslashes 函数。
这对程序员来说固然是一个很好的事情,省却了我们过滤的麻烦。但是,当用户输入用户名和密码中含有这些字符时,我们从$_POST中获取到的内容是不是也会被addslashes了呢?
这就需要我们去做判断,示例如下:
function get_post_var($var)
{
$val = $_POST[$var];
if (get_magic_quotes_gpc())
$val = stripslashes($val);
return $val;
}
function get_post_var($var)
{
$val = $_POST[$var];
if (get_magic_quotes_gpc())
$val = stripslashes($val);
return $val;
}
接下来,我们将用这个函数取post过来的数据,而不是单纯的$_POST数组。
(待续)