flaskwtf文档
https://flask-wtf.readthedocs.io/en/stable/#
wtform文档
https://wtforms.readthedocs.io/en/2.3.x/
Flask-WTF集成了WTForm功能,是带有csrf令牌的安全表单且具有全局csrf保护的功能、有文件上传(Flask-Uploads)及图形验证码功能。
安装,会自动安装WTForms:pip install Flask-WTF
定义form.py 添加
import re
from flask import session
from flask_wtf import FlaskForm
from flask_wtf.file import FileField, FileRequired, FileAllowed
from wtforms import StringField, PasswordField
from wtforms.validators import DataRequired, Length, EqualTo, ValidationError
# 定义表单项匹配到前端的表单中,继承FlaskForm
class UserForm(FlaskForm):
# field类型 'BooleanField', 'DecimalField', 'DateField', 'DateTimeField', 'FieldList', 'FloatField', 'FormField', 'IntegerField', 'RadioField', 'SelectField', 'SelectMultipleField', 'StringField', 'TimeField',
# StringField表示前端的文本框 type=text name=name DataRequired表示必填 errors为元组
# 限制格式 DataRequired EqualTo IPAddress Length NumberRange URL Email Regexp
name = StringField(label='用户名', validators=[DataRequired(), Length(min=6, max=12, message='用户名长度必须在6~12位之间')])
# 密码框
password = PasswordField(label='密 码',validators=[DataRequired(), Length(min=6, max=12, message='密码长度必须在6~12位之间')])
# 确认密码 EqualTo(name)
confirm_pwd = PasswordField(label='确认密码', validators=[ DataRequired( ), Length(min=6, max=12, message='密码长度必须在6~12位之间'), EqualTo('password', '两次密码不一致') ])
# 手机号 ctrl点击进去,点击下箭头显示其所有子类
phone = StringField(label='手机号码', validators=[ DataRequired( ), Length(min=11, max=11, message='手机号码必须11位长度') ])
# 头像上传 from flask_wtf.file import FileField, FileRequired, FileAllowed FileAllowed允许上传的扩展名
icon = FileField(label='用户头像', validators=[ FileRequired( ), FileAllowed([ 'jpg', 'png', 'gif' ], message='必须是图片文件格式') ])
# 图形验证码 RecaptchaField 使用需要google 所以直接使用StringField
recaptcha = StringField(label='验证码')
# If the form defines a ``validate_<fieldname>`` method, it is appended as an extra validator for the field's ``validate``.
# 自定义name的格式 data 就是提交过来的表单项 data.data以及 self.name.data 都为取值 2zxcgyh
def validate_name(self, data):
# self.name <input id="name" name="name" required type="text" value="2zxcgyh">
if self.name.data[ 0 ].isdigit( ):
# 抛出错误 加入元组
raise ValidationError('用户名不能以数字开头')
# 自定义phone的格式
def validate_phone(self, data):
phone = data.data
# 正则匹配 ^1[35678]\d{9}$ 开头1 第二位为3/5/6/7/8 后面9位数字
if not re.search(r'^1[35678]\d{9}$', phone):
raise ValidationError('手机号码格式错误')
def validate_recaptcha(self, data):
input_code = data.data
code = session.get('valid')
# 全改为小写
if input_code.lower() != code.lower( ):
raise ValidationError('验证码错误')
Field类型:
StringField PasswordField IntegerField DecimalField FloatField
BooleanField RadioField SelectField DatetimeField
所有的验证:
DataRequired EqualTo IPAddress Length NumberRange URL Email Regexp
在app.py中连接表单对象和html
import os
from io import BytesIO
from flask import Flask, render_template, make_response, session
from flask_bootstrap import Bootstrap
from werkzeug.utils import secure_filename
from form import UserForm
from flask_wtf.csrf import CSRFProtect
from util import generate_image
# 路径
BASE_DIR = os.path.dirname(os.path.abspath(__file__)) # 项目路径
STATIC_DIR = os.path.join(BASE_DIR, 'static') # 静态文件路径
UPLOAD_DIR = os.path.join(STATIC_DIR, 'upload') # 上传文件路径
app = Flask(__name__)
# 必须配置密钥 flaskform的csrf需要
app.config['SECRET_KEY'] = 'jfkdk73434kjfk3'
app.config['ENV'] = 'development'
# 验证码的公钥
app.config['RECAPTCHA_PUBLIC_KEY'] = '6LeYIbsSAAAAACRPIllxA7wvXjIE411PfdB2gt2J'
# 验证码的私钥
app.config['RECAPTCHA_PRIVATE_KEY '] = '6LeYIbsSAAAAAJezaIq3Ft_hSTo0YtyeFG-JgRtu'
app.config['RECAPTCHA_PARAMETERS'] = {'hl': 'zh', 'render': 'explicit'}
# 验证码主题为黑色
app.config['RECAPTCHA_DATA_ATTRS'] = {'theme': 'dark'}
# 初始化bootstrap
bootstrap = Bootstrap(app=app)
csrf = CSRFProtect(app)
@app.route('/', methods=["GET","POST"])
def hello_world():
# 创建表单类对象
uform = UserForm()
if uform.validate_on_submit( ): # 主要通过其进行校验 校验提交方式以及form的验证
name = uform.name.data
password = uform.password.data
phone = uform.phone.data
print('name:',name,' password:',password,' phone:',phone)
icon = uform.icon.data # FileStorage类型
print(type(icon))
# 构建安全文件名 存入
filename = secure_filename(icon.filename)
icon.save(os.path.join(UPLOAD_DIR, filename))
return '提交成功!'
return render_template('user.html', uform = uform)
@app.route('/image')
def get_image():
im, code = generate_image(4)
# 将image对象转成二进制
buffer = BytesIO()
# 将图片保存到buffer format=JPEG
im.save(buffer, "JPEG")
# getvalue:Retrieve the entire contents of the BytesIO object
buf_bytes = buffer.getvalue()
# 保存到session中
session['valid'] = code
# 构建response对象 存二进制
response = make_response(buf_bytes)
response.headers['Content-Type'] = 'image/jpg'
return response
# form与bootstrap结合使用
@app.route('/user1',methods=['GET', 'POST'])
def boot_form_user():
uform = UserForm()
if uform.validate_on_submit( ): # 主要通过其进行校验 校验提交方式以及form的验证
name = uform.name.data
password = uform.password.data
phone = uform.phone.data
print('name:', name, ' password:', password, ' phone:', phone)
icon = uform.icon.data # FileStorage类型
print(type(icon))
# 构建安全文件名 存入
filename = secure_filename(icon.filename)
icon.save(os.path.join(UPLOAD_DIR, filename))
return '提交成功!'
return render_template('user1.html',uform=uform)
if __name__ == '__main__':
app.run(debug=True)
在templates中建立网页user.html,form标签和提交按钮需要自己写
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<title>用户页面</title>
{# jquery cdn加速 #}
<script src="http://libs.baidu.com/jquery/2.0.0/jquery.min.js"></script>
<style>
p span {
font-size: 14px;
color: red;
}
</style>
</head>
<body>
<form action="{{ url_for('hello_world') }}" method="post" enctype="multipart/form-data">
{# 全局使用csrf保护 #}
{# uform.name.errors.0 表示取出元组里的第一个 #}
{{ uform.csrf_token }}
<p>{{ uform.name.label }}:{{ uform.name }}
<span>{% if uform.name.errors %}{{ uform.name.errors.0 }} {% endif %}</span>
</p>
<p>{{ uform.password.label }}:{{ uform.password }}
<span>{% if uform.password.errors %}{{ uform.password.errors.0 }}{% endif %}</span>
</p>
<p>{{ uform.confirm_pwd.label }}:{{ uform.confirm_pwd }}
<span>{% if uform.confirm_pwd.errors %}{{ uform.confirm_pwd.errors.0 }}{% endif %}</span>
</p>
<p>{{ uform.phone.label }}:{{ uform.phone }}
<span>{% if uform.phone.errors %}{{ uform.phone.errors.0 }}{% endif %}</span>
</p>
{# 头像上传 添加 enctype="multipart/form-data" #}
<p>{{ uform.icon.label }}:{{ uform.icon }}
<span>{% if uform.icon.errors %}{{ uform.icon.errors.0 }}{% endif %}</span>
</p>
{# 图形验证码 #}
<p>{{ uform.recaptcha.label }}:{{ uform.recaptcha }}
<span>
{% if uform.recaptcha.errors %}{{ uform.recaptcha.errors.0 }}{% endif %}
</span>
</p>
<p> <img src="{{ url_for('get_image') }}" id="img"></p>
<p><input type="submit" value="提交"></p>
</form>
<script>
// 单击图片重新加载图片 ran= 向服务器表示两次请求不同 否则服务器会返回与之前相同的结果
$('#img').click(function () {
$(this).attr('src', "{{ url_for('get_image') }}?ran=" + Math.random());
})
</script>
</body>
</html>
CSRF中文名称
跨站请求伪造,缩写为:CSRF/XSRF。
可以这么理解CSRF攻击:
攻击者盗用了受害者的身份,以受害者的名义发送恶意请求。
要完成一次CSRF攻击,受害者必须依次完成两个步骤:
登录受信任网站A,并在本地生成Cookie。在不登出A的情况下,访问危险网站B。此时B就可以携带在A处产生的cookie访问A
CSRF的防御可以从服务端和客户端两方面着手,防御效果是从服务端着手效果比较好,现在一般的CSRF防御也都在服务端进行。
{{ uform.csrf_token }}
表示表单随机产生一个值,每次请求都不同。浏览器验证cookie和随机值,都正确才通过。
文件上传
上传使用的是FileField,如果需要指定上传文件的类型需要使用:FileAllowed
icon = FileField(label='用户头像', validators=[FileRequired(), FileAllowed(['jpg', 'png', 'gif'], message='必须是图片文件格式')])
必须在form上面添加:enctype=“multipart/form-data”
保存文件
icon = uform.icon.data -----》icon是FileStorage类型
filename = secure_filename(icon.filename)
icon.save(os.path.join(UPLOAD_DIR, filename))
图形验证码
pip install pillow
util.py 生成图形验证码
import random
# pillow模块
from PIL import Image, ImageFont, ImageDraw, ImageFilter
# 随机颜色
def get_random_color():
return (random.randint(0, 255), random.randint(0, 255), random.randint(0, 255))
# 生成一张图片
def generate_image(length):
s = 'qwerQWERTYUIOtyuiopas123456dfghjklPASDFGHJKLZXCVBNMxcvbnm7890'
size = (130, 35)
# 创建画布背景 mode, size, color
im = Image.new('RGB', size, color=get_random_color())
# im.show()
# 创建字体 font, size=1, index, encoding, layout_engine windows C:\Windows\Fonts
font = ImageFont.truetype('font/GILLUBCD.TTF', size=25)
# 创建ImageDraw对象
draw = ImageDraw.Draw(im)
# 绘制验证码
code = ''
for i in range(length):
# 选择其中一个字母
c = random.choice(s)
# 拼接
code += c
# 位置 内容 颜色 字体
draw.text((9+random.randint(4,7)+25*i, 1),
text=c,
fill=get_random_color(),
font=font)
# 绘制干扰线
for i in range(5):
x1 = random.randint(0, 130)
y1 = random.randint(0, 50 / 2)
x2 = random.randint(0, 130)
y2 = random.randint(50 / 2, 50)
# xy, fill
draw.line(((x1, y1), (x2, y2)), fill=get_random_color())
# im.show( )
# 加滤镜 增强EDGE_ENHANCE 模糊BLUR
im = im.filter(ImageFilter.EDGE_ENHANCE)
# im.show( )
return im, code
if __name__ == '__main__':
generate_image(4)
bootstrap与wtfrom同时使用
{% extends 'bootstrap/base.html' %}
{# 为了和form匹配使用,需要导入 #}
{% import 'bootstrap/wtf.html' as wtf %}
{% block styles %}
{{ super() }}
{% endblock %}
{% block content %}
<form action="{{ url_for('hello_world') }}" method="post" enctype="multipart/form-data">
{# 产生CSRF token #}
{{ uform.hidden_tag() }}
{{ wtf.form_errors(uform, hiddens="only") }}
{{ wtf.form_field(uform.name,form_type="basic", horizontal_columns=('lg', 2, 10)) }}
{{ wtf.form_field(uform.password) }}
{{ wtf.form_field(uform.confirm_pwd) }}
{{ wtf.form_field(uform.phone) }}
{{ wtf.form_field(uform.icon) }}
{{ wtf.form_field(uform.recaptcha) }}
<p> <img src="{{ url_for('get_image') }}" id="img"></p>
<p><input type="submit" value="提交"></p>
</form>
{% endblock %}
{% block scripts %}
{{ super() }}
<script>
// 单击图片重新加载图片 ran= 向服务器表示两次请求不同 否则服务器会返回与之前相同的结果
$('#img').click(function () {
$(this).attr('src', "{{ url_for('get_image') }}?ran=" + Math.random());
})
</script>
{% endblock %}
项目上传https://github.com/zxy1013/flask_form