websocket之四: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. 发起请求的浏览器端,发出协商报文:
| 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)必须遵守协议规范。但这不妨碍我们在协议切换完成后,进行鉴权授权:
鉴权
- 在连接建立时,检查连接的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 实现:
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')   }, } | 
引用
转自:http://www.moye.me/2017/02/10/websocket-authentication-and-authorization/
websocket之四:WebSocket 的鉴权授权方案的更多相关文章
- WebSocket 的鉴权授权方案
		引子 WebSocket 是个好东西,为我们提供了便捷且实时的通讯能力.然而,对于 WebSocket 客户端的鉴权,协议的 RFC 是这么说的: This protocol doesn’t pres ... 
- .NET Core中的鉴权授权正确方式(.NET5)
		一.简介 前后端分离的站点一般都会用jwt或IdentityServer4之类的生成token的方式进行登录鉴权.这里要说的是小项目没有做前后端分离的时站点登录授权的正确方式. 一.传统的授权方式 这 ... 
- .Net 鉴权授权
		在这里总结一下工作中遇到的鉴权和授权的方法 ① 固定token的方案 通过在nginx或者代码中写死token,或者通过在限制外网访问的方式已来达到安全授权的方式 ② session方案 分布式会话方 ... 
- Android下的鉴权实现方案
		软件原理 不赘述,参考: 软件License认证方案的设计思路 License文件离线鉴权 机械指纹,不可逆的加密算法,如MD5 功能鉴权,可逆的不对称加密算法,服务端公钥加密,app端私钥解密,如R ... 
- Session, Token, OAuth 鉴权那些事儿
		鉴权那些事 整体思路 无论什么样的服务, Web 服务总是不能绕开鉴权这个话题的, 通过有效的鉴权手段来保护网站数据, 来为特定用户提供服务. 整体来说, 有三种方式: Session-Cookie ... 
- 无线端安全登录与鉴权一之Kerberos
		无线端登录与鉴权是安全登录以及保证用户数据安全的第一步,也是最重要的一步.之前做过一个安全登录与鉴权的方案,借这个机会,系统的思考一下,与大家交流交流 先介绍一下TX系统使用的Kerberos方案,参 ... 
- .NET CORE 鉴权
		基础信息 1.什么是鉴权授权? 鉴权是验证用户是否拥有访问系统的权利,授权是判断用户是否有权限做一些其他操作. 2.传统的Session 和Cookie 主要用于无状态请求下的的用户身份识别,只不过S ... 
- http无状态和鉴权解决四种方案
		http协议本身是无状态的,但是在实际的web开发中常有一些操作需要有状态.比如想要访问一些私人访问权限的文章,或者这种操作需要明确当前用户身份. 显然,最简单的方案就是每次都发送账户和密码,但是这样 ... 
- 聊聊OkHttp实现WebSocket细节,包括鉴权和长连接保活及其原理!
		一.序 OkHttp 应该算是 Android 中使用最广泛的网络库了,我们通常会利用它来实现 HTTP 请求,但是实际上它还可以支持 WebSocket,并且使用起来还非常的便捷. 那本文就来聊聊, ... 
随机推荐
- JQuery 实现下拉列表选中
			html代码如下: <select id="category" name="category"> <option value="&q ... 
- C++中ceil、floor和round的区别
			Math类中提供了三个与取整有关的方法:ceil,floor,round,这些方法的作用于它们的英文名称的含义相对应 1.ceil的英文意义是天花板,该方法就表示向上取整,Math.ceil(11.3 ... 
- 单例模式(Singleton-Pattern)百媚生
			1 动机 对于系统中的某些类来说,只有一个实例很重要,例如,一个系统中可以存在多个打印任务,但是只能有一个正在工作的任务;一个系统只能有一个窗口管理器或文件系统;一个系统只能有一个计时工具或ID(序号 ... 
- 用JavaScript做一个小小设计
			这个项目是我无聊时完成的,参阅过很多大神的示例,其实方法并不难主要是js和css样式的设计,我发现自己还有很多的js代码写不出来更加不用提看的明白了,(PS吐槽一下:革命尚未成功,同志还需努力啊!)此 ... 
- JAVA异常处理分析高级进界(下)
			既然Throwable是异常处理机制的核心,那么,我们就来分析下它的源码来看看它是如何实现的. 进行分析前,我们可以先想想如果让我们实现一个异常处理机制,我们需要它做什么? 1. 发生异常终止程序执行 ... 
- 解决ubuntu登陆失败,"Failed to start session"的问题
			我是在虚拟机中安装了Ubuntu 14.04 系统,在Ubuntu 中执行 apt-get update 和 apt-get upgrade 命令后,然后重启系统. 但是,在重启成功后,在登 ... 
- JS object(对象)的学习汇总
			Object(对象)是在所有的编程语言中都十分重要的一个概念,对于事物我们可以把他们看作是一个对象,而每一个事物都有自己的表示的属性和对于某一信息作出的相应的操作.而这些东西就变成了事物的属性和方法. ... 
- [转]Ubuntu默认root用户密码
			Ubuntu默认root用户密码 在试验的过程中,安装完Ubuntu后忽然意识到没有设置root密码,不知道密码自然就无法进入根用户下. 到网上搜了一下,原来是这么回事:Ubuntu的默认root密码 ... 
- IIS网页GZIP压缩
			1.开GZIP有什么好处? 答:Gzip开启以后会将输出到用户浏览器的数据进行压缩的处理,这样就会减小通过网络传输的数据量,提高浏览的速度. 2.如何启用IIS的Gzip压缩功能: 答:首先,如果你需 ... 
- easyui combobox 动态加载数组数据
			怕自己忘了,记录下来以后用方便 html部分 <input id="rzcode" name="businesItemId" style="wi ... 
