用户
 找回密码
 入住 CI 中国社区
搜索
查看: 162505|回复: 23
收起左侧

[程序 App] PHP实现跨域单点登录(CI框架)

  [复制链接]
发表于 2013-10-21 14:56:56 | 显示全部楼层 |阅读模式
本帖最后由 dolia 于 2013-10-21 14:58 编辑

最近公司需要制作单点登录系统,于是我一直在搜寻各种资料寻找最优化的单点登录制作方式,最后综合了各种想法,完成了单点登录系统,下面给大家分享下,我实现的单点登录的方式:

原文转自:www.W3PHP.com W3PHP

单点登录(Single Sign On),简称为 SSO,是目前比较流行的企业业务整合的解决方案之一。SSO的定义是在多个应用系统中,用户只需要登录一次就可以访问所有相互信任的应用系统。(百度百科)

我本人认为单点登录主要解决一下问题:

1. 一个用户账号,多站点通用;
2. 一个应用登录,其他应用同时登录;
3. 一个账号退出,其他应用账号同时退出;(非必须,此项不在单点登录的讨论范围之内,不过WEB单点登录,用到的还是挺多的);
4. 欢迎补充

PHP实现单点登录方法有多种:

1. 通过关系服务 如webservice 之类的;
2. 数据库共享session实现;
4. P3P跨站点 如ucenter phpcms 等;
5. 通过文件操作 SESSIONID
6. 欢迎补充

我使用的是第5种方法,下面简单说下实现:

一 登陆原理说明

我把SSO分为三个角色:

1. 客户端 – 指的是用户的浏览器
2. 代理端 – 指的是用户访问的网站;
3. 服务端 - 单点登录服务端,提供用户信息方;

同域实现SESSION共享非常容易,但是跨域实现网站和单点登录服务器共享SESSION,就需要做些手脚了;我是这样做的:


二. 过程说明:

登陆流程:

1. 第一次登陆某个站:

a) 站点生成随机字符串TOKEN,保存在cookie中,并携带加密过的APPID,APPKEY  URL跳转到单点登录服务器;单点登录服务器则解密APPID 和APPKEY并去验证站点合法性;同时单点登录服务端会启动SESSION,并根据TOKEN在单点登录服务端的SESSION生成路径,同时生成以TOKEN为文件名的SESSION_TOKEN,把开始启动SESSION的PHPSSID,写入伪SESSION_TOKEN中,实现SESSION共享,然后使客户端跳转至原来的站点URL;

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

b) 当前登录站点,通过CURL请求,用户验证中心验证用户名,密码的合法性。如果验证通过,则返回用户信息,不通过则返回错误信息;

c)  根据上一步的CURL请求返回的结果,当前子站对用户进行登陆处理,如果接受到错误,则进行错误处理;

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

a) 通过本站cookie或session验证用户的登录状态:如验证通过,进入正常本站处理程序;不通过则重复第一次登陆步骤;


下面把代码贴上:(使用的是CI框架)


服务端:


<?php if ( ! defined('BASEPATH')) exit('No direct script access allowed');

/**
* 单点登录控制器类
*
*/
class Sso extends CI_Controller {
        
        /**
         * session存储位置
         *
         * @var string
         */
        public $links_path;
        
    /**
     * 标识sessioin是否开启
     * @var boolean
     */
    protected $started=false;        
        
    /**
     * 当前应用
     * @var string
     */
    protected $broker = null;
        
        /**
         * 当前用户
         */        
         
        protected $user = null;

        /**
         * 构造函数
         */
        public function __construct()
        {
                parent::__construct();
                                
                $this->load->model('broker_model');
               
                $this->load->model('user_model');
               
                //如果创建连接函数没有开启,$link_path系统默认存储session目录
                //if (!function_exists('symlink')) $this->links_path = sys_get_temp_dir();
                $this->links_path = sys_get_temp_dir();
        
        }
        
        public function index()
        {
                exit('1');
        }

        
    /**
     * 登录
     */

    public function login()
    {

        $this->_session_start();
               
                $username = $this->input->post('username');
               
                $password = $this->input->post('password');
               
        if (empty($username)) $this->failLogin("no_user_name");
               
        if (empty($password)) $this->failLogin("no_password");

                //数据库验证
        
                $info = $this->user_model->_web_login($this->input->post('username'),$this->input->post('password'));
               
                if (isset($info['user'])&&$info['user']!='')
                {
                        $_SESSION['user'] = $info['user'];
                        
                        $this->info();
                        
                }else{
                        
                        $this->failLogin($info['error']);
                }
               
    }
        
        
    /**
     * PC登录
     */

