From 9d0ab551521787b99f18297223e636852a5cfad2 Mon Sep 17 00:00:00 2001 From: cheney Date: Tue, 7 Mar 2023 20:08:01 +0800 Subject: [PATCH] =?UTF-8?q?=E5=A2=9E=E5=8A=A0=E7=94=A8=E6=88=B7=E5=85=B6?= =?UTF-8?q?=E4=BB=96=E6=B5=8F=E8=A7=88=E5=99=A8=E7=99=BB=E5=BD=95=E8=87=AA?= =?UTF-8?q?=E5=8A=A8=E4=B8=8B=E7=BA=BF=E5=8A=9F=E8=83=BD?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- backend/.gitignore | 1 + backend/application/settings.py | 3 +- backend/conf/env.example.py | 4 +++ backend/dvadmin/system/models.py | 3 +- backend/dvadmin/system/views/login.py | 18 ++++++++++ backend/dvadmin/utils/myJWTAuthentication.py | 38 ++++++++++++++++++++ web/src/api/service.js | 12 +++++-- web/src/views/dashboard/workbench/index.vue | 8 ++--- web/src/views/system/log/loginLog/crud.js | 2 +- web/src/views/system/user/crud.js | 4 +-- 10 files changed, 81 insertions(+), 12 deletions(-) create mode 100644 backend/dvadmin/utils/myJWTAuthentication.py 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/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/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/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/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: {