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 提供的组件来实现。
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 对象创建表单,而不是使用 等标签创建。form 对象会自动生成插件所标识的 html 标签。
可以进行自动渲染
<form method="post">
{% form.as_table %}
form>
这样就能将 form 渲染为 table 标签,还可以使用 form.as_p、form.as_ul。
另外,也可以手动渲染字段
<form method="post">
{% 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 可以指定此字段使用的组件类型,例如让元素
type='password
。也可以自定义一些小部件。
<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)
使用 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 %}
<div class="form-group">
<label>{{ field.label }}label>
{{ field }}
<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 对于表单校验时,遵循这几个步骤:
to_python()
方法,它强制将值转换为正确的数据类型。 validate()
方法处理不适合验证器的特定字段验证。这个方法不会返回任何东西,也不应该改变值。可以覆盖它来处理不能或不想放在验证器中的验证逻辑。run_validators()
方法会运行该字段的所有验证器,不应该覆盖此方法。clean()
方法负责以正确的顺序运行to_python()
、validate()
和run_validators()
并传播它们的错误。该方法返回干净的数据,然后将其插入到表单的cleaned_data字典中。clean_()
方法是在表单子类上调用的——其中
被替换为表单字段属性的名称。这个方法做任何特定属性的清理工作,与字段的类型无关。这个方法不传递任何参数,需要在 self.cleaned_data 中查找字段的值,并且记住,此时它将是一个 Python 对象,而不是在表单中提交的原始字符串(它将在 cleaned_data 中,因为上面的一般字段 clean()
方法已经清理了一次数据)。这个方法可以用来校验特定字段,其返回值会替换 cleaned_data 中的现有值,所以它必须是 cleaned_data 中的字段值(即使这个方法没有改变它)或一个新的干净值。clean()
方法可以执行需要访问多个表单字段的验证。此方法可以覆写用作整体校验,如果需要可以返回一个完全不同的字典,这个字典将被用作 cleaned_data
。覆写时,注意先执行一下父类的此方法,进行一次校验。通过实例测试可以知道,返回的错误信息是英文的,这是因为 django 在设置上是使用英文 en-us ,可以在 setting.py 中的 LANGUAGE_CODE 设置成中文 zh-hans
如果前端使用了自定义的表单组件,在后端 django 中也可以使用 Form 组件或 ModelForm组件。
通常使用 Form 组件的流程为:
如果使用自定义表单和 Form 组件相结合的方式,则变为:
首先需要明确的是,后端使用 Form 或 ModelForm 组件区别不大,主要区别在于 Form 需要手动定义各字段,而 ModelForm 则根据数据库中列的情况自动生成字段。在前端使用自定义表单组件时,字段不会传给前端,所以基本没有区别。
对于前端来说,首先要创建表单元素,使其提交能够被 Form 对象使用的表单数据。经过分析POST信息(假设 method 为 post),得知需要提交 csrfmiddlewaretoken
和表单数据两种。
csrfmiddlewaretoken
是 csrf 校验使用的数据,可以在后台取消校验,或者添加 csrf 校验信息(注:post 请求和 cookie 中都需要有)。所以这时候前端需要做的,就是创建需要的表单元素,然后根据字段数据库字段,确定表单中各元素的 name 属性。并且根据需要,选择提交 csrf_token 的方式。
django 可以正常接收表单发送的数据,数据格式就是之前设置的键值对形式,键为表单字段,由前端元素的 name 属性确定,值为字符串列表,也可以使用 ajax 发送。
后端接收到数据后,可以使用此数据来创建表单对象并验证,和使用 django 表单组件一样使用。当使用 is_valid()
方法验证过后(也可以自定义验证方法),如果返回真则是通过验证,否则即是未通过验证。未通过验证则会将错误信息存储在 form 对象中。
经过分析表单对象可以发现,通常渲染模板时使用 {% for item in forms %}
来获取表单中各字段成为 html 元素,使用 {{ item.label }}
获取字段设置的别名,使用 {{ item.errors }}
来获取错误信息列表。相对应的,这些数据也存在于 form 表单对象中:
form.form._bound_fields_cache
获取全部元素。其类型是字典,键为各字段名称,值为 item 对象。通过这些,则可以获取错误信息返回前端。前端接收到后可以将错误信息呈现给用户,待用户更改后重新发送表单数据给 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 是基于原生 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'),
)
检查用户权限: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
在模板中也可以进行权限验证:主要使用 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 %}
每个模型默认拥有增(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()
当编辑某个 user 信息时,可以在 User permissions 栏为其设置权限。权限的规则是:
权限显示为:应用 | 模型 | 行为(增删改查等)
权限数据为:应用.行为_模型
其中 行为_模型 就是数据库表中的 codename,应用即 app_label
django 自带的权限管理机制是针对模型的,这就意味着如果一个用户对某一模型有权限,则此模型中的所有数据均有权限。如果希望实现针对单个数据的权限管理,则需要使用第三方库比如 django guardian 库。
django-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
此节引用的文章来自知乎
使用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 request.user for task as 'task_perms' %}
{% if 'view_task' in task_perms %}
显示有权限查看的数据
{% endif %}
举个例子,用户 A 拥有数据对象 O 的权限,某天用户 A 被删除了,但是分配的权限还存在数据库中,这个就是孤儿对象许可。如果又有一天,创建了用户 A ,则新建立的用户就立刻拥有了数据对象 O 的权限,这是不对的!因此当删除 User 和相关 Object 时,一定要删除相关的所有 UserObjectPermission 和 GroupObjectPermission 对象。
解决办法有三个:
python manage.py clean_orphan_obj_perms
guardian.utils.clean_orphan_obj_perms()
第二和第三个方法不是合理的生产环境的解决办法,真正想要解决,还是需要手动编码,最优雅的方式是加上 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)
有时候,在前端不好确定的代码,可以通过后端生成,然后传递给前端使用。这里以翻页的手动代码进行举例:
from django.utils.safestring import mark_safe # 将后端字符串标记为安全,可以传递到前端做为前端的 HTML 代码使用
# 前端的翻页页码
page_list = []
# 计算出当前页的前三后三页
if page >= 5: # 需要首页
ele = f'{q} &order={order}&by={by}&page=1">首页'
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'{q} &order={order}&by={by}&page={i}">{i}(current)'
else:
ele = f'{q} &order={order}&by={by}&page={i}">{i}'
page_list.append(ele)
if page <= all_page - 4:
ele = f'{q} &order={order}&by={by}&page={all_page}">尾页'
page_list.append(ele)
page_str = mark_safe(''.join(page_list)) # 使用 mark_safe() 方法将字符串变为前台代码
return render(request,'pn_list.html', page_str)
如果不使用 mark_safe 标记,传到前端的数据会以字符串形式呈现。标记后则前端认为字符串是就是前端代码。
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 的寿命周期。如未指定,则表示永久有效。
通过 request 可以获取 cookie 信息
name = request.COOKIES.get('username')
在响应中发送删除 cookie 的数据,浏览器接收到了就可以删除响应的 cookie 信息
def del_cookie(request):
resp = HttpResponse()
resp.delete_cookie('username')
return resp
需要注意的有
django 的 session 依赖于 cookie 技术,因为使用 session 会自动生成 session_id,用来确定 session 信息的归属,而 session_id 会记录在 cookie 中。
django 的 session 数据默认存储在数据库的 django_session 表中,通过 cookie 中的 sessionid 获取相应
django中默认启用了session,如果要自定义添加 session,则需要在 settings.py 中的 INSTALLED_APP 中添加,并在 MIDDLEWARE 中添加 session 的中间件。
在 django 中,使用 request.session[key] = value
能够记录 session 的信息,同时会自动生成验证 session_id 返回到用户浏览器 cookie 中,并且将 session_id 和验证字符串都存储到数据库的 django_session 表中,以便下次使用时自动验证。
如果是已经记录 session 信息的用户再次访问,则请求信息的 cookie 中会有 session_id 。django 会从数据库中查询是否存在此 session_id,如果存在则能够获取设置的 session[key] 的 value ,如果不存在则不能够获取。所以使用 request.session.get(key)
来获取设置数据就能够自动检查 session 信息。
可以使用 request.session.set_expiry(sec)
来设置 session 的超时时间,sec 单位为秒。
可以使用 del request.session[key]
的方式删除指定的 session 信息,使用 request.session.clear()
方法可以清除 session 信息,即进行注销。
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 验证方案。