文章目录
使用flask处理表单
传统的前端通用表单,需要前后端共同完成操作,前端需要使用form
标签来定义表单,而后端则需要使用request.form
来获取post
请求中的表单数据:
python
# 判断请求方式
if request.method == 'POST':
# 获取表单中name为username的文本域提交的数据
name = request.form.get('username')
# 获取表单中name为password的文本域提交的数据
password = request.form.get('password')
return name+" "+password
- 上述的方法既没有为表单提供保护措施,也不利于前后端分离的改进需求,固我们引入第三方扩展包:
flask-wtf
与wtforms
,来实现由后端单独完成的表单操作:
wtforms
安装:pip install wtforms
flask-wtf
安装:pip install Flask-WTF
或pip install flask-wtf
wtforms
依照功能类别来说wtforms
分别由以下几个类别:
Forms
: 主要用于表单验证、字段定义、HTML
生成,并把各种验证流程聚集在一起进行验证。Fields
: 包含各种类型的字段,主要负责渲染(生成HTML
文本域)和数据转换。Validator
:主要用于验证用户输入的数据的合法性。比如Length
验证器可以用于验证输入数据的长度。Widgets
:html
插件,允许使用者在字段中通过该字典自定义html
小部件。Meta
:用于使用者自定义wtforms
功能(配置),例如csrf
功能开启。Extensions
:丰富的扩展库,可以与其他框架结合使用,例如django
。
Flask-WTF
其实是对wtforms
的简单集成,也能通过添加动态token
令牌的方式,为所有Form
表单提供免受CSRF
(Cross-site request forgery
------跨站请求伪造)攻击的技术支持
我们可以采用以下方法来启用CSRF
保护:
- 定义配置文件,再将配置文件中的配置语句通过
app.config.from_object
(<配置对象>)或app.config.from_pyfile
(<'配置文件名'>)导入到flask
对象app
中,这个配置对象可以是配置模块也可以是配置类:
python
# config.py
CSRF_ENABLED = TRUE # 用于开启CSRF保护,但默认状态下都是开启的
SECRET_KEY = 'X1X2X3X4X5' # 用于生成动态令牌的秘钥
- 其中
SECRET_KEY
用于建立加密令牌token
,在我们编写程序时可以尽量定义的复杂一些;
python
from flask import Flask
from flask_wtf.csrf import CSRFProtect # 导入CSRFProtect模块
import config # 导入配置文件
app = Flask(__name__)
# 导入配置模块中的配置
app.config.from_object(config)
# 为当前应用程序启用WTF_CSRF保护,并返回一个CSRFProtect对象
csrf = CSRFProtect(app)
- 直接通过键值对的方式新增配置,即
app.config
['<配置名称>']=值添加配置到flask
对象app
中:
python
from flask import Flask
from flask_wtf.csrf import CSRFProtect # 导入CSRFProtect模块
app = Flask(__name__)
app.config['SECRET_KEY'] = 'ADJLAJDLA' # 用于生成动态令牌的秘钥
app.config['CSRF_ENABLED'] = True # 用于开启CSRF保护,但默认状态下都是开启的
# 为当前应用程序启用WTF_CSRF保护,并返回一个CSRFProtect对象
csrf = CSRFProtect(app)
- 除了使用上述方法来配置
CSRF
保护,我们还需要用到flask_wtf
与wtfroms
来定义一个支持CSRF
保护的后端表单,我们一般将其定义在一个类当中;
该类需要继承基类:flask_wtf.FlaskForm
或flask_wtf.Form
,二者完全相同,但Form
即将被FlaskForm
替换,推荐使用前者!
python
from flask import Flask,render_template,request
from flask_wtf.csrf import CSRFProtect
# 导入表单基类FlaskForm
from flask_wtf import FlaskForm
# 导入FlaskForm父类的表单字段组件(字符串文本域,密码文本域,提交按钮)
from wtforms import StringField,PasswordField,SubmitField
# 导入FlaskForm父类的表单验证组件(数据不为空,数据是否相同,数据长度)
from wtforms.validators import DataRequired,EqualTo,Length
app = Flask(__name__)
# 配置加密匙,后端为了保护网站加入的验证机制
# 不加会报错:RuntimeError: A secret key is required to use CSRF.
app.config['SECRET_KEY'] = 'ADJLAJDLA'
# app.config['CSRF_ENABLED'] = True # 可以省略
csrf = CSRFProtect(app)
# 定义表单模型类,继承FlaskForm
class Register(FlaskForm):
# 定义表单中的元素,类似于html的form中定义input标签下的内容
# label 用于点击后跳转到某一个指定的field框
# validators 用于接收一个验证操作列表
# render_kw 用于给表单字段添加属性,各属性以键值对的形式设置
user_name = StringField(label='用户名:',validators=[DataRequired(message=u'用户名不能为空'),Length(6,16,message='长度位于6~16之间')],render_kw={'placeholder':'输入用户名'})
# message中存放判断为错误时要返回的信息,EqualTo中第一个参数是要比较的field组件
password = PasswordField(label='密码:',validators=[DataRequired(message=u'密码不能为空'),EqualTo('password2',message=u'两次输入需相同'),Length(6,16,message='长度位于6~16之间')],render_kw={'placeholder':'输入密码'})
password2 = PasswordField(label='再次输入密码:', validators=[DataRequired(message=u'密码不能为空'),Length(6,16,message='长度位于6~16之间')],render_kw={'placeholder':'再次输入密码'})
submit = SubmitField(label='提交')
@app.route('/',methods=['GET','POST'])
def register():
# 实例化表单对象
form = Register()
if request.method == 'GET':
# 表单对象发送至前端
return render_template('register.html',form=form)
elif request.method == 'POST':
# form.validate_on_submit() 等价于:request.method=='post' and form.validate()
# form.validate() 用于验证表单的每个字段(控件),都满足时返回值为True
if form.validate_on_submit():
username = form.user_name.data
password = form.password.data
password2 = form.password2.data
return 'login success'
else:
# flask的form使用一个字典来储存各控件的errors列表
# print(type(form.errors))
# 输出密码字段导致validate_on_submit为false的错误原因(两种方式)
print(form.errors['password'])
print(form.password.errors)
return render_template('register.html',form=form)
if __name__ == '__main__':
app.run()
- 前端中使用后端定义的表单,同样也需要使用
jinja2
模板引擎,在{``{ }}
中调用我们传入的form
对象,且表单开头需要使用form.csrf_token
或form.hidden_tag()
语句添加动态令牌(用户不可见也不可编辑,用于验证)
html
<!DOCTYPE html>
<html lang="zh">
<head>
<meta charset="UTF-8">
<title>Flask_WTF</title>
<style type="text/css">
.div1 {
height:450px;
width:400px;
border:1px solid #8A8989;
margin:0 auto;
padding: 10px;
}
.input {
display: block;
width: 350px;
height: 40px;
margin: 10px auto;
}
.button{
background: #2066C5;
color: white;
font-size: 18px;
font-weight: bold;
height: 50px;
border-radius: 4px;
}
</style>
</head>
<body>
<div class="div1">
<form action="" method="post">
<!-- 启用CSRF验证,将token令牌置于表单内 -->
<!-- 不添加该字段,后端验证会一直为False -->
{{ form.csrf_token }}
{{ form.username.label }}
<!-- 可以在变量后添加括号,并在括号内设置变量的属性 -->
{{ form.username(class='input',id='name',size=16) }}
<!-- 错误展示 -->
{% for e in form.username.errors %}
<span style="color: red">{{ e }}</span>
{% endfor %}
<br>
{{ form.password.label }}
{{ form.password(class='input',id='pwd',size=16) }}
<!-- 错误展示 -->
{% for e in form.password.errors %}
<span style="color: red">{{ e }}</span>
{% endfor %}
<br>
{{ form.password2.label }}
{{ form.password2(class='input',id='pwd2',size=16) }}
<!-- 错误展示 -->
{% for e in form.password2.errors %}
<span style="color: red">{{ e }}</span>
{% endfor %}
<br>
{{ form.submit(class='button') }}
</form>
</div>
</body>
</html>
flash闪现的使用
导入:from flask import flash
后端的使用:flash("message")
,message
为消息内容;
前端通过遍历get_flashed_messages()
获取flash
消息内容;
示例代码(部分):
html
# --------------视图函数------------------
@app.route('/login/', methods=['GET', 'POST'])
def login():
if request.method == 'GET':
return render_template("flash.html")
else:
username = request.form.get('username')
password = request.form.get('password')
# user = User.query.filter(User.username == username, User.password == password).first()
user = User.query.filter(User.username == username).first()
if user and user.check_password(password):
session['user_id'] = user.id
session['user_name'] = user.username
session.permanent = True
return redirect(url_for("index"))
else:
flash('用户名或密码不正确,请检查!')
return render_template('flash.html')
# ---------------前端使用-----------------
<div class="warning">
{% for message in get_flashed_messages() %}
<div class="alert alert-warning alert-dismissible" role="alert">
<button type="button" class="close" data-dismiss="alert" aria-label="Close">
<span aria-hidden="true">×</span>
</button>
<strong>Warning!</strong> {{ message }}
</div>
{% endfor %}
</div>
Flask实现文件上传
文件上传指的是客户端将文件上传(post
请求发送)到服务器,服务器端进行保存的操作;
这一部分涉及到的库和拓展知识过多,将解释放在代码注释中,直接上代码再做简单的流程分析;
对于单文件的上传,主要用到flask_wtf.file
库下的上传字段类:FileField
,以及检验组件:FileRequired
和 FileAllowed
;
后端应用程序文件"flask_file.py
":
python
from flask import Flask,render_template,redirect,url_for
from flask import send_from_directory,session,flash
from flask_wtf import FlaskForm
from wtforms import SubmitField
from flask_wtf.file import FileField, FileRequired, FileAllowed
from flask_wtf.csrf import CSRFProtect
app = Flask(__name__)
app.config['SECRET_KEY'] = 'XASXA#@216.WDFAW'
csrf = CSRFProtect(app)
# 自定义表单类
class UploadForm(FlaskForm):
# flask_WTF中提供的上传字段,label仍是字段的标签
# validators接收的验证列表中:
# FileRequired()用于验证是否包含文件对象,可以设置参数message
# FileAllowed()用于验证文件的类型(后缀名),参数一为允许的文件后缀名的列表,参数二为可选的message
photo = FileField(label='Upload Image', validators=[FileRequired(), FileAllowed(['jpg','jpeg','png','gif'])])
# 采用了wtforms提供的提交字段,flask_WTF中似乎不包含该字段
submit = SubmitField()
import os
# 验证文件大小,可以通过修改配置,设置请求报文的最大长度
# 接收到超过长度的文件,服务器会中断连接,并返回413错误响应
app.config['MAX_CONTENT_LENGTH'] = 1*1024*1024
# root_path获取当前程序文件所处的目录,使用path.join将其与uploads连接形成上传路径
# 将形成的路径写入到flask程序的配置当中,上传到服务器的文件都会保存在当前目录下的uploads目录(需要手动预先创建)中
app.config['UPLOAD_PATH'] = os.path.join(app.root_path, 'uploads')
# 导入 通用唯一识别码 库
import uuid
# 自定义文件重命名方法
def random_filename(filename):
# os.path.splitext将文件路径与扩展名(文件类型标识)分开
# 这里ext获取了文件扩展名用于拼接生成新的文件名
ext = os.path.splitext(filename)[1]
# 将生成的随机uuid与扩展名结合,形成新的文件名
new_filename = uuid.uuid4().hex + ext
# 返回新的文件名
return new_filename
# 文件展示
@app.route('/uploaded-images')
def show_images():
# 响应显示文件的模板
return render_template('upload_file/uploaded.html')
# 获取文件路径
@app.route('/uploads/<path:filename>')
def get_file(filename):
# send_from_directory可以生成对应文件的下载链接
# 参数一是所有文件的存储目录(即uploads目录),参数二是该目录下要下载的文件名
return send_from_directory(app.config['UPLOAD_PATH'], filename)
# 主程序
@app.route('/upload', methods=['GET', 'POST'])
def upload():
# 实例化我们自定义的表单类UploadForm
form = UploadForm()
if form.validate_on_submit():
# 使用表单对象form.photo的data属性即可获取到上传的文件
f = form.photo.data
# 处理文件名,这里采用自定义的random_filename方法来实现
filename =random_filename(f.filename)
# 服务器端使用save方法来保存接收到的文件
# 读取配置中上传文件夹的路径,与文件名拼接后形成完整存储路径
f.save(os.path.join(app.config['UPLOAD_PATH'], filename))
# 使用flash通知用户文件上传成功
flash('Upload success.')
# 保存文件名到session,采用列表是为了后续拓展为多文件上传
session['filenames'] = [filename]
# 上传成功后显示图片,重定向到对应视图函数
return redirect(url_for('show_images'))
# 响应上传文件的模板,并把表单对象作为参数传递
return render_template('upload_file/upload.html', form = form)
Session的使用
session
是基于cookie
实现的,也就是说二者之间是存在紧密联系的
session
和cookie
都是由服务器生成的,都是用来存储特定的值(键值对应);
session
是存储在服务器的,而cookie
是会返回给客户端的。
返回形式:置于响应信息头 ------set-cookie
- 客户端(浏览器)在发送请求的时候,会自动将存活、可用的
cookie
封装在请求头中和请求一起发送。- 发送形式:置于请求信息头 ------
Cookie
cookie
和session
都是有其生命周期的;
cookie
的生命周期,一般来说,cookie
的生命周期受到两个因素的影响cookie
自身的存活时间,是服务器生成cookie
时设定的;- 客户端是否保留了
cookie
。客户端是否保留cookie
,只对客户端自身有影响,对其它封包工具是没有影响的。session
的生命周期,一般来说,session
的生命周期也是受到两个因素的影响:
服务器对于session
对象的保存的最大时间的设置。- 客户端进程是否关闭。客户端进程关闭,只对客户端自身有影响,对其它封包工具是没有影响的。
cookie
和session
都是有其作用域的。
为什么session比cookie安全?
cooke
是存储在客户端的,是(用户)可见的,是(用户)可以改变的;session
是存储在服务器端的,是(用户)不可见的,是(用户)不可改变的。
当客户端第一次请求session
对象时候,服务器会为客户端创建一个session
,并将通过特殊算法算出一个session
的ID
,用来标识该session
对象;
sessionID
是一次会话的key
,如果客户端的一次请求没有携带sessionID
,那么服务器端就会为这次会话创建一个session
用于存储内容,每个session
都有唯一的sessionID
;
设置session
- 服务器端接收到请求会自动创建
session
,我们可以通过session['name']='value'
方法来对session
内的值进行设置; name
即key
,是我们设置的变量名称;value
则是变量的值;
python
from flask import Flask,session
# 设置session
@app.route('/')
def set_session():
# 将 username=zhangsan 存储在session中
# session的存储与获取都是字典方式
session['username'] = 'zhangsan'
return 'Session设置成功!'
获取session的值
在设置了session
值后,我们有两种方法来获取我们设置的值,类似于字典的值的获取,推荐使用第二种:
① result = session['name']
→ 如果内容不存在会报错;
② result = session.get('name')
→ 如果内容不存在会返回None
;
删除session的值或清空session所有内容
也是与字典中的操作类似:
①删除单条session
值,可以采用session.pop('name')
方法;
②清空整个session
内容,则采用session.clear()
方法;
python
# 清除session
@app.route('/d')
def del_session():
# 删除session中username的这条记录
session.pop('username')
# 清空session
# session.clear()
return 'Session被删除!'
- 最终得到一个完整的服务器端
session
操作过程,代码如下:
python
from flask import Flask,session
from datetime import timedelta
app = Flask(__name__)
import os
# 使用os库下的urandom随机生成秘钥
app.config['SECRET_KEY'] = os.urandom(24)
# 配置session有效期为7天
app.config['PERMANENT_SESSION_LIFETIME'] = timedelta(days=7)
# 设置session
@app.route('/')
def set_session():
# 将 username=zhangsan 存储在session中
# session的存储与获取都是字典方式
session['username'] = 'zhangsan'
# 将有效期设为启用
session.permanent = True
return 'Session设置成功!'
# 获取session
@app.route('/g')
def get_session():
# 通过字典方式获取session中的username值
username = session.get('username')
return username or 'Session为空!'
# 清除session
@app.route('/d')
def del_session():
# 删除session中username的这条记录
session.pop('username')
# 清空session
# session.clear()
return 'Session被删除!'
if __name__ == '__main__':
app.run(debug=True)