Django实战1-权限管理功能实现-03:用户认证

1 权限管理模型设计

1.1 创建app

新建一个app, 名称叫system包含用户管理、菜单管理和权限管理等系统基础模块。

  • 使用pycharm打开我们的项目,右键项目根目录,选择 New → Python Package, 在弹出的窗口输入apps,这个包就用来存放项目中创建的所有app.
  • 选择pycharm上方Tools,点击Run manage.py Task..., 这时在pycharm下方会打开一个窗口,输入startapp system 回车创建app, 如下图:

Django实战1-权限管理功能实现-03:用户认证

  • 将刚刚创建的system 移动到 apps下
  • 为了能够顺利访问到我们新建的app,右键apps,选择Mark Directory as → Sources root
  • 修改sandboxMP/sandboxMP/settings.py 加入如下内容:
import sys
sys.path.insert(0, os.path.join(BASE_DIR, 'apps'))

1.2 创建权限认证模型

sandboxMP项目使用的是自定义权限认证模型,模型说明:

Menu: 菜单管理,用来存储系统可用的URL
Role: 角色组,通过外键关联Menu,角色组中的用户将继承Role关联菜单的访问权限
Structure:组织架构,包含单位和部门信息
UserProfile: 自定义用户认证模型,替换系统原有的User模型

下面内容就是权限认证的模型详细内容,将如下内容复制到apps/system/models.py

from django.db import models
from django.contrib.auth.models import AbstractUser


class Menu(models.Model):
    """
    菜单
    """
    name = models.CharField(max_length=30, unique=True, verbose_name="菜单名")  # unique=True, 这个字段在表中必须有唯一值.
    parent = models.ForeignKey("self", null=True, blank=True, on_delete=models.SET_NULL, verbose_name="父菜单")
    icon = models.CharField(max_length=50, null=True, blank=True, verbose_name="图标")
    code = models.CharField(max_length=50, null=True, blank=True, verbose_name="编码")
    url = models.CharField(max_length=128, unique=True, null=True, blank=True)

    def __str__(self):
        return self.name

    class Meta:
        verbose_name = '菜单'
        verbose_name_plural = verbose_name

    @classmethod
    def get_menu_by_request_url(cls, url):
        return dict(menu=Menu.objects.get(url=url))


class Role(models.Model):
    """
    角色:用于权限绑定
    """
    name = models.CharField(max_length=32, unique=True, verbose_name="角色")
    permissions = models.ManyToManyField("menu", blank=True, verbose_name="URL授权")
    desc = models.CharField(max_length=50, blank=True, null=True, verbose_name="描述")


class Structure(models.Model):
    """
    组织架构
    """
    type_choices = (("unit", "单位"), ("department", "部门"))
    name = models.CharField(max_length=60, verbose_name="名称")
    type = models.CharField(max_length=20, choices=type_choices, default="department", verbose_name="类型")
    parent = models.ForeignKey("self", null=True, blank=True, on_delete=models.SET_NULL, verbose_name="父类架构")

    class Meta:
        verbose_name = "组织架构"
        verbose_name_plural = verbose_name

    def __str__(self):
        return self.name


class UserProfile(AbstractUser):
    name = models.CharField(max_length=20, default="", verbose_name="姓名")
    birthday = models.DateField(null=True, blank=True, verbose_name="出生日期")
    gender = models.CharField(max_length=10, choices=(("male", "男"), ("female", "女")),
                              default="male", verbose_name="性别")
    mobile = models.CharField(max_length=11, default="", verbose_name="手机号码")
    email = models.EmailField(max_length=50, verbose_name="邮箱")
    image = models.ImageField(upload_to="image/%Y/%m", default="image/default.jpg",
                              max_length=100, null=True, blank=True)
    department = models.ForeignKey("Structure", null=True, blank=True, on_delete=models.SET_NULL, verbose_name="部门")
    post = models.CharField(max_length=50, null=True, blank=True, verbose_name="职位")
    superior = models.ForeignKey("self", null=True, blank=True, on_delete=models.SET_NULL, verbose_name="上级主管")
    roles = models.ManyToManyField("role", verbose_name="角色", blank=True)

    class Meta:
        verbose_name = "用户信息"
        verbose_name_plural = verbose_name
        ordering = ['id']

    def __str__(self):
        return self.name

