本小节大概需要花费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
。
在Web服务器后端第一次接到某个请求的时候,会返回一个带session_id
的Cookies给浏览器。之后浏览器每次请求,都会带上这个Cookies。为了判断用户是否登录,我们会在后端保存一个和这个session_id
相关的字段,如is_logined
以及user_id
。若is_logined
为true
,则持有这个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
下填入我们的用户认证模块的逻辑代码。在此之前还需要做几个工作:
- 将
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',
]
...
- 在
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),
]
- 在
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
这个函数。
- 如果之前没有初始化数据库,则需要初始化数据库,用来存放用户信息,以及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
为-1
,message
为Permission Denied