    public function pc_login()
    {

        $this->_session_start();
               
                $cihiuserno = $this->input->post('cihiuserno');
               
                $cihikey = $this->input->post('cihikey');
               
        if (empty($cihiuserno)) $this->failLogin("no_cihiuserno");
               
        if (empty($cihikey)) $this->failLogin("no_cihikey");

                //数据库验证
        
                $info = $this->user_model->_pc_login($this->input->post('cihiuserno'),$this->input->post('cihikey'));
               
               
                if (isset($info['user'])&&$info['user']!='')
                {
                        $_SESSION['user'] = $info['user'];
                        $this->info();
                        
                }else{
                        
                        $this->failLogin($info['error']);
                }
               
    }               
        
    /**
     * 退出
     */
    public function logout()
    {
            header('P3P: CP="CURa ADMa DEVa PSAo PSDo OUR BUS UNI PUR INT DEM STA PRE COM NAV OTC NOI DSP COR"');
               
            $this->load->model('broker_model');
               
                $brokers = $this->broker_model->get_all_broker();
               
                $res='';
                foreach ($brokers as $k=>$v)
                {
                        if (trim($v['api_url'])!=''){
                                $tmp_s = strstr($v['api_url'], '?') ? '&' : '?';
                                $res .= '<script type="text/javascript" src="'.$v['api_url'].$tmp_s.'&time='.time().' reload="1"></script>';                                
                        }
                }
               
               
       $this->_session_start();
       unset($_SESSION['user']);
           echo $res;

    }
        
        
    /**
     * 输出用户信息
     */
    public function info()
    {
        $this->_session_start();
                //如果不存在登陆用户 返回提示
            if (!isset($_SESSION['user'])) $this->failLogin("Not logged in");
               
                echo json_encode($_SESSION['user']);exit();

    }        
        
        
    /**
     * 连接session
     */
    public function attach()
    {
            //开启回话
        $this->_session_start();

                //检验broker
                $broker = $this->input->get_post('broker');
        if (empty($broker)) $this->fail("No broker specified");
                //检验token
                $token = $this->input->get_post('token');
        if (empty($token)) $this->fail("No token specified");
                //检验校验码
                $checksum = $this->input->get_post('checksum');
        if (empty($checksum) || $this->generateAttachChecksum($broker, $token) != $checksum) $this->fail("Invalid_checksum");
                //如果没有设置session存储位置
        if (!isset($this->links_path)) {
               
                //拼接session存储文件  
                $link = (session_save_path() ? session_save_path() : sys_get_temp_dir()) . "/sess_" . $this->generateSessionId($broker, $token);
                        //如果sessioin文件不存在 把本文件链接到系统的session_id上
                if (!file_exists($link)) $attached = symlink('sess_' . session_id(), $link);
                        //如果没有链接成功,报错
                if (!$attached) trigger_error("Failed to attach; Symlink wasn't created.", E_USER_ERROR);
        } else {
                //指定session路径存放session
                $link = "{$this->links_path}/" . $this->generateSessionId($broker, $token);
                if (!file_exists($link)) $attached = file_put_contents($link, session_id());
                if (!$attached) trigger_error("Failed to attach; Link file wasn't created.", E_USER_ERROR);
                }
                //跳转至broker
                $redirect = $this->input->get_post('redirect');
        if (isset($redirect)) {
            header("Location: " . $redirect, true, 307);
            exit;        
        }

        // 输出图片用于ajax登录
        header("Content-Type: image/png");
        readfile("empty.png");
    }        
               
        /*
         * 开启session并且防止session劫持
     */

    protected function _session_start()
    {
            //如果session已经开水器  false
            
       if ($this->started) return;
        $this->started = true;
        // 应用session
        $matches = null;
               
                $cookie = $this->input->cookie(session_name());
               
                //如果通过request方式获取到PHPSSID 并且匹配本规则
        if (isset($cookie) && preg_match('/^SSO-(\w*+)-(\w*+)-([a-z0-9]*+)$/', $cookie, $matches)) {
               
                $sid = $cookie;
                        
                    if (isset($this->links_path) && file_exists("{$this->links_path}/$sid")) {
                            session_id(file_get_contents("{$this->links_path}/$sid"));
                            session_start();
                            setcookie(session_name(), "", 1);
                    } else {
                                session_start();
                    }

            if (!isset($_SESSION['client_addr'])) {
                session_destroy();
                $this->fail("Not attached");
            }

            if ($this->generateSessionId($matches[1], $matches[2], $_SESSION['client_addr']) != $sid) {
                session_destroy();
                $this->fail("Invalid session id");
            }

            $this->broker = $matches[1];
            return;
        }

        // 开启用户会话
        session_start();
                //如果存在客户端IP并且客户端IP和服务端不一致,更新SESSIONID
        if (isset($_SESSION['client_addr']) && $_SESSION['client_addr'] != $_SERVER['REMOTE_ADDR']) session_regenerate_id(true);
                //如果存在客户端IP并且一致,客户端IP设置为服务端IP
        if (!isset($_SESSION['client_addr'])) $_SESSION['client_addr'] = $_SERVER['REMOTE_ADDR'];
    }        

