Flask 框架 - 模板 - 2

版权声明:本文为博主原创文章,未经博主允许不得转载。 https://blog.csdn.net/apollo_miracle/article/details/83018109

1 学习目标

  1. 能够说出Flask中模板代码复用的三种方式
  2. 能够使用代码实现模板继承的功能
  3. 能够说出可以在模板中直接使用的 Flask 变量和函数
  4. 能够使用 Flask-WTF 扩展实现注册表单
  5. 能够说出 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 小结

  1. 宏(Macro)、继承(Block)、包含(include)均能实现代码的复用。
  2. 继承(Block)的本质是代码替换,一般用来实现多个页面中重复不变的区域。
  3. 宏(Macro)的功能类似函数,可以传入参数,需要定义、调用。
  4. 包含(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 步骤

  1. 在客户端向后端请求界面数据的时候,后端会往响应中的 cookie 中设置 csrf_token 的值
  2. 在 Form 表单中添加一个隐藏的的字段,值也是 csrf_token
  3. 在用户点击提交的时候,会带上这两个值向后台发起请求
  4. 后端接受到请求,以会以下几件事件:
    • 从 cookie中取出 csrf_token
    • 从表单数据中取出来隐藏的 csrf_token 的值
    • 进行对比
  5. 如果比较之后两值一样,那么代表是正常的请求,如果没取到或者比较不一样,代表不是正常的请求,不执行下一步操作

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>

后续项目中会使用到此功能

猜你喜欢

转载自blog.csdn.net/apollo_miracle/article/details/83018109