引子
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通讯
直观的协商及通讯过程:
方案
通过对协议实现的解读可知:在 HTTP 切换到 Socket 之前,没有什么好的机会进行鉴权,因为在这个时间节点,报文(或者说请求的Headers)必须遵守协议规范。但这不妨碍我们在协议切换完成后,进行鉴权授权:
鉴权
- 在连接建立时,检查连接的HTTP请求头信息(比如cookies中关于用户的身份信息)
- 在每次接收到消息时,检查连接是否已授权过,及授权是否过期
- 以上两点,只要答案为否,则服务端主动关闭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 实现:
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):
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 的哈希方法:
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') }, } |
引用
转自:http://www.moye.me/2017/02/10/websocket-authentication-and-authorization/