Django實戰1-許可權管理功能實現-03:使用者認證

sandbox_im發表於2018-11-12

1 許可權管理模型設計

1.1 建立app

新建一個app, 名稱叫system包含使用者管理、選單管理和許可權管理等系統基礎模組。

  • 使用pycharm開啟我們的專案,右鍵專案根目錄,選擇 New → Python Package, 在彈出的視窗輸入apps,這個包就用來存放專案中建立的所有app.
  • 選擇pycharm上方Tools,點選Run manage.py Task..., 這時在pycharm下方會開啟一個視窗,輸入startapp system 回車建立app, 如下圖:

image

  • 將剛剛建立的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='預設值')
複製程式碼

更多欄位型別和欄位選項請參考:
docs.djangoproject.com/en/1.11/ref…

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(): Form實力的一個方法,用來做欄位驗證,當輸入欄位值合法時,它將返回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 媒體檔案的訪問

儘管在建立使用者時設定了預設頭像,並且已經放置了預設頭像使用的圖片,但是使用者登入後還是無法顯示頭像,所以還需要配置媒體檔案的訪問。

image

媒體檔案是由使用者上傳的檔案,路徑是變化的,比如使用者上傳的頭像檔案。
設定檔案上傳目錄

修改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前端來處理這些媒體檔案的訪問。

最新最全文件釋出在知識星球,可以通過微信搜尋公眾號“知識星球”,直接回復"52824366"獲得訪問入口
本節文件對應原始碼版本: github.com/RobbieHan/s…

非常歡迎感興趣的朋友,到我的Github或掘金上做客,閒暇之餘給個贊或Star,贈人玫瑰手留餘香
文件配套專案地址:github.com/RobbieHan/s…
輕量級辦公管理系統專案開源地址:github.com/RobbieHan/g…

相關文章