头图

本文首发于本人微信公众号:Hunter后端。

原文链接:Django笔记三十二之session登录验证操作

这一篇笔记将介绍 session 相关的内容,包括如何在系统中使用 session,以及利用 session 实现登录认证的功能。

这篇笔记将分为以下几个内容:

  1. session 的使用流程
  2. session 的配置和相关方法
  3. users 模块的准备
  4. session 验证的的实现
  5. Session 表介绍
  6. 登录验证的几种实现形式

1、session 的使用流程

cookie 和 session 的基本概念这里不做赘述,这里简单讲一下在 Django 中如何使用自定义的模块来实现登录、登出以及仅允许登录用户访问某些接口的操作。

Django 有一套自带的 auth 验证模块,包括用户以及用户及相应的权限的表和操作,我们这里没有用,而是单独自定义一个 user 模块以及相应的功能函数用来实现用户的注册、登录和登出功能。

session 在这里的使用流程大致如下:

1、通过 login 接口,验证成功后,将某些信息写入 session,可以是 user_id,或者是某个你自定义的特定的字段,反正是后续需要进行验证是否登录成功的数据

2、在访问特定的、需要登录才可查看的接口前,先检查前端返回的数据中是否包含我们在上一步中写入的数据来确保用户是处于登录状态,如果是,则允许继续访问,否则返回未登录的信息,提示用户需要先进行登录操作

3、通过 logout 接口,将用户在 login 接口里写入的登录信息抹除,返回登出成功信息

在 Django 中,系统自动为我们准备好了 session 的所有相关的操作,我们只需要在后续的登录操作中往里面写入我们需要验证的数据即可。

Django 这部分为我们准备好的 session 操作也是通过中间件的形式存在的,是 settings.py 的 MIDDLEWARE 的 'django.contrib.sessions.middleware.SessionMiddleware'

如果不指定其他存储方式,session 的数据默认存在于我们的后端表中,这个我们在第一次执行 migrate 的时候已经自动为我们创建了该表,名为 django_session

表数据的操作和查看我们在后面再详细介绍。

2、session 的配置和相关方法

前面已经介绍了 session 的操作流程,这里我们介绍一下 session 的相关配置和方法。

session 配置

以下设置都在 settings.py 中设置,事实上,这些 session 的默认配置就差不多可以使用,后续有特殊需求我们可以再来查看,这里只介绍几个我觉得方便我们使用的。

这个地方的官方文档地址在:https://docs.djangoproject.com/zh-hans/3.2/ref/settings/#sess...

SESSION_COOKIE_AGE

session 过期时间,默认为 1209600,即 14 24 60 * 60,为 14天。

我们可以在 settings.py 中配置 session 的过期时长,也可以在程序中使用方法手动配置过期时长,方法的使用我们后面再介绍。

SESSION_COOKIE_NAME

默认值为 sessionid,在用户登录之后,请求我们系统,请求的 cookie 里会带上 session key-value 的参数,这个 key 就是我们这里的 SESSION_COOKIE_NAME,默认为 sessionid。

如果想改成其他的名称直接定义即可。

SESSION_ENGING

Django 存储 session 具体数据的地方,默认值为 django.contrib.sessions.backends.db,表示存在于数据库,也就是我们前面说的在 django_session 这张表。

也可以存储在文件或者缓存里。

session 方法

这里接着介绍一下 session 相关的方法,这些方法的调用一般是在接口里通过 request.session 来操作。

这里我们只是做一下方法的作用和效果的介绍,具体用途我们在之后的示例中再详细说明。

dict 操作

我们可以将 request.session 视作一个 dict,往里面添加 user_id,is_login 等用于标识用户是否登录的信息的时候可以直接操作,比如:

request.session["user_id"] = 1
request.session["is_login"] = True

keys()

输出 request.session.keys() 返回的就是我们在前面往 session 里添加的数据。

同理,request.session.items() 输出的也是我们往里添加的数据的 key-value 的值。

