增加用户其他浏览器登录自动下线功能

pull/90/head
cheney 2023-03-07 20:08:01 +08:00
parent 1310e206a5
commit 9d0ab55152
10 changed files with 81 additions and 12 deletions

1
backend/.gitignore vendored
View File

@ -92,6 +92,7 @@ ENV/
!**/migrations/__init__.py !**/migrations/__init__.py
*.pyc *.pyc
conf/ conf/
conf/env.py
!conf/env.example.py !conf/env.example.py
db.sqlite3 db.sqlite3
media/ media/

View File

@ -283,7 +283,8 @@ REST_FRAMEWORK = {
), ),
"DEFAULT_PAGINATION_CLASS": "dvadmin.utils.pagination.CustomPagination", # 自定义分页 "DEFAULT_PAGINATION_CLASS": "dvadmin.utils.pagination.CustomPagination", # 自定义分页
"DEFAULT_AUTHENTICATION_CLASSES": ( "DEFAULT_AUTHENTICATION_CLASSES": (
"rest_framework_simplejwt.authentication.JWTAuthentication", # "rest_framework_simplejwt.authentication.JWTAuthentication",
"dvadmin.utils.myJWTAuthentication.myJWTAuthentication",
"rest_framework.authentication.SessionAuthentication", "rest_framework.authentication.SessionAuthentication",
), ),
"DEFAULT_PERMISSION_CLASSES": [ "DEFAULT_PERMISSION_CLASSES": [

View File

@ -47,3 +47,7 @@ ALLOWED_HOSTS = ["*"]
# daphne启动命令 # daphne启动命令
#daphne application.asgi:application -b 0.0.0.0 -p 8000 #daphne application.asgi:application -b 0.0.0.0 -p 8000
# 是否开启用户登录严格模式
# 开启后同一个用户同一时间只允许在一个浏览器内登录并且注销时jwt Token 立即失效
STRICT_LOGIN = True

View File

@ -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="用户账号") 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="邮箱") 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="电话") 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="头像") avatar = models.CharField(max_length=255, verbose_name="头像", null=True, blank=True, help_text="头像")
name = models.CharField(max_length=40, verbose_name="姓名", 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 = ( GENDER_CHOICES = (
(0, "未知"), (0, "未知"),
(1, ""), (1, ""),

View File

@ -1,5 +1,6 @@
import base64 import base64
import hashlib import hashlib
import uuid
from datetime import datetime, timedelta from datetime import datetime, timedelta
from captcha.views import CaptchaStore, captcha_image from captcha.views import CaptchaStore, captcha_image
@ -57,11 +58,13 @@ class LoginSerializer(TokenObtainPairSerializer):
captcha = serializers.CharField( captcha = serializers.CharField(
max_length=6, required=False, allow_null=True, allow_blank=True max_length=6, required=False, allow_null=True, allow_blank=True
) )
class Meta: class Meta:
model = Users model = Users
fields = "__all__" fields = "__all__"
read_only_fields = ["id"] read_only_fields = ["id"]
class LoginView(TokenObtainPairView): class LoginView(TokenObtainPairView):
""" """
登录接口 登录接口
@ -125,6 +128,14 @@ class LoginView(TokenObtainPairView):
if role: if role:
result['role_info'] = role.values('id', 'name', 'key') result['role_info'] = role.values('id', 'name', 'key')
refresh = LoginSerializer.get_token(user) 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["refresh"] = str(refresh)
result["access"] = str(refresh.access_token) result["access"] = str(refresh.access_token)
# 记录登录日志 # 记录登录日志
@ -165,6 +176,13 @@ class LoginTokenView(TokenObtainPairView):
class LogoutView(APIView): class LogoutView(APIView):
def post(self, request): 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="注销成功") return DetailResponse(msg="注销成功")

View File

@ -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

View File

@ -21,16 +21,23 @@ export function getErrorMessage (msg) {
util.cookies.remove('token') util.cookies.remove('token')
util.cookies.remove('uuid') util.cookies.remove('uuid')
router.push({ path: '/login' }) router.push({ path: '/login' })
router.go(0) // router.go(0)
return '登录超时,请重新登录!' return '登录超时,请重新登录!'
} }
if (msg.code === 'user_not_found') { if (msg.code === 'user_not_found') {
util.cookies.remove('token') util.cookies.remove('token')
util.cookies.remove('uuid') util.cookies.remove('uuid')
router.push({ path: '/login' }) router.push({ path: '/login' })
router.go(0) // router.go(0)
return '用户无效,请重新登录!' 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) return Object.values(msg)
} }
if (Object.prototype.toString.call(msg).slice(8, -1) === 'Array') { if (Object.prototype.toString.call(msg).slice(8, -1) === 'Array') {
@ -82,7 +89,6 @@ function createService () {
util.cookies.remove('token') util.cookies.remove('token')
util.cookies.remove('uuid') util.cookies.remove('uuid')
util.cookies.remove('refresh') util.cookies.remove('refresh')
router.push({ path: '/login' })
errorCreate(`${getErrorMessage(dataAxios.msg)}`) errorCreate(`${getErrorMessage(dataAxios.msg)}`)
break break
case 404: case 404:

View File

@ -213,7 +213,7 @@ export default {
}, },
// //
onDrag (e, item) { onDrag (e, item) {
const { key, width, height } = item const { key } = item
const parentRect = this.$refs.widgets.getBoundingClientRect() const parentRect = this.$refs.widgets.getBoundingClientRect()
let mouseInGrid = false let mouseInGrid = false
if (((mouseXY.x > parentRect.left) && (mouseXY.x < parentRect.right)) && ((mouseXY.y > parentRect.top) && (mouseXY.y < parentRect.bottom))) { 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] const el = this.$refs.gridlayout.$children[index]
el.dragging = { top: mouseXY.y - parentRect.top, left: mouseXY.x - parentRect.left } 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) { 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.i = String(index)
DragPos.x = this.layout[index].x DragPos.x = this.layout[index].x
DragPos.y = this.layout[index].y DragPos.y = this.layout[index].y
} }
if (mouseInGrid === false) { 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)) this.layout = this.layout.filter(obj => obj.i !== this.getLayoutElementNumber(key))
} }
} }

View File

@ -8,7 +8,7 @@ export const crudOptions = (vm) => {
// rowKey: true, // 必须设置true or false // rowKey: true, // 必须设置true or false
rowId: 'id', rowId: 'id',
height: '100%', // 表格高度100%, 使用toolbar必须设置 height: '100%', // 表格高度100%, 使用toolbar必须设置
highlightCurrentRow: false, highlightCurrentRow: false
}, },
rowHandle: { rowHandle: {
fixed: 'right', fixed: 'right',

View File

@ -9,8 +9,8 @@ export const crudOptions = (vm) => {
}, },
options: { options: {
height: '100%', height: '100%',
// tableType: 'vxe-table', // tableType: 'vxe-table',
//rowKey: true, // rowKey: true,
rowId: 'id' rowId: 'id'
}, },
selectionRow: { selectionRow: {