PHP版单点登陆实现方案的实例


Posted in PHP onNovember 17, 2016

摘要:

本文主要介绍了利用webservice,session,cookie技术,来进行通用的单点登录系统的分析与设计。具体实现语言为PHP。单点 登录,英文名为Single Sign On,简称为 SSO,是目前企业,网络业务的用户综合处理的重要组成部分。而SSO的定义,是在多个应用系统中,用户只需要登陆一次就可以访问所有相互信任的应用系 统。

动机:

用过ucenter的全站登录方式的朋友,应该都知道这是典型的观察者模式的解决方案。用户中心作为subject, 其所属observer的注册和删除统一在ucenter的后台进行。而各个子应用站点都对应一个observer。每次用户中心的登录动作,都会触发 js脚本回调w3c标准的子站登录接口(api/uc.php)。

这种方式的缺点,本人认为主要是两点:1. 子站点过多时,回调接口相应增多,这个在分布子站的量的限制上,如何控制来使登录效率不会太低,不好把握; 2. 当某个子站回调接口出现问题时,默认的登录过程会卡住(可以限制登录程序的执行时间,但相应出现问题子站后面的子站的回调接口就调不到了。

基于以上问题,在实际开发过程中,本人设计了另一套单点登录系统。

一. 登陆原理说明

单点登录的技术实现机制:当用户第一次访问应用系统1的时候,因为还没有登录,会被引导到认证系统中进行登录;根据用户提供的登录信息,认证系统进行身份效验,如果通过效验,应该返回给用户一个认证的凭据--ticket;用户再访问别的应用的时候,就会将这个ticket带上,作为自己认证的凭据,应用系统接受到请求之后会把ticket送到认证系统进行效验,检查ticket的合法性。如果通过效验,用户就可以在不用再次登录的情况下访问应用系统2和应用系统3了。

可以看出,要实现SSO,需要以下主要的功能:

a) 所有应用系统共享一个身份认证系统;

b) 所有应用系统能够识别和提取ticket信息;

c) 应用系统能够识别已经登录过的用户,能自动判断当前用户是否登录过,从而完成单点登录的功能

基于以上基本原则,本人用php语言设计了一套单点登录系统的程序,目前已投入正式生成服务器运行。本系统程序,将ticket信息以全系统唯一的 session id作为媒介,从而获取当前在线用户的全站信息(登陆状态信息及其他需要处理的用户全站信息)。

二. 过程说明:

登陆流程:

1. 第一次登陆某个站:

a) 用户输入用户名+密码,向用户验证中心发送登录请求

b) 当前登录站点,通过webservice请求,用户验证中心验证用户名,密码的合法性。如果验证通过,则生成ticket,用于标识当前会话的用户,并将当前登陆子站的站点标识符记录到用户中心,最后

c) 将获取的用户数据和ticket返回给子站。如果验证不通过,则返回相应的错误状态码。

d) 根据上一步的webservice请求返回的结果,当前子站对用户进行登陆处理:如状态码表示成功的话,则当前站点通过本站cookie保存ticket,并本站记录用户的登录状态。状态码表示失败的话,则给用户相应的登录失败提示。

2. 登陆状态下,用户转到另一子:

a) 通过本站cookie或session验证用户的登录状态:如验证通过,进入正常本站处理程序;否则户中心验证用户的登录状态(发送ticket到用户验证中心),如验证通过,则对返回的用户信息进行本地的登录处理,否则表明用户未登录。

登出流程

a) 当前登出站清除用户本站的登录状态 和 本地保存的用户全站唯一的随机id

b) 通过webservice接口,清除全站记录的全站唯一的随机id。webservice接口会返回,登出其他已登录子站的javascript代码,本站输出此代码。

c) js代码访问相应站W3C标准的登出脚本

三. 代码说明:

本文所涉及到相关代码,已打包上传,如有兴趣,可在本文最后下载链接处点击下载。

1. 登陆流程:

用户从打开浏览器开始,第一个登陆的子站点,必须调用UClientSSO::loginSSO()方法。该方法返回全站唯一的随机id用于标识该用户。该随机id在UClientSSO::loginSSO()中已通过本站cookie保存,即该子站点保留了用户已登陆标识的存根于本站。

