需求
處理 JWT
的時候如果在瀏覽器可以將 token 存在 localStorage
中的 socketcluster.authToken
但是如果在 React Native 中沒有 localStorage 的模組
可以使用 jest-localstorage-mock
來處理這個問題
但是這樣很醜
希望可以自己控制 Authorization 流程
所以去爬了一下 source code
資訊在參考資料
這部分文件沒有寫得很清楚
所以花了一個篇幅來記錄一下如何客製化 Authorization
Server
server.js
在 agOptions
中加入 authKey: SCC_AUTH_KEY
這時候就會把這個參數帶入 agServer.signatureKey
所有對外的服務可以放入同樣的 authKey
彼此就可以共用同樣的 token
Login
1 2 3 4 5 6 7 8 9
| expressApp.get('/login', async (req, res) => { const signedTokenString = await agServer.auth.signToken( myTokenData, agServer.signatureKey ) res.status(200).json({ token: signedTokenString, }) })
|
Websocket flow
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28
| agServer.setMiddleware( agServer.MIDDLEWARE_INBOUND, async (middlewareStream) => { for await (let action of middlewareStream) { let authToken = action.socket.authToken if (isEmpty(authToken)) { const notAllowError = new Error('not allow') notAllowError.name = 'InvalidActionError' action.block(notAllowError) action.request.error(notAllowError) console.log('AL: action.request.error', action.request.error) return } try { await agServer.auth.verifyToken(bearerToken, agServer.signatureKey) } catch (error) { const notAllowError = new Error('not allow') notAllowError.name = 'InvalidActionError' action.block(notAllowError) action.request.error(notAllowError) console.log('AL: action.request.error', action.request.error) return }
action.allow() } } )
|
websocket 連線上之前可以在 inbound 的 middleware 中做檢查
HTTP flow
因為 API 每個 Route 希望可以更彈性的來處理驗證問題
可以依據 Express 的一般驗證模式
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20
| const jwtverify = async (req, res, next) => { try { const bearerHeader = req.headers['authorization'] if (!bearerHeader) throw new Error('unauthorization')
const bearer = bearerHeader.split(' ') const bearerToken = bearer[1] req.user = await agServer.auth.verifyToken( bearerToken, agServer.signatureKey ) next() } catch (error) { return res.status(400).json({ message: error.message }) } }
expressApp.get('/health-check', jwtverify, (req, res) => { res.status(200).send('OK') })
|
可以在需要驗證的 route 加入 驗證的 Middleware
Client
Browser
最簡單的方式
1
| localStorage.setItem('socketcluster.authToken', token)
|
另外也可以 AuthEngine
可以自訂
source code 在參考資料中可以參考
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23
| let socket = socketClusterClient.create({ secure: false, authTokenName: "socketcluster.authToken", authEngine: { _internalStorage: { "socketcluster.authToken": ${token} }, isLocalStorageEnabled: true, saveToken: (name, token, options) => { this._internalStorage[name] = token; return Promise.resolve(token); }, removeToken: function(name) { const loadPromise = this.loadToken(name); delete this._internalStorage[name]; return loadPromise; }, loadToken: function(name) { const token = this._internalStorage[name] || null; return Promise.resolve(token); } }, });
|
secure
是否要使用 wss
authTokenName
設定 _internalStorage[name]
的 name
authEngine
可以自行定義針對 authToken
的行為
React Native
因為在 React Native 沒有 localStorage
所以無法利用 localStorage
處理
但是可以利用 jest-localstorage-mock
來建立
但是這個做法比較不優
所以會選用 AuthEngine 來處理
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32
| let socket = SocketClusterClient.create({ hostname: hostname(ip), port: 1234, secure: false, authTokenName: 'socketcluster.authToken', authEngine: { _internalStorage: { 'socketcluster.authToken': token, }, isLocalStorageEnabled: true, saveToken: (name, token, options) => { this._internalStorage[name] = token return Promise.resolve(token) }, removeToken: function (name) { const loadPromise = this.loadToken(name) delete this._internalStorage[name] return loadPromise }, loadToken: function (name) { const token = this._internalStorage[name] || null return Promise.resolve(token) }, }, })
;(async () => { let myChannel = socket.channel('myChannel') for await (let data of myChannel) { console.log('forawait -> data', data) } })()
|
這時候會發生這個問題
1 2
| TypeError: Invalid attempt to iterate non-iterable instance. In order to be iterable, non-array objects must have a [Symbol.iterator]() method.
|
oh!shit!
所以跑去研究了 Symbol.iterator
結果是因為 array like 的問題
所以找不到 [Symbol.iterator]() method
解決方案只要用 Array.from
轉換型態就可以了
所以變成
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41
| const connectSocketCluster = async () => { try { let socket = SocketClusterClient.create({ hostname: hostname(ip), port: 1234, secure: false, authTokenName: 'socketcluster.authToken', authEngine: { _internalStorage: { 'socketcluster.authToken': token, }, isLocalStorageEnabled: true, saveToken: (name, token, options) => { this._internalStorage[name] = token return Promise.resolve(token) }, removeToken: function (name) { const loadPromise = this.loadToken(name) delete this._internalStorage[name] return loadPromise }, loadToken: function (name) { const token = this._internalStorage[name] || null return Promise.resolve(token) }, }, })
;(async () => { const errorChannel = socket.listener('error') try { for await (let { error } of Array.from(errorChannel)) { } } catch (error) {} })() } catch (error) { console.log('connectSocketCluster -> error.message', error.message) } }
connectSocketCluster()
|
終於可以連上了
以及可以做基本的驗證
只要同一個 token 就可以在各個服務中聯繫
如果還要開其他的 broker
只要注意 SSC_AUTH_KEY
的一致性
就可以基本上保證彼此之間的 token 共用
參考資料
socketcluster
authengine
auth.js
iterable