用 Socket.IO + Passport-jwt + Express.js 驗證 Bearer Token
一開始會接觸 Socket.IO 是因為 AlphaCamp 的作業專案中,有個加入聊天室功能的挑戰。
在該專案中,我的小組是使用前後端分離的方式作業,並且會用 Bearer Token 作為身分認證憑證。
加入 Socket.IO 的話,勢必也要通過同樣方式進行憑證認證。
實作時遇到的最大的困難點就是,要如何在 Socket.IO 傳回的訊息中,帶上 Bearer Token 的資訊。
以及在 Passport-jwt 中,要怎麼接到那個 Token。
前置
本篇介紹適用於已初步了解 Node.js、Express.js、Passport-jwt 的讀者。
本篇假設讀者已經會使用 Passport-jwt 的 Strategy 和 Passport 的 authenticate 進行驗證。
請確保已安裝 Node.js、Express.js、Passport-jwt 和 Socket.IO。
關於建立 Socket.IO 的伺服器,請參考此篇文章:使用 Socket.io + Express.js 建立聊天伺服器
步驟
1. 建立讓 Socket.IO 能使用 express middleware 的 helper
首先新增一個 socketio-helpers.js 檔:
1 | module.export = middleware => (socket, next) => middleware(socket.request, {}, next) |
沒錯,就是這麼簡潔的一行。
我個人習慣把小工具都另外拆成一個 .js 檔,喜歡的話也可以直接宣告一個 function 或變數。
要說明運作原理要從 socket.io 的 middleware 參數說起:
1 | io.use((socket, next) => { |
socket.io 的 middleware 要傳入的參數為 (socket, next) ,而 express 的 middleware 要傳入的參數為 (req, res, next)。
我們在 helpers 中傳入我們要轉換給 socket.io 使用的 middleware,而 io.use 會傳入 (socket, next) ,接著我們要處理傳入的資訊,讓它們符合 express middleware 所需要的 (req, res, next) 。
- req:
socket.request裡面帶有來自 client 端的 request reference - res:這裡沒有東西,放入空物件
- next:直接使用 socket.io 傳入的
next
最後返回的就會是正確裝著 (req, res, next) 的 express middleware 了。
2. 在 client 端連線時加入 Bearer Token 的資訊
我們先找到設定 socket.io 連線的地方,改成這樣:
1 | const token = 'eyJ...' |
這樣每次 socket.io 進行 emit 等連線的時候,就會在資訊中帶上 token 資料。
他會把資料放在回傳的 socket.handshake.auth 當中。
這時我們需要了解一下平常 Bearer Token 都會放在哪裡。
在我們進行的專案中,passport-jwt 設定取得 token 的方法為 fromAuthHeaderAsBearerToken() 。
試著輸出 req.headers 的結果我們會看到這樣的內容:
1 | { |
從 passport 官網中有描述到,取得 token 的方法是可以自定義的。
可是我們要交給 passport-jwt 進行解析的是 req ,也就是 socket.request ,取到的不是 socket.handshake 也不是 socket 啊!
可是我在搜尋的時候看到有 extraHeaders 的寫法?
在許多資料內都提到可以改寫成類似這樣的格式:
1 | const token = 'eyJ...' |
很遺憾的是,雖然號稱是附加 headers,但他會放在 socket.handshake.headers 裡面,而不是 req.headers 中。
此外,如果只啟用 WebSocket的傳輸方式,extraHeaders 的選項將被忽略,因為 WebSocket API 不允許提供自定義 headers。
不過,在 Node.js 或 React-Native 中會正常運作。
在這裡我卡了很久,到底要怎麼取得 token。
最後我想到了,req.headers 裡面沒有 token 的話就自己塞 token 進去就好了。
3. 加工 helper
我們把先前寫的 helper 做點修改:
1 | module.export = middleware => (socket, next) => { |
這樣就能用 passport-jwt 的fromAuthHeaderAsBearerToken() 接到 token 了!
4. 把 authenticate 放進去
最後,我們來為 socket.io 加上 middleware 吧!
1 | const passport = reqire('./config/passport') |
這裡預設已經有寫好 passport 的 Strategy ,關於 Strategy 的設定方法可參考 passport 官網。
順利的話,這裡應該能再次看到 WebSocket 連線成功的訊息,而拿掉中間驗證用的 middleware 就會看不到訊息。
注意,由於執行 middleware 時,傳入的 socket 並沒有實際連線上 socket.io 的伺服器,如果在 middleware 間的連接失敗的話,原先要 socket 執行的動作就不會執行。
比如說,client 端傳了 socket.disconnect() 想要結束連線,若是 middleware 間連接失敗,便不會執行結束連線的動作。
這樣我們就完成了 socket.io 用 passport-jwt 驗證的寫法囉!