一、概述
Django从开始就带有一个用户认证系统;它处理用户账号、组、权限以及基于cookie的用户会话。
Django认证系统同时处理认证和授权。简单地讲,认证验证一个用户是否它们声称的那个人,授权决定一个通过了认证的用户被允许做什么。这里的词语“认证”同时指代这两项任务。
认证系统包含:
- 用户(Users)
- 权限(Permissions):二元(是/否)标志指示一个用户是否可以做一个特定的任务
- 组(Groups):对多个用户运用标签和权限的一种通用的方式
- 一个可配置的密码哈希系统
- 用于登录用户或限制内容的表单和视图
- 一个可选的后台系统
Django中的认证系统致力于变得非常通用,但它不提供在web认证系统中某些常见的功能。某些常见问题的解决方法已经在第三方包中实现:
- 密码强度检查
- 限制登录的尝试
- 第三方认证(例如OAuth)
二、安装
身份认证 ( Authentication ) 在django.contrib.auth
中作为 Django contrib 模块捆绑在一起。默认情况下,所需的配置已包含在由django-admin startproject
生成的settings.py
中,这些配置由INSTALLED_APPS
设置中列出的两个项目组成:
'django.contrib.auth'
包含认证框架的核心和默认的模型。'django.contrib.contenttypes'
是Django内容类型系统,它允许权限与你创建的模型关联。
以及MIDDLEWARE_CLASSES设置中的两个选项:
SessionMiddleware
管理请求之间的会话。AuthenticationMiddleware
使用会话将用户与请求关联起来。
有了这些设置,运行命令manage.py migrate
将为认证相关的模型创建必要的数据库表并为你的应用中定义的任意模型创建权限。以上这些默认都是启用的,直接使用即可。
三、创建用户
第一种使用manage.py创建用户,这创建的是一个超级用户:
1 |
$ python manage.py createsuperuser --username=admin --email=admin@admin.com |
将会提示你输入一个密码,在你输入一个密码后,该user将会立即创建。如果不带--username
和--email
选项,将会提示你输入这些值。
第二种使用api接口创建用户,这创建的是一个普通用户:
1 2 3 4 |
>>> from django.contrib.auth.models import User >>> user = User.objects.create_user('dkey','dkey@163.com','123456') >>> user.last_name = 'peng' >>> user.save() |
四、更改密码
Django不会在user模型上存储原始的(明文)密码,而只是一个哈希(完整的细节参见文档:密码是如何管理的)。因为这个原因,不要尝试直接操作user的password属性。这也是为什么创建一个user时要使用辅助函数。
若要修改一个用户的密码,你有几种选择:
1 |
$ python manage.py changepassword admin |
提供一种从命令行修改User密码的方法。它提示你修改一个给定user的密码,你必须输入两次。如果它们匹配,新的密码将会立即修改。如果你没有提供user,命令行将尝试修改与当前系统用户匹配的用户名的密码。
你也可以通过程序修改密码,使用set_password()
:
1 2 3 4 5 6 7 8 9 10 11 12 |
>>> from django.contrib.auth.models import User >>> u = User.objects.get(username='dkey') # set_password函数,设置密码; >>> u.set_password('123456') # check_password函数,检查密码; >>> u.check_password('123456') True # save函数,保存密码; >>> u.save() |
如果你安装了Django admin,你还可以在认证系统的admin页面修改user的密码。
Django还提供views和forms用于允许user修改他们自己密码。
五、认证
使用authenticate(request=None, **credentials)
来认证一组给定的用户名和密码。它接收关键字参数形式的凭证,使用默认配置时参数是是username
和password
,如果密码能够匹配给定的用户名,它将返回User
对象。如果凭证对任何后端无效,或者后端引发PermissionDenied
,则返回None
。例子:
1 2 3 4 5 6 7 8 9 10 11 |
from django.contrib.auth import authenticate user = authenticate(username='dkey', password='123456') if user is not None: # the password verified for the user if user.is_active: print("User is valid, active and authenticated") else: print("The password is valid, but the account has been disabled!") else: # the authentication system was unable to verify the username and password print("The username and password were incorrect.") |
六、登录
如果你有一个认证了的用户,你想把它附带到当前的会话中 – 这可以通过login()
函数完成。
从视图中登入一个用户,请使用login()
。它接受一个HttpRequest对象和一个User对象。login()
使用Django的session框架来用户的ID保存在session中。
注意,任何在匿名会话中设置的数据都会在用户登入后的会话中都会记住。
下面的示例向你演示如何使用authenticate()
和login()
:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 |
from django.contrib.auth import authenticate, login def my_view(request): username = request.POST['username'] password = request.POST['password'] user = authenticate(username=username, password=password) if user is not None: if user.is_active: login(request, user) # Redirect to a success page. else: # Return a 'disabled account' error message ... else: # Return an 'invalid login' error message. ... |
当你是手工登入一个用户时,你必须在调用login()
之前通过和authenticate()
成功地认证该用户。authenticate()
在用户上设置一个属性,注意哪个认证后端成功验证了该用户(有关详细信息,请参阅后端文档),以及此信息以后在登录过程中需要。如果你试图登入一个直接从数据库中取出的用户,将会抛出一个错误。
login()
做的事情就有给该用户设置session信息的,保证后面验证用户是否认证通过。
七、登出
若要登出一个已经通过django.contrib.auth.login()
登入的用户,可以在你的视图中使用django.contrib.auth.logout()
。 它接收一个HttpRequest对象且没有返回值。例如:
1 2 3 4 5 |
from django.contrib.auth import logout def logout_view(request): logout(request) # Redirect to a success page. |
注意,即使用户没有登入,login()
也不会抛出任何错误。
当你调用logout()时,当前请求的会话数据将被完全清除。所有存在的数据都将清除。这是为了防止另外一个人使用相同的Web浏览器登入并访问前一个用户的会话数据。如果你想在用户登出之后>可以立即访问放入会话中的数据,请在调用django.contrib.auth.logout()
之后放入。
八、只允许登录的用户访问
限制页面访问的简单、原始的方法是检查用request.user.is_authenticated()
并重定向到一个登陆页面,或者显示一个错误信息:
1 2 3 4 5 6 7 8 |
from django.conf import settings from django.shortcuts import redirect def my_view(request): if not request.user.is_authenticated(): return redirect('%s?next=%s' % (settings.LOGIN_URL, request.path)) else: do_something() |
使用login_required装饰器
login_required([redirect_field_name=REDIRECT_FIELD_NAME, login_url=None])
1 2 3 4 5 |
from django.contrib.auth.decorators import login_required @login_required def my_view(request): ... |
login_required()完成下面的事情:
1. 如果用户没有登入,则重定向到settings.LOGIN_URL,并将当前访问的绝对路径传递到查询字符串中。比如访问/polls/,默认跳转:/accounts/login/?next=/polls/。
2. 如果用户已经登入,则正常执行视图。视图的代码可以安全地假设用户已经登入。
默认情况下,在成功认证后用户应该被重定向的路径存储在查询字符串的一个叫做”next”的参数中。如果对该参数你倾向使用一个不同的名字,login_required()带有一个可选的redirect_field_name参数:
1 2 3 4 5 |
from django.contrib.auth.decorators import login_required @login_required(redirect_field_name='my_redirect_field') def my_view(request): ... |
注意,如果你提供一个值给redirect_field_name,你可能同时需要自定义你的登录模板,因为存储重定向路径的模板上下文变量将使用redirect_field_name值作为它的键,而不是默认的”next”。
login_required()还带有一个可选的login_url参数。例如:
1 2 3 |
@login_required(login_url='/polls/login/') def my_view(request): ... |
注意,如果你没有指定login_url参数,你需要确保settings.LOGIN_URL与你的登录视图正确关联。例如,使用默认值,可以添加下面几行到你的URLconf中:
1 2 3 4 5 |
from django.contrib.auth import views as auth_views urlpatterns = [ url(r'^polls/login/$', auth_views.login), .... ] |
settings.LOGIN_URL同时还接收视图函数名和命名的URL模式。这允许你自由地重新映射你的URLconf中的登录视图而不用更新设置。
注:login_required装饰器不检查user的is_active标志位。
九、综合示例
下面来完成一个简单的登录、登出、未登录限制访问等配置。先写三个views,一个登录视图login_view、一个登出视图logout_view、一个模拟访问视图index,如下。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 |
# ./polls/views.py from django.shortcuts import render, get_object_or_404 from django.http import HttpResponse, HttpResponseNotFound from django.http import HttpResponseRedirect, JsonResponse from django.contrib.auth import logout, login, authenticate from django.urls import reverse from django.contrib.auth.decorators import login_required def login_view(request): if request.method == 'POST': username = request.POST['username'] password = request.POST['password'] user = authenticate(username=username, password=password) if user is not None: if user.is_active: login(request, user) return HttpResponse('Login success') else: return HttpResponse('Login fail', status=403) else: if request.user.is_authenticated(): #捕获已经登录; return HttpResponse('User already login', status=200) return render(request, 'polls/login.html') def logout_view(request): logout(request) return HttpResponseRedirect('/polls/login/') @login_required(login_url='/polls/login/') #未登录访问此视图时跳转至登录页面; def index(request): return HttpResponse('index page') |
这里用了is_authenticated()这个认证的方法,验证是否通过,也就是通过用户名和密码判断该用户是否存在。另外还有is_anonymous()方法也常用,判断是否为匿名用户,如果你已经login,则这个方法返回始终为false。
urls.py配置如下
1 2 3 4 5 6 7 8 9 10 |
./polls/urls.py from django.conf.urls import url from . import views app_name = 'polls' urlpatterns = [ url(r'^$', views.index, name='index'), url(r'^login/$', views.login_view, name='login'), url(r'^logout/$', views.logout_view, name='logout'), ] |
提供一个login页面:
1 2 3 4 5 6 7 8 9 |
./polls/templates/polls/login.html <form action="" method="post"> {% csrf_token %} <label> Username: </label> <input type="text" name="username"></br> <label> Password: </label> <input type="password" name="password"></br> <input type="submit" value="Login in"> </form> |
整个过程如下图,正常访问:http://10.10.0.109:8000/polls/,由于没有登录会跳转到登录页面,如下:
最后,可以访问http://10.10.0.109:8000/polls/logout,退出登录,又会跳转到登录页面。
正常情况下,点击无权限登录的页面会跳转到登录页面;同时,登录完成后会跳转回源页面,就可以通过next参数来重定向:
1 |
return HttpResponseRedirect(request.GET.get('next', '/')) |
再智能点就是判断next是否有值,没有值就表示用户直接从http://10.10.0.109:8000/polls/login/地址过来的,这个时候就单独处理一下,如下:
1 2 3 4 |
if request.GET.get('next') is None: return HttpResponse('Login success') else: return HttpResponseRedirect(request.GET.get('next', '/')) |
基本上认证就这样了。
十、扩展User模型
有些时候Django的内置User
模型并不总是适合的。例如,在一些网站上,使用电子邮件地址作为身份标记而不是用户名更有意义。因此,Django允许你通过为引用自定义模型的AUTH_USER_MODEL
设置提供值来覆盖默认用户模型。
1 2 |
# settings.py AUTH_USER_MODEL = 'polls.Account' |
polls表示Django应用程序的名称(它必须位于INSTALLED_APPS
中),Account是用作用户模型的Django模型的名称。
然后就可以定义你自己的模型了,最简便的方法就是基层现有的User模型,在此之上做扩展。需要继承AbstractUser
类即可。
1 2 3 4 5 |
from django.contrib.auth.models import AbstractUser class Account(AbstractUser): permissiongroup = models.CharField(max_length=40) # 权限组 department = models.CharField(max_length=40) # 部门 |
另外,请在应用程序的admin.py
中注册模型:
1 2 3 4 5 |
from django.contrib import admin from django.contrib.auth.admin import UserAdmin from .models import User admin.site.register(User, UserAdmin) |
需要注意的是,如果在重新封装更新用户表之前,表已经更新了数据表,在数据库中已经有了 Django 相关的依赖表,就会报错。需要重建数据库。
另外,django-authtools这个项目也是关于修改默认用户模型的,不过默认使用email作为用户登录名,源码写的很好值得一看。
十一、引用User模型
不要直接引用User
,而应该使用django.contrib.auth.get_user_model()
引用User
模型。此方法将返回当前活动的用户模型 – 如果指定了用户模型,则返回自定义用户模型,否则返回User
。
在为用户模型定义外键或多对多关系时,应使用AUTH_USER_MODEL
指定自定义模型。例如:
1 2 3 4 5 6 7 8 |
from django.conf import settings from django.db import models class Article(models.Model): author = models.ForeignKey( settings.AUTH_USER_MODEL, on_delete=models.CASCADE, ) |
一般来说,用导入时执行的代码中的AUTH_USER_MODEL
来引用用户模型是最容易的,但是,也可以在 Django 导入模型时调用get_user_model()
,以便使用models.ForeignKey(get_user_model(), ...)
。
1 2 3 |
from django.contrib.auth import get_user_model User = get_user_model() |
十二、session和cookie
为什么会有cookie存在?
Http协议本身是无状态的协议,服务器在接收到浏览器的请求之后,服务器是直接返回内容给我们浏览器,不管浏览器是谁请求的。cookie是一种发送到客户浏览器的文本串句柄,并保存在客户机硬盘上,可以用来在某个WEB站点会话间持久的保持数据。
简单的说,当你登录一个网站的时候,如果浏览器使用的是cookie,那么所有的数据都保存在浏览器端。比如你登录以后,服务器设置了cookie用户名,那么当你再次请求服务器的时候,浏览器会将用户名一块发送给服务器,这些变量有一定的特殊标记。服务器会解释为cookie变量,所以只要不关闭浏览器,那么cookie变量一直是有效的,所以能够保证长时间不掉线。如果你能够截获某个用户的cookie变量,然后伪造一个数据包发送过去,那么服务器还是认为你是合法的。所以,使用cookie被攻击的可能性比较大。如果设置了的有效时间,那么它会将cookie保存在客户端的硬盘上,下次再访问该网站的时候,浏览器先检查有没有cookie,如果有的话,就读取该cookie,然后发送给服务器。如果你在机器上面保存了某个论坛cookie,有效期是一年,如果有人入侵你的机器,将你的cookie拷走,然后放在他的浏览器的目录下面,那么他登录该网站的时候就是用你的的身份登录的。所以cookie是可以伪造的。当然,伪造的时候需要主意,直接copy cookie文件到cookie目录,浏览器是不认的,他有一个index.dat文件,存储了cookie文件的建立时间,以及是否有修改,所以你必须先要有该网站的cookie文件,并且要从保证时间上骗过浏览器。
为什么会有session?
由于cookie不安全,服务器在返回账号密码等信息的时候它用到了一种session机制。其实就是根据用户名和密码生成了随机字符串,称之为session_id,浏览器把它存储在cookie中。这段字符串是有过期时间的,这个session_id是服务器生成的,是存储在服务器端的。当浏览器发起请求时会带上session_id,然后服务器通过session_id查询这个用户的session_data并解密。
简单的说,当你登录一个网站的时候,如果web服务器端使用的是session,那么所有的数据都保存在服务器上,客户端每次请求服务器的时候会发送当前会话的session_id,服务器根据当前session_id判断相应的用户数据标志,以确定用户是否登录或具有某种权限。由于数据是存储在服务器上面,所以你不能伪造,但是如果你能够获取某个登录用户的session_id,用特殊的浏览器伪造该用户的请求也是能够成功的。session_id是服务器和客户端链接时候随机分配的,一般来说是不会有重复,但如果有大量的并发请求,也不是没有重复的可能性。
Django对此引申出了session.login,就是生成session。对应的django在数据库中自动生成了django_session表用于存放用户session。表字段session_key就是浏览器中的session_id,session_data是对账号密码等信息做了加密的,expire_date是过期时间;在setting中可设置过期时间。
这个session_id是怎么做到转换回账号密码等信息的?因为我们在后台是可以直接request.username的。
1 2 3 |
INSTALLED_APPS = [ 'django.contrib.sessions', ] |
这个app是会对我们每次request和respone的请求做拦截的,拦截浏览器过来的时候就会在里面找到我们的session_id。然后来数据表查询session_data并解密,我们response的时候他也会主动加上session_id。
两个都可以用来存私密的东西,同样也都有有效期的说法,区别在于session是放在服务器上的,过期与否取决于服务期的设定,cookie是存在客户端的,过期与否可以在cookie生成的时候设置进去。cookie和session的共同之处在于:cookie和session都是用来跟踪浏览器用户身份的会话方式。cookie和session的区别是:cookie数据保存在客户端,session数据保存在服务器端。