1 学习目标
- 能够说出Flask中模板代码复用的三种方式
- 能够使用代码实现模板继承的功能
- 能够说出可以在模板中直接使用的 Flask 变量和函数
- 能够使用 Flask-WTF 扩展实现注册表单
- 能够说出 CSRF 攻击的原理
2 模板代码复用
在模板中,可能会遇到以下情况:
- 多个模板具有完全相同的顶部和底部内容
- 多个模板中具有相同的模板代码内容,但是内容中部分值不一样
- 多个模板中具有完全相同的 html 代码块内容
像遇到这种情况,可以使用 JinJa2 模板中的宏、继承、包含来进行实现
2.1 宏
对宏(macro)的理解:
- 把它看作 Jinja2 中的一个函数,它会返回一个模板或者 HTML 字符串
- 为了避免反复地编写同样的模板代码,出现代码冗余,可以把他们写成函数以进行重用
- 需要在多处重复使用的模板代码片段可以写入单独的文件,再包含在所有模板中,以避免重复
2.1.1 使用
- 定义宏
{% macro input(name,value='',type='text') %}
<input type="{{ type }}" name="{{ name }}" value="{{ value }}" class="form-control">
{% endmacro %}
- 调用宏
{{ input('name' value='zs') }}
- 这会输出
<input type="text" name="name" value="zs" class="form-control">
- 把宏单独抽取出来,封装成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() %}
2.1.2 代码演练
- 使用宏之前代码
<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 input(label="", type="text", name="", value="") %}
<label>{{ label }}</label><input type="{{ type }}" name="{{ name }}" value="{{ value }}">
{% endmacro %}
- 使用宏
<form>
{{ input("用户名:", name="username") }}<br/>
{{ input("身份证号:", name="idcard") }}<br/>
{{ input("密码:", type="password", name="password") }}<br/>
{{ input("确认密码:", type="password", name="password2") }}<br/>
{{ input(type="submit", value="注册") }}
</form>
- 结果展示:
注:也可以将宏定义到外部的html中,导入后再使用(如下图)
2.2 模板继承
模板继承是为了重用模板中的公共内容。一般Web开发中,继承主要使用在网站的顶部菜单、底部。这些内容可以定义在父模板中,子模板直接继承,而不需要重复书写。
- 标签定义的内容
{% block content %}
内容
{% endblock content %}
- 相当于在父模板中挖个坑,当子模板继承父模板时,可以进行填充。
- 子模板使用 extends 指令声明这个模板继承自哪个模板
- 父模板中定义的块在子模板中被重新定义,在子模板中调用父模板的内容可以使用super()
2.2.1 父模板
base.html
{% block top %}
顶部内容
{% endblock top %}
{% block content %}
父类中间内容
{% endblock content %}
{% block bottom %}
底部内容
{% endblock bottom %}
2.2.2 子模板
extends 指令声明这个模板继承自哪
{% extends 'base.html' %}
{% block content %}
子类中间需要填充的内容
{% endblock content %}
模板继承使用时注意点:
- 不支持多继承
- 为了便于阅读,在子模板中使用extends时,尽量写在模板的第一行。
- 不能在一个模板文件中定义多个相同名字的block标签。
- 当在页面中使用多个block标签时,建议给结束标签起个名字,当多个block嵌套时,阅读性更好。
那如果子模板中还想使用父模板坑中内容怎么办? 如下:调用super()
是否调用super() 总结:
如果是子模板要重写父模板的坑,那就不调用
如果是子模板要在父模板的基础上增加内容,那就调用
2.3 继承模板抽取演练
2.3.1 demo准备
- 我们先准备一个demo,将现成的一个案例拿过来
- 访问:注意这是静态文件,可以直接访问的
- 接下来我们就需要将静态html变为模板文件了:
分析之前的俩界面的内容:有头和脚是相同的,但是也有些不同的,比如详情的头是没有标题的
2.3.2 抽取
接下来我们来抽取:我们将 index 的所有内容copy到 news_base.html 中 (注意:这里也可以将 detail 的所有内容copy到news_base.html中)
然后分析不同之处: 将不同之处在base中变为block。
发现网页标题不一样:
发现有js不一样:
挖坑:
然后将剩下内容都折叠起来对比一下,发现结构都是一样的:
然后主要是中间的内容不一样:
将base中的这个ul干掉,变为坑:
接下来让 index 继承 base:
其俩坑好填:
主要来看一下内容这个坑:
处理后的结果如下:
接下来增加视图函数,访问:
访问:
处理 detial 和 处理 index 逻辑一样,就不在赘述,请读者自己考虑。
最终结果如下:
友情提示:这里不要着急把源代码删了,后面可能还会用到
注意这里的区别:conter_con里的内容是 一个div
访问:(发现问题:多了一个标题,少了一个发布者)
少的发布者效果如下:
2.3.3 解决问题
先来处理多出来的头的标题:
这个是index独有的,那就不应该放在base中:
处理如下:
注意:将这个选中内容剪切出去,base中不能有
index最终如下:
访问detail: (发现没有头的标题了)
但是还少一个发布者,这个发布者是detail独有的,也不能在base中,所以base需要留一个坑,先找到这个发布者效果的代码如下:
base留坑:
将发布者的代码,放到detail的坑中:
访问:没问题了
首页也没有问题:
2.4 包含
2.4.1 简介
Jinja2模板中,除了宏和继承,还支持一种代码重用的功能叫包含(Include)。它的功能是将另一个模板整个加载到当前模板中,并直接渲染。
- include的使用
{% include 'hello.html' %}
包含在使用时,如果包含的模板文件不存在时,程序会抛出TemplateNotFound异常,可以加上 ignore missing
关键字。如果包含的模板文件不存在,会忽略这条include语句。
- include 的使用加上关键字 ignore missing
{% include 'hello.html' ignore missing %}
2.4.2 代码演练
- 新建函数:
from flask import Flask, render_template
app = Flask(__name__)
@app.route("/")
def index():
return "index"
@app.route("/include")
def include():
return render_template("demo5_include.html")
if __name__ == '__main__':
app.run(debug=True)
-
新建include模板(这个模板是要被别人包含)文件名为:included.html,代码如下:
<h3>拼个天翻地覆,搏个无怨无悔</h3>
-
新建模板,并使用包含模板,代码如下:
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<title>Title</title>
</head>
<body>
hello, world!<br/>
{% include "included.html" %}<br/>
{% include "included.html" %}<br/>
{% include "included.html" %}<br/>
</body>
</html>
小技巧: 光标放在相应行,Ctrl + d 复制本行,Ctrl + y 删除本行
- 结果如下:
- 如果写错了,就会报错:
{% include "included22222.html" %}<br/>
- 可以忽略这个错误:
ignore missing:忽略丢失,刚刚的错误其实就是找不到的错误
效果如下:
2.4 小结
- 宏(Macro)、继承(Block)、包含(include)均能实现代码的复用。
- 继承(Block)的本质是代码替换,一般用来实现多个页面中重复不变的区域。
- 宏(Macro)的功能类似函数,可以传入参数,需要定义、调用。
- 包含(include)是直接将目标模板文件整个渲染出来。
3 模板中特有的变量和函数
你可以在自己的模板中访问一些 Flask 默认内置的函数和对象
- config
你可以从模板中直接访问Flask当前的config对象:
{{ config.DEBUG }}
- request
就是flask中代表当前请求的request对象:
{{ request.url }}
- session
为Flask的session对象
{{ session.name }}
- g变量
在视图函数中设置g变量的 name 属性的值,然后在模板中直接可以取出
{{ g.name }}
- url_for()
url_for会根据传入的路由器函数名,返回该路由对应的URL,在模板中始终使用url_for()就可以安全的修改路由绑定的URL,则不比担心模板中渲染出错的链接:
{{ url_for('index') }}
如果我们定义的路由URL是带有参数的,则可以把它们作为关键字参数传入url_for(),Flask会把他们填充进最终生成的URL中:
{{ url_for('post', post_id=1) }}
- get_flashed_messages()
这个函数会返回之前在flask中通过flask()传入的消息的列表,flash函数的作用很简单,可以把由Python字符串表示的消息加入一个消息队列中,再使用get_flashed_message()函数取出它们并消费掉:
{% for message in get_flashed_messages() %}
{{ message }}
{% endfor %}
完整代码如下:
# demo6_special.py
from flask import Flask, render_template, session, g, flash
app = Flask(__name__)
app.secret_key = "apollo_miracle"
@app.route("/")
def index():
return "index"
@app.route("/demo")
def demo():
session.name = "apollo"
g.name = "miracle"
flash("我是闪现信息")
return render_template("demo6_special.html")
if __name__ == '__main__':
app.run(debug=True)
{# demo6_special.html #}
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<title>Title</title>
</head>
<body>
config.DEBUG --> {{ config.DEBUG }}<br/>
request.url --> {{ request.url }}<br/>
session.name --> {{ session.name }}<br/>
g.name --> {{ g.name }}<br/>
url_for('index') --> {{ url_for('index') }}<br/>
<a href="{{ url_for("index") }}">返回首页</a><br/>
闪现信息 --> {% for message in get_flashed_messages() %}
{{ message }}<br/>
{% endfor %}
</body>
</html>
效果如下:
4 Web表单
Web 表单是 Web 应用程序的基本功能。
它是HTML页面中负责数据采集的部件。表单有三个部分组成:表单标签、表单域、表单按钮。表单允许用户输入数据,负责HTML页面数据采集,通过表单将用户输入的数据提交给服务器。
在Flask中,为了处理web表单,我们可以使用 Flask-WTF 扩展,它封装了 WTForms,并且它有验证表单数据的功能
4.1 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 | 一组指定类型的字段 |
4.2 WTForms常用验证函数
验证函数 | 说明 |
---|---|
DataRequired | 确保字段中有数据 |
EqualTo | 比较两个字段的值,常用于比较两次密码输入 |
Length | 验证输入的字符串长度 |
NumberRange | 验证输入的值在数字范围内 |
URL | 验证URL |
AnyOf | 验证输入值在可选列表中 |
NoneOf | 验证输入值不在可选列表中 |
使用 Flask-WTF 需要配置参数 SECRET_KEY。
CSRF_ENABLED是为了CSRF(跨站请求伪造)保护。 SECRET_KEY用来生成加密令牌,当CSRF激活的时候,该设置会根据设置的密匙生成加密令牌。
4.3 代码验证
4.3.1 使用 html 自带的表单
- 创建模板文件demo7_wtf.html ,在其中直接写form表单:
<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 %}
- 视图函数中获取表单数据验证登录逻辑:(之前元素的表单验证,是自己写验证逻辑)
@app.route("/register", methods=["GET", "POST"])
def register():
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("demo7_wtf.html")
4.3.2 使用 Flask-WTF 实现表单
- 使用wtf来实现表单
# 自定义注册表单
class RegisterForm(FlaskForm):
username = StringField("用户名:", validators=[InputRequired("请输入用户名")], render_kw={"placeholder": "我是占位文字"})
password = PasswordField("密码:", validators=[InputRequired("请输入密码")])
password2 = PasswordField("确认密码:", validators=[InputRequired("请输入确认密码"), EqualTo("password", "两次密码要一致")])
submit = SubmitField("注册")
代码说明:
validators:调用`validate`时调用的一系列验证器
InputRequired(“请输入用户名”):是必填的意思。如果用户没有填写内容,就提示”请输入用户名”
EqualTo是做相等判断(判断 password 与 password2 是否相等),如果不相等,就提示”两次密码要一致”
render_kw:如果提供,则提供默认关键字的字典,将在渲染时提供给窗口小部件
placeholder:占位文字
- 进行表单验证
@app.route("/register_wtf", methods=["GET", "POST"])
def register_wtf():
register_form = RegisterForm()
# 使用 wtf表单帮我们做验证
if register_form.validate_on_submit():
# 执行注册逻辑
# 取到表单中提交上来的三个参数
# 取值方式1
username = request.form.get("username")
password = request.form.get("password")
password2 = request.form.get("password2")
# 取值方式2
# username = register_form.username.data
# 假装做注册操作
print(username, password, password2)
return "success"
else:
if request.method == "POST":
flash('参数错误')
return render_template("demo7_wtf.html", form=register_form)
代码说明:
代码中有一个validate_on_submit()意思是点击注册(submit)按钮的时候会校验,校验规则如上(代码说明)
- 访问时,两次密码输入一致,点击注册,断点调试如下:
- 发现没有严重通过,而是有错误:
这个错误是确实csrftoken , 这个目前我们不处理
先做如下设置:
- 配置参数,关闭 CSRF 校验
app.config['WTF_CSRF_ENABLED'] = False
- 再次访问即没问题。
CSRF:跨站请求伪造,后续会讲到
- 模板页面:
<!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>
{# get_flashed_messages() 是函数,不能忘记后边的括号 #}
{% for message in get_flashed_messages() %}
{{ message }}
{% endfor %}
<hr>
<h3>以下表单通过 WTF 来实现:</h3><br/>
<form method="post">
{{ form.username.label }}{{ form.username }}<br/>
{{ form.password.label }}{{ form.password }}<br/>
{{ form.password2.label }}{{ form.password2 }}<br/>
{{ form.submit }}
</form>
</body>
</html>
- 视图函数:
from flask import Flask, render_template, request, flash
from flask_wtf import FlaskForm
from wtforms import StringField, PasswordField, SubmitField
from wtforms.validators import InputRequired, EqualTo
app = Flask(__name__)
# 关闭csrf验证
app.config['WTF_CSRF_ENABLED'] = False
app.secret_key = "apollo_miracle"
# 自定义注册表单
class RegisterForm(FlaskForm):
username = StringField("用户名:", validators=[InputRequired("请输入用户名")], render_kw={"placeholder": "我是占位文字"})
password = PasswordField("密码:", validators=[InputRequired("请输入密码")])
password2 = PasswordField("确认密码:", validators=[InputRequired("请输入确认密码"), EqualTo("password", "两次密码要一致")])
submit = SubmitField("注册")
@app.route("/")
def index():
return "index"
@app.route("/register", methods=["GET", "POST"])
def register():
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("demo7_wtf.html")
@app.route("/register_wtf", methods=["GET", "POST"])
def register_wtf():
register_form = RegisterForm()
# 使用 wtf表单帮我们做验证
if register_form.validate_on_submit():
# 执行注册逻辑
# 取到表单中提交上来的三个参数
# 取值方式1
username = request.form.get("username")
password = request.form.get("password")
password2 = request.form.get("password2")
# 取值方式2
# username = register_form.username.data
# 假装做注册操作
print(username, password, password2)
return "success"
else:
if request.method == "POST":
flash('参数错误')
return render_template("demo7_wtf.html", form=register_form)
if __name__ == '__main__':
app.run(debug=True)
- 测试结果:
5 CSRF 原理分析
5.1 CSRF 简介
CSRF
全拼为Cross Site Request Forgery
,译为跨站请求伪造。
CSRF
指攻击者盗用了你的身份,以你的名义发送恶意请求。
- 包括:以你名义发送邮件,发消息,盗取你的账号,甚至于购买商品,虚拟货币转账......
造成的问题:个人隐私泄露以及财产安全。
5.2 CSRF攻击示意图
客户端访问服务器时没有同服务器做安全验证
5.3 防止 CSRF 攻击
5.3.1 步骤
- 在客户端向后端请求界面数据的时候,后端会往响应中的 cookie 中设置 csrf_token 的值
- 在 Form 表单中添加一个隐藏的的字段,值也是 csrf_token
- 在用户点击提交的时候,会带上这两个值向后台发起请求
- 后端接受到请求,以会以下几件事件:
- 从 cookie中取出 csrf_token
- 从表单数据中取出来隐藏的 csrf_token 的值
- 进行对比
- 如果比较之后两值一样,那么代表是正常的请求,如果没取到或者比较不一样,代表不是正常的请求,不执行下一步操作
5.3.2 未进行 csrf 校验的网站代码演示
未进行 csrf 校验的 WebA
- 后端代码实现
from flask import Flask, render_template, make_response
from flask import redirect
from flask import request
from flask import url_for
app = Flask(__name__)
@app.route('/', methods=["POST", "GET"])
def index():
if request.method == "POST":
# 取到表单中提交上来的参数
username = request.form.get("username")
password = request.form.get("password")
if not all([username, password]):
print('参数错误')
else:
print(username, password)
if username == 'laowang' and password == '1234':
# 状态保持,设置用户名到cookie中表示登录成功
response = redirect(url_for('transfer'))
response.set_cookie('username', username)
return response
else:
print('密码错误')
return render_template('temp_login.html')
@app.route('/transfer', methods=["POST", "GET"])
def transfer():
# 从cookie中取到用户名
username = request.cookies.get('username', None)
# 如果没有取到,代表没有登录
if not username:
return redirect(url_for('index'))
if request.method == "POST":
to_account = request.form.get("to_account")
money = request.form.get("money")
print('假装执行转操作,将当前登录用户的钱转账到指定账户')
return '转账 %s 元到 %s 成功' % (money, to_account)
# 渲染转换页面
response = make_response(render_template('temp_transfer.html'))
return response
if __name__ == '__main__':
app.run(debug=True, port=9000)
- 前端登录页面代码
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<title>登录</title>
</head>
<body>
<h1>我是网站A,登录页面</h1>
<form method="post">
<label>用户名:</label><input type="text" name="username" placeholder="请输入用户名"><br/>
<label>密码:</label><input type="password" name="password" placeholder="请输入密码"><br/>
<input type="submit" value="登录">
</form>
</body>
</html>
- 前端转账页面代码
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<title>转账</title>
</head>
<body>
<h1>我是网站A,转账页面</h1>
<form method="post">
<label>账户:</label><input type="text" name="to_account" placeholder="请输入要转账的账户"><br/>
<label>金额:</label><input type="number" name="money" placeholder="请输入转账金额"><br/>
<input type="submit" value="转账">
</form>
</body>
</html>
运行测试,如果在未登录的情况下,不能直接进入转账页面,测试转账是成功的
伪造逻辑(csrf发生过程)
首先,他先来了解了一下你的转账界面:
知道了你转账的请求的url,就是当前界面,转账所带的参数有to_account和money,接下来他写了如下攻击程序,demo代码如下:
代码核心:
这里也有一个表单,里边请求的 action 是转账的网址,参数、请求方式、啥的都一样,但是to_account就是黑客自己写的了,金钱也是黑客自己写的(意思就是黑客想给谁转多少钱都可以了)
攻击网站B的代码
- 后端代码实现
from flask import Flask
from flask import render_template
app = Flask(__name__)
@app.route('/')
def index():
return render_template('temp_index.html')
if __name__ == '__main__':
app.run(debug=True, port=8000)
- 前端代码实现
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<title>Title</title>
</head>
<body>
<h1>我是网站B</h1>
<form method="post" action="http://127.0.0.1:9000/transfer">
<input type="hidden" name="to_account" value="999999">
<input type="hidden" name="money" value="190000" hidden>
<input type="submit" value="点击领取优惠券">
</form>
</body>
</html>
运行测试,在用户登录网站A的情况下,点击网站B的按钮,可以实现伪造访问
访问网站B:
点击领取优惠劵按钮:
发现竟然转账成功了(因为我们转账的网站,已经在当前浏览器登录,已经有cookie了)
5.3.3 在网站A中模拟实现 csrf_token 校验的流程
- 添加生成 csrf_token 的函数
# 生成 csrf_token 函数
def generate_csrf():
return bytes.decode(base64.b64encode(os.urandom(48)))
在渲染转账页面的,做以下几件事情:
- 生成 csrf_token 的值
- 在返回转账页面的响应里面设置 csrf_token 到 cookie 中
- 将 csrf_token 保存到表单的隐藏字段中
@app.route('/transfer', methods=["POST", "GET"])
def transfer():
...
# 生成 csrf_token 的值
csrf_token = generate_csrf()
# 渲染转换页面,传入 csrf_token 到模板中
response = make_response(render_template('temp_transfer.html', csrf_token=csrf_token))
# 设置csrf_token到cookie中,用于提交校验
response.set_cookie('csrf_token', csrf_token)
return response
- 在转账模板表单中添加 csrf_token 隐藏字段
<form method="post">
<input type="hidden" name="csrf_token" value="{{ csrf_token }}">
<label>账户:</label><input type="text" name="to_account" placeholder="请输入要转账的账户"><br/>
<label>金额:</label><input type="number" name="money" placeholder="请输入转账金额"><br/>
<input type="submit" value="转账">
</form>
- 运行测试,进入到转账页面之后,查看 cookie 和 html 源代码
多次刷新,发现这个csrf_token是变化的
- 在执行转账逻辑之前进行 csrf_token 的校验
if request.method == "POST":
to_account = request.form.get("to_account")
money = request.form.get("money")
# 取出表单中的 csrf_token
form_csrf_token = request.form.get("csrf_token")
# 取出 cookie 中的 csrf_token
cookie_csrf_token = request.cookies.get("csrf_token")
# 进行对比
if cookie_csrf_token != form_csrf_token:
return 'token校验失败,可能是非法操作'
print('假装执行转操作,将当前登录用户的钱转账到指定账户')
return '转账 %s 元到 %s 成功' % (money, to_account)
运行测试,用户直接在网站 A 操作没有问题,再去网站B进行操作,发现转账不成功,因为网站 B 获取不到表单中的 csrf_token 的隐藏字段,而且浏览器有同源策略,网站B是获取不到网站A的 cookie 的,所以就解决了跨站请求伪造的问题
5.3.4 csrf 校验的网站代码再度优化
- 打开网站A页面,登录:
- 进行转账:
- 转账之后进行刷新:
- 每刷新一次,就会“转账 100 元到 6666666666 成功”一次,what????
代码改进:重新生成 csrf_token 或者 转账成功之后跳转页面
- 重新生成 csrf_token 代码如下:
- 再次刷新:
- 搞定。。。
6 在 Flask 项目中解决 CSRF 攻击
在 Flask 中, Flask-wtf 扩展有一套完善的 csrf 防护体系,对于我们开发者来说,使用起来非常简单
首先打开csrf验证:
app.config['WTF_CSRF_ENABLED'] = True
在表单中添加隐藏域:csrf_token:
运行,访问后查看源码:
并且查看cookie:
发现wtf将csrf存储到了session中,所以这里的这个cookie中存储的其实就相当于是一个session_id,然后需要根据这个session_id 去session 中取出对应的 scrf 然后进行校验。
你会发现这俩值不一样.
因为一个是真正的csrf_token值,一个是csrf_token对应的一个id。
单独使用
- 设置应用程序的 secret_key
- 用于加密生成的 csrf_token 的值
app.secret_key = "#此处可以写随机字符串#"
- 导入 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>
后续项目中会使用到此功能