首頁  >  文章  >  php框架  >  踩坑分享:Laravel整合phpCAS流程

踩坑分享:Laravel整合phpCAS流程

藏色散人
藏色散人轉載
2021-09-19 16:53:491771瀏覽

下面由Laravel教學專欄給大家分享一個Laravel 整合 phpCAS 踩坑記,希望對需要的朋友有幫助!

Laravel 整合phpCAS 踩坑記

#CAS 是目前比較流行的單一登入協議,官方提供了php 版本的client 端phpCAS,目前為止其程式設計風格還一直停留在PEAR 時代,連命名空間都沒有使用。好在phpCAS 支援composer 引入,做過幾個Laravel 專案引入也沒有什麼問題,然而這兩天有一個專案需要從單機部署變成多機部署,萬萬沒想到在這裡踩了一些坑,在此記錄一下。

回呼坑

在跳到 CAS Server 進行認證時發現,傳入的回呼位址被加上了連接埠8080。因為是多機部署,所以存取請求會先經過負載平衡器(阿里雲 SLB),再到達 web 伺服器,而這個8080是 web 伺服器的監聽埠。

於是追蹤phpCAS 產生回呼位址的邏輯,發現有這麼一段程式碼:

if (empty($_SERVER['HTTP_X_FORWARDED_PORT'])) {
    $server_port = $_SERVER['SERVER_PORT'];
} else {
    $ports = explode(',', $_SERVER['HTTP_X_FORWARDED_PORT']);
    $server_port = $ports[0];
}

而阿里雲的SLB 並不會傳給後端伺服器X-FORWARDED-PORT 這個http 頭,所以phpCAS 就會拿到$_SERVER['SERVER_PORT'] 也就是nginx 的埠8080。

好在phpCAS 提供了setFixedServiceURL 函數,可以讓我們手動去設定回呼位址:

phpCAS::setFixedServiceURL($request->url());

這下回呼位址正常了,但是從CAS Server 返回到client 端時被告知ticket 無效。

繼續檢查日誌和程式碼,發現這裡是自己疏忽了,當CAS Server 返回client 端時頁面的url 是http://client/login?ticket=xxxxx,而client 端使用ticket 向server 換取使用者資訊時也需要帶申請該ticket 時的回呼位址(service),server 端會校驗ticket 和service 是否一致,而申請ticket 時的service 應該是http:/ /client/login,因此我們需要把url 裡的ticket 參數去掉。

phpCAS::setFixedServiceURL($this->getUrlWithoutTicket($request));

getUrlWithoutTicket 函數如下:

private function getUrlWithoutTicket(Request $request)
{
    $query = parse_query($request->getQueryString());
    unset($query['ticket']);
    $question = $request->getBaseUrl().$request->getPathInfo() == '/' ? '/?' : '?';

    return $query ? $request->url().$question.http_build_query($query) : $request->url();
}

Session 坑

這是一個 phpCAS Laravel 的組合坑,坑得死去活來沒脾氣。

PHP 預設是 Session 儲存方式是文件,因此單機變多機一個很重要的點就是處理 Session 共用。方案也很簡單,就是把 Session 儲存方式從檔案改成 redis/memecache/database 等。

Laravel 預設提供了這些 driver,於是興沖沖地改了下 .env 文件,把 SESSION_DRIVER 改成 redis。拉到線上一試,發現不行,phpCAS 對 $_SESSION 變數的變更並沒有被寫到 redis 裡,怎麼回事!

於是追了一下Laravel 的Session 實現,發現並不是想像中的使用session_set_save_handler 來註冊Session 讀寫邏輯,也就是說Laravel 的Session 其實並沒有修改php 的$_SESSION 的讀寫邏輯,直接操作$_SESSION 還是走的預設行為(讀寫本機檔案)。

那好吧,好在Laravel 的幾個SessionDriver 都實作了SessionHandlerInterface 接口,我們可以自己呼叫一下session_set_save_handler

session_set_save_handler(app(StartSession::class)->getSession($request)->getHandler());

萬萬沒想到要報錯!

session_write_close(): Session callback expects true/false return value

追了一下Laravel 的程式碼,發現redis driver 的父類別Illuminate\Session\CacheBasedSessionHandlerwrite 方法傳回的是void## 。於是提了一個 PR 打算修一下,沒想到被拒絕,原來是之前有人修過又被 revert 了,說是會導致服務器卡住,然而我並沒有找到具體的 issue。

那好吧,memcache 和 redis 都是繼承的這個父類,那我就換只好 database 試試看。

這回 session_write_close 不報錯了,但是 CAS 登入還是有問題,不斷在 CAS server 和回呼 url 之間跳轉。於是又追了一路log 和程式碼,發現database driver 類別Illuminate\Session\DatabaseSessionHandlerdestroy 方法在銷毀Session 之後沒有將$this->exists 屬性標記為false,而phpCAS 有一處邏輯是renameSession

$old_session = $_SESSION;
session_destroy();
$session_id = preg_replace('/[^a-zA-Z0-9\-]/', '', $ticket);
session_id($session_id);
session_start();
$_SESSION = $old_session;

後果就是$_SESSION = $old_session;  所對應操作session表的sql 執行的是update 而不是insert,也就是沒能將session 資料寫入session 表!

實在沒辦法了,只能自己寫一個 Session Wrapper 來處理。

從上面兩個情況來看,redis driver 比較好處理,只要能在呼叫 write 方法時回傳 true 就可以了。所以程式碼如下

namespace App\Services;

use SessionHandlerInterface;

class MySession implements SessionHandlerInterface
{
    /**
     * @var SessionHandlerInterface
     */
    protected $realHdl;

    /**
     * Session constructor.
     * @param SessionHandlerInterface $realHdl
     */
    public function __construct(SessionHandlerInterface $realHdl)
    {
        $this->realHdl = $realHdl;
    }

    public function close()
    {
        return $this->realHdl->close();
    }

    public function destroy($session_id)
    {
        return $this->realHdl->destroy($session_id);
    }

    public function gc($maxlifetime)
    {
        return $this->realHdl->gc($maxlifetime);
    }

    public function open($save_path, $name)
    {
        return $this->realHdl->open($save_path, $name);
    }

    public function read($session_id)
    {
        return $this->realHdl->read($session_id) ?: '';
    }

    public function write($session_id, $session_data)
    {
        $this->realHdl->write($session_id, $session_data);

        return true; // 这里
    }
}

然後呼叫 session_set_save_handler 變成

session_set_save_handler(new MySession(app(StartSession::class)->getSession($request)->getHandler()));

Done !

以上是踩坑分享:Laravel整合phpCAS流程的詳細內容。更多資訊請關注PHP中文網其他相關文章!

陳述:
本文轉載於:segmentfault.com。如有侵權,請聯絡admin@php.cn刪除