据UCenter安装说明: "UCenter是一个能沟通多个应用的桥梁,使各应用共享一个用户数据库,实现统一登录,注册,用户管理"。这是个很好的想法。首先众多网站虽然功能各异,但都有注册和管理用户的需要。与其各自开发用户管理功能,不如借用一个第三方开发的公认的用户管理系统,从而把精力集中用在开发网站要提供的核心生意有关的功能上。UCenter就是这样一个系统。其次UCenter还有个特点就是它支持使用它的网站之间能自由的连接起来。在一个网站登陆后,就能跨越到其它网站而不用重新登陆,所以对用户来讲,这些网站无异于一个网站一般。
Discuz就是这样一个用UCenter来管理用户的应用。怎样才能在自己的应用里使用UCenter作为用户管理呢? 最近版的UCenter 1.6 下载文件(
链接 )里除了UCenter外,还带着一个这样的应用的范例。
下载文件解压后的文件夹upload里包含了可安装的UCenter服务端软件,安装后就成为一个UCenter服务端
下载文件解压后的文件夹uc_client里包含了的UCenter应用端软件,文件夹example里包含了应用范例
将文件夹uc_client拷贝到文件夹example里成为它的子文件夹,再在一个UCenter服务端的应用列表里加入这个应用,并修改它的config.ini.php文件里的内容后,example就成为了一个整合了UCenter的应用
每一个整合UCenter的应用都需要包含UCenter应用端软件uc_client作为它的一个子目录,其中含有一个重要的文件client.php。它还要有一个名叫api 的子目录,包含了一个名叫uc.php的文件,虽然文件名可通过设置来改变,但惯例是叫uc.php。 uc_client提供了两种使用UCenter服务端的办法。一种是如果可能的话直接和UCenter服务端的数据库链接,这样一来UCenter服务端做的很多事,uc_client也能同样处理,这是uc_client里含有很多与UCenter服务端同样的代码的原因。另一种办法是向UCenter服务端发post请求,这种办法应用面更广,应用和UCenter可以在不同的服务器上。client.php可以看成是UCenter服务端驻扎在应用里的代理(proxy), 它隐藏了应用端和服务端通讯的细节,它免除了应用里的代码直接和UCenter服务端联系的需要,应用里的代码只要调用它的函数就能使用UCenter服务端提供的功能。而uc.php则提供给UCenter服务端和应用联系的渠道。一般来讲uc_client包括client.php具有普适性,不同的应用都可以直接用uc_client。而uc.php则对不同的应用需要有所改动。这是因为当UCenter服务端通知它某个事件发生时,uc.php往往要有数据需要保存到应用所用的数据库里,按那里数表的不同,uc.php要做的事也不同。每个这样的应用都要在UCenter服务端里注册,
关键的一点是两者要用一个相同的通讯密钥。当一方发起和对方的联系时,它要将通话内容用这个密钥加密,对方接到加密的内容后再用该密钥解密。这样做的好处是每方都能确认和它通讯的的确是对方。但是回复里的内容一般没有加密,即便其中包含着隐秘信息,如UCenter对应用发送的用户登录请求的回复里包含着用户密码。为何不将回复也用密钥加密呢?所以要确保两者之间联系的安全性,应该给UCenter安装SSL。
下面我们以范例里的用户登陆过程为例来看一下应用是如何和UCenter联系的。
这个流程的关键点有:
1)当用户提交登录资料后,应用是请UCenter来判别该登录资料是否正确。用户向UCenter发送的信息是用只有这个应用和这个UCenter知道的通讯密钥加密的
2)应用接到UCenter对登录资料的确认后,将用户的身份(用户ID和用户名)记录到了cookie里保存在用户的浏览器里。这样以后每次用户通过浏览器和应用联系时,应用都能从它发来的cookie得知用户的身份。注意这个cookie也是用那个通讯密钥加密的,所以别人没法在浏览器里按某用户的ID和用户名来人为添加这样的cookie
3)当应用接到UCenter的确认后,它还启动了同步登录(synlogin) 即让该用户能在同一个机器的同样的浏览器里自动登录到其它用这个UCenter的应用。做法是向UCenter发一个synlogin请求,UCenter回复里包含向其它应用发请求的代码,这样这个浏览器就自动向其它那些应用发了个请求,结果是这些应用也都设置了记载用户身份的cookie。注意这些cookie不是那些应用回复用户直接请求的结果,而是由第三方应用(即这个流程里的第一个应用)提供的网页激发的,所以这是一个跨域设置,一般浏览器是不支持的,除非那些应用的回复的头部加了个p3p设置。
其中一些关键步骤的代码如下:
1. example (a.k.a. my application) 包含了用户登陆界面: examples/code/login_db.php
echo ' ';
和对用户提交登陆资料的处理: examples/code/login_db.php
//通过接口判断登录帐号的正确性,返回值为数组
list($uid, $username, $password, $email) = uc_user_login($_POST['username'], $_POST['password']);
setcookie('Example_auth', '', -86400);
if($uid > 0) {
if(!$db->result_first("SELECT count(*) FROM {$tablepre}members WHERE uid='$uid'")) {
//判断用户是否存在于用户表,不存在则跳转到激活页面
$auth = rawurlencode(uc_authcode("$username\t".time(), 'ENCODE'));
echo '您需要需要激活该帐号,才能进入本应用程序 < a href="'.$_SERVER['PHP_SELF'].'?example=register&action=activation&auth='.$auth.'">继续';
exit;
}
//用户登陆成功,设置 Cookie,加密直接用 uc_authcode 函数,用户使用自己的函数
setcookie('Example_auth', uc_authcode($uid."\t".$username, 'ENCODE'));
//生成同步登录的代码
$ucsynlogin = uc_user_synlogin($uid);
echo '登录成功'.$ucsynlogin.' < a href="'.$_SERVER['PHP_SELF'].'" >继续';
exit;
}
2. 上面调用的uc_user_login和uc_user_synlogin都是uc_client提供的函数: client.php
function uc_user_login($username, $password, $isuid = 0, $checkques = 0, $questionid = '', $answer = '') {
$isuid = intval($isuid);
$return = call_user_func(UC_API_FUNC, 'user', 'login', array('username'=>$username, 'password'=>$password, 'isuid'=>$isuid, 'checkques'=>$checkques, 'questionid'=>$questionid, 'answer'=>$answer));
return UC_CONNECT == 'mysql' ? $return : uc_unserialize($return);
}
当我们设置 uc_client成和uc_server通过post联系时,UC_API_FUNC的值是uc_api_post。这个函数调用了uc_fopen,在其中用PHP提供的fsockopen函数向发送了post请求:
function uc_api_post($module, $action, $arg = array()) {
....
$postdata = uc_api_requestdata($module, $action, $s);
return uc_fopen2(UC_API.'/index.php', 500000, $postdata, '', TRUE, UC_IP, 20);
}
function uc_api_requestdata($module, $action, $arg='', $extra='') {
$input = uc_api_input($arg);
$post = "m=$module&a=$action&inajax=2&release=".UC_CLIENT_RELEASE."&input=$input&appid=".UC_APPID.$extra;
return $post;
}
function uc_api_input($data) {
$s = urlencode(uc_authcode($data.'&agent='.md5($_SERVER['HTTP_USER_AGENT'])."&time=".time(), 'ENCODE', UC_KEY));
return $s;
}
function uc_fopen($url, $limit = 0, $post = '', $cookie = '', $bysocket = FALSE, $ip = '', $timeout = 15, $block = TRUE) {
...
$out = "POST $path HTTP/1.0\r\n";
$out .= "Accept: */*\r\n";
//$out .= "Referer: $boardurl\r\n";
$out .= "Accept-Language: zh-cn\r\n";
$out .= "Content-Type: application/x-www-form-urlencoded\r\n";
$out .= "User-Agent: $_SERVER[HTTP_USER_AGENT]\r\n";
$out .= "Host: $host\r\n";
$out .= 'Content-Length: '.strlen($post)."\r\n";
$out .= "Connection: Close\r\n";
$out .= "Cache-Control: no-cache\r\n";
$out .= "Cookie: $cookie\r\n\r\n";
$out .= $post;
...
if(function_exists('fsockopen')) {
$fp = @fsockopen(($ip ? $ip : $host), $port, $errno, $errstr, $timeout);
} elseif (function_exists('pfsockopen')) {
$fp = @pfsockopen(($ip ? $ip : $host), $port, $errno, $errstr, $timeout);
} else {
$fp = false;
}
举例而言,当用户填写了用户名为test1和密码为123451后,应用要将下面的数据提交给UC Server验证,
username=test1&password=123451&isuid=0&checkques=0&questionid=&answer=
但其实发送过去的可能是下面的数据,原来的数据以加密的形式放在了input内:
m=user&a=login&inajax=2&release=20110501&input=15c4UYDcKE%2B4yS8EWSDEunpH%2FkFphKAjhIjjw2MzOJ7jqQ1mmE1Ju7yqqxqtlrAUG6D1%2FDDF7v6vjMo6zHxvhWUZ6nI09dSqfrkm0UqZiXpkvFOL4HUHoFm0XtuMnHI6%2FlvN4NN8d0DxNaYyGCCfPZF9HRFADqXCrskDRhoy5RHmFIhPFZCRuOllVLtJGfslNztQ4cWRFNh%2B9w&appid=3
3. UC Server接到post请求后的处理: 文件control/user.php里的函数onlogin:
if($isuid == 1) {
$user = $_ENV['user']->get_user_by_uid($username);
} elseif($isuid == 2) {
$user = $_ENV['user']->get_user_by_email($username);
} else {
$user = $_ENV['user']->get_user_by_username($username);
}
...
$status = $user['uid'];
...
return array($status, $user['username'], $password, $user['email'], $merge);
文件control/user.php里的函数onsynlogin:
foreach($this->cache['apps'] as $appid => $app) {
if($app['synlogin']) {
$synstr .= '';
if(is_array($app['extra']['extraurl'])) foreach($app['extra']['extraurl'] as $extraurl) {
$synstr .= '';
...
return $synstr;
继续上面的例子,UC Server发过去的数据也是加过密的(在code中):
4. 应用对事件的处理:api/uc.php:
$code = @$_GET['code'];
parse_str(_authcode($code, 'DECODE', UC_KEY), $get);
...
$uc_note = new uc_note();
exit($uc_note->$get['action']($get, $post));
function synlogin($get, $post) {
$uid = $get['uid'];
$username = $get['username'];
if(!API_SYNLOGIN) return API_RETURN_FORBIDDEN;
header('P3P: CP="CURa ADMa DEVa PSAo PSDo OUR BUS UNI PUR INT DEM STA PRE COM NAV OTC NOI DSP COR"');
_setcookie('Example_auth', _authcode($uid."\t".$username, 'ENCODE'));
}
注:本文中的代码里的<符号如果后面的字符是a的话,在它们中间加了一个不应该有的空格,以避免Discuz在保存日志时自动改变日志内容。