基於Python實現原生的登入驗證碼

語言: CN / TW / HK

1、概述

在前面的文章中,我有分享到 vue+drf+第三方滑動驗證碼的接入實現 (文中也留了坑分享圖片驗證碼功能的實現),即本文將要分享的是基於 python 實現原生的登入驗證碼

通常的驗證碼,人眼看上去更像是一張小圖片

html 語法中,嵌入一張圖片一般用 img 標籤實現,而 img 標籤對應的 src 一般有以下幾種寫法

  • 圖片的本地路徑
  • 圖片的url
  • 圖片的二進位制資料(base64編碼)

其中前兩種方法都需要外部具有實際存在的圖片,第三種方法則是將圖片進行編碼後填充到 img 標籤的 src

2、驗證碼實現的演進過程

2.1 路由及頁面

為了實現驗證碼的功能,需要開設一個 url 單獨處理驗證碼功能,修改全域性路由

urlpatterns = [
    ......
    url(r'^login/', views.login, name='login'),
    # 圖片驗證碼
    url(r'^get_code/', views.get_code, name='gc'),
    ...
]

然後修改前端登入頁面 login.html 的驗證碼部分

...
<div class="form-group">
    <label for="">驗證碼</label>
    <div class="row">
        <div class="col-md-6">
            <input type="text" name="code" id="id_code" class="form-control">
        </div>
        <div class="col-md-6">
            <img src="/get_code/" alt="" width="430" height="35" id="id_img">
        </div>
    </div>

</div>
<input type="button" class="btn btn-success" value="登陸" id="id_commit">
...

2.2 檢視函式中驗證碼的推導

2.2.1 圖片傳送到前端

前端介面實現了一個簡單的包含驗證碼的登入框,首先定義一個檢視函式將後端的測試圖片以二進位制的形式傳送到前端進行測試

def get_code(request):
    # 直接獲取後端現成的圖片二進位制資料傳送給前端
    with open(r'static/img/test.jpg','rb') as f:
        data = f.read()
    return HttpResponse(data)

2.2.2 引入動態圖片

為了操作圖片,主要利用的是 pillow 模組

pip3 install pillow

主要用到了 ImageImageDrawImageFont

  • Image:生成圖片
  • ImageDraw:在圖片上定義內容
  • ImageFont:控制字型樣式

因此,利用 pillow 模組動態產生圖片的方法為

import random
from PIL import Image, ImageDraw, ImageFont

def get_random():
    return random.randint(0, 255), random.randint(0, 255), random.randint(0, 255)


def get_code(request):
    # 利用pillow模組動態產生圖片
    # img_obj = Image.new('RGB',(430,35),'green')  # RGB組合、圖片尺寸、顏色
    img_obj = Image.new('RGB',(430,35),get_random())  # 通過色值指定顏色
    # 先將圖片物件儲存起來
    with open('xxx.png','wb') as f:
        img_obj.save(f,'png')
    # 再將圖片物件讀取出來
    with open('xxx.png','rb') as f:
        data = f.read()
    return HttpResponse(data)

2.2.3 記憶體管理模組圖片

上面將圖片物件儲存在讀取有些麻煩,檔案儲存繁瑣 IO 操作效率低 ,可以藉助於記憶體管理器模組實現

其中又分為 BytesIOStringIO 兩種

  • BytesIO:臨時儲存資料,返回的時候資料是二進位制
  • StringIO:臨時儲存資料,返回的時候資料是字串

記憶體管理物件就相當於上面的檔案控制代碼

import random
from PIL import Image, ImageDraw, ImageFont

def get_random():
    return random.randint(0, 255), random.randint(0, 255), random.randint(0, 255)


def get_code(request):
    img_obj = Image.new('RGB', (430, 35), get_random())
    io_obj = BytesIO()  # 生成一個記憶體管理器物件  你可以看成是檔案控制代碼
    img_obj.save(io_obj,'png')
    return HttpResponse(io_obj.getvalue())  # 從記憶體管理器中讀取二進位制的圖片資料返回給前端

這樣一來,圖片的生成以及返回就比較友好了

2.2.4 完整圖片驗證碼

上面解決了圖片如何傳遞到前端頁面的問題,剩下的就是如何生成對應的隨機驗證碼了

例如隨機驗證碼為五位數的隨機驗證碼,包含數字、小寫字母、大寫字母

