Home > 感悟 > 如何去管理php的用户名和密码(二)

如何去管理php的用户名和密码(二)1+

10,876 views / 2010.06.17 / 2:02 下午

(接上文:http://71j.cn/archives/173)

操练开始

在我们做出测试代码之前,首先要创建一个用户数据表。运行如下语句:

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中:

<form action="user-man.php" method="POST">
用户名:<br>
<input size="60"><br>
密码:<br>
<input size="60"><br>
<input value="创建新用户">
</form>

这个文件获取用户名和密码,然后提交到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();

好了,把左右文件保存好,放在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));

当我们插入相同的用户名时,程序就会报错:

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");
}

需要注意的,不管是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);
}

这个方法确实奏效,而且也很可靠。但是,我们还有更简捷的实现方法,那就是使用mysql错误码:

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;
}

接下来,我们将用这个函数取post过来的数据,而不是单纯的$_POST数组。 

(待续)

本站内容受著作权法保护。个人 blog 转载时请遵循 “署名-非商业用途-保持一致” 的创作共用协议;商业网站或未授权媒体不得复制本站内容。
Categories: 感悟 Tags: , , , ,

Comments (0) Trackbacks (1) 本篇共有 1 篇评论↓
  1. No comments yet.
  1. 十二月 15th, 2011 at 19:56 | #1