del 操作

当我们使用登出操作时,可以直接使用:

del request.session["user_id"]

这种方式会删除 session 中我们保存的 user_id 信息,这样用户在访问我们的接口的时候,如果我们做登录验证的操作,就会找不到已经登录的信息。

之前我们说过,我们的 session 数据会保存在数据库里,这种方式仅仅是删除 session 中某个特定的 key-value,并不会删除 django_session 表中这条数据

而如果想要直接删除这一条 session 数据,则可以使用 flush() 方法

flush()

下面的操作则会直接操作数据库删除这条 session 数据:

request.session.flush()

flush() 和 前面的 del 方法都可以用作我们 logout 过程中的操作。

get_expiry_age()

获取 session 过期秒数,这个值就是前面我们在 settings.py 中设置的 SESSION_COOKIE_AGE 的值。

clear_expired()

从 django_session 中移除过期的会话,下面会介绍 Session 这个 model 的相关操作,这里提前说一下这个函数。

django_session 会有一个 expire_date 字段,clear_expired() 这个操作就会删除表里 expire_date 小于当前时间的数据。

3、users 模块的准备

前面介绍了 session 的相关配置和方法以及 session 的基本使用流程。接下来我们将介绍如何在系统中使用上 session。

在介绍 session 使用前,我们自定义一个 users application 来做一下相关准备。

新建一个 application 和 相关的配置,在前面的笔记中都有介绍,这里不再做赘述,比如 app 的创建、在 settings.py 里 INSTALLED_APPS 里的定义,和 hunter/urls.py 的 patterns 里新建一条数据,指向 users/urls.py 等操作。

其中,在 hunter/urls.py 中对 users app 的 url 前缀我们定义为 users,如下:

# hunter/urls.py
from django.contrib import admin
from django.urls import path, include

urlpatterns = [
    path('admin/', admin.site.urls),
    path('blog/', include('blog.urls')),
    path('users/', include('users.urls')),
]

我们这里在 users/models.py 下新建一个 User model,然后对其进行相关的 migration 操作,使其表添加到数据库中。

# users/models.py

from django.db import models


class User(models.Model):
    username = models.CharField(max_length=20, verbose_name="登录用户名", unique=True)
    password = models.CharField(max_length=256, verbose_name="加密密码")

4、session 验证的的实现

接下来,我们将新建几个接口:

  • 用户注册接口
  • 用户登录接口
  • 用户注销接口
  • 用户信息接口

可以先看下这几个接口的代码总揽,接着我们详细介绍一下接口的操作。

users/urls.py

from django.urls import path
from users.views import LoginView, RegisterView, LogoutView, UserInfoView

urlpatterns = [
    path("register", RegisterView.as_view()),
    path("login", LoginView.as_view()),
    path("logout", LogoutView.as_view()),
    path("user/info", UserInfoView.as_view()),
]
users/views.py

from django.contrib.auth.hashers import make_password, check_password
from django.http import JsonResponse
from django.views import View
from users.models import User
import json


# 用户注册
class RegisterView(View):
    def post(self, request):
        request_json = json.loads(request.body)
        username = request_json.get("username")
        password = request_json.get("password")

        if not username or not password:
            result = {"code": -1, "msg": "username or password not valid"}
        else:
            if User.objects.filter(username=username).exists():
                result = {"code": -1, "msg": "username exists"}
            else:
                User.objects.create(username=username, password=make_password(password))
                result = {"code": 0, "msg": "success"}
        return JsonResponse(result, safe=False)


# 用户登录
class LoginView(View):
    def post(self, request):
        request_json = json.loads(request.body)
        username = request_json.get("username")
        password = request_json.get("password")

        if not username or not password:
            result = {"code": -1, "msg": "login info error"}
        else:
            user = User.objects.filter(username=username).first()
            if not user:
                result = {"code": -1, "msg": "username not found"}
            else:
                if check_password(password, user.password):
                    result = {"code": 0, "msg": "success"}
                    request.session["username"] = username
                else:
                    result = {"code": -1, "msg": "password error"}

        return JsonResponse(result, safe=False)