a) UClientSSO::loginSSO()方法如下:

<?php
/**
 * 用户验证中心 登陆用户处理
 *
 * @param string $username   - 用户名
 * @param string $password   - 用户原始密码
 * @param boolean $remember   - 是否永久记住登陆账号
 * @param boolean $alreadyEnc  - 传入的密码是否已经经过simpleEncPass加密过
 *
 * @return array  - integer $return['status'] 大于 0:返回用户 ID,表示用户登录成功
 *                        -1:用户不存在,或者被删除
 *                        -2:密码错
 *                                                 -11:验证码错误
 *                     string $return['username']   : 用户名
 *                     string $return['password']   : 密码
 *                     string $return['email']    : Email
 */

static public function loginSSO($username, $password, $remember=false, $alreadyEnc=false) {
self::_init();
self::_removeLocalSid();
$ret = array();

//
//1. 处理传入webservice接口的参数
//
$_params = array(
        'username' => $username,
        'password' => $alreadyEnc ? trim($password) : self::simpleEncPass(trim($password)),
        'ip'    => self::onlineip(),
        'siteFlag' => self::$site,
        'remember' => $remember
);
$_params['checksum'] = self::_getCheckSum($_params['username'] . $_params['password'] .
$_params['ip'] . $_params['siteFlag'] . $_params['remember']);

//
// 2.调用webservice接口,进行登陆处理
//
$aRet = self::_callSoap('loginUCenter', $_params);

if (intval($aRet['resultFlag']) > 0 && $aRet['sessID']) {
//成功登陆
//设置本地session id
self::_setLocalSid($aRet['sessID']);

//设置用户中心的统一session id脚本路径
self::$_synloginScript = urldecode($aRet['script']);

$ret = $aRet['userinfo'];
} else {

$ret['status'] = $aRet['resultFlag'];
}

return $ret;
}//end of function         

//b) 用户验证中心的webservice服务程序,接收到登陆验证请求后,调用UCenter::loginUCenter()方法来处理登陆请求。
/**
* 用户验证中心 登陆用户处理
*
* @param string $username
* @param string $password
* @param string $ip
* @param string $checksum
* @return array
*/
static public function loginUCenter($username, $password, $ip, $siteFlag, $remember=false) {
self::_init();
session_start();
$ret = array();
$arr_login_res   = login_user($username, $password, $ip);
$res_login     = $arr_login_res['status'];        //
$ret['resultFlag'] = $res_login;

if ($res_login < 1) {
//登陆失败
} else {

//登陆成功
$_SESSION[self::$_ucSessKey] = $arr_login_res;

$_SESSION[self::$_ucSessKey]['salt'] =
self::_getUserPassSalt($_SESSION[self::$_ucSessKey]['username'], $_SESSION[self::$_ucSessKey]['password']);

$ret['userinfo'] = $_SESSION[self::$_ucSessKey];
$ret['sessID']  = session_id();    //生成全站的唯一session id,作为ticket全站通行

//
//合作中心站回调登陆接口(设置用户中心的统一session id)
//
self::_createCoSitesInfo();
$uinfo = array();
$_timestamp = time();
$_rawCode = array(
            'action' => 'setSid',
            'sid'  => $ret['sessID'],
            'time'  => $_timestamp,
);
if ($remember) {
$uinfo = array(
                'remember' => 1,
                'username' => $username,
                'password' => $password
);
}

$ret['script'] = '';
$_rawStr = http_build_query(array_merge($_rawCode, $uinfo));

//
// 合作站点的全域cookie设置脚本地址
//
foreach ((array)self::$_coSitesInfo as $_siteInfo) {
$_code = self::authcode($_rawStr, 'ENCODE', $_siteInfo['key']);
$_src = $_siteInfo['url'] . '?code=' . $_code . '&time=' . $_timestamp;
$ret['script'] .= urlencode('');
}

//
// 记住已登陆战
//
self::registerLoggedSite($siteFlag, $ret['sessID']);

unset($ret['userinfo']['salt']);
}

return $ret;
}

?>

