自学Python第二十二天- Django框架(二) 表单组件、权限与后台管理、生成前端的HTML代码,cookie/session/token

Django官方文档

表单组件

form 表单的用法和 flask 等其他方法大致一样,不一样的地方在于 django 自带 csrf_token 校验。在视图函数获取表单数据时,会比对 csrf_token 的值,所以在 html 中的form 表单内部需要添加 {% csrf_token %} 用来生成随机值以进行比较。如果需要取消 csrf ,可以在settings.py 的中间件注册信息中删除 CsrfViewMiddleware (此种方法将取消全部的 csrf 验证)。如果只是部分的视图处理函数不进行 csrf 验证,则可以使用装饰器 csrf_exempt 修饰需要的视图处理函数(django.views.decorators.csrf.csrf_exempt)。

但是使用 form 表单会有很多问题,例如数据校验、错误提示、页面字段需要重写、关联表的数据呈现需要手动写入等等。所以在开发时通常使用 django 提供的组件来实现。

Form 组件

django 提供的 form 组件能够自动对数据进行校验,能够提供错误提示,能够自动生成字段。

使用 form 组件需要在 views.py 中创建一个继承自forms.Form的类,用来处理 form 的内容。

from django import forms
class MyForm(forms.Form):
	# 定义 form 的字段,并使用插件
	user = forms.CharField(widget=forms.TextInput,label='用户名',required=True)
	pwd = forms.CharField(widget=forms.Input,label='密码',required=True)
	email = forms.CharField(widget=forms.Input)

然后在视图函数中实例化此类,并传递给前端页面

def index(request):
	form = MyForm()
	return render(request, 'index.html', {
    
    'form': form})

然后前端页面使用 form 对象创建表单,而不是使用 <input> 等标签创建。form 对象会自动生成插件所标识的 html 标签。

可以进行自动渲染

<form method="post">
	<!-- 由 form 对象来创建相应标签 -->
	<!-- 自动渲染 -->
	{% form.as_table %}
</form>

这样就能将 form 渲染为 table 标签,还可以使用 form.as_p、form.as_ul。

另外,也可以手动渲染字段

<form method="post">
	<!-- 由 form 对象来创建相应标签 -->
	<!--
	{
    
    { form.user }}
	{
    
    { form.pwd }}
	{
    
    { form.email }}
	-->
	<!-- 或使用循环批量生成 -->
	{% for field in form %}
		{
   
   { field.label }} : {
   
   { field }}
	{% endfor %}	
</form>

field.label 可以获取字段的 verbose_name 名称,如果没有定义则取字段名称。

完整的 label 还可以使用 label_tag 来生成

<div>
	{
   
   { form.subject.errors.0 }}
	{
   
   { form.subject.label_tag }}
	{
   
   { form.subject }}
</div>

自定义错误信息

在表单类的 Meta 元信息中,可以自定义一些字段的错误信息,例如自定义 name 的不能为空信息

class UserForm(forms.Form):
	pass
	class Meta:
		pass
		error_messages = {
    
    		# 自定义错误信息
			'name': {
    
    			# 字段
				'required': '名称不能为空',		# 错误类型及提示信息
				'max_length': '长度不能超过20个字符',
			}
		}

小部件 widget

widget 可以指定此字段使用的组件类型,例如让<input>元素type='password。也可以自定义一些小部件。

<!-- 小部件模板 send_email_widget.html -->
<input type="{
     
     { widget.type }}" name="{
     
     { widget.name }}"
{% if widget.value != None %} value="{
     
     { widget.value|stringformat:'s' }}"{% endif %}
{% for name, value in widget.attrs.items %}
	{% if value is not False %}
		{
     
     {
     
      name }}
		{% if value is not True %}
			="{
     
     { value|stringformat:'s' }}"
		{% endif %}
	{% endif %}
{% endfor %}>
<p>
	<button type="button" onclick="send({ 
        % if widget.value != None %}"{
     
     {
     
      widget.value }}"{% endif %})>发送邮件</button>
</p>
<script>
	function send(email) {
      
      
		alert(email)
	}
</script>

注:模板文件经过格式化,方便阅读,可能实际使用中会报错。

from django.forms.widgets import input

class SendEmailWidget(input):
	input_type = 'text'
	template_name = 'send_email_widget.html'
	
	# redner_value 是否渲染现有的值
	def __init__(self, attrs=None, render_value=True):
        super().__init__(attrs)
        self.render_value = render_value

    def get_context(self, name, value, attrs):
        if not self.render_value:
            value = None
        return super().get_context(name, value, attrs)

ModelForm 组件

使用 form 组件时,所有字段还是需要手动写一遍(在 views.py 定义的 form 类中),如果觉得麻烦,django 提供了另一个组件: ModelForm 组件。此组件可以根据数据库中各列的情况,自动生成字段。

生成表单

使用 ModelForm 组件需要在 views.py 中创建一个继承自ModelForm的类,用来处理 form 的内容。

from django import forms
class MyForm(forms.ModelForm):
	# 也可以使用自定义字段
	# xx = forms.CharField()
	# 需要嵌套类 Meta
	class Meta:
		model = UserInfo	# UserInfo 是 models.py 中定义的相应数据表的类
		fields = ["name", "password", "age"]		# 确定要操作的字段
		# fields = "__all__"	# 使用全部字段
		# exclude = ["password"]	# 排除特定字段,选择其他字段
		# 使用自定义字段
		# fields = ['name', 'password', 'age', 'xx']

传递数据和前端使用上则和 Form 组件类似

自动生成的标签会丢失样式,所以实际使用中,会在定义要操作的字段时,定义字段的插件及属性,在插件属性中可以添加样式。

from django import forms
class MyForm(forms.ModelForm):
	class Meta:
		model = UserInfo
		fields = ["name", "password", "age"]
		widgets = {
    
    
            'name': forms.TextInput(attrs={
    
    'class': 'form-control'}),			# 定义字段使用的插件
            'password': forms.TextInput(attrs={
    
    'class': 'form-control'})		# 插件的参数中可以添加属性,将样式添加到这里
        }

不过这样也字段多的时候也会比较麻烦。通常项目中会在 MyForm 初始化时,通过遍历字段给各字段添加属性的方式添加样式

