引子

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. 发起请求的浏览器端,发出协商报文:

 
 
1
2
3
4
5
6
7
8
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通讯方式),其报文:

 
 
1
2
3
4
5
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 实现:

 
 
 
 
 

JavaScript

 
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
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
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):

 
 
 
 
 

JavaScript

 
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
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 的哈希方法:

 
 
 
 
 

JavaScript

 
1
2
3
4
5
6
7
8
9
import crypto from 'crypto'
 
export default {
  sha1(str) {
    const shaAlog = crypto.createHash('sha1')
    shaAlog.update(str)
    return shaAlog.digest('hex')
  },
}

引用

    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/

websocket之四:WebSocket 的鉴权授权方案的更多相关文章

  1. WebSocket 的鉴权授权方案

    引子 WebSocket 是个好东西,为我们提供了便捷且实时的通讯能力.然而,对于 WebSocket 客户端的鉴权,协议的 RFC 是这么说的: This protocol doesn’t pres ...

  2. .NET Core中的鉴权授权正确方式(.NET5)

    一.简介 前后端分离的站点一般都会用jwt或IdentityServer4之类的生成token的方式进行登录鉴权.这里要说的是小项目没有做前后端分离的时站点登录授权的正确方式. 一.传统的授权方式 这 ...

  3. .Net 鉴权授权

    在这里总结一下工作中遇到的鉴权和授权的方法 ① 固定token的方案 通过在nginx或者代码中写死token,或者通过在限制外网访问的方式已来达到安全授权的方式 ② session方案 分布式会话方 ...

  4. Android下的鉴权实现方案

    软件原理 不赘述,参考: 软件License认证方案的设计思路 License文件离线鉴权 机械指纹,不可逆的加密算法,如MD5 功能鉴权,可逆的不对称加密算法,服务端公钥加密,app端私钥解密,如R ...

  5. Session, Token, OAuth 鉴权那些事儿

    鉴权那些事 整体思路 无论什么样的服务, Web 服务总是不能绕开鉴权这个话题的, 通过有效的鉴权手段来保护网站数据, 来为特定用户提供服务. 整体来说, 有三种方式: Session-Cookie ...

  6. 无线端安全登录与鉴权一之Kerberos

    无线端登录与鉴权是安全登录以及保证用户数据安全的第一步,也是最重要的一步.之前做过一个安全登录与鉴权的方案,借这个机会,系统的思考一下,与大家交流交流 先介绍一下TX系统使用的Kerberos方案,参 ...

  7. .NET CORE 鉴权

    基础信息 1.什么是鉴权授权? 鉴权是验证用户是否拥有访问系统的权利,授权是判断用户是否有权限做一些其他操作. 2.传统的Session 和Cookie 主要用于无状态请求下的的用户身份识别,只不过S ...

  8. http无状态和鉴权解决四种方案

    http协议本身是无状态的,但是在实际的web开发中常有一些操作需要有状态.比如想要访问一些私人访问权限的文章,或者这种操作需要明确当前用户身份. 显然,最简单的方案就是每次都发送账户和密码,但是这样 ...

  9. 聊聊OkHttp实现WebSocket细节,包括鉴权和长连接保活及其原理!

    一.序 OkHttp 应该算是 Android 中使用最广泛的网络库了,我们通常会利用它来实现 HTTP 请求,但是实际上它还可以支持 WebSocket,并且使用起来还非常的便捷. 那本文就来聊聊, ...

随机推荐

  1. 【转载】linux fork死循环炸弹及其预防

    转自linux fork死循环炸弹及其预防 在Linux系统下执行这段代码 :(){ :|:& }:: 就会引起死机,一旦执行起来后,唯一的方法就是重启系统.实际上这段代码是一段无限递归代码, ...

  2. java set初始化问题

    set在执行add方法时,多次报空指针异常,后来发现Set初始化时,如果是 Set<Type> set = null; 这样的话,在执行 set.add(element)的时候会报空指针异 ...

  3. [ccf 4] 网络延时

    网络延时 问题描述 给定一个公司的网络,由n台交换机和m台终端电脑组成,交换机与交换机.交换机与电脑之间使用网络连接.交换机按层级设置,编号为1的交换机为根交换机,层级为      1.他的交换机都连 ...

  4. ansible入门02

    1.常用模块 1.1 group模块 添加或删除组             name=             state=:present(添加),absent(删除)             sy ...

  5. Web 端屏幕适配方案

    基础知识 像素相关 1.像素 :像素是屏幕显示最小的单位. 2.设备像素 :设备像素又称物理像素(physical pixel),设备能控制显示的最小单位,我们可以把这些像素看作成显示器上一个个的点. ...

  6. maven+jmeter+jenkins集成

    马上要国庆了,最近比较忙,但是感觉忙的效率很一般,之前写过ant的集成,这两天研究了下maven,其中核心的插件便是jmeter-maven-plugin,要想了解更多的朋友,可以自行去官网wiki学 ...

  7. Jenkins用户权限管理

    一.插件安装 插件:Role-based Authorization Strategy版本:2.3.2 二.全局安全配置 进入Jenkins后点击系统管理进入全局安全配置 当插件安装好的时候,授权策略 ...

  8. VMwarePlayer虚拟机下centos6的静态IP配置

    1. 把VMwarePlayer生成的网络适配器 VMware Network Adapter VMnet1 设置成自动获取IP地址. 2.在VMwarePlayer中选择对应的虚拟机,点击edit ...

  9. Java其他API介绍

    有一些类虽然不像集合.多线程.网络编程中的类那样属于Java中的核心类,但是它们在开发过程中给我们带来很多便利,这里就对它们做下简要的介绍和演示. 一.System类 System类中的构造方法是私有 ...

  10. BZOJ5312: 冒险【线段树】【位运算】

    Description Kaiser终于成为冒险协会的一员,这次冒险协会派他去冒险,他来到一处古墓,却被大门上的守护神挡住了去路,守护神给出了一个问题, 只有答对了问题才能进入,守护神给出了一个自然数 ...