转载

WebSocket 的鉴权授权方案

引子

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通讯

直观的协商及通讯过程:

WebSocket 的鉴权授权方案

方案

通过对协议实现的解读可知:在 HTTP 切换到 Socket 之前,没有什么好的机会进行鉴权,因为在这个时间节点,报文(或者说请求的Headers)必须遵守协议规范。但这不妨碍我们在协议切换完成后,进行鉴权授权:

鉴权

  1. 在连接建立时,检查连接的HTTP请求头信息(比如cookies中关于用户的身份信息)
  2. 在每次接收到消息时,检查连接是否已授权过,及授权是否过期
  3. 以上两点,只要答案为否,则服务端主动关闭socket连接

授权

服务端在连接建立时,颁发一个ticket给peer端,这个ticket可以包含但不限于:

  • peer端的uniqueId(可以是ip,userid,deviceid…任一种具备唯一性的键)
  • 过期时间的timestamp
  • token:由以上信息生成的哈希值,最好能加盐

代码实现

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}`)
  }
}

引用

  1. https://devcenter.heroku.com/articles/websocket-security
  2. https://tools.ietf.org/html/rfc6455
  3. https://zh.wikipedia.org/wiki/WebSocket
原文  http://www.moye.me/2017/02/10/websocket-authentication-and-authorization/
正文到此结束
Loading...