from django import forms
class MyForm(forms.ModelForm):
	class Meta:
		model = UserInfo
		fields = ["name", "password", "age"]
	
    def __init__(self, *args, **kwargs):
        super().__init__(*args, **kwargs)
        for name, field in self.fields.items():
        	# 设置属性
        	if field.widget.attrs:		# 如果字段中有属性,保留原属性
        		field.widget.attrs['class'] = 'form-control'
        		field.widget.attrs['placeholder'] = '请输入' + field.label
        	else:
        		field.widget.attrs = {
    
    'class': 'form-control', 'placeholder': '请输入' + field.label}

提交表单

ModelForm 组件的表单时,可以将数据返回定义的类来进行校验等操作

form = MyForm(data=request.POST)		# 创建对象,将提交的表单数据作为参数
if form.is_valid():		# 使用 is_valid 方法对数据进行有效性校验,如果有效返回 True
	print(form.cleaned_data)		# 整理数据成一个字典
	form.save()		# 保存数据
else:
	# 如果校验失败,此时错误信息会保存在 form 里,可以将其返回页面,在页面中接收
	return render(request, 'user_add_ModelForm.html', {
    
    'form': form})
{% for field in form %}
    <!-- field.label 是从数据表定义中的 verbose_name 取的 -->
    <div class="form-group">
        <label>{
   
   { field.label }}</label>
        {
   
   { field }}
        <!-- 可以通过 field.errors 获取错误信息,获取到的是一个列表 -->
        <span style="color: red">{
   
   { field.errors.0 }}</span>
    </div>
{% endfor %}

填充数据

如果需要在表单中填充数据,例如编辑的时候会有默认数据,只需要创建 form 对象时将数据库数据作为参数传给类的 instance 参数即可,其他的完全同生成表单

def user_edit(request, nid):
    """编辑用户"""
    if request.method == "GET":
        row_obj = models.UserInfo.objects.filter(id=nid).first()
        form = MyForm(instance=row_obj)
        return render(request, 'user_edit.html', {
    
    'form': form})

更新数据

在提交表单时,使用 form.save() 方法可以添加数据到数据库,但是更新原有数据时必须告诉 form 操作的是哪条数据

row_obj = models.UserInfo.objects.filter(id=nid).first()
form = UserModelForm(data=request.POST,instance=row_obj)
if form.is_valid():  # 检测数据有效性
	form.save()  # 保存进数据库
	return redirect('/user/list/')
else:
	# 校验失败,显示错误信息
	return render(request, 'user_add_ModelForm.html', {
    
    'form': form})

保存额外数据

使用 form.save() 方法保存表单数据到数据库时,实际上保存的是用户提交的表单中的数据。如果有一些其他的数据不在用户提交的保单中,则可以手动添加数据到字段,然后保存

form = UserModelForm(data=request.POST)
if form.is_valid():  # 检测数据有效性
	# 使用 form.instance.字段名 = 值 来手动添加数据,然后保存到数据库
	form.instance.position = '新入职员工'
	form.save()  # 保存进数据库
	return redirect('/user/list/')
else:
	# 校验失败,显示错误信息
	return render(request, 'user_add_ModelForm.html', {
    
    'form': form})

数据校验

简单校验

is_valid 方法默认进行空值检测,如果需要更多校验方式,例如字符位数等,需要自定义字段

from django import forms
class MyForm(forms.ModelForm):
	name = forms.Charfield(min_length=3,label="用户名")		# 此字段最小长度为3个字符
	# name = forms.Charfield(disabled=True,label="用户名")		# 不可用模式,但是提交表单时认为是空
	password = forms.Charfield(label='密码',required=True)	# 必填校验
	class Meta:
		model = models.UserInfo
		fields = ['name', 'password', 'age']

使用正则表达式校验

在自定义字段时,可以添加参数 validators,使用正则表达式 RegexValidator 类进行校验自定义的字段。

from django.core.validators import RegexValidator
class PrettyNumForm(forms.ModelForm):
    mobile = forms.CharField(
        label='手机靓号',
        validators=[RegexValidator(r'^(13[0-9]|14[01456879]|15[0-35-9]|16[2567]|17[0-8]|18[0-9]|19[0-35-9])\d{8}$','手机号格式错误')]	# 参数2表示校验失败显示内容
    )
    class Meta:
        model = models.PrettyNum
        # fields = "__all__"  # 使用所有字段
        # exclude = ['level']   # 排除某个字段,取其他的字段
        fields = ['mobile','price','level','status']
    def __init__(self, *args, **kwargs):
        super().__init__(*args, **kwargs)
        for name, field in self.fields.items():
            if type(field.widget) == django.forms.widgets.TextInput or type(field.widget) == django.forms.widgets.NumberInput:
                field.widget.attrs = {
    
    'class': 'form-control', 'placeholder': '请输入' + field.label}
            else:
                field.widget.attrs = {
    
    'class': 'form-control'}

使用校验函数校验

可以在类定义中使用 clean_字段名 方法,将相应的字段数据传入并进行校验

    def clean_mobile(self):		# 校验 mobile 字段
        from django.core.exceptions import ValidationError		
        txt_mobile = self.cleaned_data.get('mobile')		# 获取 mobile 字段的数据
        if len(txt_mobile) != 11:				# 进行检验
            raise ValidationError("格式错误")		# 没通过检验,抛出一个错误
        else:
            return txt_mobile			# 通过检验,返回值

自定义校验,返回错误信息

可以通过 form.cleaned_data 获取返回的 form 数据字典,所以可以通过 form.cleaned_data[key] 来获取需要的表单数据,来自定义一些数据校验方式。也可以使用 form.cleaned_data.pop(key) 的方式来获取特定键的值,区别在于使用 pop 方法为弹出数据。

如果自定义校验,或者进行数据核对(例如比较数据库中的信息),则可以主动添加错误信息并返回。

form = UserModelForm(data=request.POST)
if form.is_valid():  # 检测数据有效性
	if models.UserInfo.objects.filter(name=form.cleaned_data['username'], password=form.cleaned_data['password']).exists():
		# 登录成功,跳转页面
		return redirect('/user/list/')
	else:
		# 用户名和密码对不上
		form.add_error('password', '用户名或密码错误')  # 主动添加错误信息给 password 字段
# 校验失败,显示错误信息
return render(request, 'user_add_ModelForm.html', {
    
    'form': form})

前端可以使用 { { form.字段名.errors.0 }} 来获取错误信息

表单校验的步骤

