zoukankan      html  css  js  c++  java
  • 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 /chat HTTP/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 Switching Protocols
    Upgrade: websocket
    Connection: Upgrade
    Sec-WebSocket-Accept: s3pPLMBiTxaQ9kYGzzhZRbK+xOo=
    Sec-WebSocket-Protocol: chat

    3、协议切换完成,双方使用Socket通讯

      直观的协商及通讯过程:

    方案

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

    鉴权

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

    授权

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

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

    安全性的补充说明

    有朋友问这一套机制如何防范重放攻击,私以为可以从以下几点出发:

    • 可以用这里提到的expires,保证过期,如果你愿意,甚至可以每次下发消息时都发送一个新的Ticket,只要上传消息对不上这个Ticket,就断开,这样非Original Peer是没法重放的
    • 可以结合redis,实现 ratelimit,防止高频刷接口,这个可以参考 express-rate-limit,原理很简单,不展开
    • 为防止中间人,最好使用wss(TLS)

    代码实现

    WebSocket连接处理,基于 node.js 的 ws 实现:

    import url from 'url'
    import WebSocket from 'ws'
    import debug from 'debug'
    import moment from 'moment'
    import { Ticket } from '../models'
     
    const debugInfo = debug('server:global')
     
    // server 可以是 http server实例
    const wss = new WebSocket.Server({ server })
    wss.on('connection', async(ws) => {
      const location = url.parse(ws.upgradeReq.url, true)
      const cookie = ws.upgradeReq.cookie
      debugInfo('ws request from: ', location, 'cookies:', cookie)
     
      // issue & send ticket to the peer
      if (!checkIdentity(ws)) {
        terminate(ws)
      } else {
        const ticket = issueTicket(ws)
        await ticket.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)
    }
     
    async function checkTicket(ws, message) {
      const uniqueId = ws.upgradeReq.connection.remoteAddress
      const record = await Ticket.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 shortid from 'shortid'
    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
        }
      }
     
      async save() {
        return await db.from('tickets').insert(this.pojo()).returning('id')
      }
     
      static async get(uniqueId) {
        const result = await db
          .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(uniqueId, now) {
        const part1 = uniqueId
        const part2 = now.getTime().toString()
        const part3 = shortid.generate()
        return utils.sha1(`${part1}:${part2}:${part3}`)
      }
    }

    utils 的哈希方法:

    import crypto from 'crypto'
     
    export default {
      sha1(str) {
        const shaAlog = crypto.createHash('sha1')
        shaAlog.update(str)
        return shaAlog.digest('hex')
      },
    }

    转载来源:http://www.moye.me/2017/02/10/websocket-authentication-and-authorization/



  • 相关阅读:
    MSSQL大量数据时,建立索引或添加字段后保存更改超时该这么办
    POJ 3261 Milk Patterns (后缀数组)
    POJ 1743 Musical Theme (后缀数组)
    HDU 1496 Equations (HASH)
    694. Distinct Substrings (后缀数组)
    POJ 1222 EXTENDED LIGHTS OUT (枚举 或者 高斯消元)
    POJ 1681· Painter's Problem (位压缩 或 高斯消元)
    POJ 1054 The Troublesome Frog (hash散列)
    HDU 1716 排列2
    HDU 4405 Aeroplane chess (概率DP & 期望)
  • 原文地址:https://www.cnblogs.com/taich-flute/p/11971557.html
Copyright © 2011-2022 走看看