后臺學習——django(2)

經過對django的初步學習,我們已經對后臺的基本流程以及django的運作有了一定的了解,但是這還不足夠,django還有許多方法和API需要我們詳細滴學習,是時候開始進階學習了。
上期文章:后臺學習——django(1)

零、上篇文章修改

  1. 靜態文件引用修改
  • 在上篇文章就說過,要盡可能滴把前端跟后臺分開,不要前后端代碼混在一起,我們看回之前的server/learning/templates/index.html
    <!doctype html>
    <html lang="en">
    <head>
    <meta charset="UTF-8" />
    <title>登錄注冊系統</title>
    {% load staticfiles %} # 這里
    <link rel="stylesheet" type="text/css" href='{% static "css/index.css" %}' />
    </head>
    <body>
    <div>
    <form action="login_register" method="post">
    {% csrf_token %}# 還有這里
    <table>
    <tr>
    <th>帳號:</th>
    <td><input type="text" id="username" name="username" maxlength="20"/></td>
    </tr>
    <tr>
    <th>密碼:</th>
    <td><input type="password" id="password" name="password" maxlength="20"/></td>
    </tr>
    <tr>
    <th></th>
    <td>
    <label>
    <input type="radio" name="way" value="login" checked="checked"/>登錄
    </label>
    <label>
    <input type="radio" name="way" value="register"/>注冊
    </label>
    </td>
    </tr>
    <tr>
    <th></th>
    <td><input type="submit" id="submit" value="提交" onclick="return check()"/></td>
    </tr>
    </table>
    </form>
    </div>
    <script type="text/javascript">
    function check(){
    var username = document.getElementById('username');
    var password = document.getElementById('password');
    if(username.value == ''){
    alert('帳號不能為空,請重新輸入');
    username.select();
    }else if(password.value == ''){
    alert('密碼不能為空,請重新輸入');
    password.select();
    }else{
    return true;
    }
    return false;
    }
    </script>
    </body>
    </html>
  • 可以看到,里面還是存在了兩段混入了后臺代碼了,其中一個就是靜態文件引入所用到了{% load staticfiles %},實際上,我們可以把這句代碼刪掉,將引入鏈接的部分<link rel="stylesheet" type="text/css" href='{% static "css/index.css" %}' />修改成
    <link rel="stylesheet" type="text/css" href="/static/css/index.css"/>,你們可以打開服務器再次看一下頁面,實際效果是一樣的
  • 至于{% csrf_token %}這句代碼,我也沒有什么好的辦法解決,要不就不要用django的防止CSRF模式攻擊功能,要不就換一種傳輸方式,例如ajax,但是這樣做就遠離了初衷了,還是不要改吧,到時候后臺工程師自己修改好了,反正也就是添加一句話的事

一、進階學習——驗證碼