django 对于表单校验时,遵循这几个步骤:

  • 先使用Fieldto_python()方法,它强制将值转换为正确的数据类型。
  • 使用Field validate()方法处理不适合验证器的特定字段验证。这个方法不会返回任何东西,也不应该改变值。可以覆盖它来处理不能或不想放在验证器中的验证逻辑。
  • Field上的run_validators()方法会运行该字段的所有验证器,不应该覆盖此方法。
  • Field子类上的clean()方法负责以正确的顺序运行to_python()validate()run_validators()并传播它们的错误。该方法返回干净的数据,然后将其插入到表单的cleaned_data字典中。
  • clean_<fieldname>() 方法是在表单子类上调用的——其中 <fieldname> 被替换为表单字段属性的名称。这个方法做任何特定属性的清理工作,与字段的类型无关。这个方法不传递任何参数,需要在 self.cleaned_data 中查找字段的值,并且记住,此时它将是一个 Python 对象,而不是在表单中提交的原始字符串(它将在 cleaned_data 中,因为上面的一般字段 clean() 方法已经清理了一次数据)。这个方法可以用来校验特定字段,其返回值会替换 cleaned_data 中的现有值,所以它必须是 cleaned_data 中的字段值(即使这个方法没有改变它)或一个新的干净值。
  • 表单子类的 clean() 方法可以执行需要访问多个表单字段的验证。此方法可以覆写用作整体校验,如果需要可以返回一个完全不同的字典,这个字典将被用作 cleaned_data。覆写时,注意先执行一下父类的此方法,进行一次校验。

信息提示语言

通过实例测试可以知道,返回的错误信息是英文的,这是因为 django 在设置上是使用英文 en-us ,可以在 setting.py 中的 LANGUAGE_CODE 设置成中文 zh-hans

前端自定义表单组件和 Form 的结合

如果前端使用了自定义的表单组件,在后端 django 中也可以使用 Form 组件或 ModelForm组件。

通常使用 Form 组件的流程为:

  1. django 将新创建(不包含数据)的 Form 对象传至模板,模板根据此对象进行渲染。
  2. 前端将表单中的数据发回 django,django 根据接收到的数据创建(含数据的) form 对象,根据需要进行验证
  3. 如果验证未通过,则会在 form 对象中产生错误信息,django 将此验证信息传入模板并渲染。

如果使用自定义表单和 Form 组件相结合的方式,则变为:

  1. 前端需要根据 Form 对象渲染的情况创建表单组件,让提交的表单数据能够被 form 对象接收
  2. 前端提交的表单数据,django 接收此数据并创建含数据的 form 对象,根据需要进行验证
  3. 如果验证未通过,则会在 form 对象中产生错误信息。将此错误信息提取出来传给前端
  4. 前端接收到错误信息,呈现到自定义表单中

首先需要明确的是,后端使用 Form 或 ModelForm 组件区别不大,主要区别在于 Form 需要手动定义各字段,而 ModelForm 则根据数据库中列的情况自动生成字段。在前端使用自定义表单组件时,字段不会传给前端,所以基本没有区别。

前端需要创建的表单组件信息

对于前端来说,首先要创建表单元素,使其提交能够被 Form 对象使用的表单数据。经过分析POST信息(假设 method 为 post),得知需要提交 csrfmiddlewaretoken 和表单数据两种。

  • csrfmiddlewaretoken 是 csrf 校验使用的数据,可以在后台取消校验,或者添加 csrf 校验信息(注:post 请求和 cookie 中都需要有)。
  • 表单数据:表单数据以键值对形式发送(JSON 对象),键名为 form 对象的字段名(字段变量名称),值是一个列表(因为表单中出现同名字段,就会在列表中添加数据),列表成员是字符串(因为序列化的缘故,所有数值也会变成字符串)。

所以这时候前端需要做的,就是创建需要的表单元素,然后根据字段数据库字段,确定表单中各元素的 name 属性。并且根据需要,选择提交 csrf_token 的方式。

后端接收前端发送的表单数据

django 可以正常接收表单发送的数据,数据格式就是之前设置的键值对形式,键为表单字段,由前端元素的 name 属性确定,值为字符串列表,也可以使用 ajax 发送。

后端接收到数据后,可以使用此数据来创建表单对象并验证,和使用 django 表单组件一样使用。当使用 is_valid() 方法验证过后(也可以自定义验证方法),如果返回真则是通过验证,否则即是未通过验证。未通过验证则会将错误信息存储在 form 对象中。

经过分析表单对象可以发现,通常渲染模板时使用 {% for item in forms %} 来获取表单中各字段成为 html 元素,使用 { { item.label }} 获取字段设置的别名,使用 { { item.errors }} 来获取错误信息列表。相对应的,这些数据也存在于 form 表单对象中:

  • item : 通过表单对象的迭代器获取的对象,实际上可以通过 form.form._bound_fields_cache 获取全部元素。其类型是字典,键为各字段名称,值为 item 对象。
  • item.name : 字段名称,字符串类型,html元素的 name 属性值。
  • item.label : 字段别名,字符串类型
  • item.auto_id : html元素的 id,字符串类型
  • item.errors : 各字段的错误信息列表

通过这些,则可以获取错误信息返回前端。前端接收到后可以将错误信息呈现给用户,待用户更改后重新发送表单数据给 django。

以上的方法是基于基于表单模板的使用,其实对于所有的错误均保存在表单对象errors 里,且可以通过其 get_json_data() 方法获取。

form.errors.get_json_data()

返回值是一个字典,键即是字段名(对于全局的错误,键是 '__all__'),值是错误列表。列表的每个成员是一个错误字典,错误信息在字典的message字段,错误代码在code字段。

权限与后台管理

Django 项目生成时,会自动生成后台管理应用:Admin。

创建后台超级用户

和创建 app 类似,在控制台输入

python manage.py createsuperuser

然后根据提示输入用户名、邮箱、密码,就创建超级用户成功了。

登录后台

当创建了超级用户后,会在总路由里注册路由地址 admin,就可以使用这个 url 登录到默认的后台登录界面了。可以在后台界面添加用户、组、权限等。

通过在 settings.py 中修改语言、时区等设置,可以更改后台界面的

Xadmin 插件

Xadmin 是基于原生 admin 的界面,在 github 上有相应的源码。它使用了基于 bootstrap 的样式,界面进行了美化。

