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

18,857 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: , , , ,

简评file_get_contents与curl 效率及稳定性3+

70,558 views / 2010.02.06 / 5:05 下午

做过好多抓取别家网站内容的产品,习惯了使用方便快捷的file_get_contents函数,但是总是会遇到获取失败的问题,尽管按照手册中的例子设置了超时,可多数时候不会奏效:

$config[‘context’] = stream_context_create(array(‘http’ => array(‘method’ => “GET”,
   ‘timeout’ => 5//这个超时时间不稳定,经常不奏效
   )
  ));

这时候,看一下服务器的连接池,会发现一堆类似的错误,让你头疼万分:

file_get_contents(http://***): failed to open stream…

不得已,安装了curl库,写了一个函数替换:

function curl_file_get_contents($durl){
   $ch = curl_init();
   curl_setopt($ch, CURLOPT_URL, $durl);
   curl_setopt($ch, CURLOPT_TIMEOUT, 5);
   curl_setopt($ch, CURLOPT_USERAGENT, _USERAGENT_);
   curl_setopt($ch, CURLOPT_REFERER,_REFERER_);
   curl_setopt($ch, CURLOPT_RETURNTRANSFER, 1);
   $r = curl_exec($ch);
   curl_close($ch);
   return $r;
 }

如此,除了真正的网络问题外,没再出现任何问题。

这是别人做过的关于curl和file_get_contents的测试:

file_get_contents抓取google.com需用秒数:

2.31319094
2.30374217
2.21512604
3.30553889
2.30124092

curl使用的时间:

0.68719101
0.64675593
0.64326
0.81983113
0.63956594

差距很大吧?呵呵,从我使用的经验来说,这两个工具不只是速度有差异,稳定性也相差很大。建议对网络数据抓取稳定性要求比较高的朋友使用上面的curl_file_get_contents函数,不但稳定速度快,还能假冒浏览器欺骗目标地址哦!

Categories: 感悟 Tags: ,

一个抓取网站内容的函数,支持301 302跳转0+

13,992 views / 2010.01.18 / 4:04 下午

我们在抓取网站内容的时候,经常遇到稀奇古怪的防盗链,比如上次碰到一个站的图片地址是假的,访问后要301跳转一次才到真正的图片路径,这个真实的路径又做了防盗措施,判断referer是不是上个假的图片地址。用curl试了几次,终于整出一个函数,效果不错。

$curl_loops = 0;//避免死了循环必备
$curl_max_loops = 3;
 
function curl_get_file_contents($url, $referer='') {
global $curl_loops, $curl_max_loops;
$useragent = "Mozilla/4.0 (compatible; MSIE 7.0; Windows NT 6.0; SV1; .NET CLR 1.1.4322; .NET CLR 2.0.50727)";
if ($curl_loops++ >= $curl_max_loops) {
  $curl_loops = 0;
  return false;
} 
$ch = curl_init();
curl_setopt($ch, CURLOPT_URL, $url);?curl_setopt($ch, CURLOPT_HEADER, true);
curl_setopt($ch, CURLOPT_USERAGENT, $useragent);
curl_setopt($ch, CURLOPT_RETURNTRANSFER, true);
curl_setopt($ch, CURLOPT_REFERER, $referer);
$data = curl_exec($ch);
$ret = $data;
list($header, $data) = explode("\r\n\r\n", $data, 2);
$http_code = curl_getinfo($ch, CURLINFO_HTTP_CODE);
$last_url = curl_getinfo($ch, CURLINFO_EFFECTIVE_URL);
curl_close($ch);
if ($http_code == 301 || $http_code == 302) {
  $matches = array();
  preg_match('/Location:(.*?)\n/', $header, $matches);
  $url = @parse_url(trim(array_pop($matches)));
  if (!$url) {
  ?$curl_loops = 0;
  ?return $data;
  } 
  $new_url = $url['scheme'] . '://' . $url['host'] . $url['path']
   . (isset($url['query']) ? '?' . $url['query'] : '');
  $new_url = stripslashes($new_url);
  return curl_get_file_contents($new_url, $last_url);
} else {
  $curl_loops = 0;
  list($header, $data) = explode("\r\n\r\n", $ret, 2);
  return $data;
} 
}
Categories: 感悟 Tags: , ,

php curl cookie 存取示例1+

125,778 views / 2009.12.02 / 1:01 下午

好多人发来消息询问curl存取cookie文件的问题,杜工并不觉得这是个难点,因为只看手册就可以很容易把握。下面给个例子,看完后就全都明了了:

<?php
$cookie_jar_index = 'cookie.txt';
 
$url = "http://www.71j.cn/perl/login.pl";
$params = "username=dudu&password=****";
$ch = curl_init();
curl_setopt($ch, CURLOPT_URL, $url);
curl_setopt($ch, CURLOPT_COOKIEJAR, $cookie_jar_index);
//curl_setopt($ch, CURLOPT_COOKIE, "fruit=apple; colour=red");
//上面代码是直接传递cookie信息,而非文件
curl_setopt($ch, CURLOPT_POST, 1);
curl_setopt($ch, CURLOPT_POSTFIELDS, $params); 
//curl_setopt($ch, CURLOPT_NOBODY, 1);//这个不能打开,否则无法生成cookie文件
ob_start();
curl_exec($ch);
curl_close($ch);
ob_clean();
 
$url = "http://www.71j.cn/perl/myfavorites.pl";
$ch2 = curl_init();
curl_setopt($ch2, CURLOPT_URL, $url);
curl_setopt($ch2, CURLOPT_COOKIEFILE, $cookie_jar_index);
ob_start();
curl_exec($ch2);
curl_close($ch2);
$rs = ob_get_contents(); //$rs就是返回的内容
ob_clean();
 
print_r($rs);
 
?>
Categories: 分享 Tags: , ,

discuz 7.2 嵌套外站用户通行证详解0+

54,717 views / 2009.11.22 / 1:01 上午

当我们使用discuz架设论坛的时候,往往需要把全站用户打通,即让其它产品线的用户与论坛无缝衔接起来。下面我来介绍下具体实现步骤。

Step 1
修改/register.php,在最开始加入:

require_once './include/common.inc.php';
 header("location:http://passport.通行证注册url/register.php?forward=" . $boardurl);
 exit;
 .....

目的是屏蔽discuz的注册入口,让用户调转到统一的通行证注册页面去。

同时不要忘记修改/include/js/common.js中的函数showWindow:

function showWindow(k, url, mode, cache) {
 if(k == 'register'){
 location.href='/register.php';
 return false;
 }
 ....

这样注册入口就全都跳转到通行证的注册页面了。

Step 2
在include/common.inc.php最后添加上判断代码,假定统一通行证的用户cookie为$_COOKIE[“UserInfo”]:

if(!$discuz_uid){
if($_COOKIE["UserInfo"]){
parse_str($_COOKIE["UserInfo"],$cookie_info);  //解析出用户信息,让dologin.php的处理
header("location:http://".$_SERVER["HTTP_HOST"]."/ dologin.php");
}
}

Step 3
下面是关键内容。在论坛根目录下创建dologin.php,内容及功能解释如下:

<?php
 
require_once './include/common.inc.php';
require_once DISCUZ_ROOT . './uc_client/client.php';
 
// COOKIE验证
if ($_COOKIE["UserInfo"]) {
//用户如果已经登录过,下面用统一通行证的cookie处理方法解析出用户信息
$username = .....;
$password = ......;
$email = ......;
$ResultCode = "0";
} else {
// 如果从论坛登录,则需要统一通行证验证
$username = $_POST["username"];
$password = $_POST["password"];
// 验证
$ResultCode = ....//如果验证成功返回0
$email = ....;//从通行证取到用户email
}
 
if ($ResultCode == "0") {
// 先看DZ用户表里是否有这条,如果有,且密码不一样,则更新密码(防止出现通行证用户修改密码后,DZ不能登陆);没有新插入一条
if ($loginfield == 'uid') {
$isuid = 1;
} elseif ($loginfield == 'email') {
$isuid = 2;
} else {
$isuid = 0;
}
 
$ucresult = uc_user_login($username, $password, $isuid, 1, $questionid, $answer);
list($tmp['uid'], $tmp['username'], $tmp['password'], $tmp['email'], $duplicate) = daddslashes($ucresult, 1);
$ucresult = $tmp;
 
if ($duplicate && $ucresult['uid'] > 0) {
if ($olduid = $db -> result_first("SELECT uid FROM {$tablepre}members WHERE username='" . addslashes($ucresult['username']) . "'")) {
require_once DISCUZ_ROOT . './include/membermerge.func.php';
membermerge($olduid, $ucresult['uid']);
uc_user_merge_remove($ucresult['username']);
} else {
return 0;
}
}
 
if ($ucresult['uid'] == -1) {
// 用户不存在,或者被删除
$uid = uc_user_register($username, $password, $email, $questionid, $answer, $onlineip);
if ($uid <= 0) {
fail();
}
 
$inviteconfig = array();
$query = $db -> query("SELECT * FROM {$tablepre}settings WHERE variable IN ('bbrules', 'bbrulestxt', 'welcomemsg', 'welcomemsgtitle', 'welcomemsgtxt', 'inviteconfig')");
while ($setting = $db -> fetch_array($query)) {
$$setting['variable'] = $setting['value'];
}
$invitecode = $regstatus > 1 && $invitecode ? dhtmlspecialchars($invitecode) : '';
if ($regstatus > 1) {
$inviterewardcredit = $inviteaddcredit = $invitedaddcredit = '';
@extract(unserialize($inviteconfig));
}
 
$groupinfo = $db -> fetch_first("SELECT groupid, allownickname, allowcstatus, allowcusbbcode, allowsigbbcode, allowsigimgcode, maxsigsize FROM {$tablepre}usergroups WHERE " . ($regverify ? "groupid='8'" : "creditshigher<=" . intval($initcredits) . " AND " . intval($initcredits) . "<creditslower LIMIT 1"));
 
$secques = $questionid > 0 ? random(8) : '';
$idstring = random(6);
$authstr = $regverify == 1 ? "$timestamp\t2\t$idstring" : '';
$password = md5(random(10));
$db -> query("INSERT INTO {$tablepre}members (uid, username, password, secques, adminid, groupid, regip, regdate, lastvisit, lastactivity, posts, credits, extcredits1, extcredits2, extcredits3, extcredits4, extcredits5, extcredits6, extcredits7, extcredits8, email, showemail, timeoffset, pmsound, invisible, newsletter)
VALUES ('$uid', '$username', '$password', '$secques', '0', '$groupinfo[groupid]', '$onlineip', '$timestamp', '$timestamp', '$timestamp', '0', $initcredits, '$email', '0', '9999', '1', '0', '1')");
 
$db -> query("REPLACE INTO {$tablepre}memberfields (uid, authstr $fieldadd1) VALUES ('$uid', '$authstr' $fieldadd2)");
} elseif ($ucresult['uid'] == -2) {
// 密码错
if (!uc_user_edit($username, '', $password, $email, 1)) {
fail();
}
list($uid, $username, $email) = uc_get_user($username);
} else {
$uid = $ucresult['uid'];
}
 
$member = $db -> fetch_first("SELECT m.uid AS discuz_uid, m.username AS discuz_user, m.password AS discuz_pw, m.secques AS discuz_secques,
m.email, m.adminid, m.groupid, m.styleid, m.lastvisit, m.lastpost, u.allowinvisible
FROM {$tablepre}members m LEFT JOIN {$tablepre}usergroups u USING (groupid)
WHERE m.uid='$ucresult[uid]'");
 
if (!$member) {
// 需要激活
fail();
}
 
$member['discuz_userss'] = $member['discuz_user'];
$member['discuz_user'] = addslashes($member['discuz_user']);
foreach($member as $var => $value) {
$GLOBALS[$var] = $value;
}
 
if (addslashes($member['email']) != $ucresult['email']) {
$db -> query("UPDATE {$tablepre}members SET email='$ucresult[email]' WHERE uid='$ucresult[uid]'");
}
 
if ($questionid > 0 && empty($member['discuz_secques'])) {
$GLOBALS['discuz_secques'] = random(8);
$db -> query("UPDATE {$tablepre}members SET secques='$GLOBALS[discuz_secques]' WHERE uid='$ucresult[uid]'");
}
 
$GLOBALS['styleid'] = $member['styleid'] ? $member['styleid'] : $_DCACHE['settings']['styleid'];
 
$cookietime = intval(isset($_POST['cookietime']) ? $_POST['cookietime'] : 0);
 
dsetcookie('cookietime', $cookietime, 31536000);
dsetcookie('auth', authcode("$member[discuz_pw]\t$member[discuz_secques]\t$member[discuz_uid]", 'ENCODE'), $cookietime, 1, true);
dsetcookie('loginuser');
dsetcookie('activationauth');
dsetcookie('pmnum');
 
$GLOBALS['sessionexists'] = 0;
 
if ($_DCACHE['settings']['frameon'] && $_DCOOKIE['frameon'] == 'yes') {
$GLOBALS['extrahead'] .= '<script>if(top != self) {parent.leftmenu.location.reload();}</script>';
}
 
$ucsynlogin = $allowsynlogin ? uc_user_synlogin($discuz_uid) : '';
if (!empty($inajax)) {
$msgforward = unserialize($msgforward);
$mrefreshtime = intval($msgforward['refreshtime']) * 1000;
include_once DISCUZ_ROOT . './forumdata/cache/cache_usergroups.php';
$usergroups = $_DCACHE['usergroups'][$groupid]['grouptitle'];
$message = 1;
include template('login');
} else {
if ($groupid == 8) {
showmessage('login_succeed_inactive_member', 'memcp.php');
} else {
showmessage('login_succeed', dreferer());
}
}
} else {
fail();
}
 
function fail() {
showmessage('undefined_action', null, 'HALTED');
}
 
?>

Step 4

在用户登录时,要清掉通行证的cookie。需要修改logging.php

if($action == 'logout' && !empty($formhash)) {
 
if($_DCACHE['settings']['frameon'] && $_DCOOKIE['frameon'] == 'yes') {
 
$extrahead .= '<script>if(top != self) {parent.leftmenu.location.reload();}</script>';
 
}
 
if($formhash != FORMHASH) {
 
showmessage('logout_succeed', dreferer());
 
}
 
$ucsynlogout = $allowsynlogin ? uc_user_synlogout() : '';
 
clearcookies();
 
setcookie("UserInfo", "", time() - 3600, "/", ".xxx.com", 1); //删除通行证那边的cookie

上面四步完成后,清掉discuz的数据和模板缓存就大功告成了。

Categories: 感悟 Tags: , ,