在现代网页中,WebSocket连接扮演着越来越重要的角色,尤其是在需要即时通讯的场景中。本文将分享如何在 Django 中实现 WebSocket 连接的安全性。
作者:Natlee
项目地址:https://github.com/NatLee/telepy
最近,我有一个side project Telepy 使用到了web terminal,而这个terminal是基于WebSocket连接的
在这个过程中,我发现了一个问题:WebSocket连接的安全性。
第一次踩坑
一开始,我只是简单地使用了Django Channels来实现WebSocket连接。
但是这样做有一个安全隐忧:连接的建立过程中,没有任何认证机制!
这意味着任何人都可以通过WebSocket连接到我的server,这是非常危险的!
于是我开始思考该如何在WebSocket连接中实现认证。
然后我把目光转向了URL Query String,我很快就把这个功能实现出来,但这个方法有一个致命的缺点!
URL Query String是明文的!
第二次踩坑
有了第一次的经验,我又思考了一下。
如果URL不行的话,是不是有方法能够像HTTP一样传递Header?
于是我找到了WebSocket Subprotocol。
什么是Subprotocol?
Subprotocol是WebSocket的一个栏位,它允许使用者和伺服器在建立连接时传递一些参数,这些参数通常是拿来告诉伺服器接下来的传输要使用什么协议。
例如,我可以跟伺服器说:我要使用JSON
协议来传输资料,这样伺服器就会知道要JSON
用来解析资料
这个功能在WebSocket连接中非常有用,而且也很安全!
因为Subprotocol在建立连接时就会被传递,这个传递过程会被WSS保护,所以不用担心中间人攻击
但违背这个功能的初衷,我们可以利用Subprotocol来传递一些认证资讯。
用力更新一波
这次在专案中的更新主要集中在TerminalConsumer
中
里面引入了WebSocket Subprotocol的使用,这个改变主要影响了连接的建立过程,特别是在认证和参数传递方面。
引入Subprotocol
更新后的程式码码现在会检查WebSocket连接请求中的Subprotocol:
if self.scope['subprotocols']:
for protocol in self.scope['subprotocols']:
if protocol.startswith('token.'):
base64_encoded_token = protocol.split('.', 1)[1]
token = base64.b64decode(base64_encoded_token).decode()
elif protocol.startswith('server.'):
server_id = protocol.split('.', 1)[1]
elif protocol.startswith('username.'):
username = protocol.split('.', 1)[1]
elif protocol.startswith('auth.'):
subprotocol_auth = protocol
可以注意到,使用Django Channels的self.scope['subprotocols']
可以获取到Subprotocol的资讯
参数检查
在更新后的程式码中,我们会检查Subprotocol中的参数是否合法:
if not token or not server_id or not username or not subprotocol_auth:
logger. error ( "Missing required subprotocols" )
await self . close (code= 4000 )
return
如果参数不合法,连接会被强制关闭。
接受连接
在检查完参数后,我们就可以在后端接受连接了:
await self.accept(subprotocol=subprotocol_auth)
这边的subprotocol_auth
就是我们在Subprotocol中传递的认证资讯。
如果我们没有在self.accept()
中传入subprotocol
的话,所有的WS连接都会被后端拒绝。
因为对于subprotocol
来说,这本来就是跟后端沟通用的协议。
前端的更新
在前端,这是原先的WebSocket连接的建立过程:
const ws = new WebSocket ( ` ${ws_scheme} :// ${ window .location.host} /ws/terminal/?token= ${token} &server_id= ${server_id} &username= ${username} ` );
这样的连接方式是不安全的
因为token
会被暴露在URL中!
因此,我们需要将这些资讯放到Subprotocol中:
// 连接不再使用URL Query String
const ws_path = ` ${ws_scheme} :// ${ window .location.host} /ws/terminal/` ;
// 将需要的资讯放到Subprotocol中
const tokenInfo = `token . ${btoa(accessToken)} ` ;
const serverInfo = `server. ${serverID} ` ;
const usernameInfo = `username. ${username} ` ;
// 这边的ticket是一个自定义的token,用来做subprotocol的协议名称
// 使用SHA256去避免特殊符号,例如`=`
let ticket = sha256 ( ` ${serverID} . ${username} ` );
ticket = `auth. ${ticket} ` ;
const socket = new WebSocket (ws_path, [tokenInfo, serverInfo, usernameInfo, ticket]);
如此一来,就解决了URL Query String的问题,也增加了连接的安全性
前端会将这些资讯放到Subprotocol中的sec-websocket-protocol
,然后在连接时传递给后端
连接完成后,后端会选择协议跟前端使用Websocket沟通!
这边再次提醒! Subprotocol中不能有特殊字元(例如转base64常常结尾出现的=符号),否则会导致连接失败!
在开发者工具中,可以看到Subprotocol连接的资讯:
结语
这次在side project的更新学习了如何使用Subprotocol来增加WebSocket连接的安全性。
这个方法非常适合在需要认证的场景中使用,并且也很容易实现。
要注意的地方是,Subprotocol的使用需要前后端的配合,因此在更新时要注意前后端的一致性。
而且栏位的长度也有限制,因此在使用时要注意栏位的长度!
不能直接把整个JWT塞进去,这样会导致Subprotocol的长度过长。
参考资料
- Django Channels
- WebSocket Subprotocol
版权声明:本文内容转自互联网,本文观点仅代表作者本人。本站仅提供信息存储空间服务,所有权归原作者所有。如发现本站有涉嫌抄袭侵权/违法违规的内容, 请发送邮件至1393616908@qq.com 举报,一经查实,本站将立刻删除。