websocket之四:WebSocket 的鉴权授权方案

摘要:
服务器实例com升级:132。服务器响应101状态代码(即,切换到套接字通信模式):chat3。协议切换完成:在HTTP切换到套接字之前:检查连接的HTTP请求头信息(例如cookie中的用户身份信息)。每次收到消息时,服务器都会主动关闭套接字连接。当建立连接时,服务器授权服务器向对等方发出票证。

引子

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.1101Switching Protocols
Upgrade:websocket
Connection:Upgrade
Sec-WebSocket-Accept:s3pPLMBiTxaQ9kYGzzhZRbK+xOo=
Sec-WebSocket-Protocol:chat

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

直观的协商及通讯过程:

websocket之四:WebSocket 的鉴权授权方案第1张

方案

通过对协议实现的解读可知:在 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'
 
constdebugInfo=debug('server:global')
 
// server 可以是 http server实例
constwss=newWebSocket.Server({server})
wss.on('connection',async(ws)=>{
  constlocation=url.parse(ws.upgradeReq.url,true)
  constcookie=ws.upgradeReq.cookie
  debugInfo('ws request from: ',location,'cookies:',cookie)
 
  // issue & send ticket to the peer
  if(!checkIdentity(ws)){
    terminate(ws)
  }else{
    constticket=issueTicket(ws)
    await ticket.save()
    ws.send(ticket.pojo())
 
    ws.on('message',(message)=>{
      if(!checkTicket(ws,message)){
        terminate(ws)
      }
      debugInfo('received: %s',message)
    })
  }
})
 
functionissueTicket(ws){
  constuniqueId=ws.upgradeReq.connection.remoteAddress
  returnnewTicket(uniqueId)
}
 
async functioncheckTicket(ws,message){
  constuniqueId=ws.upgradeReq.connection.remoteAddress
  constrecord=await Ticket.get(uniqueId)
  consttoken=message.token
  returnrecord
    &&record.expires
    &&record.token
    &&record.token===token
    &&moment(record.expires)>=moment()
}
 
// 身份检查,可填入具体检查逻辑
functioncheckIdentity(ws){
  returntrue
}
 
functionterminate(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'
 
exportdefaultclassTicket{
  constructor(uniqueId,expiresMinutes=30){
    constnow=newDate()
    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(){
    returnawait db.from('tickets').insert(this.pojo()).returning('id')
  }
 
  staticasync get(uniqueId){
    constresult=await db
      .from('tickets')
      .select('id','unique_id','token','expires','created')
      .where('unique_id',uniqueId)
    consttickets=JSON.parse(JSON.stringify(result[0]))
    returntickets
  }
 
  staticgenerateToken(uniqueId,now){
    constpart1=uniqueId
    constpart2=now.getTime().toString()
    constpart3=shortid.generate()
    returnutils.sha1(`${part1}:${part2}:${part3}`)
  }
}

utils 的哈希方法:

 
 
 
 
 
JavaScript
 
1
2
3
4
5
6
7
8
9
import crypto from'crypto'
 
exportdefault{
  sha1(str){
    constshaAlog=crypto.createHash('sha1')
    shaAlog.update(str)
    returnshaAlog.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 的鉴权授权方案》仅用于学习参考。如对内容有疑问,请及时联系本站处理。

上篇微信小程序上传Word文档、PDF、图片等文件vue el-table 自适应表格内容宽度下篇

宿迁高防,2C2G15M,22元/月;香港BGP,2C5G5M,25元/月 雨云优惠码:MjYwNzM=

相关文章

jwt token

jwt使用配置: settings文件中添加: REST_FRAMEWORK = { # 异常处理 自定义的异常处理类 'EXCEPTION_HANDLER': 'drf_meiduo.utils.exceptions.exception_handler', 'DEFAULT_AUTHENTICATION_CLASSES': (...

Ruby语法基础(二)

Ruby语法基础(二) 继续ruby的学习,这次主要把目光放到运算符,条件判断,循环,方法,以及其他ruby特有的基本概念上 运算符 算术运算符:+,-,/,%,**,值的注意的是,ruby中一切皆为对象,a+b等价于a.+(b) 比较运算符:基本与Python的一致,不等于要用!=,还有联合运算符<=>,返回-1,0,1。.eql?判断数据...

poco脚本编写之api

连接设备后使用poco   使用connect_device连接好指定设备后,会返回一个Device对象,将这个对象传入 AndroidUiautomationPoco第一个参数里进行poco的初始化,   接下来使用此poco 实例将会获取所指定的设备的UI和对其进行操作。      from airtest.core.api import connec...

怎么在vue中引入layui

新项目想用layui框架,学习了把前辈是怎么引入layui的,这里记录下 1.index.html要引入layui.js文件 <script src="/static/layui/layui.js" type="text/javascript" charset="utf-8"></script> 2.main.js文件要配置好lay...

JavaScript核心之事件详解(EventTarget接口,js事件传播,Event对象)

事件是一种异步编程的实现方式,本质上是程序各个组成部分之间传递的特定消息。DOM支持大量的事件,本节介绍DOM的事件编程。 1 EventTarget接口DOM的事件操作(监听和触发),都定义在EventTarget接口。Element节点、document节点和window对象,都部署了这个接口。此外,XMLHttpRequest、AudioNode、A...

easyUI-增删改查

页面整体效果 首先是页面层: 因为数据中涉及到Date类型的字段,所以在一开始先做了一个拼装装换日期格式的方法 因为在easyUI的JSON解析的Data是一个long型的数字长串,关于解析的数字长串具体含义,自己查,不做多说。 让后定义点击按钮的初始情况为添加“add”,用于下面的判断,到底是添加还是修改,因为这里公用了一个弹出对话框 下面就是具体显...