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

6,873 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数组。 

(待续)

Categories: 感悟 Tags: , , , ,

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

9,336 views / 2010.06.13 / 5:05 下午

目前,多数大型的网站,尤其是SNS社区,都有用户管理系统,这些系统一般都涉及到密码认证的问题。那么这些系统是如何储存用户密码的呢?直接存储密码明文吗?答案是否定的,好的做法是把密码算出一个哈希(hash)值予以存储。我们会发现好多文章教你怎么去计算密码的hash值,杜工的博客(http://71j.cn)也有类似的文章,不过不是用来加密密码的,只是简单的散列,传统的文章推荐使用md5(),后来有些人推荐把密码连接上一个附加值(我们称之为salt值, Discuz就是这么做的),然后计算sha1(),或者hash()(SHA-256等),同时把用户名进行mysql_real_escape_string()处理。虽然这些文章介绍的方法总体方向是对的,但是并没有一篇关于密码安全存储的文章是尽善尽美的。

其实,最近的文章已经开始推荐使用phpass, 这是一个基于php的密码hash算法框架,已经被许多知名的web程序使用,如wordpress、Durpal 7等,但是目前并没有一篇详尽的,一步一步指引我们引用phpass的成型文章,而且密码安全问题,并不只是单纯的如何去hash密码予以存储的问题。

接下来,我将一步步介绍如何把用户密码管理方案引入到一套新的php程序中。首先,将简单阐述密码hash的概念以及如何安全地把这些密码送入数据库,然后通过个示例。从一个简单的创建新用户的程序开始,慢慢展开来,逐渐引入用户登录、修改(重置)密码的功能。

当然,文中会涉及其它相关的概念,如果你已了解,可直接略过。

密码HASH

合格的系统存储在数据库中的并不是密码明文,而是密码hash值。而实际上,单纯的把密码采用php函数,如md5,sha1等,加密后存储,会引发很严重的后果。只要正确的进行了hash,攻击者就很难从获取到的hash值中解析出用户密码,你只需要快速的把弱口令密码解决掉,就能把损失降到最低了。

当用户输入用户名和之前设定好的密码,点击登录进行验证后,程序先要根据用户名查询一些额外信息,如密码哈希类型,salt值,哈希重复的次数等,然后根据这些信息算出密码hash值,与已经存储的hash值进行比较,如果相同,则验证成功。

Salt方法

Salt是一个近乎随机的字符串,相同的密码使用不同的salt hash散列后, 会得到不同的结果,正确的使用这个方法,可以防止一系列的攻击,包括:

1.通过弱口令(如123456)就知道你的hash类型。

2.使用已经算好的hash值去做比对,如cmd5.com。

3.能够准确知道两个用户(或者一个用户的两个帐号)的密码是否是一样的。

Salt通常和密码hash值储存在一起,它并不是保密的。具体案例可参考Ucenter

算法强度要提升

一旦密码hash值泄漏 ,攻击者必然会采取暴力破解,一般的hash算法,如md5, sha1等,都是为了保证速度,而非为密码加密准备的,这样势必为攻击者缩短暴力破解提供了可能。因此,采取一个略微复杂点的hash算法,能够降低损失。假想下,如果你的每个密码生成时间家长0.2ms,用户不会有任何不好的体验,但对攻击者来说,这意味着什么?他要多付出几个月甚至几年的时间去破解你的密码!

关于phpass

Phpass是基于php的简单易用的密码hash算法框架。它提供三种hash方法,包括两种基于函数crypt()的—-CRYPT_BLOWFISH 和 CRYPT_EXT_DES,一种phpass自己的类md5的算法,前者只有php5.30以上的版本支持,后者这是全版本皆可。虽然,所有的算法都使用了salt及算法强度提升的方法,但考虑到效果,我们推荐使用前两种。在使用时,用户可以自定义hash迭代的次数。

在使用的时候,phpass能够自动生成salt和对进行算法拓展,你不需要为生成这些而费神。另外,phpass支持windows系统。

安全的接入数据库

SQL注入

所谓SQL注入,就是通过把SQL命令(特殊字符串,如单引号等)插入到Web表单递交或输入域名或页面请求的查询字符串,最终达到欺骗服务器执行恶意的SQL命令的行为,比如先前的很多会员网站泄露VIP会员密码大多就是通过WEB表单递交查询字符暴出的,这类表单特别容易受到SQL注入式攻击.

如何防止SQL注入

  1.永远不要信任用户的输入。对用户的输入进行转码校验,可以通过正则表达式,或限制长度;对单引号等特殊字符进行转换,如对数据进行url_encode\ stripslashes等。

  2.永远不要使用动态拼装sql,可以使用参数化的sql或者直接使用存储过程进行数据查询存取。

  3.永远不要使用管理员权限的数据库连接,为每个应用使用单独的权限有限的数据库连接。(下面有详细说明)

  4.不要把机密信息直接存放,加密或者hash掉密码和敏感的信息。

  5.应用的异常信息应该给出尽可能少的提示,最好使用自定义的错误信息对原始错误信息进行包装。

在接下来的例子中 ,如上几点将得以体现。

组建数据库预备语句

目前,php提供三种连接mysql的接口,一种是自带的,被广泛应用的mysql扩展,另外一种是mysqli扩展, 还有一个PDO(PHP Data Objects)接口,只有mysqli和PDO支持MySQL的预备语句,而且都需要php5+的支持。本文使用mysqli的方式。

预备语句的使用使数据和代码分离,虽然这并不是最完美的防止sql注入的方法,以为从头到尾,所有的数据都使用相同的socket连接传输,但比传统的语句过滤使用起来更加简单,并且不容易出错。

对数据库采用最小权限原则

除了要预防SQL注入,我们还要预防其它各方面潜在的威胁。好的做法是使用最低权限的数据库帐号,也就是说,不要使用管理员权限的数据库链接,而是为每个应用使用单独的权限的数据库连接。这样,即使你的php代码并不是很“完美”,也可以把网站被攻破而付出最小的代价。

(待续)

Categories: 感悟 Tags: , , , ,