一、说明
1、本次要实现的功能是一个账号只能登录一次,第二次登录会被挤下,像QQ一样。场景:浏览器A或者电脑A登录admin账号,浏览器B或者电脑B也登录admin账号,则浏览器A或者电脑A会收到其他地方登录的提示并退出系统。
2、本系统是采用token认证方式,登录成功后后端生成token传到前端然后保存到cookie中,同一个浏览器不管n个窗口打开都是用的同一个token(即一个窗口是登录状态,其他n个窗口保持一致)后端socket接连者用全局变量保存:[{连接者钩子函数地址: username}]
3、django本身实现websocket目前最好的方式是使用频道channels,但是本人没有找到与之匹配的前端实现websocket的插件(求找)。比如vue-socket.io插件(可以搭配flask或者tornado使用)。所以本次前端使用的是原生websocket。会出现很多问题。如下:vue-socket.io不会出现这些问题。
(1)、正常的系统应该满足如下条件:
- 浏览器A登录,登录成功后F5刷新界面,要保证socket不会断开且能正常使用系统
- 浏览器A登录,浏览器B登录相同账号,前者会收到提示退出系统,再次登录另一方也会收到。即交叉登录另一方都能正常收到提示正常退出系统且登录者能正常使用系统。
- 前端点击正常退出按钮或者token过期得通知后端做好断开连接事宜,做到前后统一。
- 说明:直接关闭浏览器和F5刷新会自动触发后端进行断开连接的,而且也只有这两个条件是自动触发后端断开的(据我所知)。如果写代码实现会比较难,这也是使用三方库好处。
- 正常满足上面条件基本就是一个正常的系统了。但是作为开发人员要保证你的系统很健壮,得满足很多异同的条件。比如下面就有一个:
- 1)、浏览器A正常登录后,又回退到登录重新再登录一次。2)、浏览器A正常登录后,另外开一个窗口再次登录,另外开N个窗口再次登录。
- 说明:市面上系统基本不会存在1)和2)列出来的问题。可能大部分不涉及到websocket。所以不用额外做啥。如果涉及到websocket那就要保证socket正常连接问题了,尤其是N个窗口,如果在另外的浏览器登录,那么这N个窗口都应收到提示信息并退出登录。
(2)、之所以列出上面 . 出现的问题,因为本次做的都存在这些问题。最终都要实现。可能因为前端使用的原生websocket缘故吧。
(3)、如果使用插件vue-socket.io搭配后端,都不会出现上述的问题,这些问题插件自己默认就解决了,所以不推荐vue使用原生websocket。由于本人没有找到django-channels匹配vue-socket.io路由的办法,所以就用了原生websocket了。
二、后端实现
1、创建django项目(quotationBackend)和App, 安装:pip install channels。
2、需要在seting.py里配置,将我们的channels加入INSTALLED_APP里。
INSTALLED_APPS = [
'django.contrib.admin',
'django.contrib.auth',
'django.contrib.contenttypes',
'django.contrib.sessions',
'django.contrib.messages',
'django.contrib.staticfiles',
'user',
......
'channels'
]
ASGI_APPLICATION = "quotationBackend.routing.application"
routing为创建的路由文件。并在里面写上路由,这里路由比价简单
websocket_url = [
path(r"ws/", UserWebSocket.as_asgi()),
]
3、开始写连接函数UserWebSocket,具体逻辑看自己
import copy
from channels.generic.websocket import WebsocketConsumer
socket_list = list()
class UserWebSocket(WebsocketConsumer):
def para_get(self):
"""获取参数,获取方式自己规定"""
para = self.scope.get("url_route").get("kwargs").get('path').split("|")
username, token = "", ""
if len(para) == 1:
username = para[0]
token = ''
elif len(para) == 2:
username = para[0]
token = para[1]
else:
self.send(text_data="1000")
return username, token
def connect(self):
"""返回给前端用数字发送。自己认领其含义"""
self.accept()
username, token = self.para_get()
if username:
repeat_login = False
for user in socket_list:
for name in user.values():
# 给原来的连接发送信息并删除原来的连接,然后添加新的连接
if name == username:
if not token:
# 断开所有值为username的连接者
repeat_login = True
if repeat_login:
break
else:
socket_list.append({self: username})
if repeat_login:
copy_socket = copy.copy(socket_list)
for user in copy_socket:
for key, name in user.items():
if name == username:
key.send(text_data="1001")
key.close()
socket_list.append({self: username})
def receive(self, text_data=None, bytes_data=None):
"""接收前端消息,其他信息暂不处理"""
if text_data in ["logout"]:
# 正常退出时断开连接
for user in socket_list:
if user.get(self):
self.close()
break
def disconnect(self, code=None):
"""
1、用户自动断开(F5刷新和关闭浏览器)的时候自动执行的这个函数,是真正断开,这里维护全局变量
2、logout和异地登录的时候调用的close函数,也会来执行这个disconnect函数,所以disconnect函数维护全局变量
3、logout和异地登录的时候如果调用disconnect函数,不会真正断开,看源码没看懂
"""
# 断开连接,维护全局变量
for user in socket_list:
if user.get(self):
socket_list.remove(user)
break
还可以使用这个类自带的groups保存连接的用户,这里需求比较简单。暂不用。
4、这里有用到浅拷贝,顺带简单说一下。
可以看到a,b(列表是可变数据类型)的内存地址不一样,里面值(1、2、3是不可变类型)的内存地址是一样。这时候再往a或者b里面增删元素相互是不受影响的。如果a里面有可变数据类型。那么如果a或者b通过某操作改变了可变数据类型的值,那么另外一个也会跟着一起变。
大致总结就是:1)、针对不可变类型的浅拷贝,只是换了一个名字,对象在内存中的地址其实是不变的,相互改变值互不影响。2)、针对可变类型如列表:列表本身的浅拷贝对象的地址和原对象的地址是不同的,因为列表是可变数据类型,但是里面的元素地址都是相同的。如果里面元素都是不可变数据类型,那么相互改变值也是互不影响的,如果有嵌套可变数据类型,改变可变数据类型的值是相互影响的。
深拷贝直接说结论:深拷贝会拷贝所有的可变数据类型,包含嵌套的数据中的可变数据。深拷贝是可变数据对象对应的值复制到新的内存地址中,而不是复制数据对应的内存地址。即只拷贝数据,会开辟新的内存地址来存放数据。深拷贝不可变数据类型直接复制数据和地址,不管可变不可变,相互改变值都不会受影响。
三、前端实现
1、新建一个js文件,定义全局变量用于所有组件监听socket,再main.js中注册搭配vue原型链上
import quoWebsocket from ‘./api/websocket.js’
Vue.prototype.$quoWebsocket = quoWebsocket
再新建一个vue组件在methods中定义连接的方法,可以在其他组件中调用,这里传参直接通过/username/flag传的,也可以用其他方式传参。
if (this.$cookie.get(‘token’)){
var url = ws://IP:端口/ws/${this.$cookie.get('username')}|flag}
}else{
var url = ws://IP:端口/ws/${this.$cookie.get('username')}
}
// this.$quoWebsocket.quo_websocket = new WebSocket(url)
this.$quoWebsocket.update_websocket(new WebSocket(url))
this.$quoWebsocket.quo_websocket.onopen = () => {}
this.$quoWebsocket.quo_websocket.onmessage = (res) => {
if (res.data == ‘1001’){
sessionStorage.clear()
this.$cookie.remove(‘username’)
this.$cookie.remove(‘token’)
this.$alert(“Your account is already logging in other place”, ‘Tips’, {
confirmButtonText: ‘Confirm’,
callback: action => {
this.$router.push({ path: “/login” })
}
})
}else if(res.data == ‘1000’){
this.$message({showClose: true, message: “Parameter parsing error!socket connect failed”, type: ‘warning’});
}
}
this.$quoWebsocket.quo_websocket.onclose = (res) => {}
},
2、由于vue中按F5刷新这里socket会自动断开,所以这时候必须要重连,想了一些方法,最终这种最简单,直接在App.vue中调用组件进行重连。
顺带写了界面刷新的方法,有时候如弹框添加一个用户,添加成功后弹框消失回到查看用户界面,这时候可以刷新看到新增加的用户,刷新有很多方式,这里得刷新有更好体验效果,App.vue中这样写了之后直接在组件中调用: 组件先引入:inject:[‘reload’],然后直接在方法中this.reload()即可 export default {inject:[‘reload’], name: “GroupRouter”, data() { return { };},
3、在登录成功后进行socket连接。就可以开始onmessage监听了。这里因为只收异地登录提示消息所以直接在一个组件中监听了,然后收到消息不管身处哪个组件都会退出。如果有组件需要实时接收后端信息,可以在单独的组件中监听。
到这里前后就差不多都完了。这里简单再总结一下前后原理:显示清楚后端保存连接者方式:列表里面字典,有多少个窗口就有多少个字典。[{连接者钩子函数地址: username}……],相同窗口的username是相同的,那怎么判断一个用户只能在一个浏览器上登录呢?还有一个是flag,表示有没有token,所以一个浏览器N个窗口有N-1个窗口是携带了token的,故而不会收到异地登录提示信息。如果另外一个浏览器登录,因为初次登录没有token信息,所以后端这时候给所有username相同的连接者发送消息并在全局变量中删除。大体逻辑就是这样:所以这里有个关系,后端socket与窗口是1对1的,有多少个窗口就有多少个socket,即点对点连接。我们一般希望的是后端socket与浏览器(不管浏览器打开N个窗口)成1对1的效果,这个可能得vue-socket.io才能实现,这里没想到方法实现。。。。当然还有其他实现的方式。比如可以限制浏览器如果有一个窗口登录了,那么其他窗口限制登录(可以访问其他界面,如果刷新,后端处理,后端保证一个浏览器保存一个连接者,但是这样就只能有一个窗口收到异地登录消息),QQ就是这样限制登录,第二次打开QQ登录提示已经登录,但是浏览器不一样。不尽其美吧。
四、部署
一般的http请求后端都是通过uwsgi处理。而这里channels后端用daphne,channels安装完毕后,Daphne已经被附带安装成功。
这里先说一下手动部署就是直接运行一个命令遇到的一个问题。
(1)nginx配置。uwsgi也已经配置好。不贴了。。直接启动这两个服务。然后命令行输入下面命令启动websocket 服务。
daphne -b 0.0.0.0 -p 8888 quotationBackend.asgi:application
去登录系统的时候出现上面的报错。找到源码报错位置只支持http请求。
解决办法:可以重写这部分代码。这里我嫌弃麻烦没有重写。换了一种方法。
原始asgi.py
更改asgi.py,使用我们自己在setting中配置的application。如下修改。
然后重新启动,可以看到访问成功。
尽管现在websocket可以正常使用了,但是还得需要有优化的点,你可以尝试下把服务端的终端关闭,你在访问发现websocket的就出现无法访问的现象,这是因为daphne不是常驻线程的,这时候我们得需要常驻线程,我们就得借助supervisor来管理他了,非常方便,大致步骤分为以下几步。
1、安装supervisor,安装成功之后,我们需要创建软连接,要不然你运行会发现找不到命令
pip install supervisor
我的软连接路径是这个,有的是/usr/bin
sudo ln -s /home/edwin/.virtualenvs/quotationEnv/bin/supervisord /usr/local/bin/supervisord
2、修改配置文件。路径和文件内容如下
3、启动supervisor
我们执行以下命令开启 supervisor。可以看到启动成功。我这里必须在python 虚拟环境中并以root用户才能启动。当然访问没问题的。
supervisord -c /etc/supervisord.conf
这时候你再测试,我们关闭终端,程序正常访问,一直在常驻线程,到这里你们就完事了,为了更加的完美,可以将supervisord加入了开机自启动设置,要不然每次重启服务器之后忘了开启supervisord。省略…..
Original: https://blog.csdn.net/zt_9773/article/details/120955290
Author: 打工人-
Title: django-channels+vue实现异地登录提示
原创文章受到原创版权保护。转载请注明出处:https://www.johngo689.com/736310/
转载文章受原作者版权保护。转载请注明原作者出处!