diff --git a/backend/.gitignore b/backend/.gitignore index f22f635..061e191 100644 --- a/backend/.gitignore +++ b/backend/.gitignore @@ -92,6 +92,7 @@ ENV/ !**/migrations/__init__.py *.pyc conf/ +conf/env.py !conf/env.example.py db.sqlite3 media/ diff --git a/backend/application/routing.py b/backend/application/routing.py index 237e5d1..3fe1a9d 100644 --- a/backend/application/routing.py +++ b/backend/application/routing.py @@ -3,6 +3,6 @@ from django.urls import path from application.websocketConfig import MegCenter websocket_urlpatterns = [ - path('ws//', MegCenter.as_asgi()), #consumers.DvadminWebSocket 是该路由的消费者 + path('ws/', MegCenter.as_asgi()), #consumers.DvadminWebSocket 是该路由的消费者 ] diff --git a/backend/application/settings.py b/backend/application/settings.py index 61adde3..88be14b 100644 --- a/backend/application/settings.py +++ b/backend/application/settings.py @@ -283,7 +283,8 @@ REST_FRAMEWORK = { ), "DEFAULT_PAGINATION_CLASS": "dvadmin.utils.pagination.CustomPagination", # 自定义分页 "DEFAULT_AUTHENTICATION_CLASSES": ( - "rest_framework_simplejwt.authentication.JWTAuthentication", + # "rest_framework_simplejwt.authentication.JWTAuthentication", + "dvadmin.utils.myJWTAuthentication.myJWTAuthentication", "rest_framework.authentication.SessionAuthentication", ), "DEFAULT_PERMISSION_CLASSES": [ diff --git a/backend/application/websocketConfig.py b/backend/application/websocketConfig.py index 80515f4..21f1efc 100644 --- a/backend/application/websocketConfig.py +++ b/backend/application/websocketConfig.py @@ -58,7 +58,7 @@ class DvadminWebSocket(AsyncJsonWebsocketConsumer): async def connect(self): try: import jwt - self.service_uid = self.scope["url_route"]["kwargs"]["service_uid"] + self.service_uid = self.scope["subprotocols"][0] decoded_result = jwt.decode(self.service_uid, settings.SECRET_KEY, algorithms=["HS256"]) if decoded_result: self.user_id = decoded_result.get('user_id') @@ -68,7 +68,7 @@ class DvadminWebSocket(AsyncJsonWebsocketConsumer): self.chat_group_name, self.channel_name ) - await self.accept() + await self.accept(subprotocol=self.service_uid) # 主动推送消息 unread_count = await _get_message_unread(self.user_id) if unread_count == 0: @@ -78,7 +78,8 @@ class DvadminWebSocket(AsyncJsonWebsocketConsumer): await self.send_json( set_message('system', 'SYSTEM', "请查看您的未读消息~", unread=unread_count)) - except InvalidSignatureError: + except InvalidSignatureError as e: + print(e.__str__()) await self.disconnect(None) async def disconnect(self, close_code): @@ -121,6 +122,7 @@ class MessageCreateSerializer(CustomModelSerializer): model = MessageCenter fields = "__all__" read_only_fields = ["id"] + def websocket_push(user_id,message): username = "user_" + str(user_id) @@ -133,6 +135,7 @@ def websocket_push(user_id,message): } ) + def create_message_push(title: str, content: str, target_type: int=0, target_user: list=[], target_dept=None, target_role=None, message: dict = {'contentType': 'INFO', 'content': '测试~'}, request= Request): if message is None: diff --git a/backend/conf/env.example.py b/backend/conf/env.example.py index 829b73a..ab36953 100644 --- a/backend/conf/env.example.py +++ b/backend/conf/env.example.py @@ -47,3 +47,7 @@ ALLOWED_HOSTS = ["*"] # daphne启动命令 #daphne application.asgi:application -b 0.0.0.0 -p 8000 + +# 是否开启用户登录严格模式 +# 开启后,同一个用户同一时间只允许在一个浏览器内登录,并且注销时jwt Token 立即失效 +STRICT_LOGIN = True diff --git a/backend/dvadmin/system/models.py b/backend/dvadmin/system/models.py index d471438..eadaf04 100644 --- a/backend/dvadmin/system/models.py +++ b/backend/dvadmin/system/models.py @@ -13,12 +13,13 @@ STATUS_CHOICES = ( ) -class Users(CoreModel,AbstractUser): +class Users(CoreModel, AbstractUser): username = models.CharField(max_length=150, unique=True, db_index=True, verbose_name="用户账号", help_text="用户账号") email = models.EmailField(max_length=255, verbose_name="邮箱", null=True, blank=True, help_text="邮箱") mobile = models.CharField(max_length=255, verbose_name="电话", null=True, blank=True, help_text="电话") avatar = models.CharField(max_length=255, verbose_name="头像", null=True, blank=True, help_text="头像") name = models.CharField(max_length=40, verbose_name="姓名", help_text="姓名") + login_flag = models.CharField(max_length=36, verbose_name="凭证失效flag", null=True, blank=True, help_text="凭证及时失效标志") GENDER_CHOICES = ( (0, "未知"), (1, "男"), diff --git a/backend/dvadmin/system/views/login.py b/backend/dvadmin/system/views/login.py index 184dba9..af3f254 100644 --- a/backend/dvadmin/system/views/login.py +++ b/backend/dvadmin/system/views/login.py @@ -1,5 +1,6 @@ import base64 import hashlib +import uuid from datetime import datetime, timedelta from captcha.views import CaptchaStore, captcha_image @@ -57,11 +58,13 @@ class LoginSerializer(TokenObtainPairSerializer): captcha = serializers.CharField( max_length=6, required=False, allow_null=True, allow_blank=True ) + class Meta: model = Users fields = "__all__" read_only_fields = ["id"] + class LoginView(TokenObtainPairView): """ 登录接口 @@ -125,6 +128,14 @@ class LoginView(TokenObtainPairView): if role: result['role_info'] = role.values('id', 'name', 'key') refresh = LoginSerializer.get_token(user) + if settings.STRICT_LOGIN: + login_flag = uuid.uuid4().__str__() + refresh['login_flag'] = login_flag + user.login_flag = login_flag + try: + user.save() + except Exception as e: + return ErrorResponse("登录失败!系统发生致命错误!") result["refresh"] = str(refresh) result["access"] = str(refresh.access_token) # 记录登录日志 @@ -165,6 +176,13 @@ class LoginTokenView(TokenObtainPairView): class LogoutView(APIView): def post(self, request): + if settings.STRICT_LOGIN: + user = request.user + try: + user.login_flag = "logout" + user.save() + except Exception as e: + pass return DetailResponse(msg="注销成功") diff --git a/backend/dvadmin/system/views/message_center.py b/backend/dvadmin/system/views/message_center.py index ad3c680..f633b50 100644 --- a/backend/dvadmin/system/views/message_center.py +++ b/backend/dvadmin/system/views/message_center.py @@ -100,7 +100,7 @@ def websocket_push(user_id, message): 主动推送消息 """ username = "user_"+str(user_id) - print(103,message) + # print(103,message) channel_layer = get_channel_layer() async_to_sync(channel_layer.group_send)( username, diff --git a/backend/dvadmin/utils/myJWTAuthentication.py b/backend/dvadmin/utils/myJWTAuthentication.py new file mode 100644 index 0000000..3f39714 --- /dev/null +++ b/backend/dvadmin/utils/myJWTAuthentication.py @@ -0,0 +1,38 @@ +from django.conf import settings +from rest_framework_simplejwt.authentication import JWTAuthentication +from rest_framework_simplejwt.exceptions import InvalidToken + + +class myJWTAuthentication(JWTAuthentication): + """ + 重写校验 + """ + def __init__(self, *args, **kwargs): + super().__init__(*args, **kwargs) + + def authenticate(self, request): + header = self.get_header(request) + if header is None: + return None + + raw_token = self.get_raw_token(header) + if raw_token is None: + return None + + validated_token = self.get_validated_token(raw_token) + user = self.get_user(validated_token) + user_login_flag = user.login_flag + if settings.STRICT_LOGIN and validated_token['login_flag'] != user_login_flag: + if user_login_flag == "logout": + raise InvalidToken({ + "detail": "Token has invalided", + "messages": "token已失效!", + }) + else: + raise InvalidToken({ + "detail": "The user has logged in elsewhere, please confirm the account security!", + "code": "User logs in elsewhere", + "messages": "用户已在其他地方登录,请确认账户安全!", + }) + else: + return user, validated_token diff --git a/web/src/api/service.js b/web/src/api/service.js index e5ab0f8..ab55e5d 100644 --- a/web/src/api/service.js +++ b/web/src/api/service.js @@ -21,16 +21,23 @@ export function getErrorMessage (msg) { util.cookies.remove('token') util.cookies.remove('uuid') router.push({ path: '/login' }) - router.go(0) + // router.go(0) return '登录超时,请重新登录!' } if (msg.code === 'user_not_found') { util.cookies.remove('token') util.cookies.remove('uuid') router.push({ path: '/login' }) - router.go(0) + // router.go(0) return '用户无效,请重新登录!' } + if (msg.code === 'User logs in elsewhere') { + util.cookies.remove('token') + util.cookies.remove('uuid') + router.push({ path: '/login' }) + // router.go(0) + return '用户已在其他地方登录,请确认账户安全!' + } return Object.values(msg) } if (Object.prototype.toString.call(msg).slice(8, -1) === 'Array') { @@ -82,7 +89,6 @@ function createService () { util.cookies.remove('token') util.cookies.remove('uuid') util.cookies.remove('refresh') - router.push({ path: '/login' }) errorCreate(`${getErrorMessage(dataAxios.msg)}`) break case 404: diff --git a/web/src/api/websocket.js b/web/src/api/websocket.js index 83a7e9d..494f80f 100644 --- a/web/src/api/websocket.js +++ b/web/src/api/websocket.js @@ -4,8 +4,9 @@ import store from '@/store' function initWebSocket (e) { const token = util.cookies.get('token') if (token) { - const wsUri = util.wsBaseURL() + 'ws/' + token + '/' - this.socket = new WebSocket(wsUri)// 这里面的this都指向vue + const wsUri = util.wsBaseURL() + 'ws/' + // const wsUri = util.wsBaseURL() + 'ws/' + token + '/' + this.socket = new WebSocket(wsUri, [token])// 这里面的this都指向vue this.socket.onerror = webSocketOnError this.socket.onmessage = webSocketOnMessage this.socket.onclose = closeWebsocket diff --git a/web/src/views/dashboard/workbench/index.vue b/web/src/views/dashboard/workbench/index.vue index 88e7227..514025f 100644 --- a/web/src/views/dashboard/workbench/index.vue +++ b/web/src/views/dashboard/workbench/index.vue @@ -213,7 +213,7 @@ export default { }, // 拖拽事件 onDrag (e, item) { - const { key, width, height } = item + const { key } = item const parentRect = this.$refs.widgets.getBoundingClientRect() let mouseInGrid = false if (((mouseXY.x > parentRect.left) && (mouseXY.x < parentRect.right)) && ((mouseXY.y > parentRect.top) && (mouseXY.y < parentRect.bottom))) { @@ -238,15 +238,15 @@ export default { } const el = this.$refs.gridlayout.$children[index] el.dragging = { top: mouseXY.y - parentRect.top, left: mouseXY.x - parentRect.left } - const new_pos = el.calcXY(mouseXY.y - parentRect.top, mouseXY.x - parentRect.left) + const newPos = el.calcXY(mouseXY.y - parentRect.top, mouseXY.x - parentRect.left) if (mouseInGrid === true) { - this.$refs.gridlayout.dragEvent('dragstart', this.getLayoutElementNumber(key), new_pos.x, new_pos.y, 1, 1) + this.$refs.gridlayout.dragEvent('dragstart', this.getLayoutElementNumber(key), newPos.x, newPos.y, 1, 1) DragPos.i = String(index) DragPos.x = this.layout[index].x DragPos.y = this.layout[index].y } if (mouseInGrid === false) { - this.$refs.gridlayout.dragEvent('dragend', this.getLayoutElementNumber(key), new_pos.x, new_pos.y, 1, 1) + this.$refs.gridlayout.dragEvent('dragend', this.getLayoutElementNumber(key), newPos.x, newPos.y, 1, 1) this.layout = this.layout.filter(obj => obj.i !== this.getLayoutElementNumber(key)) } } diff --git a/web/src/views/system/log/loginLog/crud.js b/web/src/views/system/log/loginLog/crud.js index 94dd3ab..a9f00b4 100644 --- a/web/src/views/system/log/loginLog/crud.js +++ b/web/src/views/system/log/loginLog/crud.js @@ -8,7 +8,7 @@ export const crudOptions = (vm) => { // rowKey: true, // 必须设置,true or false rowId: 'id', height: '100%', // 表格高度100%, 使用toolbar必须设置 - highlightCurrentRow: false, + highlightCurrentRow: false }, rowHandle: { fixed: 'right', diff --git a/web/src/views/system/user/crud.js b/web/src/views/system/user/crud.js index fa29cfb..234f2ae 100644 --- a/web/src/views/system/user/crud.js +++ b/web/src/views/system/user/crud.js @@ -9,8 +9,8 @@ export const crudOptions = (vm) => { }, options: { height: '100%', - // tableType: 'vxe-table', - //rowKey: true, + // tableType: 'vxe-table', + // rowKey: true, rowId: 'id' }, selectionRow: {