在一個正常的登錄系統中,驗證碼是非常重要的,用于識別人機,畢竟我們都知道,這個世界中存在著萬惡的爬蟲,驗證碼有很多種方式,有圖片的,有郵件的,有短信的,有拼圖的,不管什么樣的驗證碼,目的都是驗證訪問用戶到底是人還是機器,要對機器say no,接下來我們要實踐一個圖片性的驗證碼。

  1. URL拓展
  • 還記得URL設置文件urls.py里面,匹配路徑的是用正則表達式的么,學過正則表達式的應該會知道分組吧,其實在路徑匹配的正則表達式那里使用分組,django還可以將之匹配出來當作參數給接口函數,不多說先試試效果
  • 假設要通過URL直接傳遞兩個參數,返回兩個數相加的結果
    • 先改server/server/urls.py文件
      # -- coding:utf-8 --
      from django.conf.urls import url
      from django.contrib import admin
      from learning import views as learning
      from django.conf.urls.static import static
      from django.conf import settings
      urlpatterns = [
      url(r'^admin/', admin.site.urls),
      url(r'^$', learning.index),
      # 通過正則分組匹配兩個相加的數字
      url(r'^add/(\d+)/(\d+)/$', learning.add),
      ]
      urlpatterns+=static(settings.STATIC_URL,document_root=settings.STATIC_ROOT)
      這樣我們可以通過訪問例如localhost:8000/add/3/4/的網址(后面兩個數字可以換),來實現直接傳參的效果
    • 再在server/server/views.py添加接口函數
      def add(request, a, b):
      return HttpResponse(str(int(a) + int(b)))
      沒錯,括號里面的數字會當作參數直接就傳到接口函數里面去了,不過這些參數都是字符串,如果是要當作數字或者其他類型使用的時候,記得要轉換類型
    • 再次懷著激動的心情開啟服務器(為啥要說再次???),輸入網址localhost:8000/add/4/5/
    • 換個數字再試一遍localhost:8000/add/200/300/

      Perfect!!!
  1. 下一步我們就要開始構造驗證碼函數了,也就是返回一張驗證碼圖片的函數,怎么建呢?我們又沒有驗證碼圖片在?沒有就直接畫出來唄,python擁有一個庫pillow專門用于畫圖的,安裝命令pip install pillow
  • 首先還是要添加路由,在server/server/urls.py中添加下面的代碼
    url(r'^verify/(\d+)/(\d+)/', learning.verify)
    大家可以看得到這里用到了上面URL拓展的知識,為的就是使得這驗證碼函數可以得到重用的機會,那兩個參數就是寬度width和高度height,這樣的話以后要用到寬度或者高度不同的驗證碼都可以使用這個函數了
  • 下一步我們就要開始準備我們的畫筆構造我們的驗證碼了
    • 使用pillow畫圖,第一步就是創建一張畫布,有了畫布我們才可以在上面畫畫對不,創建畫布代碼如下
      # 創建畫布需要導入Image
      from PIL import Image
      # 用到了Imagenew函數
      # 第一個參數是顏色通道,這里使用了RGB通道,還有其他的一些通道,如CMYK之類的,但不用管
      # 第二個參數是由寬高組成的元組,數字
      # 第三個參數是圖片的背景色,這里用rgb的顏色顯示,例如( 255, 255, 255),注意這是元組
      img = Image.new('RGB', (width, height), bgColor)
    • 有了畫布,但我們用什么來畫圖,(難道是。。。畫筆?),廢話,當然是,來,給你支畫筆
      # 創建畫筆需要導入ImageDraw
      from PIL import ImageDraw
      # 用到了ImageDrawDraw函數
      # 有且只有一個參數,就是之前創建的畫布
      draw = ImageDraw.Draw(img)
    • 有了畫布和畫筆,但驗證碼中是有字的,難道我們真的要像現實一樣一筆一劃滴寫字么,而且用電腦寫好難看啊,其實pillow還可以導入電腦的字體,你不用再想些亂七八糟的東西了
      # 導入字體需要導入ImageFont
      from PIL import ImageFont
      # 用到了ImageFonttruetype函數,可以自動查詢電腦中的字體
      # 第一個參數是字體名字
      # 第二個參數是字體大小
      # 注意這個是windows系統下默認的字體,其他系統自己找
      font = ImageFont.truetype('arial.ttf', size)
  • 好了,畫布畫筆字體我們都有了,那我們開畫吧,該畫些什么呢。。。。。。。
    • 不想那么多,反正驗證碼總該有字吧,我們研究怎么寫字
      # 寫字需要使用drawtext方法
      # 第一個參數是一個坐標軸元組,分別是距離左邊和上邊的距離
      # 第二個參數是要寫的字(字符串)
      # 后面的兩個參數分別是字體和字體顏色
      draw.text((x, y), text, font=font, fill=textColor)
    • 還有可能要一些干擾元素,例如線條
      # 畫線條需要使用drawline方法
      # 第一個參數是包含了兩個坐標的元組,分別是線條一頭一尾的坐標
      # 后面的參數是線條的顏色
      draw.line((x1, y1, x2, y2), fill=lineColor)
    • 還想畫些什么,算了吧,畫那么復雜干嘛


  • 一切都準備就緒了,開工寫接口函數
    # 導入繪圖對象
    from PIL import Image, ImageFont, ImageDraw
    # 導入隨機函數,randint:生成固定范圍內的隨機整數
    from random import randint
    def verify(request, width, height):
    wordsCount = 4# 驗證碼中的字符長度
    width = int(width)# 圖片寬度
    height = int(height)# 圖片高度
    size = int(min(width/wordsCount, height)/1.5)# 字體大小設置
    bgColor = (randint(200, 255), randint(200, 255), randint(200, 255))# 隨機背景色(淺色)
    img = Image.new('RGB', (width, height), bgColor)# 創建圖像
    font = ImageFont.truetype('Arial.ttf', size)# 導入字體
    draw = ImageDraw.Draw(img)# 創建畫筆
  • 等等,我們要怎么返回生成的圖片????
    • 我們要知道,網頁間的傳輸都是字符串的傳輸,并沒有其他數據結構,所以不管你要傳輸什么,都要把它轉成字符串
    • 圖像的字符串形式實際上就是二進制數字
    • pillow中并沒有直接返回圖像二進制的功能
    • 實際上有個笨方法就是利用Image對象的save方法保存到本地,然后讀取本地文件返回
    • 但是每一次請求都要在本地存儲驗證碼圖片,會耗費大量的內存空間滴,而且這驗證碼用過一次就沒用了
    • 為了應付這需求,python有個內置模塊StringIO,可以將圖片緩存到內存里面,讀取后就清空內存,是不是很爽
    • 廢話不多說,讓我們看看要怎么弄
      # 導入StringIO模塊
      from StringIO import StringIO
      # 建立一個緩存對象
      mstream = StringIO()
      # 將圖片保存到內存中
      img.save(mstream, 'jpeg')
      # 返回內存中的圖片
      return HttpResponse(mstream.getvalue(), 'image/jpeg')
  • 還有還有,我們要畫一個什么樣驗證碼,總該定好一些規則吧
    • 規則一:均勻繪畫字符,居中
    • 規則二:字符顏色要比較深
    • 規則三:要有線條雪花等干擾元素
    • 規則四:一切能隨機的都隨機
  • 我***,那么多規則,怎么畫??!不管了不管了,先想一下文字怎么畫吧,這個是重點。然而,pillow里面是沒有旋轉文字這東西的,就只有圖像本身的旋轉
    • 首先要確定用什么字符,當然是數字加大小寫字母啦
      text = '1234567890abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ'
    • 其次要確定文字的位置,這里需要一定的數學知識(加減乘除,別說你不會)
      left = width * i / num + (width / 4 - size) / 2# i為第幾個文字
      top = (height - size) / 2
    • 還要確定文字的顏色,要隨機的顏色,顏色要比較深
      textColor = (randint(0, 160), randint(0, 160), randint(0, 160))
    • 合起來就是這樣滴
      text = '1234567890abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ'
      for i in range(wordsCount):
      textColor = (randint(0, 160), randint(0, 160), randint(0, 160))
      left = width * i / wordsCount + (width / 4 - size) / 2
      top = (height - size) / 2
      draw.text((left, top), text[randint(0, len(text) - 1)], font=font, fill=textColor)
  • 再畫雪花,話說雪花是什么東東,呵呵,就是白色的*嘛~~~
    • 顏色:白色
      textColor = (255, 255, 255)
    • 位置:隨機
      left = randint(0, width)
      top = randint(0, height)
    • 合起來:
      for i in range(30):
      textColor = (255, 255, 255)
      left = randint(0, width)
      top = randint(0, height)
      draw.text((left, top), '*', font=font, fill=textColor)
  • 最后畫線條,這個簡單
    • 位置:頭尾都隨機
      line = (randint(0, width), randint(0, height), randint(0, width), randint(0, height))
    • 顏色:隨機
      linecolor = (randint(0, 160), randint(0, 160), randint(0, 160))
    • 合起來:
      for i in range(5):
      linecolor = (randint(0, 160), randint(0, 160), randint(0, 160))
      line = (randint(0, width), randint(0, height), randint(0, width), randint(0, height))
      draw.line(line, fill=linecolor)
  • OK,把所有的步驟都合起來就是結果了
    # -- coding:utf-8 --
    import sys
    reload(sys)
    sys.setdefaultencoding('utf-8')
    from django.http import HttpResponse
    from PIL import Image, ImageFont, ImageDraw
    from StringIO import StringIO
    from random import randint
    def verify(request, width, height):
    wordsCount = 4
    width = int(width)
    height = int(height)
    size = int(min(width / wordsCount, height) / 1.3)
    bgColor = (randint(200, 255), randint(200, 255), randint(200, 255))
    img = Image.new('RGB', (width, height), bgColor)
    font = ImageFont.truetype('arial.ttf', size)
    draw = ImageDraw.Draw(img)
    text = '1234567890abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ'
    for i in range(wordsCount):
    textColor = (randint(0, 160), randint(0, 160), randint(0, 160))
    left = width * i / wordsCount + (width / 4 - size) / 2
    top = (height - size) / 2
    draw.text((left, top), text[randint(0, len(text) - 1)], font=font, fill=textColor)
    for i in range(30):
    textColor = (255, 255, 255)
    left = randint(0, width)
    top = randint(0, height)
    draw.text((left, top), '*', font=font, fill=textColor)
    for i in range(5):
    linecolor = (randint(0, 160), randint(0, 160), randint(0, 160))
    line = (randint(0, width), randint(0, height), randint(0, width), randint(0, height))
    draw.line(line, fill=linecolor)
    del draw
    mstream = StringIO()
    img.save(mstream, 'jpeg')
    return HttpResponse(mstream.getvalue(), 'image/jpeg')
  • 保存,打開服務器,打開網址localhost:8000/verify/100/40/,你會看到這樣的一張圖片
  • 刷新一下


  • 換個網址localhost:8000/verify/200/40/
    Paste_Image.png
  • 非常完美不是么
  1. 然而的然而,你還沒完成呢??!只有驗證碼圖片卻不能驗證有什么用??!
  • 要起到驗證的效果首先就要保存相應的文字,那么就要在寫文字那里保存相應的文字verifyText

  • 然后然后還要將之保存在session里面
    request.session['verify'] = verifytext

  • 這個函數基本完成了,但一個完整的驗證碼系統還是不夠的,我們來完善以下,接下來由于個人問題,實在不想看到之前的代碼了,會將之前沒用的清掉,我會把變動的文件代碼全部給出(這算做潔癖么,還是強迫癥)

  • 更改后的文件目錄
    server
    ├────learning
    | ├────migrations
    | | └──init.py
    | ├────static
    | ├────templates
    | | └──index.html
    | ├────init.py
    | ├────admin.py
    | ├────apps.py
    | ├────models.py
    | ├────tests.py
    | └────views.py
    ├────server
    | ├────init.py
    | ├────settings.py
    | ├────urls.py
    | └────wsgi.py
    ├────db.sqlite3
    └────manage.py

  • server/server/urls.py文件:
    # -- coding:utf-8 --
    from django.conf.urls import url
    from django.contrib import admin
    from learning import views as learning
    from django.conf.urls.static import static
    from django.conf import settings

       urlpatterns = [
           url(r'^admin/', admin.site.urls),
           url(r'^$', learning.index),
           url(r'^verify/(\d+)/(\d+)/$', learning.verify),
           url(r'^check/$', learning.check),
       ]
       urlpatterns+=static(settings.STATIC_URL,document_root=settings.STATIC_ROOT)
    
  • server/learning/templates/index.html文件:
    <!doctype html>
    <html lang="en">
    <head>
    <meta charset="UTF-8" />
    <title>Document</title>
    </head>
    <body>
    <form action="/check/" method="post">
    {% csrf_token %}
    <img src="verify/300/80/" /><br />
    <input type="text" name="verify" id="verify"/>
    <input type="submit"/>
    </form>
    </body>
    </html>
    這個html文件通過verify/300/80/顯示一張300x80的驗證碼,點擊提交按鈕后提交到/check/

  • server/learning/views.py文件:
    # -- coding:utf-8 --
    import sys
    reload(sys)
    sys.setdefaultencoding('utf-8')
    from django.shortcuts import render
    from django.http import HttpResponse
    from PIL import Image, ImageFont, ImageDraw
    from StringIO import StringIO
    from random import randint

       # 頁面接口:返回`index.html`頁面
       def index(request):
           return render(request, 'index.html')
    
       # 功能接口:返回驗證碼輸入正確與否(忽略大小寫)
       def check(request):
           if request.POST['verify'].lower() == request.session['verify'].lower():
               return HttpResponse('success')
           else:
               return HttpResponse('failed')
    
       # 功能接口:返回驗證碼圖片
       def verify(request, width, height):
           wordsCount = 4
           width = int(width)
           height = int(height)
           size = int(min(width / wordsCount, height) / 1.3)
           bgColor = (randint(200, 255), randint(200, 255), randint(200, 255))
           img = Image.new('RGB', (width, height), bgColor)
           font = ImageFont.truetype('arial.ttf', size)
           draw = ImageDraw.Draw(img)
           text = '1234567890abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ'
           verifytext = ''
           for i in range(wordsCount):
               textColor = (randint(0, 160), randint(0, 160), randint(0, 160))
               left = width * i / wordsCount + (width / 4 - size) / 2
               top = (height - size) / 2
               word = text[randint(0, len(text) - 1)]
               verifytext += word
               draw.text((left, top), word, font=font, fill=textColor)
           for i in range(30):
               textColor = (255, 255, 255)
               left = randint(0, width)
               top = randint(0, height)
               draw.text((left, top), '*', font=font, fill=textColor)
           for i in range(5):
               linecolor = (randint(0, 160), randint(0, 160), randint(0, 160))
               line = (randint(0, width), randint(0, height), randint(0, width), randint(0, height))
               draw.line(line, fill=linecolor)
           del draw
           mstream = StringIO()
           img.save(mstream, 'jpeg')
           request.session['verify'] = verifytext
           return HttpResponse(mstream.getvalue(), 'image/jpeg')
    
  1. OK,開始驗證成果
  • 打開網址localhost:8000,顯示頁面如下(驗證碼圖片可能不同):
  • 輸入正確的驗證碼


  • 點擊提交后,顯示success
  • 刷新頁面localhost:8000,重復上面的步驟,輸入錯誤的驗證碼,顯示failed
  • 到此,一個完整的驗證碼就完成了,如果你有更好的想象力,還可以畫出更好的驗證碼,深究請查閱pillow文檔