使用 xadmin 需先安装

pip install xadmin-py3

后台托管

如果我们自己写了相应的数据模型,例如用户信息,可以通过后台访问,这就是托管。

# models.py
from django.db import models

# 客户用户表
class UserEntity(models.Model):
	name = models.CharField(max_length=20)
	age = models.IntegerField(default=0)
	phone = models.CharField(max_length=11)
	
	class Meta:
		# 表名
		db_table = 'user' 
		# 别名
		verbose_name = '客户列表'
		# 复数别名,默认添加个 s
		verbose_name_plural = verbose_name

	def __str__(self):
		return self.name

在 app 目录下的 admin.py 中可以托管本 app 使用的数据模型

from django.contrib import admin
# 导入数据模型
from app.models import UserEntity

# 注册模型到 admin 站点中
admin.site.register(UserEntity)

然后在后台界面就能看到相应 app 下的数据模型了。需要注意的是,每条数据显示对象,所以必须使用 __str__ 方法来定制对象的输出字符串。

自定义表单

通过在 admin.py 文件中,定义 admin.ModelAdmin 的子类来自定义表单。

例如,添加 Store 的表单中只添加 name(自定义表单字段)

class StoreAdmin(admin.ModelAdmin):
	fields = ('name',)	# 字段,元组类型

admin.site.register(Store, StoreAdmin)	# 注册模型和其相关配置

分栏显示

class StoreAdmin(admin.ModelAdmin):
	fieldsets = (['Main', {
    
    'fields': ('name',)}],
				['Advance', {
    
    'fields': ('address',),
						'classes': ('collapse',)}])

内联显示

# 为外表创建内联类
class FruitInline(admin.TabularInline):
	model = Fruit

# 在主表设置内联
class StoreAdmin(admin.ModelAdmin):
	inlines = [FruitInline]

设置列表显示与搜索

在 admin.ModelAdmin 的子类中,可以这样设置:

class StoreAdmin(admin.ModelAdmin):
	list_display = ('id', 'name', 'address','custom1')	# 列表显示的字段,元组
	fields = ('id', 'name', 'address')	# 表单中使用的字段(可以和显示的不同)
	list_per_page = 2		# 分页显示,每页显示2条记录
	list_filter = ('id', 'name')	# 过滤器,一般配置分类过滤
	search_fields = ('name', 'address')			# 搜索关键字
	
	# list_display 中定义了自定义的字段,非数据实体的字段
	# 在此定义自定义字段显示内容,参数 obj 是传入的数据实体对象
	def custom1(self, obj):
		return obj.email
	
	# 自定义字段显示的标题(字段名)
	custom1.short_description = 'E-mail'

admin.site.register(Store, StoreAdmin)		# 注册模型和其相关配置

权限管理

django 自带了权限管理模块 auth,提供了用户身份认证、用户组和权限管理

与 auth 模块有关的数据库表有6个,分别是

作用 备注
auth_user 用户信息,包含 id、password、username、first_name、last_name、email、is_staff、is_active、date_joined
auth_group 组信息 每个组拥有id和name两个字段
auth_user_groups user和group之间的关系
auth_permission 用户许可、权限 每条权限有id、name、content_type_id、codename四个字段
auth_user_user_permissions user和permission之间的关系
auth_group_permissions 用户组和权限的对应关系 使用用户组管理权限是一个更方便的方法,group中包含多对多字段permissions

需注意的是,django 的缓存机制:django 会缓存每个用户对象,包括其权限 user_permissions。当手动改变某一用户的权限后,必须重新获取改用户对象,才能获取最新的权限。如不重新载入用户对象,则权限还是没有刷新。

数据模型中的相关操作

可以直接使用一些接口函数对内建的数据模型进行操作,从而做到权限及用户管理

用户操作

创建用户

from django.contrib.auth.models import User
user = User.objects.create_user(username, email, password, is_staff)	# is_staff 是个布尔值,表示是否登录admin后台
user.save()

用户认证

from django.contrib.auth import authenticate
# 认证用户的密码是否有效,认证成功返回代表该用户的 user 对象,否则返回 None。
# 需注意的是,此方法只检查认证用户是否有效,并不检查具体权限和是否激活(is_active)标识
user = authenticate(username=username, password=password)

from django.contrib.auth.hashers import check_password
# 或使用 check_password 方法,返回布尔值
user = User.objects.filter(username=username)
check_password(password, user.password)

修改用户密码

user = auth.authenticate(username=usermane, password=old_password)
if user:
	user.set_password(new_password)
	user.save()

登录

from django.contrib.auth import login
user = authenticate(username=username, password=password)	# 验证成功返回用户对象
if user:	
	if user.is_active:		# 用户已经激活
		# 登录,向 session 中添加 user 对象,便于对用户进行跟踪
		login(request, user)

获取当前用户

user = request.user

判断当前用户是否通过验证(即已经登录)

request.user.is_authenticated		# 布尔值
# 如果是视图函数需要判断是否登录,还可以使用装饰器
@login_required
def inddex(request):
	pass

退出登录

from django.contrib.auth import logout
def logout_view(request):
	logout(request)

组操作

django.contirb.auth.models.Group 定义了用户组的模型,每个用户组拥有 id 和 name 两个字段,该模型在数据库被映射为 auth_group 表。User 对象中有一个名为 groups 的多对多字段,由 auth_user_group 表维护。Group 对象可以通过 user_set 反向查询用户组中的用户。

创建组

group = Group.objects.create(name=group_name)
group.save()

删除组

group.delete()

用户加入用户组

user.groups.add(group)
# 或者
group.user_set.add(user)

用户退出用户组

user.groups.reomve(group)
# 或者
group.user_set.remove(user)

用户退出所有用户组

user.groups.clear()

用户组清除所有用户

group.user_set.clear()

权限操作

自定义模型权限

在定义 model 时可以使用 Meta 定义此模型的权限

class Discussion(models.Model):
	...
	class Meta:
		permissions = (
			('create_discussion', 'Can create a discussion'),
			('reply_discussion', 'Can reply discussion'),
		)
为用户/组增删权限

每个模型默认拥有增(add)删(delete)的权限

# 添加用户权限
user.user_permissions.add(permission)
# 删除用户权限
user.user_permissions.delete(permission)
# 清空用户权限
user.user_permissions.clear()