# 用户登出
class LogoutView(View):
    def post(self, request):
        if request.session.get("username"):
            del request.session["username"]
            # request.session.flush()
        return JsonResponse({"code": 0, "msg": "登出成功"})


# 用户信息
class UserInfoView(View):
    def post(self, request):
        username = request.session.get("username")
        if username:
            result = {"code": 0, "msg": f"登录用户为{username}"}
            status = 200
        else:
            result = {"code": -1, "msg": "用户未登录"}
            status = 401
        return JsonResponse(result, status=status)

首先介绍一下,所有请求的参数都是放在 body 里以 json 格式传递,我这里都是通过 postman 来请求测试的。

其次,在请求里,session 的处理可以直接通过 request.session 的方式进行,以下见示例。

用户注册接口

在注册接口里,这里做了参数校验的简化,直接 json.loads() 处理 body 的内容,然后通过 Django 自带的加密函数 make_password 将密码以加密的形式保存。

用户登录接口

登录接口里,首先是校验账号密码是否正确,判断正确后我们将登录用户的 username 字段写入 session,然后在用户下一次请求的时候就会自动获取该 session。

或者更正确的来说,用户登录在操作 request.session 之后,在返回 response 的时候,系统会在 django_session 里新增或者更新该用户的记录,这条数据有包含 session_key,session_data 和 expire_date 这几个字段。

session_key,在 cookie 的名称是 sessionid,postman 中第一次登录之后,在之后的每一次接口请求都会将sessionid=xx 传给后端,后端就会根据这个 session_key 的值去 django_session 表里查询相应的记录

如果这个 session_key 在表里不存在记录,或者 expire_date 过期了,那么后端系统会自动给其值赋为 None,即认定此次接口请求是未登录状态。

expire_date 字段则是一个时间字段,主要用于判断数据是否过期。

session_data 则是会包含我们写入的数据,比如我们在用户登录的时候,通过 request.session["username"] = username 的方式写入了一些特殊的标识,然后将其编码成 session_data 的值存入数据库,那么用户在下次请求接口的时候我们就可以通过解码 session_data,将值取出来用于判断用户是否登录。

将 session_data 解码的方式可以单独通过获取 django_session 的记录然后获取,但是在请求中,Django 为我么做了这些解码工作,我们可以直接通过前面介绍的 request.session.items() 的方式来查看在当前登录的 session_data 里写入的 key-value 数据。

注意: 前后端并不直接将 session_data 作为值传递,而是会传递 session_key 这个参数,一些校验的数据也都是放在 session_key 对应记录的 session_data 中存在后台的数据库中。

用户信息接口

我们假定获取用户信息接口要求用户必须处于登录状态,实际上也是,因为用户不登录无法定位到用户,然后获取用户的信息。

那么我们在进行下一步的实际操作前,我们肯定需要尝试从 session 中获取用户相应的信息,如果获取到了,则判断是处于登录状态,否则是处于未登录状态,无法获取用户信息。

所以我们这里的判断是从 session 中获取 username 字段,通过判断 username 是否有值来判断用户是否处于登录状态。

用户注销接口

用户注销,也就是登出接口,我们这里用的是 del 的方式,这个主要是看我们验证用户登录的方式,比如我们是通过向 session 中取值 username 来判断用户是否登录,那么 del request.session["username"] 的操作即可实现注销的功能。

注意: 这里执行的 del 操作仅仅是删除 session_data 中的 {"username": "xxx"} 的数据,这条 session_key 对应的数据还存在。

可以看到,在这条代码的下一行还有一条是执行的 flush() 操作,这个操作是直接在数据库里删除这条 session 记录,这是一种更为彻底的登出操作。

