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唯一性,(注册、登录和网站通用视图除外)

首先创建自定义用户模块

  1. 要登陆就需要有用户模块,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

  1. 编写用户 注册 登录修改密码 方法, 注册和登录方法 用于用户上线,修改密码方法用于测试用户的 合法性 和 唯一性 如下:
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测试用户的 合法性 和 唯一性

  1. 完成上述步骤后,就可以通过postman进行测试了.

首先测试用户合法性:
注册账号,
登录并复制返回的token
粘贴token到请求头,然后携带请求头进行密码修改,
提示密码修改成功.

然后测试用户唯一性:
再次登录刚才的账号获得一个新的token,但是我们不用这个token,
仍然使用刚才的那个token进行密码修改,这时会提示 ‘token比对失败,非法操作,请勿跳过登录方法!’,
这样就确保了最新一次的登录会覆盖掉之前的登录,保证了用户的唯一性.