用户拥有所在组的权限,使用用户组管理权限是一个更方便的方法。Group 中包含多对多字段 permissions,在数据库中由 auth_group_permissions 表进行维护

# 添加组权限
group.permissions.add(permission)
# 删除组权限
group.permissions.delete(permission)
# 清空组权限
group.permissions.clear()

还可以获取某个特定用户的相关权限信息

# 特定用户所在用户组的权限
user_A.get_group_permissions()
# 特定用户的所有权限
user_A.get_all_permissions()

权限验证

可以在访问视图时,进行一些权限验证:

登录验证

登录验证的原始方式就是检查 request.user.is_authenticated 并重定向到登录页面,或返回一个错误信息

from django.conf import settings
from django.shortcuts import redirect, render


def my_view(request):
    if not request.user.is_authenticated:
        return redirect(f"{
      
      settings.LOGIN_URL}?next={
      
      request.path}")
        # return render(request, "myapp/login_error.html")

比较便捷的是使用装饰器 login_required

from django.contrib.auth.decorators import login_required

@login_required
def my_view(request):
	pass

login_required 会判断是否登录。如果没有登录,会重定向到 settings.LOGIN_URL 定义的路径中,并传递当前绝对路径到查询字符串中。例如: /accounts/login/?next=/polls/3/。如果用户已经登录,则正常执行视图函数。

默认情况下,未登录跳转时将跳转前的绝对路径添加到名为 next 的查询字符串中,也可以在装饰器中自定义,使用参数 @login_required(redirect_field_name="my_redirect_field")

另外如果重定向不想使用 settings.LOGIN_URL ,也可以添加到装饰器的参数中:@login_required(login_url="/login/")

需注意的是 login_required 虽然不检查用户的 is_active 标识,但是默认的 AUTHENTICATION_BACKENDS 会拒绝非正常用户。除非在 settings.py 中进行设置:AUTHENTICATION_BACKENDS = ['django.contrib.auth.backends.AllowAllUsersModelBackend']

管理人员登录验证

如果是类似于管理后台,需要验证登录用户是否是管理人员(User.is_staff=True),则可以使用装饰器 @staff_member_requiredstaff_member_required 的使用方式和操作行为类似于 login_required 区别在于 staff_member_required 不仅检查 is_authenticated ,还检查 is_staffis_active

验证权限

检查用户权限:user.has_perm 方法用于检查用户是否拥有操作某个视图或模型的权限,若有则返回 True

user.has_perm('blog.add_article')
user.has_perm('blog.change_article')
user.has_perm('blog.delete_article')

抛出异常的权限检查:has_perm 仅是进行权限检查,即使用户没有权限它也不会阻止程序执行相关操作。可以使用 @permission_required 装饰器代替 has_perm 并在用户没有相应权限时重定向到登录页或抛出异常。

视图中使用这个装饰器验证是一个好的选择,但是如果使用视图类(CBV),而不是视图函数(FBV),则不能使用此装饰器。需要继承 RermissionRequiredMixin 这个类。

# permission_required(perm[, login_url=None, raise_exception=False])

@permission_required('blog.add_article')
def post_atricle(request):
	pass

permission_required 也可以接受可迭代权限,此时必须拥有所有权限才能访问视图。

模板中也可以进行权限验证:主要使用 perms 这个全局变量。perms 对当前用户的 user.has_module_perms 和 user.has_perm 方法进行了封装。例如判断当前用户是否拥有 blog 应用下所有的权限:

{
   
   { perms.blog }}

这样结合 if 标签,可以选择性的根据用户权限显示不同内容了

{% if perms.blog.add_article %}
You can add atricles.
{% endif %}
{% if perms.blog.delete_article %}
You can delete atricles.
{% endif %}
自定义验证

使用装饰器 @user_passes_test 可以自定义一个函数用以验证用户,该函数接收一个参数是当前用户的 django.contrib.auth.models.User 对象。当函数返回 False 时执行重定向。

# 使用格式为 user_passes_test(test_func, login_url=None, redirect_field_name='next')
from django.contrib.auth.decorators import user_passes_test

def email_check(user):
    return user.email.endswith("@example.com")


@user_passes_test(email_check)
def my_view(request):
	pass

Admin 中的权限分配

当编辑某个 user 信息时,可以在 User permissions 栏为其设置权限。权限的规则是:

权限显示为:应用 | 模型 | 行为(增删改查等)
权限数据为:应用.行为_模型
其中 行为_模型 就是数据库表中的 codename,应用即 app_label

django guardian

django 自带的权限管理机制是针对模型的,这就意味着如果一个用户对某一模型有权限,则此模型中的所有数据均有权限。如果希望实现针对单个数据的权限管理,则需要使用第三方库比如 django guardian 库。

django-guardian 官网英文文档

guardian 的安装和使用准备

使用 pipy 安装

pip install django-guardian

安装完成后,需要将添加到项目中,首先添加 app,然后添加身份验证后端

# settings.py

INSTALLED_APPS = ( 
    # ... 
    'guardian',
)

AUTHENTICATION_BACKENDS = ( 
    'django.contrib.auth.backends.ModelBackend', # 这是Django默认的
    'guardian.backends.ObjectPermissionBackend', # 这是guardian的
) 

然后创建 guardian 的数据库表。创建完成后会多出两张表:guardian_groupobjectpermission 和 guardian_userobjectpermission,分别记录了用户组/用户与model及具体object的权限对应关系

python manage.py migrate

下面是表中各字段的含义

字段 说明
id 默认主键
object_pk object 的 id,标识具体是哪个对象需要授权,对应的是具体某一数据
content_type_id 记录具体哪个表的id,对应的是django系统表django_content_type内的某条数据,django所有注册的model都会在这个表里记录
group_id/user_id 记录是那个组/用户会有权限,对应的是auth_group/auth_user表里的某条记录
permission_id 记录具体的某个权限,对应的是auth_permission表里的某条记录

需注意的是,一旦将 django-guardian 配置进项目,当调用 migrate 命令时将会创建一个匿名用户的实例(名为 AnonymousUser)。guardina 的匿名用户与 django 的匿名用户不同,django 匿名用户在数据库中没有条目,但 guardian 匿名用户有。这意味着以下代码将会返回意外的结果:

request.user.is_anonymous = True

其他的一些配置

在 settings.py 中可以进行其他的一些配置