import random
from PIL import Image, ImageDraw, ImageFont

def get_random():
    return random.randint(0, 255), random.randint(0, 255), random.randint(0, 255)


def get_code(request):
    # 寫圖片驗證碼
    img_obj = Image.new('RGB', (430, 35), get_random())
    img_draw = ImageDraw.Draw(img_obj)  # 產生一個畫筆物件
    # 引入本地的字型檔案,指定字型樣式及字型大小
    img_font = ImageFont.truetype('static/font/222.ttf', 30) 
    # 隨機驗證碼 五位數的隨機驗證碼  數字 小寫字母 大寫字母
    code = ''
    for i in range(5):
        random_upper = chr(random.randint(65, 90))  # 隨機大寫字母
        random_lower = chr(random.randint(97, 122))  # 隨機小寫字母
        random_int = str(random.randint(0, 9))  # 隨機數字
        # 每次從上面三個裡面隨機選擇一個
        tmp = random.choice([random_lower, random_upper, random_int])
        # 將產生的隨機字串寫入到圖片上,需要調整每個字型所在的座標變換
        img_draw.text((i * 60 + 60, -2), tmp, get_random(), img_font)
        # 拼接隨機字串
        code += tmp
    print(code)
    # 隨機驗證碼在登陸的檢視函式裡面需要要比對,所以要找地方存起來並且其他檢視函式也能拿到
    request.session['code'] = code
    io_obj = BytesIO()
    img_obj.save(io_obj, 'png')
    return HttpResponse(io_obj.getvalue())

這裡有一點,在寫入隨機文字的時候一個個寫而不是生成好了之後再寫,是因為一個個寫能夠控制每個字型的間隙。而生成好之後再寫的話,間隙就沒法控制了

2.3 登入驗證中使用驗證碼

上面將每次生成的驗證碼儲存到了 session 中,這樣在前端傳過來的驗證碼,登入校驗時就可以進行比對了

......
def login(request):
    if request.method == 'POST':
        back_dic = {'code': 1000, 'msg': ''}
        username = request.POST.get('username')
        password = request.POST.get('password')
        code = request.POST.get('code')
        # 1 先校驗驗證碼是否正確  自己決定是否忽略大小寫 統一轉大寫或小寫再比較
        if request.session.get('code').upper() == code.upper():
            # 校驗使用者名稱和密碼是否正確
            user_obj = auth.authenticate(request,username=username,password=password)
            if user_obj:
                # 儲存使用者狀態
                auth.login(request,user_obj)
                back_dic['url'] = '/home/'
            else:
                back_dic['code'] = 2000
                back_dic['msg'] = '使用者名稱或密碼錯誤'
        else:
            back_dic['code'] = 3000
            back_dic['msg'] = '驗證碼錯誤'
        return JsonResponse(back_dic)
    return render(request, 'login.html')

2.4 前端頁面點選自動重新整理

最後,還留下一點小問題,前端在輸入驗證碼錯誤後不會自動重新整理,如果點選驗證碼也不會進行重新整理,只能通過重新整理登入頁面才能重新整理驗證碼,因此需要想辦法讓使用者在點選驗證碼時自動重新整理(單獨觸發驗證碼的檢視函式)

每次在點選時,修改對應 src 的值即可,可以通過一小段 js 實現

...
       <div class="form-group">
            <label for="">驗證碼</label>

            <div class="row">
                <div class="col-md-6">
                    <input type="text" name="code" id="id_code" class="form-control">
                </div>
                <div class="col-md-6">
                    <img src="/get_code/" alt="" width="430" height="35" id="id_img">
                </div>
            </div>

        </div>
        <input type="button" class="btn btn-success" value="登陸" id="id_commit">
...
<script>
    $("#id_img").click(function () {
        // 獲取標籤之前的src
        let oldVal = $(this).attr('src');
        $(this).attr('src',oldVal += '?')
    })
</script>

3、效果展示

最終前端的驗證碼效果如圖

4、小結

本文基於 python 以及相關的庫原生實現了登入驗證碼邏輯~

其實寫本文也是因為之前有過想法但是一段時間就忘了,最近通過某銀行手機銀行 app 重置登入密碼的時候,發現其驗證碼顯示效果和本文效果竟然神似~,於是撿起來寫了此文

其手機 app 驗證碼效果如下

See you ~