用户系统是现代网站的重要组成部分,对用户进行分组权限管理是非常必要的。
Django内置了一套用户和身份验证系统,不用太多代码开发就可以使用这个系统。
Django 的身份验证系统包括:
• 用户
• 权限:二元(是或否)旗标,指明用户是否能执行特定的任务
• 分组:把标注和权限赋予多个用户的通用方式
• 可配置的密码哈希系统
• 管理身份验证和权限核准的表单
• 登录用户或限制内容的视图工具
• 可更换的后端系统
Django 的身份验证系统十分通用,没有提供 Web 身份验证系统中某些常用的功能。
某些常用功能通过第三 方包实现:
• 密码强度检查
• 登录尝试次数限制
• 通过第三方验证身份(如 OAuth)
1、User对象
User 对象是这个身份验证系统的核心,通常用于标识与网站交互的人,还用于限制访问、记录用户资料,以 及把内容与创建人关联起来,等等。
在 Django 的身份验证框架中,只有一个用户类存在,因此 superusers 或管理后台的 staff 用户只是设定了特殊属性的用户对象,而不是分属不同类的用户对象。
默认用户主要有 下面几个属性:
• username
• password
• first_name
• last_name
1.1 创建超级用户
超级用户使用 createsuperuser 命令创建:
python manage.py createsuperuser --username=teacherWang --email=wangchongyang@126.com
上述命令会提示你输入密码。输入密码后,立即创建指定的超级用户。
如果没有指定 --username 或 --email 选项,会提示你输入这两个值。
Email address: wangchongyang@126.com
Password:
Password (again):
This password is too short. It must contain at least 8 characters.
This password is too common.
Bypass password validation and create user anyway? [y/N]: n
Password:
Password (again):
This password is too common.
Bypass password validation and create user anyway? [y/N]: y
Superuser created successfully.
最终看到 Superuser created successfully说明创建成功了。
1.2 创建用户
创建和管理用户最简单、最不易出错的方式是使用 Django 管理后台。
管理后台的使用我们在第七章((1条消息) <学习笔记>从零开始自学Python-之-web应用框架Django( 七)Django管理后台_阿尔法羊的博客-CSDN博客)详细讲过了
当然也可以用Django内置的辅助函数create_user()来实现同样的功能
from django.contrib.auth.models import User
user = User.objects.create_user('teacherWang', 'wangchongyang@126.com', 'password')
# 此时,user 是一个 User 对象,而且已经保存到数据库中
# 如果想修改其他字段的值,可以继续修改属性
user.last_name = 'wang'
user.save()
1.3 在 Web 请求中验证身份
Django 使用会话和中间件把身份验证系统插入 request 对象,为每个请求提供 request.user 属性,表示当前用户。如果未登陆,这个属性的值是一个 AnonymousUser 实例,否则是是一个 User 实例。
这两种情况可以使 用 is_authenticated() 方法区分,例如:
if request.user.is_authenticated():
# 处理通过身份验证的用户
else:
# 处理匿名用户
2、在视图中操作身份验证
2.1 登录与退出
2.1.1 在视图中使用 login() 登录用户。
它的参数是一个 HttpRequest 对象和一个 User 对象。login() 使用 Django 的会话框架把用户的 ID 保存到会话中。注意,匿名期间设定的会话数据在用户登录后依然存在。
下述示例 展示 authenticate() 和 login() 的用法:
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)
# 重定向到成功登录页面
else:
# 返回“账户未激活”错误消息
else:
# 返回“无效登录”错误消息
注意:自己动手登录用户时,必须在 login() 之前调用 authenticate()。authenticate() 在 User 对象 上设定一个属性,指明成功验证用户身份的是哪个身份验证后端,而登录过程中需要使用这个信息。如果直接登录从数据库中检索的用户对象,Django 会报错。
2.1.2 在视图中使用logout()退出
在视图中退出通过 login() 登录的用户使用 logout()。这个函数的参数是一个 HttpRequest 对象,而且没有返回值。
例如:
from django.contrib.auth import logout
def logout_view(request):
logout(request)
# 重定向到成功退出页面
注意,如果用户未登录,logout() 函数不报错。调用 logout() 函数后,当前请求的会话数据完全清除,所有 数据将被删除。这样能避免其他人在登录的 Web 浏览器中访问用户之前的会话数据。 如果想让会话中的数据在退出后依然可用,调用 logout() 函数之后再把数据存入会话。
2.2 限制已登录用户的访问
2.2.1 直接方式
限制访问页面简单直接的方式是检查 request.user.is_authenticated(),如果未通过,可以重定向到登录页面:
from django.shortcuts import redirect
def my_view(request):
if not request.user.is_authenticated():
return redirect('/login/?next=%s' % request.path)
# ... 也可以显示一个错误消息:
from django.shortcuts import render
def my_view(request):
if not request.user.is_authenticated():
return render(request, 'books/login_error.html')
2.2.2 装饰器
也可以使用便利的 login_required() 装饰器:
from django.contrib.auth.decorators import login_required
@login_required
def my_view(request):
...
login_required() 的作用如下:
• 如果用户未登录,重定向到 LOGIN_URL,并把当前绝对路径添加到查询字符串中。
例如:/accounts/ login/?next=/reviews/3/。
• 如果用户已登录,正常执行视图。视图代码可以放心假定用户已登录。
默认,成功通过身份验证后重定向的目标路径存储在名为 next 的查询字符串参数中。如果想为这个参数提供 其他名称,可以设定 login_required() 可选的 redirect_field_name 参数:
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 参 数。例如:
from django.contrib.auth.decorators import login_required
@login_required(login_url='/accounts/login/')
def my_view(request):
...
注意,如果不指定 login_url 参数,要确保把 LOGIN_URL 设为正确的登录视图。例如,使用默认配置时,要 把下述代码添加到 URL 配置中:
from django.contrib.auth import views as auth_views
url(r'^accounts/login/$', auth_views.login),
LOGIN_URL 的值还可以是视图函数名称或具名 URL 模式。这样,无需修改设置就可以在 URL 配置中自由映射登录视图。
2.3 permission_required 装饰器
检查用户有没有特定权限是比较常见的任务。鉴于此,Django 提供了一种简便的方式——permission_required() 装饰器:
from django.contrib.auth.decorators import permission_required @permission_required('reviews.can_vote')
def my_view(request):
...
与 has_perm() 方法一样,参数名称的形式为“.”(例如,reviews.can_vote 是 reviews 应用中某个模型定义的权限)。这个装饰器的参数也可以是一个权限列表。注意,permission_required() 也有可选的 login_url 参数。例如:
from django.contrib.auth.decorators import permission_required @permission_required('reviews.can_vote', login_url='/loginpage/')
def my_view(request):
...
与 login_required() 装饰器一样,login_url 的默认值是 LOGIN_URL。如果指定了 raise_exception 参数,这 个装饰器不会重定向到登录页面,而是抛出 PermissionDenied 异常,显示 403(HTTP Forbidden)视图。
2.4、身份验证视图
Django 为登录、退出和密码管理提供了视图。这些视图使用 auth 包中内置的表单,不过也可以传入自己编写的视图。Django 没有为身份验证视图提供默认的模板,不过下文将说明各个视图的模板上下文。
在项目中使用这些视图要实现不同的方法,不过最简单也是最常见的做法是把 django.contrib.auth.urls 提 供的 URL 配置添加到项目的 URL 配置中。例如:
urlpatterns = [url('^', include('django.contrib.auth.urls'))]
这样,各个视图在默认的 URL 上(后文详述)。 这些内置的视图都返回一个 TemplateResponse 实例,这样便于在渲染之前定制响应数据。多数内置的身份验 证视图提供了 URL 名称,易于引用。
2.4.1 login 视图
登录用户。
默认 URL:/login/。
可选参数:
• template_name:这个视图使用的模板名称。默认为 registration/login.html。
• redirect_field_name:GET 参数中指定登录后重定向 URL 的字段名称。默认为 next。
• authentication_form:验证身份的可调用对象(通常是一个表单类)。默认为 AuthenticationForm。
• current_app:一个提示,指明当前视图所在的应用。
• extra_context:一个字典,包含额外的上下文数据,随默认的上下文数据一起传给模板。 login 视图的作用如下:
• 如果通过 GET 调用,显示登录表单,其目标地址与当前 URL 一样。稍后详述。
• 如果通过 POST 调用,发送用户提交的凭据,尝试登录用户。如果登录成功,重定向到 next 指定的 URL。如果没有 next,重定向到 LOGIN_REDIRECT_URL(默认为 /accounts/profile/)。如果登录失败,重新显示登录表单。 登录视图的模板由你提供,模板文件默认名为 registration/login.html。
模板上下文:
• form:表示 AuthenticationForm 的 Form 对象。
• next:成功登录后重定向的目标 URL。自身可能也包含查询字符串。
• site:当前 Site,根据 SITE_ID 设置确定。如果未安装网站框架,其值默认为 RequestSite 实例,从 当前 HttpRequest 对象中获取网站名称和域名。
• site_name:site.name 的别名。如果未安装网站框架,其值为 request.META['SERVER_NAME'] 的值。 如果不想把模板命名为 registration/login.html,可以为 URL 配置提供额外的参数,设定 template_name 参数。
2.4.2 logout 视图
退出用户。
默认 URL:/logout/
可选的参数:
• next_page:退出后重定向的目标 URL。
• template_name:一个模板全名,在用户退出后显示。如果未提供这个参数,默认为 registration/ logged_out.html。
• redirect_field_name:GET 参数中指定退出后重定向 URL 的字段名称。默认为 next。如果提供这个参数,next_page 将被覆盖。
• current_app:一个提示,指明当前视图所在的应用。
• extra_context:一个字典,包含额外的上下文数据,随默认的上下文数据一起传给模板。
模板上下文:
• title:本地化之后的字符串“Logged out”。
• site:当前 Site,根据 SITE_ID 设置确定。如果未安装网站框架,其值默认为 RequestSite 实例,从 当前 HttpRequest 对象中获取网站名称和域名。
• site_name:site.name 的别名。如果未安装网站框架,其值为 request.META['SERVER_NAME'] 的值。
2.4.3 logout_then_login 视图
退出用户,然后重定向到登录页面。
默认 URL:未提供。
可选的参数:
• login_url:重定向到的登录页面的 URL。如果未提供,默认为 LOGIN_URL。
• current_app:一个提示,指明当前视图所在的应用。
• extra_context:一个字典,包含额外的上下文数据,随默认的上下文数据一起传给模板。
2.5 内置的表单
如果不想使用内置的视图,也不想为这些功能编写表单,可以使用身份验证系统在 django.contrib.auth.forms 中内置的几个表单(表 11-1)。 这些内置的表单对用户模型有些假设,如果自定义了用户模型,可能要自己动手为身份验证系统编写表单。
表单名 | 说明 |
---|---|
AdminPasswordChangeForm | 在管理后台中用于修改用户密码的表单。第一个位置参数是用户对象。 |
AuthenticationForm | 登录表单。请求对象是第一个位置参数,存储在表单中,供子类使用。 |
PasswordChangeForm | 修改密码的表单。 |
PasswordResetForm | 用于生成并发送带有重设密码链接的电子邮件。 |
SetPasswordForm | 让用户修改密码的表单,无需输入旧密码。 |
UserChangeForm | 在管理后台中用于修改用户信息和权限的表单。 |
UserCreationForm | 创建新用户的表单。 |
2.6 模板中的身份验证数据
使用 RequestContext 时,当前登录用户及其权限可通过模板上下文访问。
2.6.1 用户
渲染模板的 RequestContext 时,当前登录用户,不管是 User 实例还是 AnonymousUser 实例,都存储在模板变 量 {{ user }} 中:
{% if user.is_authenticated %}
<p>Welcome, {{ user.username }}. Thanks for logging in.</p>
{% else %}
<p>Welcome, new user. Please log in.</p>
{% endif %}
如果使用的不是 RequestContext,这个模板上下文变量不可用。
2.6.2 权限
当前登录用户的权限存储在模板变量 {{ perms }} 中。它的值是 django.contrib.auth.context_processors.PermWrapper 的一个实例,对模板友好。在 {{ perms }} 对象中,单属性查找由 User.has_module_perms 代理。只要当前登录用户在 foo 应用中有权限,下述示例就返回 True:
{{ perms.foo }}
两层属性查找由 User.has_perm 代理。如果当前登录用户有 foo.can_vote 权限,下述示例返回 True:
{{ perms.foo.can_vote }}
因此,在模板中可以使用 {% if %} 语句检查权限:
{% if perms.foo %}
<p>You have permission to do something in the foo app.</p>
{% if perms.foo.can_vote %}
<p>You can vote!</p>
{% endif %} {% if perms.foo.can_drive %}
<p>You can drive!</p>
{% endif %} {% else %}
<p>You don't have permission to do anything in the foo app.</p>
{% endif %}
此外,还可以使用 {% if in %} 语句检查权限。
{% if 'foo' in perms %}
{% if 'foo.can_vote' in perms %}
<p>In lookup works, too.</p>
{% endif %}
{% endif %}
3、案例:自己动手做一个网站用户系统
基于我们前面已经做好的网站,我们现在做一个简单的用户登录和注册系统,然后设置权限
3.1 登录系统
先写一个简单的登录页面login.html
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<title>登录</title>
</head>
<body>
<div class="container" style="margin-top: 100px">
<div class="row">
<div>
<div>
<div>
<h3>登录</h3>
</div>
<div>
<form action="" method="post" novalidate>
{% csrf_token %}
{# 方法一: 写出form表单中的指定字段 #}
{# {{ login_form.username.label_tag }}:#}
{# {{ login_form.username }}#}
{# <p>{{ login_form.errors.username.0 }}</p>#}
{# {{ login_form.password.label_tag }}:#}
{# {{ login_form.password }}#}
{# <p>{{ login_form.errors.password.0 }}</p>#}
{# <div>{{ login_form.non_field_errors }}</div>#}
{# 方法二: 遍历出form表单中的所有字段 #}
{% for field in login_form %}
{{ field.label_tag }}
{{ field }}
<p>{{ field.errors.as_text }}</p>
{% endfor %}
{# 此处错误信息会返回clean联合校验,也就是非单个字段校验的错误信息#}
<span>{{ login_form.non_field_errors }}</span>
<input type="submit" value="登录" class="btn">
</form>
</div>
</div>
</div>
</div>
</div>
<br>
<div>
<a href="/register">注册</a>
</div>
</body>
</html>
然后我们在forms.py里面创建一个LoginForm类,用于接收前端表单输入的数据,并实现验证
from django import forms
from django.contrib.auth import authenticate
from django.contrib.auth.models import User
class LoginForm(forms.Form):
username = forms.CharField(
label="用户名",
min_length=3,
widget=forms.TextInput(attrs={"class": "form-control", "placeholder": "请输入用户名"}),
error_messages={
"required": "用户名不能为空",
"min_length": "用户名最小长度为3位",
},
)
password = forms.CharField(
label="密码",
min_length=6,
error_messages={
"min_length": "密码最小长度为6位",
"required": "密码不能为空",
},
widget=forms.PasswordInput(attrs={"class": "form-control", "placeholder": "请输入密码"}),
)
def clean(self):
username = self.cleaned_data.get("username", "")
password = self.cleaned_data.get("password", "")
user = authenticate(username=username, password=password)
if not user:
raise forms.ValidationError("用户名或密码错误")
self.cleaned_data["user"] = user
return self.cleaned_data
然后在views.py 里面写好一个登录的函数
def user_login(request):
if request.method == "GET":
username = request.GET.get("username")
login_form = LoginForm()
return render(request, "login.html", context={"login_form": login_form})
elif request.method == "POST":
login_form = LoginForm(request.POST)
if login_form.is_valid():
# 注意:验证用户名和密码是否正确放到forms中去验证了
# login(request, request.user) # 此处不能使用request.user,因为他还没有验证,是匿名用户
# 所以需要在form中校验通过后传递过来user
login(request, login_form.cleaned_data["user"])
user = request.POST["username"]
next = request.GET.get("next", reverse("index"))
return redirect(next,{'user':user})
else:
return render(request, "login.html", {"login_form": login_form})
如果从form类返回的验证正确,就跳转到指定网页,这里指定到 index 页面,注意这里这个 index 是 路由的名字(name,就是设置path时候指定的name)。
最后在urls.py 里面加上路由
path('login/',user_login,name="login"),
我们访问http://127.0.0.1:8000/login
因为没有加上css,界面看上去比较朴素,但是已经可以使用了
3.2 注册系统
当然这时候我们只有一个前面创建的超级用户,要想让普通用户注册使用,还要做一个注册系统
一样,先写好前端的表单页面 register.html
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<title>注册</title>
</head>
<body>
<div class="container" style="margin-top: 100px">
<div class="row">
<div>
<div>
<div>
<h3>注册</h3>
</div>
<div>
<form action="" method="post" novalidate>
{% csrf_token %}
{% for field in register_form %}
{{ field.label_tag }}
{{ field }}
<p>{{ field.errors.as_text }}</p>
{% endfor %}
<span>{{ register_form.non_field_errors }}</span>
<input type="submit" value="注册">
</form>
</div>
</div>
</div>
</div>
</div>
</body>
</html>
在forms.py里面写一个注册用的 RegisterForm类
class RegisterForm(LoginForm):
password_again = forms.CharField(
label="确认密码",
min_length=6,
error_messages={
"required": "确认密码不能为空",
"min_length": "密码最小长度为6位",
},
widget=forms.PasswordInput(attrs={"class": "form-control", "placeholder": "确认密码"}),
)
email = forms.EmailField(
label="邮箱",
required=False,
error_messages={
"required": "密码不能为空",
},
widget=forms.EmailInput(attrs={"class": "form-control", "placeholder": "请输入邮箱"})
)
def clean_username(self):
username = self.cleaned_data["username"]
if User.objects.filter(username=username).exists():
raise forms.ValidationError("用户名已存在")
return username
def clean_email(self):
email = self.cleaned_data["email"]
if email and User.objects.filter(email=email).exists():
raise forms.ValidationError("邮箱已存在")
return email
def clean(self):
password = self.cleaned_data.get("password", "")
password_again = self.cleaned_data.get("password_again", "")
if password != password_again:
raise forms.ValidationError("两次密码不一致,请重新输入")
return self.cleaned_data
再在views.py里面 写好视图函数
def user_register(request):
if request.method == "GET":
register_form = RegisterForm()
return render(request, "register.html", context={"register_form": register_form})
elif request.method == "POST":
register_form = RegisterForm(request.POST)
if register_form.is_valid():
username = register_form.cleaned_data["username"]
password = register_form.cleaned_data["password"]
email = register_form.cleaned_data.get("email", "")
# 创建用户
user = User.objects.create_user(username, email, password)
next = request.GET.get("next", reverse("index"))
return redirect(next)
else:
return render(request, "register.html", {"register_form": register_form})
最后加上路由
path('register/',user_register,name="register"),
我们访问 http://127.0.0.1:8000/register
我们注册一个名为testone的用户,登录之后可以看到, 用户名正确显示出来了
3.3 退出登录
这里我们前端加了一个退出登录的功能按钮
Django内置了一个logout 函数,实现logout功能非常容易
def user_logout(request):
logout(request)
return redirect("login")
3.4 设置权限
既然使用了用户系统,当然对用户访问就要做些限制,我们在主页视图函数前加上装饰器,并指定如果未登录时候的跳转页面(如果不指定,默认的跳转页面是('/accounts/login/')
from django.contrib.auth.decorators import login_required
@login_required(login_url='/login/')
def classnotice(request):
print(request.GET)
now=datetime.datetime.now()
context={}
context['student_list']=Student.objects.all().order_by('-score')
context['teacher']='王重阳'
context['now']=now
return render(request,'class3.html',context=context,status=200)
再访问主页,就会跳转到登录页面
最后我们再来设置一下权限。比如之前我们做了一个add_student的表单,用于增加Student模型的实例。现在我们给这个功能加上权限,这样没有权限的用户就会退到登录界面
from django.contrib.auth.decorators import permission_required
@permission_required('classManage.add_student',login_url='/login/')
def add_student(request):
if request.method == 'GET':
student_list = addStudent()
return render(request,'addStudent.html',{'students':student_list},status=200)
else:
student_list = addStudent(request.POST)
if student_list.is_valid():
student_list.save()
return render(request,'addStudent.html',{'student_list':student_list},status=200)
这时候,我们用teacherWang账户登录可以访问 ‘/add_student/’,但是用testone账户访问'/add_student'就会跳转到登录界面。
我们用程序给testone账户赋予权限
from classManage.models import Student
from django.contrib.auth.models import Group, Permission
from django.contrib.contenttypes.models import ContentType
user = User.objects.get(username='testone')
permission = Permission.objects.get(codename='add_student')
user.user_permissions.add(permission)
然后再用testone账户登录,再访问http://127.0.0.1:8000/add_student
发现可以访问了