# 如果GUARDIAN_RAISE_403设置为True,guardian将会抛出django.core.exceptions.PermissionDenied异常,而不是返回一个空的django.http.HttpResponseForbidden
# 需注意的是GUARDIAN_RENDER_403和GUARDIAN_RAISE_403不能同时设置为True。否则将抛出django.core.exceptions.ImproperlyConfigured异常
GUARDIAN_RAISE_403 = False

# 如果GUARDIAN_RENDER_403设置为True,将会尝试渲染403响应,而不是返回空的django.http.HttpResponseForbidden。模板文件将通过GUARDIAN_TEMPLATE_403来设置。
GUARDIAN_RENDER_403 = True
GUARDIAN_TEMPLATE_403 = '403.html'
# 用来设置匿名用户的用户名,默认为AnonymousUser
ANONYMOUS_USER_NAME = 'AnonymousUser'

# Guardian支持匿名用户的对象级权限,但是在我们的项目中,我们使用自定义用户模型,默认功能可能会失败。这可能导致guardian每次migrate之后尝试创建匿名用户的问题。将使用此设置指向的功能来获取要创建的对象。一旦获取,save方法将在该实例上被调用。默认值为guardian.ctypes.get_default_content_type
GUARDIAN_GET_INIT_ANONYMOUS_USER = guardian.ctypes.get_default_content_type

# Guardian允许应用程序提供自定义函数以从对象和模型中检索内容类型。当类或类层次结构以ContentType非标准方式使用框架时,这是有用的。大多数应用程序不必更改此设置。
# 例如,当使用django-polymorphic适用于所有子模型的基本模型上的权限时,这是有用的。在这种情况下,自定义函数将返回ContentType多态模型的基类和ContentType非多态类的常规模型。默认为guardian.ctypes.get_default_content_type
GUARDIAN_GET_CONTENT_TYPE = guardian.ctypes.get_default_content_type

admin 管理界面中使用

此节引用的文章来自知乎

使用Guardian最直观的特色就是在django-admin页面可以图形化地使用对象权限功能。 首先,在admin.py开头,从guardian添加两个导入:

from guardian.admin import GuardedModelAdminMixin
from guardian.shortcuts import get_objects_for_user, assign_perm

GuardedModelAdminMixin 是一个类,包含权限管理的功能,其中Mixin(混入)代表这个类不能单独作为ModelAdmin类使用,需要与其他的ModelAdmin类共同作为子类的父类,新的子类即可既有ModelAdmin的功能也有Guardian权限管理的功能。 但是,GuardedModelAdminMixin本身的功能还是欠缺了点,或者说它本来就是希望开发者自定义重写的。网上有大神将此类继承后重写,完善了其功能,我们将代码抄过来即可(可根据自己项目的特点修改其代码):