2. 本站登陆成功后,进行本地化的用户登陆处理,其后验证用户是否登陆只在本地验证。(本地存取登陆用户状态的信息,请设置为关闭浏览器就退出)

 3. 当检测用户登陆状态时,请先调用本地的验证处理,若本地验证不通过,再调用UClientSSO::checkUserLogin()方法到用户中心检测用户的登陆状态。

a) UClientSSO::checkUserLogin()方法如下:

<?php
 /**
 * 用户单点登陆验证函数
 *
 * @return array  - integer $return['status'] 大于 0:返回用户 ID,表示用户登录成功
 *                                                  0:用户没有在全站登陆
 *                        -1:用户不存在,或者被删除
 *                        -2:密码错
 *                        -3:未进行过单点登陆处理
 *                                                 -11:验证码错误
 *                     string $return['username']   : 用户名
 *                     string $return['password']   : 密码
 *                     string $return['email']    : Email
 */
 public static function checkUserLogin(){
 self::_init();
 $ret = array();
 $_sessId = self::_getLocalSid();
 if (empty($_sessId)) {
 //永久记住账号处理
 if(isset($_COOKIE[_UC_USER_COOKIE_NAME]) && !empty($_COOKIE[_UC_USER_COOKIE_NAME])) {
 
 //
 // 根据cookie里的用户名和密码判断用户是否已经登陆。
 //
 $_userinfo = explode('|g|', self::authcode($_COOKIE[_UC_USER_COOKIE_NAME], 'DECODE', self::$_authcodeKey));
 
 $username = $_userinfo[0];
 $password = isset($_userinfo[1]) ? $_userinfo[1] : '';
 if (empty($password)) {
 $ret['status'] = -3;
 } else {
 return self::loginSSO($username, $password, true, true);
 }
 
 } else {
 $ret['status'] = -3;
 }
 
 } else {
 //
 //本站原先已经登陆过,通过保留的sesson id存根去用户中心验证
 //
 $_params = array(
             'sessId'  => $_sessId,
             'siteFlag' => self::$site,
             'checksum' => md5($_sessId . self::$site . self::$_mcComunicationKey)
 );
 $aRet = self::_callSoap('getOnlineUser', $_params);
 if (intval($aRet['resultFlag']) > 0) {
 //成功登陆
 $ret = $aRet['userinfo'];
 } else {
 $ret['status'] = $aRet['resultFlag'];
 }
 }
 
 return $ret;
 }       
 
 b) 用户验证中心的webservice服务程序,接收到检验登陆的请求后,调用UCenter::getOnlineUser()方法来处理登陆请求:
 [php]/**
 * 根据sid,获取当前登陆的用户信息
 *
 * @param string $sessId    - 全站唯一session id,用做ticket
 * @return array
 */
 /**
 * 根据sid,获取当前登陆的用户信息
 *
 * @param string $sessId    - 全站唯一session id,用做ticket
 * @return array
 */
 static public function getOnlineUser($sessId, $siteFlag) {
 self::_init();
 session_id(trim($sessId));
 session_start();
 
 $ret = array();
 $_userinfo = $_SESSION[self::$_ucSessKey];
 
 if (isset($_userinfo['username']) && isset($_userinfo['password']) &&
 self::_getUserPassSalt($_userinfo['username'], $_userinfo['password'])) {
 $ret['resultFlag'] = "1";
 $ret['userinfo'] = $_userinfo;
 
 self::registerLoggedSite($siteFlag, $sessId);        //记住已登陆战
 unset($ret['userinfo']['salt']);
 } else {
 $ret['resultFlag'] = "0";
 }
 
 return ($ret);
 }
 ?>

 4. 单点登出时,调用UClientSSO::logoutSSO()方法。调用成功后,如需其他已登陆站立即登出,请调用 UClientSSO::getSynloginScript()方法获取W3C标准的script,在页面输出。

a) UClientSSO::logoutSSO()方法如下:        

