Single Sign On (SSO) 讓用戶在多個互信應用系統中,「一次登入,處處可用」。核心機制包含三大支柱:
核心創新:以全站唯一 Session ID 作為 Ticket,存儲於認證中心 Session 中,子站點僅保留 Ticket 存根,驗證時攜帶 Ticket 請求認證中心。
用戶訪問子站 A
↓ (未登入) 重導向到認證中心
用戶輸入帳密 → 認證中心驗證
↓ (成功) 生成 Ticket (Session ID)
↓ 記錄「子站 A 已登入」到用戶在線表
↓ 返回用戶資料 + Ticket + 同步登入腳本
子站 A 儲存 Ticket 到 Cookie/Session → 建立本地登入狀態
↓ 輸出同步登入腳本 (JS) → 通知其他子站建立登入狀態
用戶訪問子站 B
↓ 檢查本地 Cookie/Session
↓ (無本地狀態) 攜帶 Ticket 請求認證中心
認證中心驗證 Ticket 有效性
↓ (有效) 返回用戶資料 + 記錄「子站 B 已登入」
子站 B 建立本地登入狀態
用戶在子站 A 點擊登出
↓ 子站 A 清除本地狀態 + 請求認證中心銷毀 Ticket
認證中心銷毀 Session + 返回同步登出 JS 代碼
↓ 子站 A 輸出 JS → 瀏覽器並行請求其他已登入子站的登出接口
所有子站清除本地登入狀態
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 等
}
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),提供 WSDLUCenterSSO.class.php:核心業務邏輯類cookie_mgr.php:W3C 標準跨域 Cookie 管理腳本UClientSSO.class.php:客戶端 SDK,需配置 setSite('站點ID')api/logout_sso.php:接收同步登出請求的端點UClientSSO::checkUserLogin() 調用UClientSSO::getSynlogoutScript()// 統一配置於各站點公共入口
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) 架構,解決了傳統觀察者模式的效能與可靠性瓶頸。核心優勢:
實際落地時,建議引入 Redis 儲存 Session 與在線映射表,使用 JWT 替代純 Session ID 增強無狀態特性,並完善監控告警體系。