class GuardedMixin(GuardedModelAdminMixin):
    # app是否在主页面中显示,由该函数决定
    def has_module_permission(self, request):
        if super().has_module_permission(request):
            return True
        return self.get_model_objs(request,'view').exists()

    # 在显示数据列表时候,哪些数据显示,哪些不显示,由该函数控制
    def get_queryset(self, request):
        if request.user.is_superuser:
            return super().get_queryset(request)
        data = self.get_model_objs(request)
        return data
        
    # 内部用来获取某个用户有权限访问的数据行
    def get_model_objs(self, request, action=None, klass=None):
        opts = self.opts
        actions = [action] if action else ['view', 'change', 'delete']
        klass = klass if klass else opts.model
        model_name = klass._meta.model_name
        data = get_objects_for_user(
            user=request.user, 
            perms=[f'{
      
      perm}_{
      
      model_name}' for perm in actions],
            klass=klass, any_perm=True
        )
        if hasattr(request.user, 'teacher'):
            data = teacher.objects.filter(id=request.user.teacher.id) | data
        return data
    # 用来判断某个用户是否有某个数据行的权限
    def has_perm(self, request, obj, action):
        opts = self.opts
        codename = f'{
      
      action}_{
      
      opts.model_name}'
        if hasattr(request.user, 'teacher') and obj == request.user.teacher:
            return True
        if obj:
            return request.user.has_perm(f'{
      
      opts.app_label}.{
      
      codename}', obj)
        else:
            return self.get_model_objs(request, action).exists()

    # 是否有查看某个数据行的权限
    def has_view_permission(self, request, obj=None):
        return self.has_perm(request, obj, 'view')

    # 是否有修改某个数据行的权限
    def has_change_permission(self, request, obj=None):
        return self.has_perm(request, obj, 'change')

    # 是否有删除某个数据行的权限
    def has_delete_permission(self, request, obj=None):
        return self.has_perm(request, obj, 'delete')

    # 用户应该拥有他新增的数据行的所有权限
    def save_model(self, request, obj, form, change):
        result = super().save_model(request, obj, form, change)
        if not request.user.is_superuser and not change:
            opts = self.opts
            actions = ['view', 'add', 'change', 'delete']
            [assign_perm(f'{
      
      opts.app_label}.{
      
      action}_{
      
      opts.model_name}', request.user, obj) for action in actions]
        return result

当然,这些代码不是尽善尽美的,我们可根据自己项目的特点适当修改这些代码。 而后,将这个我们自己写的GuardedMixin类作为我们自己原来的模型的ModelAdmin类的父类之一:

class TeacherAdmin(GuardedMixin,ModelAdmin):
    # 详情表单页
    inlines = [Class_head_yearInline,FamilyMemberInline]
    fieldsets = [
        # ...
    ]

admin.py 就编辑完成了,在admin管理页面的Teacher页面中就可以设置某个管理员针对某个teacher对象的权限了。

在图形界面具体的数据对象详情页,会有 对象权限 ,即可以设置某一用户针对此对象的权限设置。共有增删改查四项。

模型操作

除了图形界面外,在视图等地方需要使用代码来操作数据模型。guardian 使用的用户和组和 django 的一样,只有权限划分中有部分区别

自定义模型权限

和 django 一样,可以对模型进行自定义权限

class CommonTask(models.Model):
	...
	class Meta:
        permissions = (
                ('view_task', '查看任务权限'),
                ('change_task', '更改任务权限'),
                ('stop_task', '停止任务权限'),
            )
为用户/组增删权限

可以使用 guardina.shortcuts.assign_perm() 方法来分配对象权限

from django.contrib.auth.models import User, Group
from guardian.shortcuts import assign_perm

# 获取数据对象
from models import CommonTask
obj = CommonTask.objects.get(pk=1)

# 获取用户对象
user = User.objects.get(name='test_account')
# 获取用户组对象
group = Group.objects.get(name='test')

# 确认用户是否对数据对象有权限
if not user.has_perm('view_task', obj):
	# 给用户处理数据对象的权限
	assign_perm('view_task', user, obj)		# 注:这里的 user 和 obj 都可以是 QuerySet,即可以将多个数据对象权限赋给多个用户

# 用户加入组
user.groups.add(group)
# 确认用户是否对数据对象有权限
if not user.has_perm('view_task', obj):
	# 给组处理数据对象的权限
	assign_perm('view_task', group, obj)	# 同样能将多个数据对象赋权给多个组

需要注意的是,guardian.shortcuts.assign_perm(perm, user_or_group, obj=None) 是针对某一具体对象赋权,但并没有对整个 model 赋权,所以 has_perm('app.view_task') 时,会返回 False。另外 assign_perm 方法的第三个参数如果使用 None,则第一个参数格式必须为 app.perm_codename,此时为赋予 model 的权限而不是具体数据对象。

可以使用 guardian.shortcuts.remove_perm(perm, user_or_group=None, obj=None) 方法移除授权,需注意的是第二个参数不能是QuerySet而必须是instance,所以不能同时去除多个用户的权限。移除完同样需要刷新用户对象,保证缓存最新的权限。

from guardian.shortcuts import remove_perm

# 移除用户/组的特定数据对象权限
remove_perm('view_task', user, obj)
# 移除用户/组的所有对象(即整个数据模型)权限
remove_perm('app.view_task', group)
验证权限

验证是否有权限可以使用上例中的 user.has_perm() 方法。此外,guardian 还提供了一些其他的方法

# get_perms(user_or_group,obj) 方法可以根据用户或组以及对象来获取权限(has_perm 不能通过组验证)
from guardian.shortcuts import get_perms

get_perms(group, obj)		# 返回一个权限列表 ['view_task']
'permcodename' in get_perms(user_or_group, obj)		# 返回一个布尔值
# get_objects_for_user(user, perms, klass=None, use_groups=True, any_perm=False) 
from guardian.shortcuts import get_objects_for_user

# 此方法可以根据用户和权限获取数据对象,获取的是一个 QuerySet
get_objects_for_user(user, 'app.view_task')
# 第二个参数可以写成列表,返回同时满足权限的数据对象
get_objects_for_user(user, ['app.view_task', 'app.stop_task'])
# 或使用 any_perm=True,满足列表任意权限条件即可
get_objects_for_user(user, ['app.view_task', 'app.stop_task'], any_perm=True)
# get_users_with_perms(obj, attach_perms=False, with_superusers=False, with_group_users=True, only_with_perms_in=None)
from guardian.shortcuts import get_users_with_perms

# 可以根据数据对象的权限获取用户,返回 QuerySet
get_users_with_perms(obj)
# 默认返回的用户中没有 superuser,可以通过 with_superusers=True 让超级用户包含在内
get_users_with_perms(obj, with_superusers=True)
# 参数 attach_perms=True 可以返回一个字典,可以查看各用户拥有的具体权限
get_users_with_perms(obj, with_superusers=True, attach_perms=True)
# 如果仅想查看某个权限的用户,可以使用 only_with_perms_in 参数
get_users_with_perms(obj, with_superusers=True, only_with_perms_in=['view_task'])
# 默认用户加入了组后拥有组的权限,如果不想看继承自组的权限,则使用 with_group_users=False
get_users_with_perms(obj, with_superusers=True, with_group_users=False)

# get_groups_with_perms 与 get_users_with_perms 方法类似,但是只接收2个参数 obj 和 attach_perms

guardian 对装饰器 permission_required 做了扩展,能够对对象权限进行校验。使用方式同 permission_required,但是增加了第二个参数,这个参数是一个元组,格式为(model, model_field, value)。通过第二个参数能够获取到一个数据对象 QuerySet (查询逻辑为 model.objects.get(model_field=value)),如果用户对这个对象有第一个参数的权限,则可以使用视图函数

guardian 也提供了模板标签,方便在模板中对数据对象的权限进行校验

<!-- 加载标签 -->
{% load guardian_tags %}
<!-- 获取权限列表 -->
<!--	格式为:
{% get_obj_perms user/group for obj as 'context_var' %}
例如:	-->
{% get_obj_perms request.user for task as 'task_perms' %}
{% if 'view_task' in task_perms %}
显示有权限查看的数据
{% endif %}

孤儿对象许可

举个例子,用户 A 拥有数据对象 O 的权限,某天用户 A 被删除了,但是分配的权限还存在数据库中,这个就是孤儿对象许可。如果又有一天,创建了用户 A ,则新建立的用户就立刻拥有了数据对象 O 的权限,这是不对的!因此当删除 User 和相关 Object 时,一定要删除相关的所有 UserObjectPermission 和 GroupObjectPermission 对象。

解决办法有三个:

  1. 显式编写代码
  2. 执行 django 命令

python manage.py clean_orphan_obj_perms

  1. 定期调用 guardian.utils.clean_orphan_obj_perms()
    该函数会返回删除的对象数目。可以使用 celery 定期调度这个任务

第二和第三个方法不是合理的生产环境的解决办法,真正想要解决,还是需要手动编码,最优雅的方式是加上 post_delete 信号给User或Object 对象,例如

from django.contrib.auth.models import User
from django.contrib.contenttypes.models import ContentType
from django.db.models import Q
from django.db.models.signals import pre_delete
from guardian.models import UserObjectPermission
from guardian.models import GroupObjectPermission
from models import Task	# 自定义的模型

def remove_obj_perms_connected_with_user(sender, instance, **kwargs):
    filters = Q(content_type=ContentType.objects.get_for_model(instance), object_pk=instance.pk)
    UserObjectPermission.objects.filter(filters).delete()
    GroupObjectPermission.objects.filter(filters).delete()

pre_delete.connect(remove_obj_perms_connected_with_user, sender=Task)

生成html代码

有时候,在前端不好确定的代码,可以通过后端生成,然后传递给前端使用。这里以翻页的手动代码进行举例:

from django.utils.safestring import mark_safe  # 将后端字符串标记为安全,可以传递到前端做为前端的 HTML 代码使用
    # 前端的翻页页码
    page_list = []

    # 计算出当前页的前三后三页
    if page >= 5:  # 需要首页
        ele = f'<li><a href="?q={
      
      q}&order={
      
      order}&by={
      
      by}&page=1">首页</a></li>'
        page_list.append(ele)
    for i in range(page - 3, page + 4):
        if i <= 0 or i > all_page:
            continue
        if i == page:
            ele = f'<li class="active"><a href="?q={
      
      q}&order={
      
      order}&by={
      
      by}&page={
      
      i}">{
      
      i}<span class="sr-only">(current)</span></a></li>'
        else:
            ele = f'<li><a href="?q={
      
      q}&order={
      
      order}&by={
      
      by}&page={
      
      i}">{
      
      i}</a></li>'
        page_list.append(ele)
    if page <= all_page - 4:
        ele = f'<li><a href="?q={
      
      q}&order={
      
      order}&by={
      
      by}&page={
      
      all_page}">尾页</a></li>'
        page_list.append(ele)
    page_str = mark_safe(''.join(page_list))			# 使用 mark_safe() 方法将字符串变为前台代码
    