<?php
/**
* 全站单点登出
* - 通过webservice请求注销掉用户的全站唯一标识
*
* @return integer  1: 成功
*                   -11:验证码错误
*/
public static function logoutSSO(){
    self::_init();
    $_sessId = self::_getLocalSid();
    //
    //本站没有登陆的话,不让同步登出其他站
    //
    if (empty($_sessId)) {
        self::_initSess(true);
        return false;
    }
    $_params = array(
        'sessId'  => $_sessId,
        'siteFlag' => self::$site,
        'checksum' => md5($_sessId . self::$site . self::$_mcComunicationKey)
    );
    $aRet = self::_callSoap('logoutUCenter', $_params);
    if (intval($aRet['resultFlag']) > 0) {
        //成功登出
        self::_removeLocalSid();        //移除本站记录的sid存根
        self::$_synlogoutScript = urldecode($aRet['script']);
        $ret = 1;
    } else {
        $ret = $aRet['resultFlag'];
    }
    return intval($ret);
}          [/php]
    b) 用户验证中心的webservice服务程序,接收到全站登出请求后,调用UCenter::loginUCenter()方法来处理登陆请求:
/**
* 登出全站处理
*
* @param string - 全站唯一session id,用做ticket
* @return boolean
*/
static public function logoutUCenter($sessId) {
    self::_init();
    session_id(trim($sessId));
    session_start();
    $_SESSION = array();
    return empty($_SESSION) ? true : false;
}
?>

四. 代码部署:         

1. 用户验证中心设置                 

a) 用户验证中心向分站提供的webservice服务接口文件,即UserSvc.php部署在hostname/webapps/port/ UserSvc.php中。查看wsdl内容,请访问https://hostname/port/ UserSvc.php?wsdl

 b) 用户中心用户单点服务类文件为UCenterSSO.class.php,文件路径为在hostname/webapps/include /UCenterSSO.class.php。该文件为用户单点登陆处理 的服务端类,被hostname/webapps/port/ UserSvc.php调用。用于获取用户的登陆信息,是否单点登陆的状态信息,单点登出处理等。

 c) 用户验证中心通过W3C标准,利用cookie方式记录,删除全站统一的用户唯一随机id 的脚本文件为hostname/webapps/port/cookie_mgr.php.

2. 子站点设置                 

a) 各子站点请将,UClientSSO.class.php部署在用户中心服务客户端目录下。部署好后,请修改最后一行的UClientSSO::setSite('1'); 参数值为用户验证中心统一分配给各站的标识id.

b) 在部署的用户中心服务客户端包下的api目录下下,请将logout_sso.php脚本转移到此处,并编写进行本站登出的处理脚本。

c) 在子站点验证用户登陆状态的代码部分,额外增加到用户中心的单点登陆验证的处理。

即在首先通过本站验证用户的登陆状态,如果未通过验证,则去用户中心验证。验证操作要调用UClientSSO::checkUserLogin();接口,接口含义请查看代码注释。

d) 在分站的登出处理脚本中,通过UClientSSO::getSynlogoutScript();获取script串输出即可。

五. 扩展功能:       

1. 记录跟踪所有在线用户

因为所有用户的登录都要经过用户验证中心,所有用户的ticket都在验证中心生成,可以将用户和该ticket(session id)在内存表中建立一个映射表。得到所有在线用户的记录表。

后期如有必要跟踪用户状态来实现其他功能,只要跟踪这个映射表就可以了。其他功能可以为: 获取在线用户列表,判断用户在线状态,获取在线用户人数等。

 2. 特殊统计处理

因为整个系统登录登出要经过用户验证中心,所以可以针对用户的特殊统计进行处理。如用户每天的登录次数,登陆时间,登陆状态失效时间,各时段的在线用户人数走势等。

六. 其他事项:       

1. 本站登陆状态有效时间问题:                  

全站要求用户登陆状态在关闭浏览器时就失效。要求各分站对session或cookie的处理方式按照如下进行:

a) Session方式记录用户登陆状态的站点

请在站点公用脚本开始处,添加一下代码

<?php
 session_write_close();
 ini_set('session.auto_start', 0);          //关闭session自动启动
 ini_set('session.cookie_lifetime', 0);      //设置session在浏览器关闭时失效
 ini_set('session.gc_maxlifetime', 3600); //session在浏览器未关闭时的持续存活时间   
 ?>

b) cookie方式记录用户登陆状态的站点

请在设置用户登陆状态的cookie时,设置cookie有效时间为null.

原文链接:http://www.cnblogs.com/linzhenjie/archive/2012/08/24/2653585.html

以上就是本文的全部内容,希望对大家的学习有所帮助,也希望大家多多支持三水点靠木。