这里还需要注意的一点是,del 操作的前提是 session 数据里必须要有 username 这个 key,否则会引起报错,所以我们这里用了一个 if 判断逻辑,我们还可以使用 try-except 操作,或者更为彻底的操作是直接使用 flush() 操作。

至此,用户登录登出以及 session 数据的基本使用操作就介绍完毕了,下面我们额外介绍一些操作。

5、Session 表介绍

django_session 表的单独获取查看操作一般在程序里不会出现,因为前后端都是通过 cookie 中 sessionid 直接获取到对应的数据,但为了以防万一,或者你对这张表有一些兴趣,这里额外介绍一下如何单独操作这张表里的数据。

django_session 表的引入方式如下:

from django.contrib.sessions.models import Session

然后通过 session_key 来获取这条数据,比如 session_key 为 nqu3s71e38279bl5cbgju6sut64tnqmx,就可以:

session_key = "nqu3s71e38279bl5cbgju6sut64tnqmx"

session = Session.objects.get(pk=session_key)
# session = Session.objects.get(session_key=session_key)

其中,我们向 session 里写入的数据都包含在 session.session_data 里,我么可以直接通过 get_decoded() 方法来获取:

session.get_decoded()

# {'username': 'root'}

6、登录验证的几种实现形式

获取用户信息这个接口需要用户登录才可以接着获取用户信息,我们这里的操作是直接判断 session 里是否含有 username 字段。

但是如果我们系统里大部分接口都是需要用户先登录才可访问,这样在每个 views 里都要先加这个判断的操作,这样的显然是不实际的。

那么我们可以怎么操作来实现这个重复性的操作呢?

这里提供两个方式,一个是装饰器,一个是写在中间件里。

装饰器实现登录验证

其实如果直接使用 Django 自带的登录验证的功能,是可以直接使用系统自带的装饰器的,但是我们这里的表都是手动操作的,所以这个功能的装饰器我这里就自己实现了一个,相关代码如下:

def login_required_manual(func):
    def wrapper(*args, **kwargs):
        request = args[1]
        if not request.session.get("username"):
            return JsonResponse({"code": -1, "msg": "not login"}, status=401)
        return func(*args, **kwargs)
    return wrapper


class UserInfoView(View):
    @login_required_manual
    def post(self, request):
        username = request.session.get("username")
        return JsonResponse({"code": 0, "msg": f"登录用户{username}"})

可以看到,使用了登录验证的装饰器之后,我们的代码都简洁了很多。

我们可以尝试在调用登出接口后,再调用用户信息接口,可以看到系统就自动返回了未登录的信息了。

中间件实现登录验证

这里我们假定目前仅仅是注册和登录不需要登录即可访问,然后我们创建一个中间件如下:

# hunter/middlewares/auth_middleware.py

from django.http import JsonResponse

class AuthMiddleware:

    def __init__(self, get_response):
        self.get_response = get_response

    def __call__(self, request):
        path = request.path

        # url 路径为 /users/register 和 /users/login 的接口不需要进行判断验证
        if path not in [
            "/users/register",
            "/users/login",
        ]:
            session = request.session
            if not session.get("username"):
                return JsonResponse({"code": -1, "msg": "not login"}, status=401)

        response = self.get_response(request)
        return response

然后在 hunter/settings.py 里加上这个中间件:

# hunter/settings.py

INSTALLED_APPS = [
    ...
    'hunter.middlewares.auth_middleware.AuthMiddleware',
    ...
]

这样,在每个接口请求到达 views 视图前,都会经历这个验证的中间件,这里将接口路径的判断简化成注册接口和登录接口,这两个接口不需要登录即可访问,其他接口都设置成需要登录才可访问。

相比于装饰器的做法,这里更推荐中间件的操作方式,这样首先就不用在每个 views 前加上装饰器,另外,需要登录才可访问的接口都可以在中间件部分统一列举出来,方便查看。

以上就是本篇笔记关于 session 的全部内容。

如果想获取更多相关文章,可扫码关注阅读:
image.png


Hunter
27 声望12 粉丝