    /**
         * 通过session token生成session id
     *
     * @return string
     */
    protected function generateSessionId($broker, $token, $client_addr=null)
    {
                //验证broker
                $info = $this->broker_model->get_broker_by_broker($broker);        
                if ($info) {
                        $secret = $info['secret'];
                }else{
                        return null;
                }
                //如果客户端地址没有设置,获取客户端IP
        if (!isset($client_addr)) $client_addr = $_SERVER['REMOTE_ADDR'];
                //根据 参数生出客户端session文件名称
        return "SSO-{$broker}-{$token}-" . md5('session' . $token . $client_addr . $secret);
    }
        
    /**
         * 通过session token生成session id
     *
     * @return string
     */
    protected function generateAttachChecksum($broker, $token)
    {
            
                //验证broker
                $info = $this->broker_model->get_broker_by_broker($broker);        
                if ($info) {
                        $secret = $info['secret'];
                }else{
                        return null;
                }
               
        return md5('attach' . $token . $_SERVER['REMOTE_ADDR'] . $secret);
    }
        
    /**
     * 错误
     *
     * @param string $message
     */
    protected function fail($message)
    {
        header("HTTP/1.1 406 Not Acceptable");
        echo $message;
        exit;
    }
        
    /**
     * 登录失败
     *
     * @param string $message
     */
    protected function failLogin($message)
    {
        header("HTTP/1.1 401 Unauthorized");
        echo $message;
        exit;
    }
        
}

/* End of file sso.php */
/* Location: ./application/controllers/sso.php */


代理端:

<?php
/**
* 单点登录服务
*/
class Sso
{
        /**
         * 取消服务端 HTTP401
         */
        public $pass401=false;

    /**
     * SSO服务地址
     * @var string
     */
    public $url = "http://sso.lab.mycihi.cn/sso/";

    /**
     * 代理ID
     * @var string
     */
    public $broker = "cihi";

    /**
     * 秘药
     * @var string
     */
    public $secret = "1A9624F80F6FCE52325B74255ABE7A94";

    /**
     * 不能比服务端设置的小
     * @var string
     */
    public $sessionExpire = 1800;

    /**
     * SESSION hash
     * @var string
     */
    protected $sessionToken;

    /**
     * 用户信息
     * @var array
     */
    protected $userinfo;
        
        
        /**
         * 错误信息
         *
         * 1. no_username 缺少用户名
         * 2. no_password 缺少密码
         * 3. user_not_exists 用户不存在
         * 4. bad_password 密码错误
         * 4. unknown  未知
         *
         */
        public  $error;


    /**
     * 构造函数
     */
    public function __construct($auto_attach=true)
    {
            $this->test_connectiong();
            //如果cookie存在session_token
        if (isset($_COOKIE['session_token'])) $this->sessionToken = $_COOKIE['session_token'];
        //如果设置自动粘贴token并且不存在sessiontoken,带上参数跳转的服务端
        if ($auto_attach && !isset($this->sessionToken)) {
                //跳转至SSO
            header("Location: " . $this->getAttachUrl() . "&redirect=". urlencode("http://{$_SERVER["SERVER_NAME"]}{$_SERVER["REQUEST_URI"]}"), true, 307);
            exit;
        }
    }

        //测试通讯

        public function test_connectiong ()
        {
                if(isset($_GET['testsso'])&&$_GET['testsso']==1){
                        echo "connected";
                }
        }

    /**
     * 获取客户端的session_token
     *
     * @return string
     */
    public function getSessionToken()
    {
            //如果没有生成过session_token 生成session_token
        if (!isset($this->sessionToken)) {
                //随机申城session_token
            $this->sessionToken = md5(uniqid(rand(), true));
                        //吧session_token写入cookie
            setcookie('session_token', $this->sessionToken, time() + $this->sessionExpire);
        }

        return $this->sessionToken;
    }

    /**
     * 生成session id
     *
     * @return string
     */
    protected function getSessionId()
    {
                if (!isset($this->sessionToken)) return null;
        return "SSO-{$this->broker}-{$this->sessionToken}-" . md5('session' . $this->sessionToken . $_SERVER['REMOTE_ADDR'] . $this->secret);
    }

    /**
         * 获取URL并传递session到sso服务器
     *
     * @return string
     */
    public function getAttachUrl()
    {
                $token = $this->getSessionToken();
                //根据token和IP和代理端秘药生成校验码传递给服务端
                $checksum = md5("attach{$token}{$_SERVER['REMOTE_ADDR']}{$this->secret}");
                //拼接URL 传递 sessioin_token和校验码到服务端
        return "{$this->url}attach?broker={$this->broker}&token=$token&checksum=$checksum";
    }   


