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 相关文章推荐
杏林同学录(六)
Oct 09 PHP
php 上一篇,下一篇文章实现代码与原理说明
May 09 PHP
PHP遍历某个目录下的所有文件和子文件夹的实现代码
Jun 28 PHP
php防止网站被刷新的方法汇总
Dec 01 PHP
PHP面向对象之后期静态绑定功能介绍
May 18 PHP
浅析ThinkPHP缓存之快速缓存(F方法)和动态缓存(S方法)(日常整理)
Oct 26 PHP
php分页原理 分页代码 分页类制作教程
Sep 23 PHP
php str_getcsv把字符串解析为数组的实现方法
Apr 05 PHP
php基于自定义函数记录log日志方法
Jul 21 PHP
PHP实现mysqli批量执行多条语句的方法示例
Jul 22 PHP
PHP7中I/O模型内核剖析详解
Apr 14 PHP
laravel 解决ajax异步提交数据,并还回填充表格的问题
Oct 15 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
安健A254立体声随身听的分析与打磨
2021/03/02 无线电
PHP开发过程中常用函数收藏
2009/12/14 PHP
PHP管理依赖(dependency)关系工具 Composer的自动加载(autoload)
2014/08/18 PHP
使用PHP处理数据库数据如何将数据返回客户端并显示当前状态
2016/02/16 PHP
php基于curl主动推送最新内容给百度收录的方法
2016/10/14 PHP
PDO::getAvailableDrivers讲解
2019/01/28 PHP
Yii框架 session 数据库存储操作方法示例
2019/11/18 PHP
网络图片延迟加载实现代码 超越jquery控件
2010/03/27 Javascript
Asp.net下使用Jquery Ajax传送和接收DataTable的代码
2010/09/12 Javascript
javascript Window及document对象详细整理
2011/01/12 Javascript
javascript操作table(insertRow,deleteRow,insertCell,deleteCell方法详解)
2013/12/16 Javascript
jQuery 获取兄弟元素的几种不错方法
2014/05/23 Javascript
JS实现的不规则TAB选项卡效果代码
2015/09/18 Javascript
关于获取DIV内部内容报错的原因分析及解决办法
2016/01/29 Javascript
灵活使用数组制作图片切换js实现
2016/07/28 Javascript
基于jQuery实现的幻灯图片切换
2016/12/02 Javascript
使用get方式提交表单在地址栏里面不显示提交信息
2017/02/21 Javascript
Bootstrap实现各种进度条样式详解
2017/04/13 Javascript
vue多页面开发和打包正确处理方法
2018/04/20 Javascript
JS手写一个自定义Promise操作示例
2020/03/16 Javascript
python常见排序算法基础教程
2017/04/13 Python
Pycharm学习教程(3) 代码运行调试
2017/05/03 Python
详解tensorflow训练自己的数据集实现CNN图像分类
2018/02/07 Python
Python并发之多进程的方法实例代码
2018/08/15 Python
在python中pandas的series合并方法
2018/11/12 Python
Python如何计算语句执行时间
2019/11/22 Python
pytorch模型存储的2种实现方法
2020/02/14 Python
Python Django form 组件动态从数据库取choices数据实例
2020/05/19 Python
python获得命令行输入的参数的两种方式
2020/11/02 Python
澳大利亚领先的宠物用品商店:VetSupply
2017/09/08 全球购物
幼儿园大班毕业教师寄语
2014/04/03 职场文书
阳光体育活动总结
2014/04/30 职场文书
教师个人发展总结
2015/02/11 职场文书
初一年级组工作总结
2015/08/12 职场文书
golang 生成对应的数据表struct定义操作
2021/04/28 Golang
JavaScript原型链中函数和对象的理解
2022/06/16 Javascript