SSO(Single Sign On)讓用戶在多系統間一次登錄、處處可用。最常見的「集中式 Ticket 模式」:用戶訪問業務系統 → 重定向 SSO 登錄頁 → 登錄成功回調帶 ticket → 業務系統憑 ticket 換取用戶信息 → 本地創建/匹配用戶 → 登入。本文給出完整接入代碼與關鍵點。
interface SSOLogin {
/** 獲取 SSO 授權頁面 URL,攜帶回調地址 */
public function getAuthUrl(): string;
/** 憑 ticket 向 SSO 服務端換取用戶信息
* @return array ['username'=>'', 'truename'=>'', 'email'=>'', ...] 或空數組
*/
public function getInfoFromTicket(string $ticket): array;
}
class SsoController extends Controller {
public function actionCheck() {
$ticket = Yii::$app->request->get('ticket');
// 1. 無 ticket → 導向 SSO 登錄
if (!$ticket) {
return $this->render('auth_error', [
'msg' => '請先授權登錄',
'loginUrl' => SSOLogin::getInstance()->getAuthUrl()
]);
}
// 2. 換取用戶信息
$userInfo = SSOLogin::getInstance()->getInfoFromTicket($ticket);
if (empty($userInfo['username'])) {
return $this->render('auth_error', [
'msg' => '授權失效,請重新登錄',
'loginUrl' => SSOLogin::getInstance()->getAuthUrl()
]);
}
// 3. 本地用戶匹配/創建
$username = $this->normalizeUsername($userInfo['username']);
$user = User::find()->where(['username' => $username])->andWhere(['status' => User::STATUS_ACTIVE])->one();
if (!$user) {
$user = new User([
'username' => $username,
'email' => $userInfo['email'] ?? $username . '@sso.local',
'realname' => $userInfo['truename'] ?? '',
'role' => User::ROLE_DEV,
'is_email_verified' => 1,
'status' => User::STATUS_ACTIVE,
]);
$user->setPassword(bin2hex(random_bytes(16))); // 隨機密碼,純 SSO 登錄
$user->generateAuthKey();
$user->save();
}
// 4. 登入(記住 30 天)
if (Yii::$app->user->login($user, 3600 * 24 * 30)) {
return $this->redirect('/task/index');
}
return $this->render('auth_error', ['msg' => '登入失敗,請聯繫管理員']);
}
protected function normalizeUsername(string $raw): string {
// 統一處理:去除域前綴、特殊字符轉換等
return strtolower(preg_replace('/[^a-zA-Z0-9_-]/', '_', $raw));
}
}
class SSOLogin implements SSOLogin {
private $ssoServerUrl; // 如 https://sso.company.com
private $appId;
private $appSecret;
private $callbackUrl; // 業務系統回調地址
public function __construct($config) {
$this->ssoServerUrl = rtrim($config['server_url'], '/');
$this->appId = $config['app_id'];
$this->appSecret = $config['app_secret'];
$this->callbackUrl = $config['callback_url'];
}
public function getAuthUrl(): string {
$params = [
'app_id' => $this->appId,
'redirect_uri' => $this->callbackUrl,
'state' => bin2hex(random_bytes(16)), // CSRF 防護
'scope' => 'userinfo',
];
$_SESSION['sso_state'] = $params['state'];
return $this->ssoServerUrl . '/oauth/authorize?' . http_build_query($params);
}
public function getInfoFromTicket(string $ticket): array {
// 校驗 state 防 CSRF
if (($_SESSION['sso_state'] ?? '') !== ($_GET['state'] ?? '')) {
return [];
}
// 服務端驗證 ticket(通常調用 /oauth/userinfo 或 /api/ticket/verify)
$url = $this->ssoServerUrl . '/api/ticket/verify';
$post = ['ticket' => $ticket, 'app_id' => $this->appId, 'app_secret' => $this->appSecret];
$ch = curl_init($url);
curl_setopt_array($ch, [
CURLOPT_POST => true,
CURLOPT_POSTFIELDS => json_encode($post),
CURLOPT_HTTPHEADER => ['Content-Type: application/json'],
CURLOPT_RETURNTRANSFER => true,
CURLOPT_TIMEOUT => 5,
]);
$res = curl_exec($ch);
curl_close($ch);
$data = json_decode($res, true);
if (($data['code'] ?? -1) === 0 && !empty($data['data'])) {
return $data['data']; // ['username'=>'zhangsan', 'truename'=>'張三', 'email'=>'zhangsan@company.com']
}
return [];
}
}
用戶 業務系統(A) SSO服務端 | | | |--- 訪問 /task --->| | | |--- 302 /sso/check?ticket=... (無ticket) | | | | |<-- 302 SSO登錄頁 --| | | | |--- 輸入賬號密碼 -->| | | | |--- 驗證用戶 --> | | |<-- 簽發 ticket --| | | | |<-- 302 /sso/check?ticket=TICKET&state=STATE --| | | | | |--- 驗證 state -->| | |--- POST /api/ticket/verify (ticket+app_secret) -->| | | |--- 查詢用戶信息 --> | | |<-- 返回用戶信息 --| | | | | |--- 本地查找/創建用戶 --> | |--- 設置登錄 Session --> |<-- 302 /task/index (登錄成功) --| | | |
state,Session 存儲,回調校驗/api/ticket/verify 時使用,不暴露前端app_id -> logout_url 映射POST /sso/logout(攜帶 user_id、簽名)| 場景 | 處理策略 |
|---|---|
| SSO 服務不可用 | 降級提示「統一登錄維護中」,允許本地賬號密碼登錄(若有) |
| Ticket 已失效/重複使用 | 引導用戶重新點擊登錄鏈接 |
| 用戶信息變更(姓名/郵箱) | 每次登錄同步更新本地字段,或定時全量同步 |
| 同帳號多系統角色不同 | 本地維護角色映射表,SSO 僅提供身份認證,授權由各系統獨立管理 |