1.3 使用模型

定义好模型后,还要告诉Django使用这些模型,我们需要修改settings.py文件,在INSTALLED_APPS中添加models.py所在应用的名称:

INSTALLED_APPS = [
     ...原内容省略...
     'system',
]

想要使用自定义的认证模型UserProfile, 还需要在setting.py中添加下面内容:

AUTH_USER_MODEL = 'system.UserProfile'

注意:
在定义用户模型的时候使用到了ImageField字段类型,在执行makemigrations前需要安装依赖包:pillow,打开CMD窗口,进入本项目的python虚拟环境,然后安装pillow:

C:\Users\RobbieHan>workon sandboxMP
(sandboxMP) C:\Users\RobbieHan>pip install pillow

也可以在pycharm 的Terminal终端窗口执行安装命令: pip install pillow

最后执行makemigrations 和 migrate来生成数据表, 使用pycharm Tools,点击Run manage.py Task..., 在manage.py窗口输入下面命令:

makemigrations
migrate

1.4 模型(Models)相关知识点

字段类型:

在权限认证模型中使用到的字段类型如下:

CharField: 用来存储字符串,必须制定一个参数 max_length用来限定字段最大长度
Foreignkey: 是一个关联字段,创建多表之间的多对一关系,如果创建同表之间的递归关联关系,可以使用models.ForeignKey('self')
ManyToManyField: 用来实现多对多的关联关系
DateField: 日期时间字段
EmailField: email字段,用来检查email地址是否合法
ImageField: 图片字段,用来定义图片上传和图片检查,需要安装pillow库

字段选项:

unique: 设置为True, 则表示这个字段必须有唯一值,这是从数据库级别来强制数据唯一,后面我们还会介绍通过form验证来确保数据输入的唯一
verbose_name:
blank: 默认值是False, 设置为True,则该字段允许为空
null: 默认值是False,如果为True,Django会在数据库中将空值转存为NULL
choices: 是一个可迭代结构(元祖),每个元组中的第一个元素,是存储在数据库中的值;第二个元素是使人容易理解的描述。

on_delete :在django2.0版本以前,定关联字段时,on_delete选项并不是必须的,而在django2.0版本后,在定义关联字段时on_delete是必须要定义的,常用参数如下:

on_delete=models.CASCADE,     # 删除关联数据,与之关联也删除
on_delete=models.DO_NOTHING,  # 删除关联数据,什么也不做
on_delete=models.PROTECT,     # 删除关联数据,引发错误ProtectedError
on_delete=models.SET_NULL,    # 删除关联数据,与之关联的值设置为null
on_delete=models.SET_DEFAULT, # 删除关联数据,与之关联的值设置为默认值

需要注意的是在使用SET_NULL的时候,该字段在模型定义的时候需要设置可为空,例如:

user = models.ForeignKey(User, on_delete=models.SET_NULL, blank=True, null=True)

同样在使用SET_DEFAULT的时候,需要预先定义default:

user = models.ForeignKey(User, on_delete=models.SET_DEFAULT, default='默认值')

更多字段类型和字段选项请参考:
https://docs.djangoproject.com/en/1.11/ref/models/fields/#model-field-types

2 用户认证和访问限制

用户登录认证的需求如下:

  • 用户登陆系统才可以访问某些页面,
  • 如果用户没有登陆而直接访问就会跳转到登陆界面,
  • 用户在跳转的登陆界面中完成登陆后,自动访问跳转到之前访问的地址,
  • 用户可以使用用户名、手机号码或者其他字段作为登陆用户名。

在pycharm中,选中sandboxMP/apps/system,右键,选择 New → Python File, 在弹出的窗口输入名称:views_user,在刚创建的页面中导入需要的模块:

from django.shortcuts import render
from django.views.generic.base import View
from django.http import HttpResponseRedirect
from django.contrib.auth import authenticate, login, logout
from django.urls import reverse

说明: 以下创建的视图,都是写在sandboxMP/apps/system/views_user.py文件中

2.1 创建index页面视图

index页面视图,是本项目创建的第一个视图:

class IndexView(View):

    def get(self, request):
        return render(request, 'index.html')

知识点介绍:

1、视图: Django官方文档对“视图”的介绍是用来封装处理用户请求和返回响应的逻辑。
我们可以定义视图函数,用来接受Web请求并返回Web响应,也可以使用基于类的视图对象,本项目的视图实现都是基于类创建的视图,和基于函数的视图相比据有一定的区别和优势:

  • 可以通过单独的方法编写与HTTP方法相关的代码(GET, POST等),无需通过条件分支来判断HTTP方法
  • 可将代码分解成可重用的组件,例如Mixin(多继承),发挥面向对象技术优势,使用更加灵活,易于扩展

2、render函数: Django的快捷函数,结合给定的模板和一个给定的上下文字典,并返回一个渲染后的HttpRespose对象,语法:render(request, template_name, context=None, content_type=None, status=None, using=None),其中 request 和template_name必须参数,其它为可选参数。

2.2 创建用户登陆视图

在创建用户登陆视图前,先创建一个sandboxMP/apps/system/forms.py文件,用来做登陆用户的输入验证,内容如下:

from django import forms


class LoginForm(forms.Form):
    username = forms.CharField(required=True, error_messages={"requeired": "请填写用户名"})
    password = forms.CharField(required=True, error_messages={"requeired": "请填写密码"})

创建用户登陆视图:

from .forms import LoginForm


class LoginView(View):

    def get(self, request):
        if not request.user.is_authenticated:
            return render(request, 'system/users/login.html')
        else:
            return HttpResponseRedirect('/')

    def post(self, request):
        redirect_to = request.GET.get('next', '/')
        login_form = LoginForm(request.POST)
        ret = dict(login_form=login_form)
        if login_form.is_valid():
            user_name = request.POST['username']
            pass_word = request.POST['password']
            user = authenticate(username=user_name, password=pass_word)
            if user is not None:
                if user.is_active:
                    login(request, user)
                    return HttpResponseRedirect(redirect_to)
                else:
                    ret['msg'] = '用户未激活!'
            else:
                ret['msg'] = '用户名或密码错误!'
        else:
            ret['msg'] = '用户和密码不能为空!'
        return render(request, 'system/users/login.html', ret)

知识点介绍:
Django使用会话和中间件来拦截认证系统中的请求对象。它们在每一个请求上提供一个request.user属性,表示当前的用户。如果当前的用户没有登入,该属性将设置成AnonymousUser的一个实例,否则将会是User实例。
1、request.user.is_authenticated: 用来判断用户是否登入,如LoginView中:

 # 当用户访问登陆页面时,判断用户如果未登入则访问登陆页面,如果登入则跳转到首页
if not request.user.is_authenticated:
    return render(request, 'system/users/login.html')
else:
    return HttpResponseRedirect('/')

2、is_valid(): Forms实例的一个方法,用来做字段验证,当输入字段值合法时,它将返回True,同时将表单的数据存放到cleaned_data属性中。
3、authenticate(request=None, **credentials): 用来认证用户,credentials为关键字参数,默认为username和password,如果通过认证后端检查,则返回一个User对象。
4、login(request, user, backend=None): 用来从视图中登陆一个用户,同时将用户的ID保存在session表中。注意:在调用login()之前必须使用authenticate()成功认证这个用户。
5、HttpResponseRedirect[source]: 用来重定向访问,参数是重定向的地址,可以是完整的URL,也可以相想读与项目的绝对路径。

2.3 创建用户登出视图

class LogoutView(View):

    def get(self, request):
        logout(request)
        return HttpResponseRedirect(reverse('login'))

知识点介绍:
1、logout(request): 登出用户。
2、reverse(viewname): 根据url name来进行url的反向解析。

2.4 配置用户URL路由

