增加用户其他浏览器登录自动下线功能
parent
1310e206a5
commit
9d0ab55152
|
@ -92,6 +92,7 @@ ENV/
|
|||
!**/migrations/__init__.py
|
||||
*.pyc
|
||||
conf/
|
||||
conf/env.py
|
||||
!conf/env.example.py
|
||||
db.sqlite3
|
||||
media/
|
||||
|
|
|
@ -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": [
|
||||
|
|
|
@ -47,3 +47,7 @@ ALLOWED_HOSTS = ["*"]
|
|||
|
||||
# daphne启动命令
|
||||
#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="用户账号")
|
||||
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, "男"),
|
||||
|
|
|
@ -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="注销成功")
|
||||
|
||||
|
||||
|
|
|
@ -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('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:
|
||||
|
|
|
@ -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))
|
||||
}
|
||||
}
|
||||
|
|
|
@ -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',
|
||||
|
|
|
@ -10,7 +10,7 @@ export const crudOptions = (vm) => {
|
|||
options: {
|
||||
height: '100%',
|
||||
// tableType: 'vxe-table',
|
||||
//rowKey: true,
|
||||
// rowKey: true,
|
||||
rowId: 'id'
|
||||
},
|
||||
selectionRow: {
|
||||
|
|
Loading…
Reference in New Issue