PHP 相关文章推荐
Discuz 5.0 中读取纯真IP数据库函数分析
Mar 16 PHP
如何在PHP程序中防止盗链
Apr 09 PHP
将文件夹压缩成zip文件的php代码
Dec 14 PHP
使用PHP编写的SVN类
Jul 18 PHP
PHP循环输出指定目录下的所有文件和文件夹路径例子(简单实用)
May 10 PHP
php上传文件并显示上传进度的方法
Mar 24 PHP
利用PHP脚本在Linux下用md5函数加密字符串的方法
Jun 29 PHP
PHP实现GIF图片验证码
Nov 04 PHP
Yii CGridView用法实例详解
Jul 12 PHP
jquery+thinkphp实现跨域抓取数据的方法
Oct 15 PHP
PHP等比例压缩图片的实例代码
Jul 26 PHP
PHP实现两种排课方式
Jun 26 PHP
php实现URL加密解密的方法
Nov 17 #PHP
PHP读取zip文件的方法示例
Nov 17 #PHP
php安装php_rar扩展实现rar文件读取和解压的方法
Nov 17 #PHP
php arsort 数组降序排序详细介绍
Nov 17 #PHP
php array_values 返回数组的值实例详解
Nov 17 #PHP
PHP获取input输入框中的值去数据库比较显示出来
Nov 16 #PHP
PHP foreach遍历多维数组实现方式
Nov 16 #PHP
You might like
PHP+SQL 注入攻击的技术实现以及预防办法
2010/12/29 PHP
PHP常用数组函数介绍
2014/07/28 PHP
php利用gd库为图片添加水印
2016/11/09 PHP
使用PHP+Redis实现延迟任务,实现自动取消订单功能
2019/11/21 PHP
用javascript动态调整iframe高度的代码
2007/04/10 Javascript
Jquery+WebService 校验账号是否已被注册的代码
2010/07/12 Javascript
JQuery的ready函数与JS的onload的区别详解
2013/11/21 Javascript
用js代码改变单选框选中状态的简单实例
2013/12/18 Javascript
JavaScript原型链示例分享
2014/01/26 Javascript
JavaScript编写连连看小游戏
2015/07/07 Javascript
如何防止JavaScript自动插入分号
2015/11/05 Javascript
解析javascript瀑布流原理实现图片滚动加载
2016/03/10 Javascript
JavaScript页面实时显示当前时间实例代码
2016/10/23 Javascript
详解用原生JavaScript实现jQuery的某些简单功能
2016/12/19 Javascript
JS ES6中setTimeout函数的执行上下文示例
2017/04/27 Javascript
浅谈vue2 单页面如何设置网页title
2017/11/08 Javascript
基于Bootstrap表单验证功能
2017/11/17 Javascript
JavaScript折半查找(二分查找)算法原理与实现方法示例
2018/08/06 Javascript
详解如何解决vue开发请求数据跨域的问题(基于浏览器的配置解决)
2018/11/12 Javascript
angularjs模态框的使用代码实例
2019/12/20 Javascript
javascript 数组精简技巧小结
2020/02/26 Javascript
[43:58]DOTA2-DPC中国联赛定级赛 LBZS vs SAG BO3第一场 1月8日
2021/03/11 DOTA
Python基于matplotlib绘制栈式直方图的方法示例
2017/08/09 Python
django2 快速安装指南分享
2018/01/05 Python
python集合比较(交集,并集,差集)方法详解
2018/09/13 Python
pygame实现成语填空游戏
2019/10/29 Python
意大利香水和彩妆护肤品购物网站:Ditano
2017/08/13 全球购物
网上常见的一份Linux面试题(多项选择部分)
2015/02/07 面试题
绿化工程实施方案
2014/03/17 职场文书
工资收入证明样本(5篇)
2014/09/16 职场文书
简易版租房协议书范本
2014/10/13 职场文书
2014年图书室工作总结
2014/12/09 职场文书
办公室规章制度范本
2015/08/04 职场文书
家庭教育教师培训学习体会
2016/01/14 职场文书
2016大学生国家助学贷款承诺书
2016/03/25 职场文书
JS前端使用canvas实现扩展物体类和事件派发
2022/08/05 Javascript