Полный анализ исходного кода междоменного единого входа (SSO)

задняя часть сервер GitHub Curl

В этой статье представлено решение SSO с открытым исходным кодом для PHP, которое может быть полностью междоменным, а его реализация относительно проста.Адрес исходного кода:GitHub.com/юридические вещи…

Принцип реализации

Всего 3 роли:

  • Клиент - браузер пользователя

  • Брокер - сайт, который посещает пользователь

  • Сервер - где хранится информация о пользователе и учетные данные

Каждый Брокер имеет идентификатор и пароль, которые Брокер и Сервер знают заранее.

  1. Когда Клиент обращается к Брокеру в первый раз, он создает случайный токен, который сохраняется в файле cookie. Затем Брокер перенаправляет Клиента на Сервер, передавая идентификатор Брокера и токен. Сервер использует идентификатор брокера, пароль и токен для создания хэша, и этот хеш используется в качестве ключа для хранения идентификатора текущего пользовательского сеанса. Затем сервер перенаправит клиента обратно к брокеру.

  2. Брокер может создать такой же хэш, используя токен (из файла cookie), собственный идентификатор и пароль. Включите этот хэш при выполнении запроса.

  3. Когда сервер получает запрос, он извлекает хеш, а затем получает ранее сохраненный идентификатор сеанса пользователя в соответствии с хэшем, а затем устанавливает его в качестве текущего идентификатора сеанса. Поэтому Брокер и Клиент используют один и тот же сеанс. Когда другой брокер присоединяется, он также будет использовать тот же сеанс. Они могут обмениваться информацией о пользователях, сохраненной в сеансе, что обеспечивает возможность единого входа.

жизненный опыт

Сеанс представляет собой процесс сеанса между сервером и клиентом. Пока не истечет сессия (закроется сервер) или не закроется клиент. Сессия хранится на сервере, и для каждого клиента (клиента) разные пользователи различаются по Session ID. Подробное введение в сессию см.эта статья. Сеанс, упомянутый ниже, относится к сеансу.

Подробная инструкция по внедрению

Ниже приведена диаграмма процесса в его GitHub:

第一次访问流程图
Блок-схема первого посещения

При первом доступе к Брокеру будет выполнена операция присоединения, которая в основном включает в себя следующие действия:

  1. Создайте токен и сохраните его в файле cookie.
  2. Перейти к серверу с идентификатором брокера и токеном в качестве параметров URL.
  3. Сервер запрашивает пароль брокера на основе идентификатора брокера и генерирует хэш с загруженным токеном, который используется в качестве ключа для сохранения идентификатора сеанса между браузером текущего пользователя и сервером. Эти данные должны быть сохранены, и можно указать время истечения срока действия.
  4. Наконец, верните адрес, который посетил первоначальный пользователь.

Фрагмент кода прикрепления со стороны брокера:

   /**
     * 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();
    }

Фрагмент кода прикрепления на стороне сервера:

   /**
     * 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();
    }

При повторном доступе к Брокеру, поскольку токен можно получить из файла cookie, операция присоединения не будет выполняться снова. Когда Брокер пытается получить информацию о пользователе (getUserInfo), он будет связываться с сервером через CURL, а значение ключа хеша будет передано в параметре для проверки подлинности брокера.

   /**
     * 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;
    }

Пара на стороне сервера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);
    }

Если пользователь не вошел в систему, полученная информация о пользователе будет нулевой.В это время на стороне брокера будет запущена процедура входа в систему, и страница перейдет к интерфейсу входа, чтобы запросить пользователя войти в систему. Проверка входа пользователя выполняется на стороне сервера, и информация о пользователе сохраняется в сеансе предыдущего идентификатора, и информация о пользователе может быть получена напрямую при следующем посещении.

   /**
     * 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();
    }

Улучшения решения

  1. Интерфейс входа развернут в брокере, что означает, что каждый брокер должен поддерживать набор логики входа. Интерфейс входа можно развернуть на стороне сервера. Когда вам нужно войти в систему, вам нужно перейти на сервер, чтобы войти , В этом случае вам нужно передать адрес для перехода после завершения входа в систему.
  2. Каждый раз при получении userInfo необходимо обращаться к серверу.Если объем доступа велик, нагрузка на сервер относительно высока. Можно изменить так, чтобы каждый брокер получал информацию о пользователе со стороны сервера только один раз, а затем сохранял ее в сеансе брокера. Однако следует отметить две вещи:
    • Когда пользователь выходит из каждого брокера, он не будет синхронизирован.Если требования выше, процедуру выхода необходимо вызывать отдельно для каждого брокера.
    • Если пользовательский Брокер и Сервер развернуты под одним и тем же доменным именем, сессия должна быть закрыта до выполнения curl_exec, а затем открыта после выполнения. В противном случае используемый сеанс не может быть запущен на сервере, что приводит к длительному ожиданию.