return render(request,'pn_list.html', page_str)

如果不使用 mark_safe 标记,传到前端的数据会以字符串形式呈现。标记后则前端认为字符串是就是前端代码。

cookie、session 和 token

cookie 信息

创建、设置 cookie

django 可以通过设置响应来设置 cookie 信息,使用HttpResponse.set_cookie() 方法来设置 Response 的 cookie:

def set_cookie(request):
	resp = HttpResponse()
	resp.set_cookie('username', 'zhangsan', expires=datetime.now()+timedelta(days=3))
	return resp

设置 cookie 时可以设置 max-age 或 expire,来确定 cookie 的寿命周期。如未指定,则表示永久有效。

  • max_age 有效时长,单位为秒。默认为0,关闭浏览器即失效。
  • expire 持续到某一时间节点。使用 datetime.datetime 对象

获取 cookie

通过 request 可以获取 cookie 信息

name = request.COOKIES.get('username')

删除 cookie

响应中发送删除 cookie 的数据,浏览器接收到了就可以删除响应的 cookie 信息

def del_cookie(request):
	resp = HttpResponse()
	resp.delete_cookie('username')
	return resp

注意

需要注意的有

  • cookie 不支持中文(可能部分浏览器支持,最好不要使用中文)
  • 不能跨浏览器,不能跨域

session 信息

django 的 session 依赖于 cookie 技术,因为使用 session 会自动生成 session_id,用来确定 session 信息的归属,而 session_id 会记录在 cookie 中。

session 的存储

django 的 session 数据默认存储在数据库的 django_session 表中,通过 cookie 中的 sessionid 获取相应。也可以在 settings.py 里设置,例如保存至 redis。

# settings.py

# 配置缓存
CACHES = {
    
    
    'session_redis': {
    
      # 保存方案名称
        'BACKEND': 'django_redis.cache.RedisCache',  # 缓存方案
        'LOCATION': 'redis://127.0.0.1:6379/1',  # redis 的主机地址、端口和数据库编号
        'OPTIONS': {
    
      # 其他的一些选项
            'CLIENT_CLASS': 'django_redis.client.DefaultClient',  # 连接客户端
            'PASSWORD': '123456',  # 口令
            'SOCKET_CONNECT_TIMEOUT': 5,  # 连接超时时间,单位秒
            'SOCKET_TIMEOUT': 5,  # 读写超时时间,单位秒
            'CONNECTION_POOL_KWARGS': {
    
    'max_connections': 10, }  # 连接池参数
        },
    },
}

# 配置 SESSION 缓存
SESSION_ENGINE = "django.contrib.sessions.backends.cache"	# 保存至缓存
SESSION_CACHE_ALIAS = "session_redis"			# 保存缓存方案

启用 session

django中默认启用了session,如果要自定义添加 session,则需要在 settings.py 中的 INSTALLED_APP 中添加,并在 MIDDLEWARE 中添加 session 的中间件。

创建 session 信息

在 django 中,使用 request.session[key] = value 能够记录 session 的信息,同时会自动生成验证 session_id 返回到用户浏览器 cookie 中,并且将 session_id 和验证字符串都存储到数据库的 django_session 表中,以便下次使用时自动验证。

获取并检查 session 信息

如果是已经记录 session 信息的用户再次访问,则请求信息的 cookie 中会有 session_id 。django 会从数据库中查询是否存在此 session_id,如果存在则能够获取设置的 session[key] 的 value ,如果不存在则不能够获取。所以使用 request.session.get(key) 来获取设置数据就能够自动检查 session 信息。

session 超时

可以使用 request.session.set_expiry(sec) 来设置 session 的超时时间,sec 单位为秒。

删除、注销 session 信息

可以使用 del request.session[key] 的方式删除指定的 session 信息,使用 request.session.clear() 方法可以清除 session 信息,即进行注销。

token

token 是身份令牌,表示一个有权限的访问用户成功登录。以后再访问时如果有 token (且 token 有效)就不再进行登录验证。token 的信息一般都是自定义的,较简单的方式是在成功登录后,产生一个 uuid 到 cookie 中,这个 uuid 就是 token。

def add_token(request):
	# 生成token
	token = uuid.uuid4().hex
	resp = HttpResponse('增加了 token 到 cookie 中')
	resp.set_cookie('token', token, max_age=60*60*24)
	request.session['token'] = token
	return resp

token 和 sessionid 都是用来确定访问用户的,其最大的区别在于 session 会占用服务器资源,而 token 会交给客户端,通过请求头来维护,节省了服务器资源。session 的主要目的是给无状态的 HTTP 协议添加状态保持,通常在浏览器作为客户端的情况下使用;而 Token 主要目的是鉴权,所以更多用在第三方 API。所以目前基于Token的鉴权机制几乎已经成了前后端分离架构或者对外提供API访问的鉴权标准,得到广泛使用。

对于 JSON Web Token(JWT) 的方案,详细的可以看这里

Django+JWT实现Token认证

在 api 设计中,使用 django-RESTful 是个比较方便的方案,且 django-RESTful 框架包含了方便的 token 验证方案。

猜你喜欢

转载自blog.csdn.net/runsong911/article/details/127936061