目录
- 1.Django中的身份认证模块
- 1.1 用户模型
- 1.2 认证模块
- 1.3 项目搭建演示
- 2.权限管理架构
- 2.1 权限相关数据模型
- 2.2 权限相关功能函数
- 2.3 权限分配函数
- 2.4 权限设置
- 3.资源访问管理
1.Django中的身份认证模块
1.1 用户模型
Django中有内建的用户模块django.contrib.auth.models.User
,该类型通过定义网站中用户的基本数据完成身份认证功能支持。代码如下:
class User(AbstractUser):
"""
Users within the Django authentication system are represented by this
model.
Username and password are required. Other fields are optional.
"""
class Meta(AbstractUser.Meta):
swappable = "AUTH_USER_MODEL"
可以看到它继承了AbstractUser,其代码如下:
class AbstractUser(AbstractBaseUser, PermissionsMixin):
"""
An abstract base class implementing a fully featured User model with
admin-compliant permissions.
Username and password are required. Other fields are optional.
"""
username_validator = UnicodeUsernameValidator()
username = models.CharField(
_("username"),
max_length=150,
unique=True,
help_text=_(
"Required. 150 characters or fewer. Letters, digits and @/./+/-/_ only."
),
validators=[username_validator],
error_messages={
"unique": _("A user with that username already exists."),
},
)
first_name = models.CharField(_("first name"), max_length=150, blank=True)
last_name = models.CharField(_("last name"), max_length=150, blank=True)
email = models.EmailField(_("email address"), blank=True)
is_staff = models.BooleanField(
_("staff status"),
default=False,
help_text=_("Designates whether the user can log into this admin site."),
)
is_active = models.BooleanField(
_("active"),
default=True,
help_text=_(
"Designates whether this user should be treated as active. "
"Unselect this instead of deleting accounts."
),
)
date_joined = models.DateTimeField(_("date joined"), default=timezone.now)
objects = UserManager()
EMAIL_FIELD = "email"
USERNAME_FIELD = "username"
REQUIRED_FIELDS = ["email"]
class Meta:
verbose_name = _("user")
verbose_name_plural = _("users")
abstract = True
def clean(self):
super().clean()
self.email = self.__class__.objects.normalize_email(self.email)
def get_full_name(self):
"""
Return the first_name plus the last_name, with a space in between.
"""
full_name = "%s %s" % (self.first_name, self.last_name)
return full_name.strip()
def get_short_name(self):
"""Return the short name for the user."""
return self.first_name
def email_user(self, subject, message, from_email=None, **kwargs):
"""Send an email to this user."""
send_mail(subject, message, from_email, [self.email], **kwargs)
可以看到,AbstractUser有自己的属性和方法,同时也从AbstractBaseUser, PermissionsMixin继承了其他属性和方法,具体内容可参看源码。
同时,如果用户没有身份信息,访问网站时,Django框架通过django.contrib.auth.models.AnonymousUser
类型为其赋予了一个匿名身份。其源码如下:
class AnonymousUser:
id = None
pk = None
username = ""
is_staff = False
is_active = False
is_superuser = False
_groups = EmptyManager(Group)
_user_permissions = EmptyManager(Permission)
def __str__(self):
return "AnonymousUser"
def __eq__(self, other):
return isinstance(other, self.__class__)
def __hash__(self):
return 1 # instances always return the same hash value
def __int__(self):
raise TypeError(
"Cannot cast AnonymousUser to int. Are you trying to use it in place of "
"User?"
)
def save(self):
raise NotImplementedError(
"Django doesn't provide a DB representation for AnonymousUser."
)
def delete(self):
raise NotImplementedError(
"Django doesn't provide a DB representation for AnonymousUser."
)
def set_password(self, raw_password):
raise NotImplementedError(
"Django doesn't provide a DB representation for AnonymousUser."
)
def check_password(self, raw_password):
raise NotImplementedError(
"Django doesn't provide a DB representation for AnonymousUser."
)
@property
def groups(self):
return self._groups
@property
def user_permissions(self):
return self._user_permissions
def get_user_permissions(self, obj=None):
return _user_get_permissions(self, obj, "user")
def get_group_permissions(self, obj=None):
return set()
def get_all_permissions(self, obj=None):
return _user_get_permissions(self, obj, "all")
def has_perm(self, perm, obj=None):
return _user_has_perm(self, perm, obj=obj)
def has_perms(self, perm_list, obj=None):
if not is_iterable(perm_list) or isinstance(perm_list, str):
raise ValueError("perm_list must be an iterable of permissions.")
return all(self.has_perm(perm, obj) for perm in perm_list)
def has_module_perms(self, module):
return _user_has_module_perms(self, module)
@property
def is_anonymous(self):
return True
@property
def is_authenticated(self):
return False
def get_username(self):
return self.username
正常情况下,上面的用户信息是不能满足我们的业务需求的,在实际项目中,可以根据需求自定义用户的扩展资料,将扩展资料和内建用户进行一对一关联,这样可以使用户的资料会大幅提升。
1.2 认证模块
Django框架内建的django.contrib.auth
模块封装了身份认证和状态保持操作,可以使用django.contrib.auth.authenticate
完成核心的身份认证处理。源代码如下:
@sensitive_variables("credentials")
def authenticate(request=None, **credentials):
"""
If the given credentials are valid, return a User object.
"""
for backend, backend_path in _get_backends(return_tuples=True):
backend_signature = inspect.signature(backend.authenticate)
try:
backend_signature.bind(request, **credentials)
except TypeError:
# This backend doesn't accept these credentials as arguments. Try
# the next one.
continue
try:
user = backend.authenticate(request, **credentials)
except PermissionDenied:
# This backend says to stop in our tracks - this user should not be
# allowed in at all.
break
if user is None:
continue
# Annotate the user object with the path of the backend.
user.backend = backend_path
return user
# The credentials supplied are invalid to all backends, fire signal
user_login_failed.send(
sender=__name__, credentials=_clean_credentials(credentials), request=request
)
同时,使用django.contrib.auth
封装的login()函数完成了状态保持,将用户保持到当前会话中;使用logout()函数可以移除状态保持。具体的内容可以参考源码。
1.3 项目搭建演示
1.创建项目
# 创建项目perm_demo
django-admin startproject perm_demo
# 创建blog子项目
cd perm_demo/
django-admin startapp blog
2.创建数据模型
在blog子项目中编辑models.py模块,添加用户扩展资料:
class UserProfile(models.Model):
"""用户扩展资料"""
C_GENDER = (
("0", "女"),
("1", "男"),
)
# 关联内建用户
user = models.OneToOneField(verbose_name="用户", to=User, related_name="profile", on_delete=models.CASCADE)
# 用户性别
gender = models.CharField(verbose_name="性别", max_length=5, choices=C_GENDER, default="1")
# 用户年龄
age = models.IntegerField(verbose_name="年龄", default=0)
# 联系方式
phone = models.CharField(verbose_name="手机", max_length=15, null=True, blank=True)
# 所述组织
org = models.CharField(verbose_name="组织", max_length=200, null=True, blank=True)
# 个人介绍
intro = models.TextField(verbose_name="简介", null=True, blank=True)
3.创建表单模块
在blog项目中创建forms.py模块,添加代码:
from django import forms
from django.contrib.auth.models import User
class UserRegisterForm(forms.ModelForm):
"""用户注册表单"""
confirm = forms.CharField(label='确认密码', min_length=6, max_length=18)
class Meta:
model = User
fields = ['username', 'password', 'confirm']
def clean_username(self):
"""自定义用户名称验证规则"""
u_list = User.objects.filter(username=self.cleaned_data['username'])
if len(u_list) > 0:
raise forms.ValidationError("账号已经存在,请使用其他账号注册")
return self.cleaned_data['username']
def clean_confirm(self):
"""自定义确认密码验证规则"""
if self.cleaned_data['password'] != self.cleaned_data['confirm']:
raise forms.ValidationError("两次密码输入不一致")
return self.cleaned_data['confirm']
4.用户登录业务逻辑
编辑blog/views.py模块,代码如下:
from django.shortcuts import render, redirect, get_object_or_404, get_list_or_404
from django.urls import reverse
from django.contrib.auth import authenticate, login, logout
from django.views.decorators.http import require_GET
from django.contrib.auth.decorators import login_required, permission_required
from django.contrib.auth.models import User
from . import forms
from .models import Article
def user_login(request):
"""用户登录"""
if request.method == "GET":
return render(request, 'blog/login.html', {})
elif request.method == "POST":
# 接受登录数据
username = request.POST.get("username")
password = request.POST.get("password")
# 验证表单数据
user = authenticate(request, username=username, password=password)
if user and user.is_active:
login(request, user)
# 返回首页
return redirect(reverse("blog:user_index"))
else:
return render(request, 'blog/login.html', {'msg_code': "-1",
'msg_info': "账号或者密码有误."})
def user_logout(request):
"""用户退出"""
logout(request)
return redirect(reverse("blog:user_index"))
def user_register(request):
"""用户注册"""
if request.method == "GET":
return render(request, "blog/register.html", {})
elif request.method == "POST":
# 接收用户注册数据
form_register = forms.UserRegisterForm(request.POST)
# 判断注册数据有效性
if form_register.is_valid():
# 验证通过,保存注册数据
User.objects.create_user(username=form_register.instance.username,
password=form_register.instance.password)
# 跳转登录界面
return redirect(reverse("blog:user_login"), kwargs={"msg_code": "0",
"msg_info": "账号注册成功"})
else:
return render(request, "blog/register.html", {"form": form_register,
"msg_code": "-1",
"msg_info": "注册失败"})
def user_index(request):
# 查看文章列表
articles = Article.objects.all()
return render(request, "blog/index.html", {"articles": articles})
5.添加网页文件
创建blog/templates/blog文件夹,添加网页文件
添加基础模板文件base.html,其他文件继承它:
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<title>身份认证-权限管理-{% block title %}{% endblock %}</title>
</head>
<body>
{% block page_body %}
{% endblock %}
</body>
</html>
添加登录网页文件login.html:
{% extends 'blog/base.html' %}
{% block title %}个人博客用户登录{% endblock %}
{% block page_body %}
<h2>会员登录</h2>
<p>
{{ form.errors }}
{{ msg_info }}
</p>
<form action="{% url 'blog:user_login' %}" method="POST">
{% csrf_token %}
<label for="username">账号: </label>
<input type="text" name="username" id="username">
<span style="color:red">{{ form.errors.username }}</span>
<br />
<label for="password">密码: </label>
<input type="password" name="password" id="password">
<span style="color:red">{{ form.errors.password }}</span>
<br />
<input type="submit" value="登录">
</form>
{% endblock %}
添加注册网页文件register.html:
{% extends 'blog/base.html' %}
{% block title %}个人博客用户注册{% endblock %}
{% block page_body %}
<h2>新用户注册
<small>{{ form.errors }}</small>
</h2>
<hr>
<form action="{% url 'blog:user_register' %}" method="POST">
{% csrf_token %}
<label for="username">账号:</label>
<input type="text" name="username" id="username"><br />
<label for="password">密码:</label>
<input type="password" name="password" id="password"><br />
<label for="confirm">确认密码:</label>
<input type="password" name="confirm" id="confirm"><br />
<input type="submit" value="提交注册">
</form>
{% endblock %}
添加首页网页文件index.html:
{% extends 'blog/base.html' %}
{% block title %}个人博客首页{% endblock %}
{% block page_body %}
<h2>个人博客首页
<small>尊敬的用户{{ request.user }},欢迎访问本系统</small>
</h2>
<hr>
<a href="{% url 'blog:user_logout'%}">退出</a>
<p>博客网页内容</p>
<ul>
<li><a href="{% url 'blog:article_publish' %}">发表文章</a></li>
</ul>
<ul>
{% for article in articles %}
<li>标题:<a href="{% url 'blog:article_detail' article.id %}">{{ article.title }}</a> -- 作者:{{ article.author.username }} --
<a href="{% url 'blog:article_delete' article.id %}">删除</a> <a href="{% url 'blog:article_update' article.id %}">编辑</a></li>
{% endfor %}
</ul>
{% endblock %}
6.添加路由
编辑blog/urls.py
from django.urls import path
from django.views.generic import TemplateView
from . import views
app_name = 'blog'
urlpatterns = [
path("perm_refused/", TemplateView.as_view(template_name='blog/permission_refused.html'), name="perm_refused"),
path("login/", views.user_login, name="user_login"),
path("logout/", views.user_logout, name="user_logout"),
path("register/", views.user_register, name="user_register"),
path("", views.user_index, name="user_index"),
]
7.启动项目
访问http://127.0.0.1:8000/blog/register,先注册一个用户,容纳后访问首页http://127.0.0.1:8000/blog/让先登录,登录后到了博客首页,如下:
2.权限管理架构
权限管理是指用户对网站资源的访问管理操作,对于不同的用户身份,对资源的访问方式应有所区分。在用户访问资源时,需要先判断是否有权限。
对于很多用户,可能有级别相似的权限,权限设置造成大量冗余,于是引入角色,通过角色对权限进行管理,用户拥有某个角色,相当于拥有该角色的权限。
2.1 权限相关数据模型
对于整个权限管理,Django内置了用户模块、角色模块、权限模块来尽心管理:
django.contrib.auth.models.User
:用户模块中的用户类型django.contrib.auth.models.Group
:用户模块中的用户组类型django.contrib.auth.models.Permission
:用户模块中的权限类型
2.2 权限相关功能函数
Django内置的django.contrib.auth模块中,除了有上面用于描述实力的模块,也封装了用于身份认证和权限管理的函数。
django.contrib.auth.authenticate
:用户身份认证函数,传入用户身份信息,默认参数为账号、密码。如果认证通过,返回当前用户对象,否则返回None。django.contrib.auth.login
:用户身份状态记录函数,传入请求对象和需要记录的用户对象。该函数负责将用户信息记录到所属的会话对象(如session)中存储。django.contrib.auth.loglout
:登录状态清除函数,传入请求对象,将记录在当前请求所属会话中的用户对象user清空并设置匿名用户Anonymous。django.contrib.auth.decorators.login_required
:用户身份认证资源访问装饰器,验证用户是否通过身份认证,如果验证通过,则允许该用户访问装饰器下面的函数,否则跳转到login_url参数指定的路径。如果不提供该参数,则自动获取配置文件里的LOGIN_UTL配置路径。django.contrib.auth.decorators.permission_required
:数据资源访问装饰器,验证当前用户是否拥有制指定权限。如果验证通过,则允许该用户访问装饰器下面的函数,否则跳转到login_url参数指定的路径。如果不提供该参数,则自动获取配置文件里的LOGIN_UTL配置路径。
另外再第七章视图处理函数中,还有require_GET、require_POST、require_http_method等装饰器,用于限制请求类型。
2.3 权限分配函数
用户通过身份认证后,可以在操作过程中根据实际场景进行用户组和用户权限的独立分配,相关函数如下:
user.groups.set([group_list])
:为用户设置用户组,可以设置到多个组里。user.groups.add(group1, group2, ...)
:为用户设置用户组。user.groups.remove(group1, group2, ...)
:将当前用户从指定用户组删除。user.groups.clear()
:把用户从所有的用户组删除。user.user_permissions.set([permission_list])
:为用户设置权限,可以指定多个。user.user_permissions.add(perm1, perm2, ...)
:为用户设置权限。user.user_permissions.remove(perm1, perm2, ...)
:删除用户指定权限。user.user_permissions.clear()
:删除用户所有权限。
2.4 权限设置
在Django中,会自动设置在配置INSTALLED_APPS中的app内部模型类的默认访问权限,有增删改查4种,如下:
- add:如blog子项目的Article模型,设置了权限blog.add_article;
- change:如blog子项目的Article模型,设置了权限blog.change_article;
- delete:如blog子项目的Article模型,设置了权限blog.delete_article;
- view:如blog子项目的Article模型,设置了权限blog.view_article;
使用上一节perm_demo项目,添加模型类Article,如下:
class Article(models.Model):
"""文章类型"""
# 文章主键编号
id = models.UUIDField(verbose_name="文章编号", primary_key=True, default=uuid4)
# 文章标题
title = models.CharField(verbose_name="文章编号", max_length=20)
# 文章内容
content = models.TextField(verbose_name="文章内容")
# 发布时间
publish_time = models.DateTimeField(verbose_name="发表时间", auto_now_add=True)
# 修改时间
update_time = models.DateTimeField(verbose_name="修改时间", auto_now=True)
# 文章作者
author = models.ForeignKey(verbose_name="作者", to=User, on_delete=models.CASCADE)
def get_absolute_url(self):
return reverse("blog:article_detail", kwargs={"article_id": self.id})
登录后台管理系统访问http://127.0.0.1:8000/admin/auth/user/4/change/可查看所有的权限,如下:
3.资源访问管理
可以通过装饰器@permission_required
进行权限认证。
以上面perm_demo项目为例,编辑blog/views.py,在与文章相关的视图处理函数中添加权限认证操作:
@permission_required("blog.view_article")
@login_required
@require_GET
def user_index(request):
# 查看文章列表
print("index")
articles = Article.objects.all()
print(request.user.get_all_permissions())
print(request.user.has_perm("blog.view_article"))
return render(request, "blog/index.html", {"articles": articles})
@login_required
@permission_required("blog.add_article", login_url='/blog/perm_refused/')
def article_publish(request):
"""发表文章"""
if request.method == "GET":
return render(request, "blog/article_publish.html", {})
elif request.method == "POST":
# 接受文章数据
form = forms.ArticleForm(request.POST)
# 验证文章数据是否正确
if form.is_valid():
# 发表文章
form.save()
# 跳转到文章详情页面
return redirect(form.instance)
return render(request, "blog/article_publish.html", {'form': form})
@permission_required("blog.view_article", login_url='/blog/perm_refused/')
@require_GET
def article_detail(request, article_id):
"""查看文章"""
if request.method == "GET":
article = get_object_or_404(Article, pk=article_id)
return render(request, "blog/article_detail.html", {"article": article})
@login_required
@permission_required("blog.change_article", login_url='/blog/perm_refused/')
def article_update(request, article_id):
"""修改文章"""
article = get_object_or_404(Article, pk=article_id)
if request.method == "GET":
return render(request, "blog/article_update.html", {"article": article})
elif request.method == "POST":
# 接受文章数据
form = forms.ArticleForm(request.POST, instance=article)
# 验证文章数据是否正确
if form.is_valid():
print("文章修改数据验证通过")
# 存储文章数据
form.save()
# 跳转到文章详情页面
return redirect(form.instance)
return render(request, "blog/article_update.html", {'form': form})
@login_required
@permission_required("blog.delete_article", login_url='/blog/perm_refused/')
def article_delete(request, article_id):
"""删除文章"""
# 查询文章数据
article = get_object_or_404(Article, pk=article_id)
# 删除文章
article.delete()
# 返回首页
return redirect(reverse("blog:user_index"))
上面的代码中看到,访问首页(user_index视图函数)和文章详情(article_detail视图函数)都需要当前用户拥有blog.view_article权限;发表文章(article_publish视图函数)需要当前用户拥有blog.add_article权限;修改文章(article_update视图函数)需要当前用户拥有blog.change_article权限;同理删除需要blog.delete_article权限。
上面的操作方式可以满足大部分需求,到那时如果有定制化的权限架构,可参考Django的设计进行实现。