Django實現驗證碼

jackson.liang發表於2018-06-13

背景知識

1. 驗證碼的作用

  • 防惡意破解密碼:防止,使用程式或機器人惡意去試密碼.為了提高使用者的體驗,使用者輸入錯誤以後,才會要求輸入驗證碼.
  • 防論壇灌水:這個是很常見的。有一種程式叫做頂帖機,如果無限制的刷,整個論壇可能到處是拉圾資訊,比如,百度貼吧 ,你只要是新使用者或者剛剛關注的貼吧,要是發帖,會馬上出現驗證碼。
  • 有效防止註冊,以防,使用程式或機器人去無限制註冊賬號.
  • 防刷票,網上有很多投票類的網站. ####2. 驗證碼的原理 驗證碼於伺服器端生成,傳送給客戶端,並以影象格式顯示。客戶端提交所顯示的驗證碼,客戶端接收並進行比較,若比對失敗則不能實現登入或註冊,反之成功後跳轉相應介面。

驗證碼原理與流程

程式碼實現

廢話不多說,先上程式碼:

# encoding:utf-8
from PIL import Image, ImageDraw, ImageFont
import random, StringIO
import os
from math import ceil
import base64

current_path = os.path.normpath(os.path.dirname(__file__))


class Captcha(object):
# 定義一個驗證碼類,
def __init__(self, request):
    self.django_request = request
    self.session_key = request.session.session_key
    self.words = []

    # image size (pix)
    self.img_width = 150
    self.img_height = 30

    # default type
    self.type = 'number'

def _get_font_size(self):
    """  將圖片高度的80%作為字型大小
    """
    s1 = int(self.img_height * 0.8)
    s2 = int(self.img_width / len(self.code))
    return int(min((s1, s2)) + max((s1, s2)) * 0.05)

def _get_words(self):
    """ The words list
    """
    # 擴充單詞列表
    if self.words:
        return set(self.words)

    file_path = os.path.join(current_path, 'words.list')
    f = open(file_path, 'r')
    return set([line.replace('\n', '') for line in f.readlines()])

def _set_answer(self, answer):
    """  設定答案
    """
    self.django_request.session[self.session_key] = str(answer)

def _yield_code(self):
    """  生成驗證碼數字,以及答案
    """
   # 數字公式驗證碼
    def number():
        m, n = 1, 50
        x = random.randrange(m, n)
        y = random.randrange(m, n)

        r = random.randrange(0, 2)
        if r == 0:
            code = "%s - %s = ?" % (x, y)
            z = x - y
        else:
            code = "%s + %s = ?" % (x, y)
            z = x + y
        self._set_answer(z)
        return code

    fun = eval(self.type.lower())
    return fun()

