WebSocket 是个好东西,为我们提供了便捷且实时的通讯能力。然而,对于 WebSocket 客户端的鉴权,协议的 RFC 是这么说的:
This protocol doesn’t prescribe any particular way that servers can
authenticate clients during the WebSocket handshake. The WebSocket
server can use any client authentication mechanism available to a
generic HTTP server, such as cookies, HTTP authentication, or TLS
authentication.
也就是说,鉴权这个事,得自己动手
WebSocket 是独立的、创建在 TCP 上的协议。
为了创建Websocket连接,需要通过浏览器发出请求,之后服务器进行回应,这个过程通常称为“握手”。
实现步骤:
1. 发起请求的浏览器端,发出协商报文:
GET /chatHTTP/1.1 Host: server.example.com Upgrade: websocket Connection: Upgrade Sec-WebSocket-Key: dGhlIHNhbXBsZSBub25jZQ== Origin: http://example.com Sec-WebSocket-Protocol: chat, superchat Sec-WebSocket-Version: 13
2. 服务器端响应101状态码(即切换到Socket协议),其报文:
HTTP/1.1 101 SwitchingProtocols Upgrade: websocket Connection: Upgrade Sec-WebSocket-Accept: s3pPLMBiTxaQ9kYGzzhZRbK+xOo= Sec-WebSocket-Protocol: chat
3. 协议切换完成,双方使用Socket通讯
直观的协商及通讯过程:
通过对协议实现的解读可知:在 HTTP 切换到 Socket 之前,没有什么好的机会进行鉴权,因为在这个时间节点,报文(或者说请求的Headers)必须遵守协议规范。但这不妨碍我们在协议切换完成后,进行鉴权授权:
服务端在连接建立时,颁发一个ticket给peer端,这个ticket可以包含但不限于:
WebSocket连接处理,基于 node.js 的 ws 实现:
importurlfrom 'url'
importWebSocketfrom 'ws'
import { Ticket } from '../models'
// server 可以是 http server实例
const wss = new WebSocket.Server({ server })
wss.on('connection', async(ws) => {
const location = url.parse(ws.upgradeReq.url, true)
const cookie = url.parse(ws.upgradeReq.cookie, true)
debugInfo('ws request from: ', location, 'cookies:', cookie)
// issue & send ticket to the peer
if (!checkIdentity(ws)) {
terminate(ws)
} else {
const ticket = issueTicket(ws)
awaitticket.save()
ws.send(ticket.pojo())
ws.on('message', (message) => {
if (!checkTicket(ws, message)) {
terminate(ws)
}
debugInfo('received: %s', message)
})
}
})
function issueTicket(ws) {
const uniqueId = ws.upgradeReq.connection.remoteAddress
return new Ticket(uniqueId)
}
asyncfunction checkTicket(ws, message) {
const uniqueId = ws.upgradeReq.connection.remoteAddress
const record = awaitTicket.get(uniqueId)
const token = message.token
return record
&& record.expires
&& record.token
&& record.token === token
&& moment(record.expires) >= moment()
}
// 身份检查,可填入具体检查逻辑
function checkIdentity(ws) {
return true
}
function terminate(ws) {
ws.send('BYE!')
ws.close()
}
授权用到的 Ticket(这里存储用到的是knex + postgreSQL):
import { utils } from '../components'
import { db } from './database'
export default class Ticket {
constructor(uniqueId, expiresMinutes = 30) {
const now = new Date()
this.unique_id = uniqueId
this.token = Ticket.generateToken(uniqueId, now)
this.created = now
this.expires = moment(now).add(expiresMinutes, 'minute')
}
pojo() {
return {
...this
}
}
asyncsave() {
return awaitdb.from('tickets').insert(this.pojo()).returning('id')
}
static asyncget(uniqueId) {
const result = awaitdb
.from('tickets')
.select('id', 'unique_id', 'token', 'expires', 'created')
.where('unique_id', uniqueId)
const tickets = JSON.parse(JSON.stringify(result[0]))
return tickets
}
static generateToken(name, now) {
const part1 = name
const part2 = now.getTime().toString()
const part3 = shortid.generate()
return utils.sha1(`${part1}:${part2}:${part3}`)
}
}