增加用户其他浏览器登录自动下线功能
parent
1310e206a5
commit
9d0ab55152
|
@ -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/
|
||||||
|
|
|
@ -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": [
|
||||||
|
|
|
@ -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
|
||||||
|
|
|
@ -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, "男"),
|
||||||
|
|
|
@ -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="注销成功")
|
||||||
|
|
||||||
|
|
||||||
|
|
|
@ -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
|
|
@ -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:
|
||||||
|
|
|
@ -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))
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -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',
|
||||||
|
|
|
@ -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: {
|
||||||
|
|
Loading…
Reference in New Issue