首頁
編程語言
C語言|JAVA編程
Python編程
網頁編程
ASP編程|PHP編程
JSP編程
數據庫
MYSQL數據庫|SqlServer數據庫
Oracle數據庫|DB2數據庫
您现在的位置: 程式碼之家 >> 編程語言 >  >> 網頁程式設計 >> PHP 程式設計 >> PHP 綜合知識

PHP 單點登入 (SSO) 系統架構設計與完整實作指南

     編輯:PHP 綜合知識

單點登入 (SSO) 核心概念

Single Sign On (SSO) 讓用戶在多個互信應用系統中,「一次登入,處處可用」。核心機制包含三大支柱:

  1. 統一身份認證中心 (IdP):集中管理用戶憑證、簽發驗證票據
  2. 票據 (Ticket/Token):跨系統傳遞的身份憑證,通常為加密的 Session ID
  3. 子系統 (SP):依賴 IdP 驗證票據,建立本地登入狀態

架構演進:從 UCenter 觀察者模式到 WebService 票據模式

傳統觀察者模式缺點

  • 子站點增加 → 登入回調鏈條變長 → 登入延遲線性增長
  • 單一子站點回調失敗 → 整個登入流程阻塞或超時
  • 難以支援異構系統(不同語言、不同網域)

本方案:WebService + 統一 Session ID (Ticket)

核心創新:以全站唯一 Session ID 作為 Ticket,存儲於認證中心 Session 中,子站點僅保留 Ticket 存根,驗證時攜帶 Ticket 請求認證中心。

完整流程設計

1. 首次登入流程

用戶訪問子站 A
    ↓ (未登入) 重導向到認證中心
用戶輸入帳密 → 認證中心驗證
    ↓ (成功) 生成 Ticket (Session ID)
    ↓ 記錄「子站 A 已登入」到用戶在線表
    ↓ 返回用戶資料 + Ticket + 同步登入腳本
子站 A 儲存 Ticket 到 Cookie/Session → 建立本地登入狀態
    ↓ 輸出同步登入腳本 (JS) → 通知其他子站建立登入狀態

2. 跨站訪問流程

用戶訪問子站 B
    ↓ 檢查本地 Cookie/Session
    ↓ (無本地狀態) 攜帶 Ticket 請求認證中心
認證中心驗證 Ticket 有效性
    ↓ (有效) 返回用戶資料 + 記錄「子站 B 已登入」
子站 B 建立本地登入狀態

3. 單點登出流程

用戶在子站 A 點擊登出
    ↓ 子站 A 清除本地狀態 + 請求認證中心銷毀 Ticket
認證中心銷毀 Session + 返回同步登出 JS 代碼
    ↓ 子站 A 輸出 JS → 瀏覽器並行請求其他已登入子站的登出接口
所有子站清除本地登入狀態

核心程式碼實作 (關鍵片段)

客戶端類別 (UClientSSO) - 部署於各子站點

class UClientSSO {
    private static $site;           // 子站點標識
    private static $_synloginScript; // 同步登入 JS
    private static $_synlogoutScript; // 同步登出 JS
    
    /** 單點登入入口 */
    static public function loginSSO($username, $password, $remember = false) {
        self::_init();
        self::_removeLocalSid();
        
        // 1. 準備 WebService 參數
        $params = [
            'username' => $username,
            'password' => self::simpleEncPass($password),
            'ip' => self::onlineip(),
            'siteFlag' => self::$site,
            'remember' => $remember,
        ];
        $params['checksum'] = self::_getCheckSum(implode('', $params));
        
        // 2. 調用認證中心 WebService
        $result = self::_callSoap('loginUCenter', $params);
        
        if (intval($result['resultFlag']) > 0 && $result['sessID']) {
            // 3. 登入成功處理
            self::_setLocalSid($result['sessID']); // 儲存 Ticket 到本地 Cookie
            self::$_synloginScript = urldecode($result['script']); // 同步登入 JS
            return $result['userinfo'];
        }
        return ['status' => $result['resultFlag']];
    }
    
    /** 檢查登入狀態 (本地優先,失效則查認證中心) */
    static public function checkUserLogin() {
        self::_init();
        $sid = self::_getLocalSid();
        
        if (empty($sid)) {
            // 嘗試從記住我 Cookie 恢復
            if (isset($_COOKIE['uc_user']) && $decoded = self::authcode($_COOKIE['uc_user'], 'DECODE')) {
                list($user, $pass) = explode('|g|', $decoded);
                return self::loginSSO($user, $pass, true, true);
            }
            return ['status' => -3]; // 未進行過 SSO 處理
        }
        
        // 持有 Ticket,驗證有效性
        $params = [
            'sessId' => $sid,
            'siteFlag' => self::$site,
            'checksum' => md5($sid . self::$site . self::$_mcComunicationKey)
        ];
        $result = self::_callSoap('getOnlineUser', $params);
        
        return intval($result['resultFlag']) > 0 ? $result['userinfo'] : ['status' => $result['resultFlag']];
    }
    
    /** 單點登出 */
    static public function logoutSSO() {
        $sid = self::_getLocalSid();
        if (empty($sid)) return false;
        
        $params = ['sessId' => $sid, 'siteFlag' => self::$site, 'checksum' => ...];
        $result = self::_callSoap('logoutUCenter', $params);
        
        if (intval($result['resultFlag']) > 0) {
            self::_removeLocalSid();
            self::$_synlogoutScript = urldecode($result['script']);
            return 1;
        }
        return $result['resultFlag'];
    }
    
