本文介紹的是一種PHP的開源SSO解決方案,可徹底跨域,實現較簡潔,源碼地址:github.com/legalthings…php
一共分爲3個角色:git
Client - 用戶的瀏覽器github
Broker - 用戶訪問的網站數據庫
Server - 保存用戶信息和憑據的地方json
每一個Broker有一個ID和密碼,Broker和Server事先已知道。跨域
當Client第一次訪問Broker時,它會建立一個隨機令牌,該令牌存儲在cookie中。而後Broker將Client重定向到Server,傳遞Broker的ID和令牌。Server使用Broker的ID、密碼和令牌建立哈希,此哈希做爲Key鍵保存當前用戶會話的ID。以後Server會將Client重定向回Broker。瀏覽器
Broker可使用令牌(來自cookie)、本身的ID和密碼建立相同的哈希。在執行請求時包含此哈希。服務器
Server收到請求會提取哈希,而後根據哈希獲取以前保存的用戶會話ID,而後將其設置成當前會話ID。所以,Broker和Client使用相同的會話。當另外一個Broker加入時,它也將使用相同的會話。它們能夠共享會話中保存的用戶信息,進而實現了單點登陸功能。cookie
Session表明着服務器和客戶端一次會話的過程。直到session失效(服務端關閉),或者客戶端關閉時結束。Session 是存儲在服務端的,並針對每一個客戶端(客戶),經過Session ID來區別不一樣用戶的。關於session的詳細介紹請看這篇文章。下面說的會話即指Session。session
如下是其GitHub中的過程圖:
首次訪問Broker時會進行attach操做,attach主要有如下幾個動做:
Broker側attach代碼片斷:
/** * Attach our session to the user's session on the SSO server. * * @param string|true $returnUrl The URL the client should be returned to after attaching */
public function attach($returnUrl = null) {
/* 經過檢測Cookie中是否有token來判斷是否已attach 若已經attach,就再也不進行attach操做了 */
if ($this->isAttached()) return;
/* 將當前訪問的地址做爲返回地址,attach結束以後會返回到returnUrl */
if ($returnUrl === true) {
$protocol = !empty($_SERVER['HTTPS']) ? 'https://' : 'http://';
$returnUrl = $protocol . $_SERVER['HTTP_HOST'] . $_SERVER['REQUEST_URI'];
}
$params = ['return_url' => $returnUrl];
/* 在getAttachUrl函數中會生成token並保存到cookie中, 同時將Broker ID和token做爲url的參數傳遞給Server */
$url = $this->getAttachUrl($params);
/* 跳轉到SSO Server並退出 */
header("Location: $url", true, 307);
echo "You're redirected to <a href='$url'>$url</a>";
exit();
}
複製代碼
Server側attach代碼片斷:
/** * Attach a user session to a broker session */
public function attach() {
/* 檢測返回類型 */
$this->detectReturnType();
/* 檢測attach的url上是否帶有Broker ID和token信息 */
if (empty($_REQUEST['broker'])) return $this->fail("No broker specified", 400);
if (empty($_REQUEST['token'])) return $this->fail("No token specified", 400);
if (!$this->returnType) return $this->fail("No return url specified", 400);
/* 根據Broker ID對應的密碼和token生成校驗碼,與請求參數中的校驗碼匹配,若是相同則認爲 attach的Broker是已在SSO Server註冊過的 */
$checksum = $this->generateAttachChecksum($_REQUEST['broker'], $_REQUEST['token']);
if (empty($_REQUEST['checksum']) || $checksum != $_REQUEST['checksum']) {
return $this->fail("Invalid checksum", 400);
}
/* 開啓session */
$this->startUserSession();
/* 根據Broker ID對應的密碼和token生成哈希sid */
$sid = $this->generateSessionId($_REQUEST['broker'], $_REQUEST['token']);
/* 將哈希sid做爲鍵值保存session id到cache中,cache具備持久保存能力,文本文件或數據庫都可 */
$this->cache->set($sid, $this->getSessionData('id'));
/* 根據返回類型返回 */
$this->outputAttachSuccess();
}
複製代碼
當再次訪問Broker時,因爲能夠從cookie中獲取token,因此不會再進行attach操做了。當Broker試圖獲取用戶信息(getUserInfo
)時,會經過CURL方式和Server通訊,參數中會攜帶哈希Key值做爲Broker合法身份的驗證。
/** * Execute on SSO server. * * @param string $method HTTP method: 'GET', 'POST', 'DELETE' * @param string $command Command * @param array|string $data Query or post parameters * @return array|object */
protected function request($method, $command, $data = null) {
/* 判斷是否已attach */
if (!$this->isAttached()) {
throw new NotAttachedException('No token');
}
/* 獲取SSO Server地址 */
$url = $this->getRequestUrl($command, !$data || $method === 'POST' ? [] : $data);
/* 初始化CURL並設置參數 */
$ch = curl_init($url);
curl_setopt($ch, CURLOPT_RETURNTRANSFER, true);
curl_setopt($ch, CURLOPT_CUSTOMREQUEST, $method);
/* 添加哈希Key值做爲身份驗證 */
curl_setopt($ch, CURLOPT_HTTPHEADER, ['Accept: application/json', 'Authorization: Bearer '. $this->getSessionID()]);
if ($method === 'POST' && !empty($data)) {
$post = is_string($data) ? $data : http_build_query($data);
curl_setopt($ch, CURLOPT_POSTFIELDS, $post);
}
/* 執行CURL並獲取返回值 */
$response = curl_exec($ch);
if (curl_errno($ch) != 0) {
$message = 'Server request failed: ' . curl_error($ch);
throw new Exception($message);
}
/* 對返回數據進行判斷及失敗處理 */
$httpCode = curl_getinfo($ch, CURLINFO_HTTP_CODE);
list($contentType) = explode(';', curl_getinfo($ch, CURLINFO_CONTENT_TYPE));
if ($contentType != 'application/json') {
$message = 'Expected application/json response, got ' . $contentType;
throw new Exception($message);
}
/* 對返回值按照json格式解析 */
$data = json_decode($response, true);
if ($httpCode == 403) {
$this->clearToken();
throw new NotAttachedException($data['error'] ?: $response, $httpCode);
}
if ($httpCode >= 400) throw new Exception($data['error'] ?: $response, $httpCode);
return $data;
}
複製代碼
Server端對getUserInfo
的響應片斷:
/** * Start the session for broker requests to the SSO server */
public function startBrokerSession() {
/* 判斷Broker ID是否已設置 */
if (isset($this->brokerId)) return;
/* 從CURL的參數中獲取哈希Key值sid */
$sid = $this->getBrokerSessionID();
if ($sid === false) {
return $this->fail("Broker didn't send a session key", 400);
}
/* 嘗試從cache中經過哈希Key值獲取保存的會話ID */
$linkedId = $this->cache->get($sid);
if (!$linkedId) {
return $this->fail("The broker session id isn't attached to a user session", 403);
}
if (session_status() === PHP_SESSION_ACTIVE) {
if ($linkedId !== session_id()) throw new \Exception("Session has already started", 400);
return;
}
/******** 下面這句代碼是整個SSO登陸實現的核心 ******** * 將當前會話的ID設置爲以前保存的會話ID,而後啓動會話 * 這樣就能夠獲取以前會話中保存的數據,從而達到共享登陸信息的目的 * */
session_id($linkedId);
session_start();
/* 驗證CURL的參數中獲取哈希Key值sid,獲得Broker ID */
$this->brokerId = $this->validateBrokerSessionId($sid);
}
/** * Ouput user information as json. */
public function userInfo() {
/* 啓動以前保存的ID的會話 */
$this->startBrokerSession();
$user = null;
/* 從以前的會話中獲取用戶信息 */
$username = $this->getSessionData('sso_user');
if ($username) {
$user = $this->getUserInfo($username);
if (!$user) return $this->fail("User not found", 500); // Shouldn't happen
}
/* 響應CURL,返回用戶信息 */
header('Content-type: application/json; charset=UTF-8');
echo json_encode($user);
}
複製代碼
若是用戶沒有登陸,那麼獲取到的userInfo將是null,此時在Broker側會觸發登陸程序,頁面會跳轉到登陸界面,請求用戶登陸。用戶登陸的校驗是在Server側完成的,同時將用戶信息保存到以前的ID的會話當中,等到下次再訪問的時候就能夠直接獲取到用戶信息了。
/** * Authenticate */
public function login() {
/* 啓動以前保存的ID的會話 */
$this->startBrokerSession();
/* 檢查用戶名和密碼是否爲空 */
if (empty($_POST['username'])) $this->fail("No username specified", 400);
if (empty($_POST['password'])) $this->fail("No password specified", 400);
/* 校驗用戶名和密碼是否正確 */
$validation = $this->authenticate($_POST['username'], $_POST['password']);
if ($validation->failed()) {
return $this->fail($validation->getError(), 400);
}
/* 將用戶信息保存到當前會話中 */
$this->setSessionData('sso_user', $_POST['username']);
$this->userInfo();
}
複製代碼