java websocket 认证_websocket之四:WebSocket 的鉴权授权方案

引子

WebSocket 是个好东西,为我们提供了便捷且实时的通讯能力。然而,对于 WebSocket 客户端的鉴权,协议的 RFC 是这么说的:

This protocol doesn’t prescribe any particular way that servers canauthenticate clients during the WebSocket handshake. The WebSocketserver can use any client authentication mechanism available to ageneric HTTP server, such as cookies, HTTP authentication, or TLSauthentication.

也就是说,鉴权这个事,得自己动手

协议原理

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通讯

直观的协商及通讯过程:

9c7462035dd138dc10b3c5f9fe0753c2.png

方案

通过对协议实现的解读可知:在 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'

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')

},

}

引用

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

  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值