幾種開發時安全驗證的實現

bay1發表於2018-06-22

額,一個突然的交流讓我想起來我耽擱許久各種驗證的實現遲遲沒做過 趁著這個機會就搞了一下 分為三部分:郵箱驗證,簡訊驗證,圖片驗證碼

文章完整程式碼

郵箱驗證

這個部分是主要參考的經典書籍-狗書 思路就是根據使用者某些資訊通過JSON Web簽名生成token,然後再傳送郵件驗證,經典思路 生成和驗證函式都載入在模型中

itsdangerous中文文件

這裡介紹了幾種簽名方式

token生成和驗證 TimedJSONWebSignatureSerializer,看這個表面詞的意思可以看出這裡序列化加入了當前時間 這也是實現設定過期時間的依據吧 檢視itsdangerous原始碼可以看到具體的加密方式

from itsdangerous import (TimedJSONWebSignatureSerializer as Serializer, BadSignature, SignatureExpired)
...
class User(db.Model):
    __tablename__ = 'user'
    id = db.Column(db.Integer,primary_key=True)
    name=db.Column(db.String(64),unique=True,index=True)

    def genter_auth_token(self,expiration=300): #設定有效期
        s=Serializer(current_app.config['SECRET_KEY'],expires_in=expiration)
        return s.dumps({'code':self.name}) #將使用者名稱當作簽名物件

    @staticmethod
    def verify_auth_token(token):
        s=Serializer(current_app.config['SECRET_KEY'])
        try:
            data=s.loads(token) #載入資料
        except BadSignature:
            return None
        except SignatureExpired:
            return None
        return data
複製程式碼

結果

同時這裡itdangerous類的簽名方式都可以接收一個salt 文件中這樣描述了salt的作用:

itsdangerous中的鹽,是為了一個截然不同的目的而產生的。你可以將它視為成名稱空間 假設你想簽名兩個連結。你的系統有個啟用連結,用來啟用一個使用者賬戶,並且你有一個升級連結,可以讓一個使用者賬戶升級為付費使用者,這兩個連結使用email傳送。在這兩種情況下,如果你簽名的都是使用者ID,那麼該使用者可以在啟用賬戶和升級賬戶時,複用URL的可變部分。現在你可以在你簽名的地方加上更多資訊(如升級或啟用的意圖),但是你也可以用不同的鹽

即只有使用相同鹽的序列化器才能成功把值載入出來

def genter_auth_token(self,expiration=300):
	s=Serializer(current_app.config['SECRET_KEY'],salt='activate-salt',expires_in=expiration)
    return s.dumps({'code':self.name})

@staticmethod
def verify_auth_token(token):
    s=Serializer(current_app.config['SECRET_KEY'],salt='activate-salt')
複製程式碼

圖片驗證碼

這個驗證碼可以直接呼叫一些平臺的智慧驗證,也可以用另一種 另一個也許是比較傳統的思路,就是自己生成的圖片水印,儲存驗證碼 python和php裡都有相應的圖片操作方法,這裡就寫下python的

流程就是生成任意的數字,儲存,新增圖片水印

這裡肯定要用的python強大的圖片處理庫PIL,其中用到了 加線條,濾鏡等增加干擾 下面是完整程式碼,該做註釋的地方我已經加了註釋 看程式碼之前,最好先好好看下PIL官方文件,和一些基本概念 部分我參考的博文也貼在了文末

#!/usr/bin/env python 
#coding=utf-8
import os
import random
from flask import Flask,send_from_directory
from PIL import Image,ImageFont,ImageDraw,ImageFilter

app=Flask(__name__)
app.debug=True

class picture:
    def __init__(self):
        self.size = (240,60)
        self.mode="RGB"
        self.color="white"
        self.font = ImageFont.truetype("C:\Windows\Fonts\Arial.ttf", 36) #設定字型大小

    def randChar(self):
        basic='23456789abcdefghijklmnpqrstwxyzABCDEFGHIJKLMNPQRSTWXYZ'
        return basic[random.randint(0,len(basic)-1)] #隨機字元

    def randBdColor(self):
        return (random.randint(64,255),random.randint(64,255),random.randint(64,255)) #背景

    def randTextColor(self):
        return (random.randint(32, 127), random.randint(32, 127), random.randint(32, 127)) #隨機顏色

    def proPicture(self):
        new_image=Image.new(self.mode,self.size,self.color) #建立新影象有三個預設引數:尺寸,顏色,模式
        drawObject=ImageDraw.Draw(new_image) #建立一個可以對image操作的物件
        line_num = random.randint(4,6) # 干擾線條數
        for i in range(line_num):
            #size=(240,60)
            begin = (random.randint(0, self.size[0]), random.randint(0, self.size[1]))
            end = (random.randint(0, self.size[0]), random.randint(0, self.size[1]))
            drawObject.line([begin, end], self.randTextColor())

        for x in range(240):
            for y in range(60):
                tmp = random.randint(0,50)
                if tmp>30: #調整干擾點數量
                    drawObject.point((x,y),self.randBdColor())

        randchar=''  
        for i in range(5):
            rand=self.randChar()
            randchar+=rand
            drawObject.text([50*i+10,10],rand,self.randTextColor(),font=self.font) #寫入字元

        new_image = new_image.filter(ImageFilter.SHARPEN) # 濾鏡    

        return new_image,randchar