    /** 獲取同步登出 JS 輸出到頁面 */
    static public function getSynlogoutScript() {
        return self::$_synlogoutScript;
    }
    
    // ... 內部輔助方法:_callSoap, _getLocalSid, _setLocalSid, authcode 等
}

認證中心服務端 (UCenterSSO) - 部署於中心

class UCenterSSO {
    private static $_ucSessKey = 'UC_USER_INFO';
    
    /** WebService: 登入驗證 */
    static public function loginUCenter($username, $password, $ip, $siteFlag, $remember) {
        session_start();
        $loginResult = login_user($username, $password, $ip); // 業務邏輯驗證
        
        if ($loginResult['status'] < 1) {
            return ['resultFlag' => $loginResult['status']];
        }
        
        // 登入成功
        $_SESSION[self::$_ucSessKey] = $loginResult;
        $_SESSION[self::$_ucSessKey]['salt'] = self::_getUserPassSalt(...);
        $sessID = session_id(); // 這就是全站通行的 Ticket
        
        // 記錄已登入站點
        self::registerLoggedSite($siteFlag, $sessID);
        
        // 生成同步登入腳本 (給其他子站建立登入狀態)
        $script = '';
        $rawCode = ['action' => 'setSid', 'sid' => $sessID, 'time' => time()];
        if ($remember) $rawCode += ['remember' => 1, 'username' => $username, 'password' => $password];
        $rawStr = http_build_query($rawCode);
        
        foreach ((array)self::$_coSitesInfo as $siteInfo) {
            $code = self::authcode($rawStr, 'ENCODE', $siteInfo['key']);
            $script .= urlencode("");
        }
        
        return [
            'resultFlag' => 1,
            'sessID' => $sessID,
            'userinfo' => $_SESSION[self::$_ucSessKey],
            'script' => $script
        ];
    }
    
    /** WebService: 根據 Ticket 獲取在線用戶 */
    static public function getOnlineUser($sessId, $siteFlag) {
        session_id(trim($sessId));
        session_start();
        
        $userinfo = $_SESSION[self::$_ucSessKey] ?? null;
        if ($userinfo && isset($userinfo['username']) && self::_getUserPassSalt(...)) {
            self::registerLoggedSite($siteFlag, $sessId);
            return ['resultFlag' => 1, 'userinfo' => $userinfo];
        }
        return ['resultFlag' => 0];
    }
    
    /** WebService: 登出銷毀 Ticket */
    static public function logoutUCenter($sessId) {
        session_id(trim($sessId));
        session_start();
        $_SESSION = [];
        return empty($_SESSION);
    }
    
    /** 記錄用戶已登入站點 (用於登出時同步) */
    static private function registerLoggedSite($siteFlag, $sessId) {
        // 存入快取/資料庫:sessId -> [siteFlag1, siteFlag2, ...]
    }
}

部署架構與配置要點

認證中心部署

  • UserSvc.php:WebService 入口 (SOAP),提供 WSDL
  • UCenterSSO.class.php:核心業務邏輯類
  • cookie_mgr.php:W3C 標準跨域 Cookie 管理腳本

子站點部署

  • UClientSSO.class.php:客戶端 SDK,需配置 setSite('站點ID')
  • api/logout_sso.php:接收同步登出請求的端點
  • 在登入檢查處加入 UClientSSO::checkUserLogin() 調用
  • 登出處輸出 UClientSSO::getSynlogoutScript()

Session 存活策略:瀏覽器關閉即失效

// 統一配置於各站點公共入口
session_write_close();
ini_set('session.auto_start', 0);
ini_set('session.cookie_lifetime', 0);      // 瀏覽器關閉失效
ini_set('session.gc_maxlifetime', 3600);    // 最大存活 1 小時

擴展功能:在線用戶追蹤與統計

認證中心持有所有有效 Ticket (Session ID) → 用戶映射表,可實現:

  • 即時在線用戶列表 / 人數統計
  • 用戶登入頻次、時段、時長分析
  • 異地登入檢測、強制下線
  • 單點登入審計日誌

安全性加固建議

風險點防護措施
Ticket 洩露被盜用HTTPS 全站強制、Ticket 綁定 IP/User-Agent、短效期 + 自動續期
同步登入/登出 JS 劫持簽名驗證 (checksum)、單次性 Token、CSP 標頭限制腳本來源
暴力破解登入驗證碼、IP 限流、帳號鎖定策略
Session 固定攻擊登入成功後 session_regenerate_id(true)

總結

本方案採用 WebService + 統一 Session ID (Ticket) 架構,解決了傳統觀察者模式的效能與可靠性瓶頸。核心優勢:

  • 登入流程不依賴子站點回調鏈條,效能穩定
  • Ticket 機制天然支援異構系統 (PHP/Java/Go/Node 等)
  • 同步登入/登出採用瀏覽器並行請求,避免串行阻塞
  • 認證中心集中管理,便於審計、統計、風控

實際落地時,建議引入 Redis 儲存 Session 與在線映射表,使用 JWT 替代純 Session ID 增強無狀態特性,並完善監控告警體系。

  1. 上一篇文章:
  2. 下一篇文章:
Copyright © 程式碼之家 All Rights Reserved