03-模板(过滤器,代码复用,表单,CSRF)
模块代码复用
在模板中,可能会遇到以下情况:
- 多个模板具有完全相同的顶部和底部内容
- 多个模板中具有相同的模板代码内容,但是内容中部分值不一样
- 多个模板中具有完全相同的 html 代码块内容
像遇到这种情况,可以使用 JinJa2 模板中的 宏、继承、包含来进行实现
宏
对宏(macro)的理解:
- 把它看作 Jinja2 中的一个函数,它会返回一个模板或者 HTML 字符串
- 为了避免反复地编写同样的模板代码,出现代码冗余,可以把他们写成函数以进行重用
- 需要在多处重复使用的模板代码片段可以写入单独的文件,再包含在所有模板中,以避免重复
使用
定义宏:
{# 宏(macro) #} {% macro input(name,value='',type='text') %} <input type="{{type}}" name="{{name}}" value="{{value}}" class="form-control"> {% endmacro %}
调用宏:
{{ input('name' value='lst')}}
这样会输出:
<input type="text" name="name" value="lst" class="form-control">
把宏单独抽出来,封装成html文件,其他模块中导入使用,文件名可以自定义macro.html:
{# 单独抽出来封装成html文件,macro.html文件 #} {% macro function(type='text', name='', value='') %} <input type="{{type}}" name="{{name}}" value="{{value}}" class="form-control"> {% endmacro %}
在其他模块文件中先导入,在调用
{% import 'macro.html' as func %}
#在代码块使用宏函数
{{ func.function() }}
实际练习代码:
在使用宏之前的代码:
<form> <label>用户名:</label><input type="text" name="username"><br/> <label>身份证号:</label><input type="text" name="idcard"><br/> <label>密码:</label><input type="password" name="password"><br/> <label>确认密码:</label><input type="password" name="password2"><br/> <input type="submit" value="注册"> </form>
定义宏macro.html:
{#定义宏,相当于定义一个函数,在使用的时候直接调用该宏,传入不同的参数就可以了#} {% macro input(label="", type="text", name="", value="") %} {# 细心观察,就是将需要替换的值变成一个变量,然而这个值是自己传入 #} <label>{{ label }}</label><input type="{{ type }}" name="{{ name }}" value="{{ value }}"> {% endmacro %}
使用宏:
<!DOCTYPE html> <html lang="en"> <head> <meta charset="UTF-8"> <title>模板</title> </head> <body> {# 因为macro在其他文件中,因此需要先导入,在使用 #} {% import 'macro.html' as func %} <form> {{ func.input("用户名:", name="username") }}<br/> {{ func.input("身份证号:", name="idcard") }}<br/> {{ func.input("密码:", type="password", name="password") }}<br/> {{ func.input("确认密码:", type="password", name="password2") }}<br/> {{ func.input(type="submit", value="注册") }} </form> </body> </html>
模板继承
模板继承是为了重用模板中的公共内容。一般Web开发中,继承主要使用在网站的顶部菜单、底部。这些内容可以定义在父模板中,子模板直接继承,而不需要重复书写。
标签定义的内容:
{% block top %}
{% endblock %}
- 相当于在父模板中挖个坑,当子模板继承父模板时,可以进行填充。
- 子模板使用 extends 指令声明这个模板继承自哪个模板
- 父模板中定义的块在子模板中被重新定义,在子模板中调用父模板的内容可以使用super()
父模板:(base.html)
{% block top %} 顶部菜单 {% endblock top %} {% block content %}
{% endblock content %} {% block bottom %} 底部 {% endblock bottom %}
子模板:
extends指令声明这个模板继承自哪:
{% extends 'base.html' %} {% block content %} 需要填充的内容 {% endblock content %}
模板继承使用时注意点:
- 不支持多继承
- 为了便于阅读,在子模板中使用extends时,尽量写在模板的第一行。
- 不能在一个模板文件中定义多个相同名字的block标签。
- 当在页面中使用多个block标签时,建议给结束标签起个名字,当多个block嵌套时,阅读性更好。
包含
Jinja2模板中,除了宏和继承,还支持一种代码重用的功能,叫包含(Include)。它的功能是将另一个模板整个加载到当前模板中,并直接渲染。
include的使用:
{% include 'hello.html' %}
包含在使用时,如果包含的模板文件不存在时,程序会抛出TemplateNotFound异常,可以加上 ignore missing
关键字。如果包含的模板文件不存在,会忽略这条include语句。
include 的使用加上关键字ignore missing:
{% include 'hello.html' ignore missing %}
小结:
- 宏(Macro)、继承(Block)、包含(include)均能实现代码的复用。
- 继承(Block)的本质是代码替换,一般用来实现多个页面中重复不变的区域。
- 宏(Macro)的功能类似函数,可以传入参数,需要定义、调用。
- 包含(include)是直接将目标模板文件整个渲染出来。
模板中的特有变量和函数
你可以在自己的模板中访问一些Flask默认内置的函数和对象
config:
你可以从模板中直接访问Flask当前的config对象:
{{config.SQLALCHEMY_DATABASE_URI}}
sqlite:///database.db
request:
就是flask中代表当前请求的request对象:
{{request.url}} http://127.0.0.1
session:
为flask的session对象:
{{session.new}} True
g变量:
在视图函数中设置g变量的name属性的值,然后在模板中直接可以取出:
{{ g.name }}
url_for:
url_for会根据传入的路由器函数名,返回该路由对应的URL,在模板中始终使用url_for()就可以安全的修改路由绑定的URL,则不比担心模板中渲染出错的链接:
{{url_for('home')}} /
如果我们定义的路由URL是带有参数的,则可以把它们作为关键字参数传入url_for(),Flask会把他们填充进最终生成的URL中:
{{ url_for('post', post_id=1)}} /post/1
get_flashed_messages():
这个函数会返回之前在flask中通过flask()传入的消息的列表,flash函数的作用很简单,可以把由Python字符串表示的消息加入一个消息队列中,再使用get_flashed_message()函数取出它们并消费掉:
{%for message in get_flashed_messages()%} {{message}} {%endfor%}
Web表单
Web 表单是 Web 应用程序的基本功能。
它是HTML页面中负责数据采集的部件。表单有三个部分组成:表单标签、表单域、表单按钮。表单允许用户输入数据,负责HTML页面数据采集,通过表单将用户输入的数据提交给服务器。
在Flask中,为了处理web表单,我们可以使用 Flask-WTF 扩展,它封装了 WTForms,并且它有验证表单数据的功能
WTForms支持的HTML标准字段:
字段对象 | 说明 |
---|---|
StringField | 文本字段 |
TextAreaField | 多行文本字段 |
PasswordField | 密码文本字段 |
HiddenField | 隐藏文件字段 |
DateField | 文本字段,值为 datetime.date 文本格式 |
DateTimeField | 文本字段,值为 datetime.datetime 文本格式 |
IntegerField | 文本字段,值为整数 |
DecimalField | 文本字段,值为decimal.Decimal |
FloatField | 文本字段,值为浮点数 |
BooleanField | 复选框,值为True 和 False |
RadioField | 一组单选框 |
SelectField | 下拉列表 |
SelectMutipleField | 下拉列表,可选择多个值 |
FileField | 文件上传字段 |
SubmitField | 表单提交按钮 |
FormField | 把表单作为字段嵌入另一个表单 |
FieldList | 一组指定类型的字段 |
WTForms常用验证函数
验证函数 | 说明 |
---|---|
DataRequired | 确保字段中有数据 |
EqualTo | 比较两个字段的值,常用于比较两次密码输入 |
Length | 验证输入的字符串长度 |
NumberRange | 验证输入的值在数字范围内 |
URL | 验证URL |
AnyOf | 验证输入值在可选列表中 |
NoneOf | 验证输入值不在可选列表中 |
使用Flask-WTF需要配置SECRET_KEY。
CSRF_ENABLED是为了CSRF(跨站请求伪造)保护。 SECRET_KEY用来生成加密令牌,当CSRF激活的时候,该设置会根据设置的密匙生成加密令牌。
代码验证:
使用html自带的表单来验证:
1、创建模板文件login.html,在其中直接写form表单:
<!DOCTYPE html> <html lang="en"> <head> <meta charset="UTF-8"> <title>Title</title> </head> <body> <form method="post"> <label>用户名:</label><input type="text" name="username" placeholder="请输入用户名"><br/> <label>密码:</label><input type="password" name="password" placeholder="请输入密码"><br/> <label>确认密码:</label><input type="password" name="password2" placeholder="请输入确认密码"><br/> <input type="submit" value="注册"> </form> {% for message in get_flashed_messages() %} {{ message }} {% endfor %} </body> </html>
2、在视图中的逻辑处理:
from flask import Flask,render_template,request,flash app = Flask(__name__) #需要先配置参数SECRET_KEY 不然访问会出问题 app.config["SECRET_KEY"] = "lishuntaolovemyself" @app.route('/') def hello_world(): #渲染模板 return render_template("temp_demo1.html") @app.route("/demo1",methods=["get","post"]) def demo1(): if request.method == "POST": #取到表单数据提交上来的三个数 键值对 username = request.form.get("username") password = request.form.get("password") password2 = request.form.get("password2") #验证 密码是否全部又 if not all([username,password,password2]): #这个时候向前端发送信息,提示请填表格在发送 flash("请填完表格在提交") elif password != password2: flash("两次密码输入不一致") else: #可以说注册成功 print(username,password,password2) return "success!!!" return render_template("login.html") if __name__ == '__main__': app.run(port=8005,debug=True)
使用Flask-WTF
配置参数,关闭CSRF校验:
# 配置参数,关闭CSRF校验 app.config["WTF_CSRF_ENABLED"] = False
模板页面:
<!DOCTYPE html> <html lang="en"> <head> <meta charset="UTF-8"> <title>Title</title> </head> <body> <form method="post">
{# 点label就用label标签渲染第一个参数,点属性就生成input表格 #}
{{ form.username.label }} {{ form.username }}<br/> {{ form.password.label }} {{ form.password }}<br/> {{ form.password2.label }} {{ form.password2 }}<br/> {{ form.submit }} </form> </body> </html>
视图函数:
#导入Flask,渲染模板render_template,以及推送消息flash from flask import Flask,render_template,flash,request #导入wtf扩展的表单类 from flask_wtf import FlaskForm #导入自定义表单需要的字段 from wtforms import SubmitField,StringField,PasswordField #导入wtf扩展提供的表单验证器 from wtforms.validators import DataRequired,EqualTo #实例化app 参数告诉Flask模块包在哪里 app = Flask(__name__) #配置密钥口令牌 app.config["SECRET_KEY"] = "SECRET_KEY" #关闭CSRF验证 app.config['WTF_CSRF_ENABLED'] = False #自定义表单类,文本字段,密码字段,提交按钮 #自定义的表单类需要继承FlaskForm class RegisterForm(FlaskForm): #需要输入的注册信息,因为是用户,所以是字符串字段 验证器 #第一个参数是表格名字: username = StringField("用户名:",validators=[DataRequired("请输入用户名")],render_kw={"placeholder": "请输入用户名"}) password = PasswordField("密码:", validators=[DataRequired("请输入密码")]) #EqualTo("password", "两次密码不一致") 验证不成功,继续输入密码 password2 = PasswordField("确认密码:", validators=[DataRequired("请输入确认密码"), EqualTo("password", "两次密码不一致")]) submit = SubmitField("注册") """这是在HTML中的form表单渲染变量 第一个参数是表格名字:点label就用label标签渲染第一个参数 validators验证提示 并且是个列表传关键字参数 前面渲染label标签 这个渲染input标签 {{ form.username.label }} {{ form.username }}<br/> {{ form.password.label }} {{ form.password }}<br/> {{ form.password2.label }} {{ form.password2 }}<br/> {{ form.submit }} """ #定义由路由视图函数,生成表单对象,获取表单数据,进行表单数据验证 @app.route("/",methods=["get","post"]) def demo2(): #从这里实例化表单类 register_form = RegisterForm() #验证表单 if register_form.validate_on_submit(): #如果代码能走到这里的话,说明表单中的所有数据都能验证成功 #取出表单输入提交的值 username = request.form.get("username") password = request.form.get("password") password2 = request.form.get("password2") # 假装做注册操作 return "success" else: if request.method == "POST": flash("参数有误或者不完整") return render_template("temp_register.html",form=register_form) if __name__ == '__main__': app.run(debug=True,port=8080)
CSRF
CSRF
全拼为Cross Site Request Forgery
,译为跨站请求伪造。CSRF
指攻击者盗用了你的身份,以你的名义发送恶意请求。- 包括:以你名义发送邮件,发消息,盗取你的账号,甚至于购买商品,虚拟货币转账......
- 造成的问题:个人隐私泄露以及财产安全。
CSRF攻击示意图:
客户端访问服务器时没有同服务器做安全验证:
总结就是:用户访问其他攻击网站,攻击网站发出一个请求,获取浏览器的cookie信息,然后去访问带有cookie的网站,操作个人隐私操作,获取想要的价值信息或者钱财。
防止CSRF攻击
步骤:
- 在客户端向后端请求界面数据的时候,后端会往响应中的 cookie 中设置 csrf_token 的值
- 在 Form 表单中添加一个隐藏的的字段,值也是 csrf_token
- 在用户点击提交的时候,会带上这两个值向后台发起请求
- 后端接受到请求,以会以下几件事件:
- 从 cookie中取出 csrf_token
- 从 表单数据中取出来隐藏的 csrf_token 的值
- 进行对比
- 如果比较之后两值一样,那么代表是正常的请求,如果没取到或者比较不一样,代表不是正常的请求,不执行下一步操作
在Flask项目中解决CSRF攻击
在 Flask 中, Flask-wtf 扩展有一套完善的 csrf 防护体系,对于我们开发者来说,使用起来非常简单。
在FlaskForm中实现校验
1、设置应用程序的secret_key。用于加密生成的csrf_token的值。
app.secret_key = "#此处可以写随机字符串#"
在模板的表单中添加代码:
<form method="post"> {# 添加的csrf防护体系 #} {{ form.csrf_token() }} {{ form.username.label }} {{ form.username }}<br/> {{ form.password.label }} {{ form.password }}<br/> {{ form.password2.label }} {{ form.password2 }}<br/> {{ form.submit }} </form>
csrf_token会在前端渲染在form表单内。
单独使用:
1、设置应用程序的secret_key。用于加密生成的csrf_token的值。
app.secret_key = "#此处可以写随机字符串#"
2、导入 flask_wtf.csrf 中的 CSRFProtect 类,进行初始化,并在初始化的时候关联 app
from flask.ext.wtf import CSRFProtect CSRFProtect(app)
如果模板中有表单,不需要做任何事情。与之前一样
<form method="post"> {{ form.csrf_token }} ... </form>
如果没有表单,仍需要CSRF令牌:
<form method="post" action="/"> <input type="hidden" name="csrf_token" value="{{ csrf_token() }}" /> </form>