二、進階學習——用戶管理系統

  1. 先不說那么多,先清空兩個文件先(真的是強迫癥),分別是:
  • server/learning/admin.py文件:
    from django.contrib import admin
    # Register your models here.
  • server/learning/models.py文件:
    from future import unicode_literals
    from django.db import models
    # Create your models here.
  • 清完同步數據庫:
    python manage.py makemigrations
    python manage.py migrate
  1. 再修改server/learning/templates/idnex.html,用于應用相關接口函數:
    <!doctype html>
    <html lang="en">
    <head>
    <meta charset="UTF-8" />
    <title>登錄注冊系統</title>
    </head>
    <body>
    <div>
    <form action="check/" method="post">
    {% csrf_token %}
    <table>
    <tr>
    <th>帳號:</th>
    <td><input type="text" id="username" name="username" maxlength="20" /></td>
    </tr>
    <tr>
    <th>密碼:</th>
    <td><input type="password" id="password" name="password" maxlength="20" /></td>
    </tr>
    <tr>
    <th>驗證碼</th>
    <td>
    <input type="text" id="verify" name="verify" maxlength="4"/>
    <img src="verify/100/30/" alt="" />
    </td>
    </tr>
    <tr>
    <th></th>
    <td>
    <label><input type="radio" name="way" value="login" checked="checked"/>登錄</label>
    <label><input type="radio" name="way" value="register"/>注冊</label>
    </td>
    </tr>
    <tr>
    <th></th>
    <td><input type="submit" id="submit" value="提交" onclick="return check()" /></td>
    </tr>
    </table>
    </form>
    </div>
    <script type="text/javascript">
    function check() {
    var username = document.getElementById('username');
    var password = document.getElementById('password');
    var verify = document.getElementById('verify');
    if(username.value == '') {
    alert('帳號不能為空,請重新輸入');
    username.select();
    } else if(password.value == '') {
    alert('密碼不能為空,請重新輸入');
    password.select();
    } else if(verify.value.length != 4){
    alert('驗證碼輸入錯誤,請重新輸入');
    verify.select();
    } else {
    return true;
    }
    return false;
    }
    </script>
    </body>
    </html>
  • 打開服務器,進入網址localhost:8000,結果如下:
  • 很難看,不過將就一下吧


  1. 修改server/learning/views.py文件中的check函數,并添加兩個空函數:
    # check函數的作用已經在之前涉及過了,這里就不講了
    def check(request):
    if request.POST['verify'].lower() == request.session['verify'].lower():
    username = request.POST['username']
    password = request.POST['password']
    if request.POST['way'] == 'login':
    return HttpResponse(login(request, username, password))
    elif request.POST['way'] == 'register':
    return HttpResponse(register(request, username, password))
    else:
    return HttpResponse('驗證碼錯誤')

    def login(request, username, password):
        pass
    
    def register(request, username, password):
        pass
    
  2. OK,終于準備好所有東西了,我們開始研究django的用戶認證系統吧

  • 用戶的基本操作一共有四個,分別是:
    • 注冊
    • 登錄
    • 登出
    • 判斷是否已經登錄
  • django封裝了一個比較完善的用戶認證系統,各種操作都非常方便,注冊便是如此
    • 添加一個用戶只需用到下面的代碼:
      from django.contrib.auth.models import User
      # username為用戶名,password為密碼
      User.objects.create_user(username=username, password=password).save()
    • 然而注冊需要考慮到一種狀況:用戶已存在,其實如果是這樣的話,在創建的時候就會報錯,做好錯誤處理就好
      try:
      User.objects.create_user(username=username, password=password).save()
      return '注冊成功'
      except:
      return '已存在用戶'
  • 有了注冊好的用戶,自然需要登錄:
    • 登錄之前肯定要驗證帳號密碼是否正確啦
      from django.contrib.auth import authenticate
      # username為用戶名,password為密碼
      # 該函數會返回一個user對象,如果不存在該用戶或者密碼錯誤則返回None
      authenticate(username=username, password=password)
    • 驗證正確自然要儲存登錄信息,通常而言都會用session來存儲,而django對此方法進行了包裝,只需使用以下代碼就行了:
      from django.contrib.auth import login
      # request為請求對象,user為用戶對象
      login(request, user)
  • 登錄后,怎么登出呢,其實這個更簡單,其他后臺或許直接清掉session就可以了,django也一樣將其封裝好了:
    from django.contrib.auth import logout
    # request為請求對象
    logout(request)
  • 最后一個,判斷是否已經登錄,這個django用request對象中的user對象中的is_authenticated()方法(此處應該有哭笑不得的表情)
    # 用戶登錄時,該函數返回True,否則返回False
    request.user.is_authenticated()
  1. OK,一切都準備就緒后就可以寫之前那兩個函數了,代碼如下:
    from django.contrib.auth.models import User
    from django.contrib import auth

     def login(request, username, password):
         user = auth.authenticate(username=username, password=password)
         if user is None:
             return '登錄錯誤'
         else:
             auth.login(request, user)
             return '登錄成功'
    
     def register(request, username, password):
         try:
             User.objects.create_user(username=username, password=password).save()
             # 注冊后默認為登錄狀態
             login(request, username, password)
             return '注冊成功'
         except:
             return '已存在用戶'
    
  • 大家可以打開服務器試一下,分別注冊新的用戶,注冊已經存在相同用戶名的用戶,登錄帳號密碼正確的用戶, 登錄不存在的用戶,登錄密碼錯誤的用戶,是不是都如想象中一般返回了想要返回的信息?是的話就恭喜你完成了登錄和注冊的操作了
  1. 然而還沒有完呢,登錄注冊完總該有個用戶界面吧,在用戶界面總該有登出的設置吧,訪問用戶界面時總該有確認是否已經登錄了吧
  • 為了有一個更好的應用場景,這里需要添加一個html文件server/learning/templates/user.html,內容如下:
    <!doctype html>
    <html lang="en">
    <head>
    <meta charset="UTF-8" />
    <title>user</title>
    </head>
    <body>
    <script type="text/javascript">
    username = location.search.match(/username=(.*?)(&|$)/);
    if(username) {
    document.write('歡迎' + username[1] + '的到來<a href="/logout/">注銷</a>');
    }
    </script>
    </body>
    </html>
    現在的目錄如下:
    server
    ├────learning
    | ├────migrations
    | | └──init.py
    | ├────static
    | ├────templates
    | | ├──index.html
    | | └──user.html
    | ├────init.py
    | ├────admin.py
    | ├────apps.py
    | ├────models.py
    | ├────tests.py
    | └────views.py
    ├────server
    | ├────init.py
    | ├────settings.py
    | ├────urls.py
    | └────wsgi.py
    ├────db.sqlite3
    └────manage.py

  • 還要修改一下server/server/urls.py文件,接上對應的接口函數:
    urlpatterns = [
    url(r'^admin/', admin.site.urls),
    url(r'^$', learning.index),
    url(r'^user$', learning.user),
    url(r'^verify/(\d+)/(\d+)/$', learning.verify),
    url(r'^check/$', learning.check),
    url(r'^logout/$', learning.logout)
    ]

  • 最后寫上各個接口函數就搞定啦,具體細節看注釋:
    # -- coding:utf-8 --
    import sys
    reload(sys)
    sys.setdefaultencoding('utf-8')
    from django.shortcuts import render
    # 這里新導入了一個HttpResponseRedirect函數,用于頁面的重定向
    from django.http import HttpResponse,HttpResponseRedirect
    from PIL import Image, ImageFont, ImageDraw
    from StringIO import StringIO
    from random import randint
    from django.contrib.auth.models import User
    from django.contrib import auth

       def index(request):
           return render(request, 'index.html')
    
       def user(request):
           # 使用request.user.is_authenticated()來判斷是否已經登錄
           if request.user.is_authenticated():
               return render(request, 'user.html')
           else:
               # 如果沒有登錄的話自動重定向到首頁
               return HttpResponseRedirect('/')
    
       def logout(request):
           # 登出設置,并重定向到首頁
           auth.logout(request)
           return HttpResponseRedirect('/')
    
       def check(request):
           if request.POST['verify'].lower() == request.session['verify'].lower():
               username = request.POST['username']
               password = request.POST['password']
               if request.POST['way'] == 'login':
                   # 這里修改為直接返回函數返回的值
                   return login(request, username, password)
               elif request.POST['way'] == 'register':
                   return register(request, username, password)
           else:
               return HttpResponse('驗證碼錯誤')
    
       def login(request, username, password):
           user = auth.authenticate(username=username, password=password)
           if user is None:
               return HttpResponse('登錄錯誤')
           else:
               auth.login(request, user)
               # 如果登錄成功就自動跳轉到用戶頁面
               return HttpResponseRedirect('/user?username=' + username)
    
       def register(request, username, password):
           try:
               User.objects.create_user(username=username, password=password).save()
               # 剛注冊完肯定可以登錄成功,直接返回用戶頁面
               return login(request, username, password)
           except:
               return HttpResponse('已存在用戶')
       # 驗證碼函數已省略
    

    這里解析一下什么是重定向,其實也就是服務器端的網頁跳轉,重定向后就轉到不同的路徑去了,比如你要訪問a網頁,在地址欄輸入a網頁的URL,但a網頁會重定向到b網頁,那么返回來的是b網頁的信息,而且地址欄也是顯示b網頁的URL(當然這個只是給一臉懵的人看的,想了解更詳細的信息,就看百度百科的解析吧)

  1. 我們來測試一下成果,如果你看到的跟下面的一樣就證明你已經搞定了
  • 打開服務器,進入網址localhost:8000,進入首頁:
  • 注冊一個正確的帳號,自動跳轉到用戶頁面:


  • 點擊注銷,跳轉到首頁:


  • 注冊一個已經存在的用戶,返回錯誤信息:


  • 返回首頁輸入正確的帳號密碼,登錄帳號,自動跳轉到用戶頁面:


  • 注銷返回首頁,登錄不存在的用戶,返回錯誤信息:


  • 返回首頁,登錄密碼錯誤的用戶,返回錯誤信息:


  1. 基本功能通通都實現了,然而還不足夠完善,我們還需要更多的功能
  • 我們可以發現,之前的代碼在出現登錄錯誤的時候是無法分辨是用戶不存在還是密碼錯誤的,而且我們日后肯定會經常需要查詢用戶存不存在的信息,不一定要用戶密碼都正確才可以查詢得到。但是django的用戶認證模塊并區分這兩個錯誤,不過我們可以發現用戶認證是基于django的模型的,也就是之前說的數據庫操作那里,我們可以通過數據庫操作的方法只通過用戶名來查詢是否存在該用戶,登錄接口函數修改如下:
    def login(request, username, password):
    try:
    # 當不存在該用戶時會出現錯誤
    User.objects.get(username=username)
    user = auth.authenticate(username=username, password=password)
    if user is None:
    return HttpResponse('密碼錯誤')
    else:
    auth.login(request, user)
    return HttpResponseRedirect('/user?username=' + username)
    except:
    return HttpResponse('不存在該用戶')
  • 還有,我們以后會有大量的頁面需要登錄用戶才可以訪問,但如果每次都使用request.user.is_authenticated()來判斷會顯得很麻煩,有沒有更好的方法呢?答案是有的,django在設計的時候就已經考慮到這個問題了,它提供了一種裝飾器,只要在需要登錄后訪問的頁面接口函數前加上這個裝飾器就可以自動判斷是否已經登錄了,使用方法如下:
    # 導入裝飾器
    from django.contrib.auth.decorators import login_required
    # 使用裝飾器,在函數前@這個裝飾器
    # 參數login_url為沒有登錄的時候需要跳轉到哪里,這里設置為跳轉到首頁
    # 參數redirect_field_name為重定向時傳輸當前的頁面路徑,這里設置為沒有
    @login_required(login_url='/', redirect_field_name=None)
    def user(request):
    # 不再需要判斷是否已經登錄了
    return render(request, 'user.html')
  • 有時候我們存儲的用戶信息可不單單只有這些(包括username、first_name、last_name、email、password),我們還需要額外添加一些用戶信息,如登錄時間,注冊時間(其實也有滴,不過我就懶得想其他啦,反正原理都一樣)等,但是如果直接更改原本的模型又很麻煩,這個時候就需要使用模型的代理繼承功能了。
    • 模型的代理繼承實際上借用了python類的繼承,修改server/learning/models.py代碼如下:
      from future import unicode_literals
      from django.db import models
      from django.contrib.auth.models import User
      # 繼承與django默認的User模型
      class learningUser(User):
      # 添加兩個字段,分別是登錄日期和注冊日期(自動填入)
      logindate = models.DateField(auto_now=True)
      registerdate = models.DateField(auto_now_add=True)

    • 實際上,這并不是一種正規的寫法(但經驗證,是可以這樣做的),正規的寫法如下:
      class learningUser(models.Model):
      # 設置與User對象一一對應
      user = models.OneToOneField(User)
      logindate = models.DateField(auto_now=True)
      registerdate = models.DateField(auto_now_add=True)
      但是這樣一來引用的時候會顯得很麻煩,創建用戶的時候還要創建多一個對象,而且到時候要獲取用戶的logindate的時候還要用user.learningUser.logindate來獲取,雖然這樣可以動態改變相關model,但如果當前項目中每一個用戶都需要有這個拓展就顯得很麻煩了,所以之后還是以第一種方法來拓展User模型

    • 接下來就是模型改動必要的程序了,記住以后都要這樣做
      修改server/learning/admin.py文件:
      from django.contrib import admin
      from learning.models import learningUser
      admin.site.register(learningUser)
      終端執行代碼:
      python manage.py makemigrations
      python manage.py migrate

    • 最后一步就是修改接口了,將from django.contrib.auth.models import User替換成from learning.models import learningUser
      修改登錄注冊函數為如下:
      def login(request, username, password):
      try:
      # 使用learning_user模型獲取用戶
      guest = learningUser.objects.get(username=username)
      user = auth.authenticate(username=username, password=password)
      if user is None:
      return HttpResponse('密碼錯誤')
      else:
      auth.login(request, user)
      # 輸出我們之前新添加的兩個屬性(懶的在頁面上顯示了)
      print guest.logindate, guest.registerdate
      return HttpResponseRedirect('/user?username=' + username)
      except:
      return HttpResponse('不存在該用戶')

       def register(request, username, password):
           try:
               # 記住也要用learningUser創建對象
               learningUser.objects.create_user(username=username, password=password).save()
               login(request, username, password)
               return HttpResponseRedirect('/user?username=' + username)
           except:
               return HttpResponse('已存在用戶')
      

      這里要提醒的是如果使用的是django用戶認證的函數,他們的操作對象都是最原本的User對象,而不是我們新創建的用戶對象

    • 我們重新注冊一個帳號,在終端可以看到登錄和注冊的日期


  • 如果我們要對用戶進行分級又該怎么辦呢?比如要分管理員和普通用戶(注意這里不是可以進入admin頁面的超級用戶,管理員可以管理用戶,刪除用戶等),或者更負責的分類又怎么辦呢?其實這里涉及到用戶權限還有組的問題,組用于標識用戶的類別(一個用戶可以多個類),權限對應具體的權限,一個組可以對應多個權限。說這么多,不用理解,先看下面就會明白(我會告訴你我也研究了好久么):
    • 首先我們要知道權限是什么,有什么用。權限通俗來講就是你能不能夠做某種事,就像男人不能生孩子(先天性上帝沒有給你權限,除了某個從上帝盜取的權限的人),權限的存在可以很方便滴管理用戶,一些特殊的功能就只能給有權限的用戶使用,而組就是對用戶的一種分類,組包含了多個權限,用戶屬于這個組的話就擁有了這些權限,這樣就不需要很麻煩滴對用戶添加多個權限了,而且分組后還可以更直接滴對用戶進行管理,比如一個班有部分人當選了組長(組),他們可以管理組員,檢查作業、管理組員、組織小組活動等(權限),如果老師需要所有人的名單,可以直接讓組長提供小組名單(對組的管理),這樣一來就不需要一個一個權限的管理了,上升到組的層次。

    • 最近廢話有點多,還是專心研究代碼吧(以下代碼在server/learning/views.py文件寫的)。首先你要懂得怎樣創建一個新的權限:
      # 在django中權限就是一個Permission模型的實例
      from django.contrib.auth.models import Permission
      from django.contrib.contenttypes.models import ContentType
      # 用get_or_create防止重復創建
      newPermission = Permission.objects.get_or_create(**{
      # 創建Permission對象需要三個參數(都是必須的)
      # codename是代碼中使用的名字
      # name是顯示出來的名字
      # content_type要通過創建ContentType對象獲取一個該模型的ContentType實例
      'codename': 'new_permission',
      'name': 'New Permission',
      'content_type': ContentType.objects.get_for_model(learningUser)
      })[0]
      本質上講ContentType實例可以從更高的層次操作模型(這里操作learningUser模型,不過你們都不需要知道啦,現在知道怎么寫就好,重點是codenamename

    • 創建之后總該要添加吧,其實添加很簡單,假設user是一個User的實例(實際上可以是User的繼承模型,如我們之前設置的learningUser模型):
      # 注意所有的newPermission都是Permission的實例對象
      # 設定user對象的權限為列表中的權限
      user.permission = [newPermission1, newPermission2, ...]
      # 向user對象添加權限
      user.permission.add(newPermission1, newPermission2, ...)
      # 移除user對象中的權限
      user.permission.remove(newPermission1, newPermission2, ...)
      # 清空user對象的權限
      user.permission.clear()

    • user對象有了權限,那么我們應該怎么判斷user到底有沒有該權限呢?這時候就需要調用user對象的has_perm()或者has_perms()方法了
      # 還記得之前寫的codename么,判斷權限的時候就需要用到了
      # 格式為:app_name.codename
      user.has_perm('learning.new_permission')
      # 這個是判斷同時擁有多個權限的(傳一個列表或者元組過去)
      user.has_perms(['learning.new_permission'])

    • 像登錄一樣,權限判斷也有相對因的裝飾器在需要權限的函數前面寫上這個裝飾器就可以了
      # 第一個參數是權限(多個權限時,傳遞一個列表)
      @permission_required('learning.new_permission', login_url='/')

    • 權限搞定了,那么組又該怎么做呢?其實組也有相對應的模型Group,每個組都是Group的實例,我們先創建一個:
      newGroup = Group.objects.get_or_create(name='manager')[0]

    • 這個時候,這個組是不沒有任何的權限的,我們要添加權限到組里面,添加方法跟添加到User對象類似:
      # 注意所有的newPermission都是Permission的實例對象
      # 設定newGroup對象的權限為列表中的權限
      newGroup.permission = [newPermission1, newPermission2, ...]
      # 向newGroup對象添加權限
      newGroup.permission.add(newPermission1, newPermission2, ...)
      # 移除newGroup對象中的權限
      newGroup.permission.remove(newPermission1, newPermission2, ...)
      # 清空newGroup對象的權限
      newGroup.permission.clear()

    • 創建好組就可以添加到user對象中了,添加方法還是一樣滴:
      # 注意所有的newGroup都是Group的實例對象
      # 設定user對象的權限為列表中的組
      user.groups= [newGroup1, newGroup2, ...]
      # 向user對象添加組
      user.groups.add(newGroup1, newGroup2, ...)
      # 移除user對象中的組
      user.groups.remove(newGroup1, newGroup2, ...)
      # 清空user對象的組
      user.groups.clear()

    • 用戶屬于一個組,就擁有了這個組所包含的權限,判斷有沒有權限的方法跟之前用戶直接擁有某權限的方法一樣,這里就不寫了

    • 為了更好滴應用到權限和組的功能,這里用一個實例(只有是manager的用戶才可以查看所有的用戶名字)來演示

    • 先添加一個html文件server/learning/templates/manager.html,內容如下:
      <!doctype html>
      <html lang="en">
      <head>
      <meta charset="UTF-8" />
      <title>manager</title>
      <script src="http://code.jquery.com/jquery-latest.js" type="text/javascript" charset="utf-8"></script>
      </head>
      <body>
      <script type="text/javascript">
      $.ajax({
      type:"get",
      url:"/show_all_user/",
      dataType:"json",
      success:function(data){
      write = '當前有以下用戶<br />'
      for(var i = 0; i < data.length; i++){
      write += data[i] + '<br />'
      }
      document.write(write);
      }
      });
      </script>
      </body>
      </html>

    • 修改server/server/urls.py文件,添加以下幾個路徑:
      url(r'^manager$', learning.manager),
      url(r'^show_all_user/$', learning.show_all_user),

    • 修改server/learning/views.py文件,以下列出要新導入的方法和要修改或者添加的函數:
      from django.contrib.auth.decorators import user_passes_test
      from django.contrib.auth.models import Group, Permission
      from django.contrib.contenttypes.models import ContentType

       # 獲取組的函數,傳遞組的名字和對應的權限名(列表,自動創建)
       def get_or_create_group(name, permissions=None):
           group = Group.objects.get_or_create(name=name)[0]
           if permissions:
               permissions_codename = [i.codename for i in group.permissions.all()]
               for i in permissions:
                   if i not in permissions_codename:
                       group.permissions.add(Permission.objects.get_or_create(**{
                           'codename': i,
                           'name': ' '.join(i.split('_')).title(),
                           'content_type': ContentType.objects.get_for_model(learningUser)
                       })[0])
           return group
      
       # 因為django沒有提供判斷是否有組的裝飾器,自己就寫了一個
       # 這個裝飾器跟權限裝飾器一樣,是不過傳遞的組名而不是權限名
       def group_required(group, **kwargs):
           def check_group(user):
               return set(groups).issubset(set([i.name for i in user.groups.all()]))
           groups = group if isinstance(group, (list, tuple)) else (group, )
           # 這個函數用于跳轉,check_group判斷是否存在組,然后返回布爾值
           return user_passes_test(check_group, **kwargs)
      
       # 組的裝飾器都寫了,就不介意自己寫一個權限的裝飾器吧
       def permission_required(perm, **kwargs):
           perms = perm if isinstance(perm, (list, tuple)) else (perm, )
           return user_passes_test(lambda user:user.has_perms(perms), **kwargs)
      
       # 必須是擁有組manager的用戶才可以訪問
       @group_required('manager', login_url='/', redirect_field_name=None)
       def manager(request):
           return render(request, 'manager.html')
      
       # 必須是擁有learning.show_all_user權限的才可以訪問
       @permission_required('learning.show_all_user', login_url='/', redirect_field_name=None)
       def show_all_user(request):
           return HttpResponse(json.dumps([i.username for i in User.objects.all()]))
       
       def register(request, username, password):
           try:
               user = learningUser.objects.create_user(username=username, password=password)
               # 直接默認給新添加的用戶添加manager組(主要是懶)
               user.groups = [get_or_create_group('manager', permissions=['show_all_user'])]
               user.save()
               login(request, username, password)
               return HttpResponseRedirect('/user?username=' + username)
           except: 
               return HttpResponse('已存在用戶')
      
    • 現在你用新注冊的用戶去訪問localhost:8000/manager頁面就會顯示如下:

    • 如果是沒有權限的用戶就直接返回首頁

  1. 好了,現在為止用戶系統才基本完善,其實還有很多需要深入的,不過目前為止還是了解到這為好,因為我們又要進入下一輪的進階學習了。

下期文章:后臺學習——django(3)(還無有)

最后編輯于
?著作權歸作者所有,轉載或內容合作請聯系作者
  • 序言:七十年代末,一起剝皮案震驚了整個濱河市,隨后出現的幾起案子,更是在濱河造成了極大的恐慌,老刑警劉巖,帶你破解...
    沈念sama閱讀 227,702評論 6 531
  • 序言:濱河連續發生了三起死亡事件,死亡現場離奇詭異,居然都是意外死亡,警方通過查閱死者的電腦和手機,發現死者居然都...
    沈念sama閱讀 98,143評論 3 415
  • 文/潘曉璐 我一進店門,熙熙樓的掌柜王于貴愁眉苦臉地迎上來,“玉大人,你說我怎么就攤上這事?!?“怎么了?”我有些...
    開封第一講書人閱讀 175,553評論 0 373
  • 文/不壞的土叔 我叫張陵,是天一觀的道長。 經常有香客問我,道長,這世上最難降的妖魔是什么? 我笑而不...
    開封第一講書人閱讀 62,620評論 1 307
  • 正文 為了忘掉前任,我火速辦了婚禮,結果婚禮上,老公的妹妹穿的比我還像新娘。我一直安慰自己,他們只是感情好,可當我...
    茶點故事閱讀 71,416評論 6 405
  • 文/花漫 我一把揭開白布。 她就那樣靜靜地躺著,像睡著了一般。 火紅的嫁衣襯著肌膚如雪。 梳的紋絲不亂的頭發上,一...
    開封第一講書人閱讀 54,940評論 1 321
  • 那天,我揣著相機與錄音,去河邊找鬼。 笑死,一個胖子當著我的面吹牛,可吹牛的內容都是我干的。 我是一名探鬼主播,決...
    沈念sama閱讀 43,024評論 3 440
  • 文/蒼蘭香墨 我猛地睜開眼,長吁一口氣:“原來是場噩夢啊……” “哼!你這毒婦竟也來了?” 一聲冷哼從身側響起,我...
    開封第一講書人閱讀 42,170評論 0 287
  • 序言:老撾萬榮一對情侶失蹤,失蹤者是張志新(化名)和其女友劉穎,沒想到半個月后,有當地人在樹林里發現了一具尸體,經...
    沈念sama閱讀 48,709評論 1 333
  • 正文 獨居荒郊野嶺守林人離奇死亡,尸身上長有42處帶血的膿包…… 初始之章·張勛 以下內容為張勛視角 年9月15日...
    茶點故事閱讀 40,597評論 3 354
  • 正文 我和宋清朗相戀三年,在試婚紗的時候發現自己被綠了。 大學時的朋友給我發了我未婚夫和他白月光在一起吃飯的照片。...
    茶點故事閱讀 42,784評論 1 369
  • 序言:一個原本活蹦亂跳的男人離奇死亡,死狀恐怖,靈堂內的尸體忽然破棺而出,到底是詐尸還是另有隱情,我是刑警寧澤,帶...
    沈念sama閱讀 38,291評論 5 357
  • 正文 年R本政府宣布,位于F島的核電站,受9級特大地震影響,放射性物質發生泄漏。R本人自食惡果不足惜,卻給世界環境...
    茶點故事閱讀 44,029評論 3 347
  • 文/蒙蒙 一、第九天 我趴在偏房一處隱蔽的房頂上張望。 院中可真熱鬧,春花似錦、人聲如沸。這莊子的主人今日做“春日...
    開封第一講書人閱讀 34,407評論 0 25
  • 文/蒼蘭香墨 我抬頭看了看天上的太陽。三九已至,卻和暖如春,著一層夾襖步出監牢的瞬間,已是汗流浹背。 一陣腳步聲響...
    開封第一講書人閱讀 35,663評論 1 280
  • 我被黑心中介騙來泰國打工, 沒想到剛下飛機就差點兒被人妖公主榨干…… 1. 我叫王不留,地道東北人。 一個月前我還...
    沈念sama閱讀 51,403評論 3 390
  • 正文 我出身青樓,卻偏偏與公主長得像,于是被迫代替她去往敵國和親。 傳聞我的和親對象是個殘疾皇子,可洞房花燭夜當晚...
    茶點故事閱讀 47,746評論 2 370

推薦閱讀更多精彩內容