Django(15):身份和权限认证

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模块中,除了有上面用于描述实力的模块,也封装了用于身份认证和权限管理的函数。

  1. django.contrib.auth.authenticate:用户身份认证函数,传入用户身份信息,默认参数为账号、密码。如果认证通过,返回当前用户对象,否则返回None。
  2. django.contrib.auth.login:用户身份状态记录函数,传入请求对象和需要记录的用户对象。该函数负责将用户信息记录到所属的会话对象(如session)中存储。
  3. django.contrib.auth.loglout:登录状态清除函数,传入请求对象,将记录在当前请求所属会话中的用户对象user清空并设置匿名用户Anonymous。
  4. django.contrib.auth.decorators.login_required:用户身份认证资源访问装饰器,验证用户是否通过身份认证,如果验证通过,则允许该用户访问装饰器下面的函数,否则跳转到login_url参数指定的路径。如果不提供该参数,则自动获取配置文件里的LOGIN_UTL配置路径。
  5. django.contrib.auth.decorators.permission_required:数据资源访问装饰器,验证当前用户是否拥有制指定权限。如果验证通过,则允许该用户访问装饰器下面的函数,否则跳转到login_url参数指定的路径。如果不提供该参数,则自动获取配置文件里的LOGIN_UTL配置路径。

另外再第七章视图处理函数中,还有require_GET、require_POST、require_http_method等装饰器,用于限制请求类型。

2.3 权限分配函数

用户通过身份认证后,可以在操作过程中根据实际场景进行用户组和用户权限的独立分配,相关函数如下:

  1. user.groups.set([group_list]):为用户设置用户组,可以设置到多个组里。
  2. user.groups.add(group1, group2, ...):为用户设置用户组。
  3. user.groups.remove(group1, group2, ...):将当前用户从指定用户组删除。
  4. user.groups.clear():把用户从所有的用户组删除。
  5. user.user_permissions.set([permission_list]):为用户设置权限,可以指定多个。
  6. user.user_permissions.add(perm1, perm2, ...):为用户设置权限。
  7. user.user_permissions.remove(perm1, perm2, ...):删除用户指定权限。
  8. 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的设计进行实现。

  • 1
    点赞
  • 3
    收藏
    觉得还不错? 一键收藏
  • 打赏
    打赏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包

打赏作者

Ethan-running

你的鼓励将是我创作的最大动力

¥1 ¥2 ¥4 ¥6 ¥10 ¥20
扫码支付:¥1
获取中
扫码支付

您的余额不足,请更换扫码支付或充值

打赏作者

实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值