    /**
     * WEB登录
     *
     * @param string $username
     * @param string $password
     * @return boolean
         *
     */
    public function login($username, $password)
    {   
        list($ret, $body) = $this->serverCmd('login', array('username'=>$username, 'password'=>$password));

        switch ($ret) {
               
            case 200: $this->parseInfo($body);
                      return 1;
            case 401: $this->error= $body;
                                          return 0;
            default:  $this->error= $body;
                                          return 0;

        }
    }
        
    /**
     * PC登录
     *
     * @param string $username
     * @param string $password
     * @return boolean
         *
     */
    public function pc_login($cihiuserno, $cihikey)
    {   
        list($ret, $body) = $this->serverCmd('pc_login', array('cihiuserno'=>$cihiuserno, 'cihikey'=>$cihikey));

        switch ($ret) {
               
            case 200: $this->parseInfo($body);
                      return 1;
            case 401: $this->error= $body;
                                          return 0;
            default:  $this->error= $body;
                                          return 0;

        }
    }

        
                        

    /**
     * 退出单点登录
     */
    public function logout()
    {
                if(isset($_GET['testsso'])&&$_GET['testsso']==1){
                        echo "connected";exit();
                }
            //header('P3P: CP="CURa ADMa DEVa PSAo PSDo OUR BUS UNI PUR INT DEM STA PRE COM NAV OTC NOI DSP COR"');
               
        list($ret, $body) = $this->serverCmd('logout');

                echo $body;

                setcookie('session_token', '');
               
               
               
               


    }



    /**
     * 获取SSO当前登陆用户信息
     */
    public function getInfo()
    {
        if (!isset($this->userinfo)) {
               
            list($ret, $body) = $this->serverCmd('info');
            switch ($ret) {
                     case 200:
                              return $this->parseInfo($body);
                     case 401: $this->error= $body;
                                                  return 0;
                     default:  $this->error= $body;
                                                  return 0;
            }
        }

        return $this->userinfo;
    }

    /**
     * 执行CURL请求
     *
     * @param string $cmd   Command
     * @param array  $vars  Post variables
     * @return array
         *
     */
    protected function serverCmd($cmd, $vars = array())
    {
            
        $curl = curl_init($this->url . '/' . urlencode($cmd));
        curl_setopt($curl, CURLOPT_RETURNTRANSFER, true);
        curl_setopt($curl, CURLOPT_COOKIE, "PHPSESSID=" . $this->getSessionId());
        
        if (!empty($vars)) {
            curl_setopt($curl, CURLOPT_POST, true);
            curl_setopt($curl, CURLOPT_POSTFIELDS, $vars);
       }

                curl_setopt($curl, CURLOPT_SSL_VERIFYPEER, false);

        $body = curl_exec($curl);

               
        $ret = curl_getinfo($curl, CURLINFO_HTTP_CODE);
        
        if (curl_errno($curl) != 0) throw new Exception("SSO failure: HTTP request to server failed. " . curl_error($curl));

        return array($ret, $body);
    }
        

    /**
     * 解析返回用户信息数据
     *
     * @param string $json
     */
    protected function parseInfo($json)
    {
                $josn = json_decode($json);
        return $this->userinfo = (array)$josn;
    }
        
        /**
         * 获取错误信息
         */
        
        public function get_error()
        {
                return $this->error;
        }        
}










发表于 2017-10-23 17:59:38 | 显示全部楼层
楼主可以发一份全套代码吗?3Q  715241005@qq.com
发表于 2016-1-4 17:35:38 | 显示全部楼层
楼主可以发一份全套代码吗?3Q  617003229@qq.com
发表于 2019-6-18 20:37:02 | 显示全部楼层
你好,可以提供一下完整代码吗?谢谢,1563185851@qq.com
发表于 2013-10-21 15:52:37 | 显示全部楼层
赞一个 谢谢分享
发表于 2013-10-29 23:36:56 | 显示全部楼层
发表于 2013-11-9 23:13:45 | 显示全部楼层
这个有用,收藏了
发表于 2014-3-19 09:26:30 | 显示全部楼层
记号,收藏一下。
发表于 2014-3-27 15:06:43 | 显示全部楼层
谢谢
发表于 2014-3-27 16:29:20 | 显示全部楼层
不错的思路,mark一记,以备后用
发表于 2014-4-24 15:33:16 | 显示全部楼层
  学习了  备用~~
发表于 2015-3-12 10:12:25 | 显示全部楼层
能给一份代码我参考么?楼主

本版积分规则