@app.route('/<filename>')
def get_file(filename):
    return send_from_directory(os.getcwd(),filename)

@app.route('/')
def index():
    test=picture()
    image,code=test.proPicture()
    image.save('new.jpg')
    url="http://127.0.0.1:5000/new.jpg"
    return '<img src='+url+' /><br/>'+"圖中的code為:"+code 
    #這裡有快取,需要CTRL+F5才會有效果

if __name__=="__main__":
    app.run()
複製程式碼

另外,有的前輩會再加入了扭曲影象增加分辨難度

# 圖形扭曲引數 
params = [1 - float(random.randint(1, 2)) / 100, 
              0, 
              0, 
              0, 
              1 - float(random.randint(1, 10)) / 100, 
              float(random.randint(1, 2)) / 500, 
              0.001, 
              float(random.randint(1, 2)) / 500 
              ] 
img = img.transform(size, Image.PERSPECTIVE, params) # 建立扭曲
複製程式碼

這裡有篇文章詳細的介紹了下:

對當前影象進行透視變換,產生給定尺寸的新影象。 變數data是一個8元組(a,b,c,d,e,f,g,h),包括一個透視變換的係數。對於輸出影象中的每個畫素點,新的值來自於輸入影象的位置的(a x + b y + c)/(g x + h y + 1), (d x+ e y + f)/(g x + h y + 1)畫素,使用最接近的畫素進行近似

這個的源定義就牽涉到了一個仿射變換,涉及一些數學的計算 看得我有點懵逼,就沒加到我的程式碼中,先留坑

效果

這個地方現在很多網站會使用另一種回答問題的方式,這個方法的實現 我個人感覺也是應該也是相同的手段,只是將隨機的字串改為問題,將驗證方式改為答案 不過這裡或許要把問題和答案存進資料庫,更方便點,也才能實現

另一種

簡訊驗證

有時候想自己是不是出生太晚了。。。。。想寫的東西,都能搜到很好的博文,如下:flask開發restful api系列(5)-簡訊驗證碼 這裡雲通訊是文中所用平臺的開發文件,不過平臺可以自由選擇,結果都是一樣 這裡就簡化一下前輩的程式碼,把關於驗證碼處理的重點程式碼擼了出來,用到了Redis,我也趁機學了一波,的確挺好用的

import redis
import random

phonenumber=188888888
#這裡可以利用正則過濾一下電話號碼,比如:
#/^(13[0-9]|14[5-9]|15[0-9]|16[6]|17[0-8]|18[0-9]|19[8-9])\d{8}$/

conn=redis.StrictRedis(host='127.0.0.1',port=6379)

def producCode():
    verifyCode=str(random.randint(100000,999999))

    pipe=conn.pipeline() #新增管道,可以一次連線執行多次命令
    pipe.set("phone%s"%phonenumber,verifyCode)
    pipe.expire("phone%s"%phonenumber,60) #設定過期時間一分鐘
    pipe.execute()

def checkCode():
    pipe=conn.pipeline() #新增管道,可以一次連線執行多次命令
    pipe.set('postNum%s'%phonenumber,'0')
    validate_number = request.get_json().get('validate_number')
    pipe.incr('postNum%s'%phonenumber) #記錄提交次數防止爆破
    if conn.get('postNum%s'%phonenumber)>3:
        pass
    ...
    if validate_number != validate_number_in_redis:
        return jsonify({'code': 0, 'message': '驗證沒有通過'})
    pipe.set('is_validate:%s' % phone_number, '1') #通過驗證碼設定value為1
    pipe.expire('is_validate:%s' % phone_number, 120)
    pipe.execute()

    return jsonify({'code': 1, 'message': '驗證通過'})
def postMessage():
    result=conn.get("phone%s"%phonenumber)
    #此時如果通過驗證碼,result為1,否則為0
    ...
    #剩下的其他操作
複製程式碼

這裡提到了洩露介面導致驗證碼爆破的情況,我也新增了一些程式碼 另外就是在某些功能模組,也易出現漏洞,比如修改資料處,驗證碼不僅僅要與phone一致 也要檢查使用者名稱的一致性,要不然如果只是通過驗證碼,使用者修改為自己的號碼,驗證碼手機號都通過驗證 (感覺一般人不會出現這種錯誤)

而你的程式碼又是直接傳入使用者名稱進行修改操作,這將可能導致任意使用者重置密碼 或者你的程式碼直接將phone作為索引進行修改

參考文章: 狗書authentication

雜項之影象處理pillow

PIL一些基本概念

PIL中的Image模組

Python PIL ImageDraw和ImageFont模組學習

Python影象處理庫PIL的ImageFilter模組介紹

Redis中文文件

redis-py

相關文章