Socketcluster-client-AuthorizationEngine

需求

處理 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

文章目录
  1. 1. 需求
    1. 1.1. Server
      1. 1.1.1. Login
      2. 1.1.2. Websocket flow
      3. 1.1.3. HTTP flow
    2. 1.2. Client
      1. 1.2.1. Browser
    3. 1.3. React Native
  2. 2. 參考資料
|