(三) Django REST实践:WEB登录原理与用户系统实现

本小节大概需要花费30分钟。

在第二节,我们已经学会了如何写一个最简单的REST API。在这一节,我们将利用这种REST API通讯模型,构建一个用户系统,包括注册、登录、访问控制、登出。


Web登录原理

当我们发起一个HTTP请求的时候,服务器如何根据这个请求判断我们是否登录了呢?这就要说到Cookies和Session了。

Cookies是服务器用来临时在客户端(也就是浏览器)上保存数据的,是一个个的key-value对。当我们向服务器发起一个HTTP请求时,服务器的Response会携带Cookies的信息。这个时候我们的浏览器就会保存这些Cookies的信息,并在下一次发送HTTP请求的时候携带上这些Cookies信息。

在早先的时候,服务器为了保存某一个会话相关的信息,会把一些比较重要的数据放到用户的Cookies里面。这样做不太安全,于是有人提出了Session。服务器通过给客户端的Cookies设置一个session_id(一个随机字符串)来标识会话,并在服务器端保存和这个session_id相关的数据。这样服务器端既能知道当前是哪个会话,又能知道和这个会话相关的数据都有什么,同时还避免了将一些重要的数据存放到客户端。如下图,是在Chrome里截取的HTTP Request Header,这个Request Header里面的Cookies段里所表示的,就是访问某个Django后端时用到的session_id

(三) Django REST实践:WEB登录原理与用户系统实现_第1张图片
session_id

在Web服务器后端第一次接到某个请求的时候,会返回一个带session_id的Cookies给浏览器。之后浏览器每次请求,都会带上这个Cookies。为了判断用户是否登录,我们会在后端保存一个和这个session_id相关的字段,如is_logined以及user_id。若is_loginedtrue,则持有这个session_id的请求就判断为登录了,且相关用户的id为user_id,否则判断为未登录。同时,我们可以为session_id这个Cookies加一个超时时间,来控制一次登录操作可以维持多长时间(如一小时、一星期等)。超过这个时间,session_id会失效,并要求用户重新登录。

Session

Django中,Session相关的信息是存储在数据库中的。所以在使用Session之前,还需要初始化数据库:

> python manage.py makemigrations
No changes detected
> python manage.py migrate
Operations to perform:
  Apply all migrations: admin, auth, contenttypes, sessions
Running migrations:
  Applying contenttypes.0001_initial... OK
  Applying auth.0001_initial... OK
  Applying admin.0001_initial... OK
... ...

在Django中操作Session的方法也比较直接:

def test_session(request):
    print(request.session.get("privacy"))
    request.session["privacy"] = "This message should not be stored in Cookies"

    return HttpResponse()

可以看到,可以直接通过request.session来获取和改变和这个session相关的值。

Cookies

对于Cookies,可以用如下方法进行访问、修改:

def test_cookies(request):
    print(request.COOKIES)
    response = HttpResponse()
    response.set_cookie("this_is_key", "this is value")
    return response

在第一次访问这个接口的时候,打印出的Cookies只包含sessionid。之后的请求会打印出如下Cookies:

{'this_is_key': 'this is value', 'sessionid': '32xgq0cw0bqgnfg0qu81sp19n2el49yl'}

总结一下,针对Cookies的基本操作有以下几种:

  • 可以通过request.COOKIES这个dict直接访问请求中携带的Cookies
  • 可以通过response.set_cookie(, )来设置Cookies
  • 可以通过response.delete_cookie()来删除某个Cookies

其中set_cookies函数还可以加入Cookies超时时间、可用域等属性。有兴趣的可以在浏览器里试一下,这些操作会产生怎样的效果。

注册、登录、访问控制、登出功能

有了上面的基础,想必你已经想到如何在Django中自己实现一套用户认证模块了:

  • 注册:用户进行注册请求,在数据库中保存其用户名,以及加密后的密码
  • 登录:在用户进行登录请求时,判断其用户名密码是否正确,是的话即将session中的is_logined置为true
  • 访问控制:用户访问一个需要登录的接口时,首先判断request.session中的is_logined是否为true。若为true则继续执行,否则将用户重定向到登录页面
  • 登出:用户登出时,只需要将request.session中的is_logined置为false即可(或将该session_id置为无效)

但是Django提供了一套比较方便的用户认证模块,其基本原理就像我们上面所说的。何不直接使用它呢?

准备

为了不同类型接口之间功能隔离起见,我们在Django中新建一个app:

> python manage.py startapp account

Django中app主要就是为了起到功能隔离的作用,不同模块之间保持相对独立的状态。这样既有利于模块之间的解耦,也有利于单个模块的复用。比如我们今天做的认证模块,就可以实现成可以被复用的模块。

可以看到,项目下多了一个account目录。其目录结构和task_platform相似。我们将在account/views.py下填入我们的用户认证模块的逻辑代码。在此之前还需要做几个工作:

  1. account填入task_platform/settings.py里的INSTALLED_APPS中:
...
INSTALLED_APPS = [
    'django.contrib.admin',
    'django.contrib.auth',
    'django.contrib.contenttypes',
    'django.contrib.sessions',
    'django.contrib.messages',
    'django.contrib.staticfiles',
    'account',
]
...
  1. account/urls.py中添加url路由(没有该文件则自己创建一个):
from django.urls import path

from account import views

urlpatterns = [
    path('login/', views.user_login),
    path('logout/', views.user_logout),
    path('register/', views.user_register),
    path('detail/', views.user_detail),
]
  1. task_platform/urls.py将与account相关的url代理到account这个app中:
from django.urls import path, include

urlpatterns = [
    ...
    path('account/', include('account.urls')),
]

这段代码的意思是:只要是以account/开头的url,都转移给第一步中我们创建的account.urls进行代理。

经过这几个设置后,当我们访问account/login/的时候,服务器就会相应的执行account.views.user_login这个函数。

  1. 如果之前没有初始化数据库,则需要初始化数据库,用来存放用户信息,以及Session相关的信息:
> python manage.py makemigrations
> python manage.py migrate

下面将在account/views.py中填入具体逻辑。

注册

account/views.py前面一段为:

import json

from django.contrib.auth import authenticate, login, logout
from django.contrib.auth.decorators import login_required
from django.contrib.auth.models import User
from django.http import JsonResponse

def __get_response_json_dict(data, err_code=0, message="Success"):
    ret = {
    'err_code': err_code,
    'message': message,
    'data': data
    }
    return ret

注册具体逻辑如下:

def user_register(request):
    received_data = json.loads(request.body.decode('utf-8'))

    username = received_data["username"]
    password = received_data["password"]

    user = User(username=username)
    user.set_password(password)

    user.save()

    return JsonResponse(__get_response_json_dict(data={}))

其中:

  • User为Django认证模块内置的model,用来存放用户的基本信息,包括用户名以及密码
  • set_password将password进行加密存储
  • save将我们在程序中新建的User对象保存到数据库中

其请求为:

{
    "username": "Marry",
    "password": "password"
}

响应为:

{"err_code": 0, "message": "Success", "data": {}}

登录

def user_login(request):
    received_data = json.loads(request.body.decode('utf-8'))

    username = received_data["username"]
    password = received_data["password"]

    user = authenticate(username=username, password=password)
    if user:
        login(request, user)
        return JsonResponse(__get_response_json_dict(data={}))
    else:
        return JsonResponse(__get_response_json_dict(data={}, err_code=-1, message="Invalid username or password"))

其中:

  • authenticate为Django认证模块函数,用于检查用户名、密码是否正确。若正确,则返回相应的User对象,否则返回None
  • login函数为Django认证模块函数,用于标记用户的登录标志,类似于我们之前说的,将与该session相关的is_logined字段置为true

其请求为:

{
    "username": "Marry",
    "password": "password"
}

响应为:

{"err_code": 0, "message": "Success", "data": {}}

访问控制

@login_required
def user_detail(request):

    response_data = {"username": request.user.username}

    return JsonResponse(__get_response_json_dict(data=response_data))

其中

  • @login_required为Django认证模块自带的装饰器,用于在进入处理函数之前,对其是否已登录进行检查。其原理类似于检查与该请求session_id相对应的is_logined字段是否为true
  • 如果用户已经登录,则可以直接通过request.user获取到登录用户的User对象,并通过request.user.<属性>来访问该User对象的属性

其响应为:

{"err_code": 0, "message": "Success", "data": {"username": "Marry"}}

登出

def user_logout(request):
    logout(request)
    return JsonResponse(__get_response_json_dict(data={}))

其中:

  • logout为Django认证模块内置函数,用于将用户在会话中登出。其原理类似于将与该session相关的is_logined字段置为false,或者直接将该Cooikes(session_id)置为无效。

其响应为:

{"err_code": 0, "message": "Success", "data": {}}

登出后,如果我们再访问account/detail/,则会收到一个code为302的response,将我们重定向到accounts/login/这个url。这是个行为是@login_required的默认行为,用于用户在访问某些需要登录的接口时,直接将其重定向到登录界面。这个重定向到的url可以通过修改task_platform/settings.py内的LOGIN_REDIRECT_URL来该设置。

总结

本节中,我们分析了Web应用实现用户认证模块的基本原理(Session,Cookies),并利用Django自带的用户认证模块,实现了一套简单的用户认证REST API,包含注册、登录、访问控制、登出。本文中所罗列的逻辑,只包含了最核心的代码。还有一些异常情况(如注册同用户名账号时产生的异常),需要你自己写代码来处理。当你实现好这些后,几乎可以将account这个模块用于所有需要认证的Django项目中。

练习

  • 考虑上面所实现的用户认证函数中的需要处理的异常情况,罗列出来,并逐一实现
  • 考虑如何用已有知识,实现二次认证(如邮箱认证)
  • 考虑如何自己实现一个@login_required,以将其在用户未登录时,进行“重定向”这个默认行为,改变为返回一个Json Response,其err_code-1messagePermission Denied

你可能感兴趣的:((三) Django REST实践:WEB登录原理与用户系统实现)