def display(self):
    """  把生成的驗證碼圖片改成資料流返回
    """

    # 字型顏色
    self.font_color = ['black', 'darkblue', 'darkred']

    # 背景顏色,隨機生成
    self.background = (random.randrange(230, 255), random.randrange(230, 255), random.randrange(230, 255))

    # 字型
    self.font_path = os.path.join(current_path, 'timesbi.ttf')
    # self.font_path = os.path.join(current_path,'Menlo.ttc')

    # 生成的驗證碼只做一次驗證,就會清空
    self.django_request.session[self.session_key] = ''

    # 使用 PIL建立畫布
    im = Image.new('RGB', (self.img_width, self.img_height), self.background)
    
    # 生成驗證碼
    self.code = self._yield_code()

    # 設定字型大小
    self.font_size = self._get_font_size()

    # 例項化一個繪圖
    draw = ImageDraw.Draw(im)

    # 在畫布繪圖,寫驗證碼
    if self.type == 'word':
        c = int(8 / len(self.code) * 3) or 3
    elif self.type == 'number':
        c = 4

    for i in range(random.randrange(c - 2, c)):
        line_color = (random.randrange(0, 255), random.randrange(0, 255), random.randrange(0, 255))
        xy = (
            random.randrange(0, int(self.img_width * 0.2)),
            random.randrange(0, self.img_height),
            random.randrange(3 * self.img_width / 4, self.img_width),
            random.randrange(0, self.img_height)
        )
        draw.line(xy, fill=line_color, width=int(self.font_size * 0.1))
        # draw.arc(xy,fill=line_color,width=int(self.font_size*0.1))
    # draw.arc(xy,0,1400,fill=line_color)
    # code part
    j = int(self.font_size * 0.3)
    k = int(self.font_size * 0.5)
    x = random.randrange(j, k)  # starts point
    for i in self.code:
        # 上下抖動量,字數越多,上下抖動越大
        m = int(len(self.code))
        y = random.randrange(1, 3)
        if i in ('+', '=', '?'):
            # 對計算符號等特殊字元放大處理
            m = ceil(self.font_size * 0.8)
        else:
            # 字型大小變化量,字數越少,字型大小變化越多
            m = random.randrange(0, int(45 / self.font_size) + int(self.font_size / 5))
        self.font = ImageFont.truetype(self.font_path.replace('\\', '/'), self.font_size + int(ceil(m)))
        draw.text((x, y), i, font=self.font, fill=random.choice(self.font_color))
        x += self.font_size * 0.9
    del x
    del draw
    # 序列化處理
    buf = StringIO.StringIO()
    im.save(buf, 'gif')
    buf.closed
    data = base64.encodestring(buf.getvalue())
    return data

def validate(self, code):
    """
    檢查使用者輸入和伺服器上的密碼是否一致
    """
    if not code:
        return False
    _code = self.django_request.session.get(self.session_key) or ''
    self.django_request.session[self.session_key] = ''
    return _code.lower() == str(code).lower()

def check(self, code):
    """
    檢查使用者輸入和伺服器上儲存的密碼是否一致
    """
    return self.validate(code)
複製程式碼

上面使用的庫如下:

	from PIL import Image, ImageDraw, ImageFont
	import random, StringIO
	import os
	from math import ceil
	import base64
複製程式碼

說明:

  • PIL 畫圖,生成圖片
  • random 隨機生成數 math用於計算
  • StringIO將圖片格式轉成資料流用於網路傳輸
  • base64,使用者編碼,資料傳輸,應前端要求處理跨域API的問題

需要強調的是: 我把使用者的驗證碼的答案儲存在使用者的session中,儲存在伺服器上

self.django_request.session[self.session_key] = str(answer)
複製程式碼

每一個使用者訪問都是會例項化一個request.seesion物件,所以,使用者區分開了.

self.session_key = request.session.session_key
複製程式碼

同一使用者在不同地方同時登入,對應的request.session.session_key不同,所以也區分了異地同時登入,出現混亂的情況.

django的view檢視

from common.CaptchaVerify import Captcha

def captchaCode(request):
    ca = Captcha(request)
    ca.type = 'number'
    raw = ca.display()
    response = JsonResponse(raw)
    return response

def login(request):
    _code = request.POST.get('code') or ''
    if not _code:
        data = {'code': 1, 'messeage': 'verify fail, captchacode error'}
        response = ReturnJson(data, status=401).get()
        return response
    ca = Captcha(request)
    if not ca.check(_code):
        data = {'code': 1, 'messsage': 'verify fail, captchacode error'}
        response = ReturnJson(data, status=401).get()
        return response
複製程式碼

這裡,使用者可以直接呼叫,那個類,如果需要定製,可以自己在類中修改,符合自己的業務需求.

ps:

驗證碼繪製規則

  • 均勻繪畫字元,居中
  • 字元顏色要比較深
  • 要有線條雪花等干擾元素
  • 一切能隨機的都隨機
  • 考慮到使用者的體驗,老是錯誤,開始降低難度(哈哈哈!!)

相關文章