額,一個忽然的交流讓我想起來我耽擱許久各類驗證的實現遲遲沒作過
趁着這個機會就搞了一下
分爲三部分:郵箱驗證,短信驗證,圖片驗證碼php
這個部分是主要參考的經典書籍-狗書
思路就是根據用戶某些信息經過JSON Web簽名生成token,而後再發送郵件驗證,經典思路
生成和驗證函數都加載在模型中
完整代碼
itsdangerous中文文檔這裏介紹了幾種簽名方式html
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的做用:python
itsdangerous中的鹽,是爲了一個大相徑庭的目的而產生的。你能夠將它視爲成命名空間
假設你想簽名兩個連接。你的系統有個激活連接,用來激活一個用戶帳戶,而且你有一個升級連接,可讓一個用戶帳戶升級爲付費用戶,這兩個連接使用email發送。在這兩種狀況下,若是你簽名的都是用戶ID,那麼該用戶能夠在激活帳戶和升級帳戶時,複用URL的可變部分。如今你能夠在你簽名的地方加上更多信息(如升級或激活的意圖),可是你也能夠用不一樣的鹽
即只有使用相同鹽的序列化器才能成功把值加載出來git
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的github
流程就是生成任意的數字,保存,添加圖片水印
這裏確定要用的python強大的圖片處理庫PIL,其中用到了
加線條,濾鏡等增長干擾
下面是完整代碼,該作註釋的地方我已經加了註釋
看代碼以前,最好先好好看下PIL官方文檔,和一些基本概念
部分我參考的博文也貼在了文末redis
#!/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) # 建立扭曲
這裏有篇文章詳細的介紹了下:json
對當前圖像進行透視變換,產生給定尺寸的新圖像。
變量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
這個地方如今不少網站會使用另外一種回答問題的方式,這個方法的實現
我我的感受也是應該也是相同的手段,只是將隨機的字符串改成問題,將驗證方式改成答案
不過這裏或許要把問題和答案存進數據庫,更方便點,也才能實現
有時候想本身是否是出生太晚了。。。。。想寫的東西,都能搜到很好的博文,以下:flask開發restful api系列(5)-短信驗證碼
這裏雲通信是文中所用平臺的開發文檔,不過平臺能夠自由選擇,結果都是同樣
這裏就簡化一下前輩的代碼,把關於驗證碼處理的重點代碼擼了出來,用到了Redis,我也趁機學了一波,的確挺好用的api
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