DRF + jwt + mysql 防止用户重复登录
- 实现目标说明
- 首先创建自定义用户模块
- 安装配置djangorestframework-jwt模块
- 编写 检查重复登录 的方法
- 编写用户 注册 登录 和 修改密码 的view
- 通过postman测试用户的 合法性 和 唯一性
实现目标说明
用户登录最少需要做到如下两点::
1.合法性,即 是他自己登录的账号(做法:token + https ,token验证可以确定操作源是该用户本人,https可以在很大程度上保证token在传输的过程中不被截获篡改,是目前较为安全的一种做法)
2.唯一性,即 不能对同一个账号重复登录(做法:服务器保存生成的token后再将其发送给用户,每次用户请求数据时,先验证token是否相同,然后再验证token是否有效。相同,则说明用户唯一,有效则说明用户合法。注意:就算传入token和数据库中的token相同,也不能说明用户合法,因为数据库中的token可能已经过期无效了,如果token过期就让用户重新登录,然后再次签发和保存token。ps:token必须具有时效性,否则用户数据被破解只是时间问题)
做法总结:
1.djangorestframework + djangorestframework-jwt 实现用户的token登录。全局验证token有效性(注册、登录和网站通用视图除外),并且只通过登录视图发放token给用户。
2.通过mysql保存用户token,确保单个账号只有一个用户在线。用户的每次操作都需要对比请求头中的token和数据库中的token是否相同,全局验证token唯一性,(注册、登录和网站通用视图除外)
首先创建自定义用户模块
- 要登陆就需要有用户模块,django本身自带auth用户模块,但不能满足现在五花八门的用户字段需求。好在django还提供了自定义用户模块的功能,创建方法如下:
输入命令创建user
# 输入命令
python manage.py startapp user
编写自定义用户
from django.db import models
from django.contrib.auth.models import AbstractUser
# 继承 django 的 AbstractUser
class UserInfo(AbstractUser):
"""
自定义的用户模块
"""
nick_name = models.CharField(max_length=15, default="NoOne", verbose_name="昵称")
sign = models.TextField(max_length=100, default="要不写点什么,反正你挺闲的、、", verbose_name="签名")
token = models.CharField(max_length=300, null=True, blank=True, verbose_name="用户认证token")
class Meta:
verbose_name = "用户信息"
verbose_name_plural = verbose_name
def __str__(self):
return self.username
配置settings.py文件
# 在 settings.py 文件中添加如下内容,其中 UserInfo 是自己定义的用户模块名称
AUTH_USER_MODEL = 'user.UserInfo' # 采用自己定制的用户model
# 在settings中注册自定义的用户模块
INSTALLED_APPS = [
'django.contrib.admin',
'django.contrib.auth',
'django.contrib.contenttypes',
'django.contrib.sessions',
'django.contrib.messages',
'django.contrib.staticfiles',
'rest_framework',
'crispy_forms',
'corsheaders',
'apps.user',
]
注册用户到django admin
# 别忘记注册到 django admin 后台管理系统
# 如下两种方式选一种就行
# 1 显示所有字段
admin.site.register(UserModel)
# 2 显示自定义的字段
# @admin.register(UserInfo)
# class CategoryAdmin(admin.ModelAdmin):
# list_display = ('nick_name', 'sign', 'username', 'email')
# fields = ('nick_name', 'sign', 'username', 'password', 'email')
最后 makemigrations 和 migrate 同步数据库,然后打开数据库管理工具就可以看到自定义的用户表了。到此,自定义用户模块完成。
安装配置djangorestframework-jwt模块
2.安装并配置token验证模块djangorestframework-jwt,如下:
输入命令安装
# 安装
pip install djangorestframework-jwt
配置settings.py文件
# 配置
# DRF中间件
REST_FRAMEWORK = {
'DEFAULT_PERMISSION_CLASSES': (
# 提供的权限↓
# AllowAny 允许所有用户
# IsAuthenticated 仅通过认证的用户
# IsAdminUser 仅管理员用户
# IsAuthenticatedOrReadOnly 认证的用户可以完全操作,否则只能get读取
'rest_framework.permissions.IsAuthenticated',
),
'DEFAULT_AUTHENTICATION_CLASSES': (
# 认证方式
# 此方法是自定义方法,用于检查用户是否重复登录,后面会讲
'utils.my_authentication.LoginRepeatAuth',
# 此方法是 djangorestframework-jwt 的方法,用于检查用户token是否合法
'rest_framework_jwt.authentication.JSONWebTokenAuthentication',
),
}
JWT_AUTH = {
# 指明Token的有效期
'JWT_EXPIRATION_DELTA': datetime.timedelta(days=1),
'JWT_AUTH_HEADER_PREFIX': 'JWT',
}
编写 检查重复登录 的方法
3.编写检查用户重复登录的方法:
from rest_framework import exceptions
from rest_framework.authentication import BaseAuthentication
from apps.user.models import UserInfo
class LoginRepeatAuth(BaseAuthentication):
# authenticate必须在DRF认证内被重写,request参数必须传入
def authenticate(self, request):
'''
将获取的token去token表内进行比对,存在信息即验证通过
- 获取token表内token更新时间,若超时则验证失败重新登陆(用于数据的清理)
- 验证通过:返回空 - 表示后续仍能继续其他验证
= 验证通过:返回认证用户和当前数据记录对象 - 后续不再进行验证
- 对应DRF内Request对象User类内_authenticate方法执行
- from rest_framework.request import Request
:param request:
:return:
'''
# 数据放在header内传输,request.META获取
# meta查询key值格式:HTTP_大写字段名 例如:token - HTTP_TOKEN
token = request.META.get('HTTP_AUTHORIZATION')
# token = request.query_params.get('token')
# 查找是否存在token值和请求头中的token值相同的用户
user = UserInfo.objects.filter(token=token).first()
if not user:
# 如果没有,就认为是跳过了登录阶段的非法操作.
# 因为只有登录方法会保存生成的token并返回给用户,如果保存的token和用户携带的token不同,说明用户token被篡改,或有人尝试破解用户的token.
# 此时应终止方法,并要求用户重新登录,以便保存并返回给用户新的token
raise exceptions.APIException('token比对失败,非法操作,请勿跳过登录方法!')
# 查询到对应用户信息,认证通过
# 此时如果接下来还有验证方法,就返回 None
# 如果接下来没有验证方法了,那么返回 当前认证用户 和 当前token记录对象
# 返回的数据可通过 request.user, request.auth进行获取
return None
编写用户 注册 登录 和 修改密码 的view
- 编写用户 注册 登录 和 修改密码 方法, 注册和登录方法 用于用户上线,修改密码方法用于测试用户的 合法性 和 唯一性 如下:
from rest_framework.views import APIView
from rest_framework.renderers import JSONRenderer
from rest_framework.response import Response
from rest_framework import status
from rest_framework_jwt.settings import api_settings
from .models import UserInfo
class RegisterView(APIView):
"""
用户注册
parm = [ username, password ]
"""
renderer_classes = [JSONRenderer] # json渲染器
authentication_classes = [] # 此方法不验证JWT
permission_classes = [] # 此方法不设权限
def post(self, request):
username = request.data.get("username", 0)
password = request.data.get("password", 0)
if username and password:
# 校验注册,名字不可重复
user = UserInfo.objects.filter(username=username).first()
if user:
content = {'msg': '用户已存在'}
return Response(content, status=status.HTTP_400_BAD_REQUEST)
else:
# 注册成功,创建用户
UserInfo.objects.create_user(
username=username,
password=password
)
content = {'msg': '注册成功'}
return Response(content, status=status.HTTP_201_CREATED)
content = {'msg': '账号或密码不能为空'}
return Response(content, status=status.HTTP_403_FORBIDDEN)
class LoginView(APIView):
"""
用户登录
parm = [ username, password]
"""
renderer_classes = [JSONRenderer] # json渲染器
authentication_classes = [] # 此方法不验证JWT
permission_classes = [] # 此方法不设权限
def post(self, request):
# 登录的业务逻辑 start
username = request.data.get("username", 0)
password = request.data.get("password", 0)
if not username or not password:
content = {'msg': '输入的账号或密码有误'}
return Response(content, status=status.HTTP_400_BAD_REQUEST)
else:
user = UserInfo.objects.filter(username=username).first()
if not user or not user.check_password(password):
content = {'msg': '输入的账号或密码有误'}
return Response(content, status=status.HTTP_400_BAD_REQUEST)
else:
# 生成token的业务逻辑 start
jwt_payload_handler = api_settings.JWT_PAYLOAD_HANDLER
jwt_encode_handler = api_settings.JWT_ENCODE_HANDLER
payload = jwt_payload_handler(user)
token = jwt_encode_handler(payload)
token_with_JWT = "JWT " + token # 为token添加JWT头后保存到数据库
user.token = token_with_JWT
user.save()
# 生成token的业务逻辑 end
content = {'Authorization': token_with_JWT}
return Response(content, status=status.HTTP_200_OK)
# 登录的业务逻辑 end
class ChangePassWord(APIView):
"""
用户改密
parm = [ username, password, new_password ]
"""
renderer_classes = [JSONRenderer] # json渲染器
def post(self, request):
username = request.data.get("username", 0)
old_password = request.data.get("old_password", 0)
new_password = request.data.get("new_password", 0)
if username and old_password and new_password:
# 校验用户名和密码
user = UserInfo.objects.filter(username=username).first()
if user and user.check_password(old_password):
# 校验成功,保存新密码
user.set_password(new_password)
user.save()
content = {'msg': '密码修改成功'}
return Response(content, status=status.HTTP_205_RESET_CONTENT)
else:
content = {'msg': '原密码输入有误'}
return Response(content, status=status.HTTP_400_BAD_REQUEST)
content = {'msg': '账号或密码不能为空'}
return Response(content, status=status.HTTP_400_BAD_REQUEST)
配置三个视图的url
# 别忘记将视图的url配置好,此处的 DEV_NAME 是我定义的应用名,可以去掉
# 用户登录(JWT唯一获取接口)
path(DEV_NAME + 'login/', LoginView.as_view()),
# 用户注册
path(DEV_NAME + 'register/', RegisterView.as_view()),
# 用户修改密码
path(DEV_NAME + 'change_pw/', ChangePassWord.as_view()),
通过postman测试用户的 合法性 和 唯一性
- 完成上述步骤后,就可以通过postman进行测试了.
首先测试用户合法性:
注册账号,
登录并复制返回的token
粘贴token到请求头,然后携带请求头进行密码修改,
提示密码修改成功.
然后测试用户唯一性:
再次登录刚才的账号获得一个新的token,但是我们不用这个token,
仍然使用刚才的那个token进行密码修改,这时会提示 ‘token比对失败,非法操作,请勿跳过登录方法!’,
这样就确保了最新一次的登录会覆盖掉之前的登录,保证了用户的唯一性.