想要通过URL来访问视图应用,还需要配置URL路由,修改sandboxMP/sandboxMP/urls.py:

from django.contrib import admin
from django.urls import path

from system.views_user import IndexView, LoginView, LogoutView

urlpatterns = [
    path('admin/', admin.site.urls),
    path('', IndexView.as_view(), name='index'),
    path('login/', LoginView.as_view(), name='login'),
    path('logout/', LogoutView.as_view(), name='logout'),
]

2.5 创建认证用户

在pycharm选择 Tools,点击Run manage.py Task..., 在打开的窗口中输入createsuperuser,根据提示输入用户名,邮箱和密码,操作过程如下:

manage.py@sandboxMP > createsuperuser
"C:\Program Files\JetBrains\PyCharm2017.3.2\bin\runnerw.exe" C:\Users\RobbieHan\Envs\sandboxMP\Scripts\python.exe "C:\Program Files\JetBrains\PyCharm2017.3.2\helpers\pycharm\django_manage.py" createsuperuser D:/ProjectFile/sandboxMP
用户名:  admin
邮箱:  robbie_han@outlook.com
Warning: Password input may be echoed.
Password:  !qaz@wsx
Warning: Password input may be echoed.
Password (again):  !qaz@wsx
Superuser created successfully.

运行项目,访问系统:http://127.0.0.1:8000,我们并没有登入用户,直接可以访问首页,这和我们的要求不符。接下来实现页面访问限制,要求必须登入用户才能访问。

2.6 页面访问限制

页面访问限制的实现需求:

  • 用户登录系统才可以访问某些页面
  • 如果用户没有登陆而直接访问就会跳转到登陆界面
  • 用户在跳转的登陆页面完成登陆后,自动访问跳转前的访问地址

新建sandboxMP/apps/system/mixin.py,写入如下内容:

from django.contrib.auth.decorators import login_required


class LoginRequiredMixin(object):
    @classmethod
    def as_view(cls, **init_kwargs):
        view = super(LoginRequiredMixin, cls).as_view(**init_kwargs)
        return login_required(view)

修改sandboxMP/sandboxMP/settings.py, 加入LOGIN_URL

LOGIN_URL = '/login/'

需要登入用户才能访问的视图,只需要继承LoginRequiredMixin即可,修改后的IndexView视图如下:

from .mixin import LoginRequiredMixin

class IndexView(LoginRequiredMixin, View): 

    def get(self, request):
        return render(request, 'index.html')

注意:LoginRequiredMixin位于继承列表最左侧位置

重启项目,我们再次访问首页,打开浏览器,输入http://127.0.0.1:8000,这时我们会发现,浏览器中的URL会变成:http://127.0.0.1:8000/login/?next=/, 需要我们先登陆后才会跳转到首页。
使用我们在2.5小节中创建的用户:admin,密码: !qaz@wsx登陆系统

2.7 媒体文件的访问

尽管在创建用户时设置了默认头像,并且已经放置了默认头像使用的图片,但是用户登录后还是无法显示头像,所以还需要配置媒体文件的访问。

Django实战1-权限管理功能实现-03:用户认证

媒体文件是由用户上传的文件,路径是变化的,比如用户上传的头像文件。
设置文件上传目录

修改sandboxMP/sandboxMP/settings.py文件,添加如下配置:

MEDIA_URL = '/media/'

MEDIA_ROOT = os.path.join(BASE_DIR, 'media')

打开sandboxMP/sandboxMP/urls.py,新增如下配置:

from django.conf import settings
from django.urls import re_path
from django.views.static import serve

if settings.DEBUG:
    urlpatterns += [
        re_path(r'^media/(?P<path>.*)$', serve, {"document_root": settings.MEDIA_ROOT}),
    
    ]

刷新页面就可以看到用户头像了,需要注意的是,这里之所以使用if settings.DEBUG,是因为这种配置模式应该仅限用于开发模式,在生产环境应该通过web前

上一篇:rbac组件之数据库设计(一)


下一篇:django models 自定义用户表替换系统默认表