Skip to content

34. 实践练习:第 14 版

版本 14 的 [http-servers/09] 文件夹是通过复制版本 13 的 [http-servers/08] 文件夹获得的。

34.1. 简介

CSRF(跨站请求伪造)是一种会话劫持技术。维基百科(https://fr.wikipedia.org/wiki/Cross-site_request_forgery)对此解释如下:

假设爱丽丝是某论坛的管理员,并通过会话系统登录。玛洛丽是该论坛的成员,想要删除其中一篇帖子。由于她的账户没有相应的权限,她便通过 CSRF 攻击利用了爱丽丝的账户。
  1. 玛洛丽设法找到了允许她删除该帖子的链接。
  2. 玛洛丽向爱丽丝发送了一条包含待显示伪图片(实际上是一个脚本)的消息。该图片的URL正是指向删除目标消息的脚本链接。
  3. 爱丽丝的浏览器中必须已打开玛洛莉目标网站的会话。这是攻击悄无声息地成功实施的先决条件,否则会触发身份验证请求,从而引起爱丽丝的警觉。该会话必须具备执行玛洛莉破坏性请求所需的权限。目标网站的浏览器标签页无需处于打开状态,甚至浏览器无需正在运行,只要会话处于活动状态即可。
  4. 爱丽丝阅读了玛洛丽的消息;她的浏览器利用了爱丽丝的已建立会话,并未请求交互式身份验证。浏览器尝试获取图片内容。在此过程中,浏览器触发了链接并删除了消息,随后将一个纯文本网页作为图片内容返回。 由于浏览器无法识别该图片的关联类型,因此不会显示图片,爱丽丝也并未意识到玛洛丽刚刚迫使她违背意愿删除了这条消息。

即便这样解释,CSRF 技术仍难以理解。让我们画个图示:

Image

  • [1-2]中,爱丽丝与论坛(网站A)进行通信。该论坛为每位用户维护一个会话。爱丽丝的浏览器将此会话cookie存储在本地,并在每次向网站A发出新请求时将其发回;
  • [3]中,玛洛莉向爱丽丝发送了一条消息。爱丽丝在浏览器中阅读了这条消息。该消息采用HTML格式,其中包含一个指向网站B上某张图片的链接。实际上,这个链接指向的是一段JavaScript脚本,该脚本会在到达爱丽丝的浏览器后自动运行;
  • 随后,该 JavaScript 脚本向网站 A 发起请求。爱丽丝的浏览器会自动将该请求连同本地存储的会话 Cookie 一并发送出去。攻击就在此时发生:玛洛莉已成功利用爱丽丝的会话凭据访问了网站 A。从这一刻起,无论后续发生什么,攻击都已得逞;

为防范此类攻击,网站 A 可采取以下措施:

  • 在与爱丽丝的每次交互[1-2]中,网站A都会发送一个密钥(以下简称CSRF令牌),爱丽丝必须在下次请求中返回该令牌。因此,每次请求时,爱丽丝必须发送两项信息:
    • 会话 Cookie;
    • 上次向网站 A 发送请求时在响应中收到的 CSRF 令牌;

保护机制的原理在于:虽然浏览器会自动将会话 Cookie 发回给网站 A,但不会自动发送 CSRF 令牌。因此,攻击脚本执行的第 6-7 次交互将被拒绝,因为第 6 次请求中并未包含 CSRF 令牌;

对于 HTML 应用程序,网站 A 可以通过多种方式向爱丽丝发送 CSRF 令牌:

  • 它可以在每次请求中发送一个HTML页面,其中所有链接都包含CSRF令牌,例如 [http://siteA/chemin/csrf_token]。当爱丽丝在下次请求中点击其中一个链接时,网站A只需从请求URL中提取CSRF令牌并验证其有效性。本文将采用此方法;
  • 对于包含表单的 HTML 页面,网站 A 可以发送一个包含隐藏字段 [input type='hidden'] 的表单,该字段中包含 CSRF 令牌。当爱丽丝提交页面时,该令牌将随表单自动提交。网站 A 将从请求主体中提取 CSRF 令牌;
  • 还有其他可能的技术;

34.2. 配置

Image

我们在应用程序的 [parameters] 配置中添加两个布尔值:

  • [with_redissession]:设置为 True 时,应用程序使用 Redis 会话;设置为 False 时,应用程序使用标准的 Flask 会话;
  • [with_csrftoken]:当设置为 True 时,应用程序的 URL 中包含一个 CSRF 令牌;

        # durée pause thread en secondes
        "sleep_time"0,
        # serveur Redis
        "with_redissession"True,
        "redis": {
            "host""127.0.0.1",
            "port"6379
        },
        # token csrf
        "with_csrftoken"False,

34.3. CSRF 实现

我们将确保在以下情况下:


config['parameters']['with_csrftoken']

设置为 [True] 时,应用程序发送给客户端浏览器的网页链接中将包含一个 CSRF 令牌。

34.3.1. [flask_wtf] 模块

CSRF 令牌将通过 [flask_wtf] 模块实现,我们可在 PyCharm 终端中安装该模块:


(venv) C:\Data\st-2020\dev\python\cours-2020\python3-flask-2020\packages>pip install flask_wtf
Collecting flask_wtf

34.3.2. 视图模板

我们在模型中引入了一个新类:

Image

[AbstractBaseModelForView] 类如下所示:

from abc import abstractmethod

from flask import Request
from flask_wtf.csrf import generate_csrf
from werkzeug.local import LocalProxy

from InterfaceModelForView import InterfaceModelForView

class AbstractBaseModelForView(InterfaceModelForView):

    @abstractmethod
    def get_model_for_view(self, request: Request, session: LocalProxy, config: dict, résultat: dict) -> dict:
        pass

    def get_csrftoken(self, config: dict):
        #  csrf_token
        if config['parameters']['with_csrftoken']:
            return f"/{generate_csrf()}"
        else:
            return ""
  • 第 9 行:[AbstractBaseModelForView] 类实现了由模型类实现的 [InterfaceModelForView] 接口;
  • 第 11–13 行:[get_model_for_view] 方法未实现;
  • 第 15–20 行:如果应用程序已配置为使用 CSRF 令牌,[get_csrftoken] 方法将生成 CSRF 令牌。根据具体情况,该函数会返回一个前面带斜杠 (/) 的令牌或一个空字符串。[generate_csrf] 函数对于给定的客户端请求总是生成相同的值。 处理请求涉及执行多个函数。在这些函数中使用 [generate_csrf] 始终会生成相同的值。但在下一次请求时,将生成一个新的 CSRF 令牌;

视图 V 的所有 M 模型都将按以下方式包含 CSRF 令牌:

class ModelForAuthentificationView(AbstractBaseModelForView):

    def get_model_for_view(self, request: Request, session: LocalProxy, config: dict, résultat: dict) -> dict:
        #  we encapsulate the paged data in the model
        modèle = {}
        

        #  csrf token
        modèle['csrf_token'] = super().get_csrftoken(config)

        #  we render the model
        return modèle
  • 每个模型类都继承自基类 [AbstractBaseModelForView]
  • 第 8 行:从父类请求 CSRF 令牌。 我们得到的要么是空字符串,要么是类似 [/Ijk4NjQ2ZDdjZjI0ZDJiYTVjZTZjYmFhZGNjMjE3Y2U5M2I3ODI0NzYi.Xy5Okg.n-kSR_nslkndfT7AFVy2UDtdb8c] 的字符串;

34.3.3. 视图

根据刚才的讨论,所有视图 V 都会在其模板 M 中包含 CSRF 令牌。因此,它们可以在其中的链接中使用该令牌。让我们看几个示例:

身份验证片段 [v_authentification.html]


<!-- form HTML - post its values with the [authenticate-user] action -->
<form method="post" action="/authentifier-utilisateur{{modèle.csrf_token}}">
 
    <!-- title -->
    <div class="alert alert-primary" role="alert">
        <h4>Veuillez vous authentifier</h4>
    </div>

 
</form>
  • 第 2 行:根据刚才所见,[action] 属性的 URL 将为:

[/authentifier-utilisateur/Ijk4NjQ2ZDdjZjI0ZDJiYTVjZTZjYmFhZGNjMjE3Y2U5M2I3ODI0NzYi.Xy5Okg.n-kSR_nslkndfT7AFVy2UDtdb8c]

[/authentifier-utilisateur]

这取决于应用程序是否已配置为使用 CSRF 令牌;

税费计算片段 [v-calcul-impot.html]


<!-- form HTML posted -->
<form method="post" action="/calculer-impot{{modèle.csrf_token}}">
    <!-- 12-column message on blue background -->
    <div class="col-md-12">
        <div class="alert alert-primary" role="alert">
            <h4>Remplissez le formulaire ci-dessous puis validez-le</h4>
        </div>
    </div>
    
</form>

模拟部分 [v-liste-simulations.html]


{% if modèle.simulations is undefined or modèle.simulations|length==0 %}
<!-- message on blue background -->
<div class="alert alert-primary" role="alert">
    <h4>Votre liste de simulations est vide</h4>
</div>
{% endif %}
 
{% if modèle.simulations is defined and modèle.simulations|length!=0 %}
<!-- message on blue background -->
<div class="alert alert-primary" role="alert">
    <h4>Liste de vos simulations</h4>
</div>
 
<!-- simulation table -->
<table class="table table-sm table-hover table-striped">
    
    <!-- table body (data displayed) -->
    <tbody>
    <!-- display each simulation by browsing the simulation table -->
    {% for simulation in modèle.simulations %}
 
    <!-- display a table row with 6 columns - <tr> tag -->
    <!-- column 1: row header (simulation no.) - <th scope='row' tag -->
    <!-- column 2: parameter value [married] - <td> tag -->
    <!-- column 3: parameter value [children] - <td> tag -->
    <!-- column 4: parameter value [salary] - <td> tag -->
    <!-- column 5: [tax] parameter value - <td> tag -->
    <!-- column 6: parameter value [surcôte] - <td> tag -->
    <!-- column 7: parameter value [discount] - <td> tag -->
    <!-- column 8: parameter value [reduction] - <td> tag -->
    <!-- column 9: parameter value [rate] (of tax) - <td> tag -->
    <!-- column 10: link to delete simulation - <td> tag -->
    <tr>
        <th scope="row">{{simulation.id}}</th>
        <td>{{simulation.marié}}</td>
        <td>{{simulation.enfants}}</td>
        <td>{{simulation.salaire}}</td>
        <td>{{simulation.impôt}}</td>
        <td>{{simulation.surcôte}}</td>
        <td>{{simulation.décôte}}</td>
        <td>{{simulation.réduction}}</td>
        <td>{{simulation.taux}}</td>
        <td><a href="/supprimer-simulation/{{simulation.id}}{{modèle.csrf_token}}">Supprimer</a></td>
    </tr>
    {% endfor %}
    </tr>
    </tbody>
</table>
{% endif %}

菜单代码片段 [v-menu.html]


<!-- bootstrap menu -->
<nav class="nav flex-column">
    <!-- display a list of links HTML -->
    {% for optionMenu in modèle.optionsMenu %}
    <a class="nav-link" href="{{optionMenu.url}}{{modèle.csrf_token}}">{{optionMenu.text}}</a>
    {% endfor %}
</nav>

34.3.4. 路由

目前有两种类型的路由,取决于它们是否使用 CSRF 令牌:

Image

  • [routes_without_csrftoken] 指不使用 CSRF 令牌的路由。这些是上一版本中的路由;
  • [routes_with_csrftoken] 是包含 CSRF 令牌的路由。

[routes_with_csrftoken] 中,路由现在多了一个参数,即 CSRF 令牌:

#  the front controller
def front_controller() -> tuple:
    #  forward the request to the main controller
    main_controller = config['mvc']['controllers']['main-controller']
    return main_controller.execute(request, session, config)

@app.route('/', methods=['GET'])
def index() -> tuple:
    #  redirect to /init-session/html
    return redirect(url_for("init_session", type_response="html", csrf_token=generate_csrf()), status.HTTP_302_FOUND)

#  init-session
@app.route('/init-session/<string:type_response>/<string:csrf_token>', methods=['GET'])
def init_session(type_response: str, csrf_token: str) -> tuple:
    #  execute the controller associated with the action
    return front_controller()

#  authenticate-user
@app.route('/authentifier-utilisateur/<string:csrf_token>', methods=['POST'])
def authentifier_utilisateur(csrf_token: str) -> tuple:
    #  execute the controller associated with the action
    return front_controller()

#  calculate-tax
@app.route('/calculer-impot/<string:csrf_token>', methods=['POST'])
def calculer_impot(csrf_token: str) -> tuple:
    #  execute the controller associated with the action
    return front_controller()

#  batch tax calculation
@app.route('/calculer-impots/<string:csrf_token>', methods=['POST'])
def calculer_impots(csrf_token: str):
    #  execute the controller associated with the action
    return front_controller()

#  lister-simulations
@app.route('/lister-simulations/<string:csrf_token>', methods=['GET'])
def lister_simulations(csrf_token: str) -> tuple:
    #  execute the controller associated with the action
    return front_controller()

#  delete-simulation
@app.route('/supprimer-simulation/<int:numero>/<string:csrf_token>', methods=['GET'])
def supprimer_simulation(numero: int, csrf_token: str) -> tuple:
    #  execute the controller associated with the action
    return front_controller()

#  end of session
@app.route('/fin-session/<string:csrf_token>', methods=['GET'])
def fin_session(csrf_token: str) -> tuple:
    #  execute the controller associated with the action
    return front_controller()

#  display-calculation-tax
@app.route('/afficher-calcul-impot/<string:csrf_token>', methods=['GET'])
def afficher_calcul_impot(csrf_token: str) -> tuple:
    #  execute the controller associated with the action
    return front_controller()

#  get-admindata
@app.route('/get-admindata/<string:csrf_token>', methods=['GET'])
def get_admindata(csrf_token: str) -> tuple:
    #  execute the controller associated with the action
    return front_controller()

现在所有路由的参数中都包含 CSRF 令牌,包括 [/init-session] 路由。这意味着客户端无法通过直接输入 URL [/init-session/html] 来启动应用程序,因为此时会缺少 CSRF 令牌。现在必须通过第 7–10 行中的 [/] URL 进行访问。

路由是在主脚本 [main] 中选定的:


#  the main thread no longer needs the logger
logger.close()

#  if there has been an error, we stop
if erreur:
    sys.exit(2)

#  import routes from web application
if config['parameters']['with_csrftoken']:
    import routes_with_csrftoken as routes
else:
    import routes_without_csrftoken as routes

#  route configuration
routes.config = config

#  start Flask application
routes.execute(__name__)
  • 第 9–13 行:根据应用程序是否使用 CSRF 令牌来选择路由;

34.3.5. [MainController]

对于每个请求,服务器必须验证 CSRF 令牌是否存在我们将在主控制器 [MainController] 中实现这一功能该控制器负责处理所有请求:

from flask_wtf.csrf import generate_csrf, validate_csrf

       #  we process the request
        try:
            #  logger
            logger = Logger(config['parameters']['logsFilename'])

            

            #  path elements are retrieved
            params = request.path.split('/')

            #  action is the 1st element
            action = params[1]

            

            if config['parameters']['with_csrftoken']:
                #  the csrf_token is the last element of the path
                csrf_token = params.pop()
                #  check token validity
                #  an exception will be thrown if the csrf_token is not the expected one
                validate_csrf(csrf_token)

            

        except ValidationError as exception:
            #  csrf token invalid
            résultat = {"action": action, "état": 121, "réponse": [f"{exception}"]}
            status_code = status.HTTP_400_BAD_REQUEST

        except BaseException as exception:
            #  other (unexpected) exceptions
            résultat = {"action": action, "état": 131, "réponse": [f"{exception}"]}
            status_code = status.HTTP_400_BAD_REQUEST

        finally:
            pass

        #  add the csrf_token to the result
        résultat['csrf_token'] = generate_csrf()

        #  we log the result sent to the customer
        log = f"[MainController] {résultat}\n"
        logger.write(log)
  • 第 20 行:从表单的请求 URL [http://machine:port/path/action/param1/param2/…/csrf_token] 中获取 CSRF 令牌。会话令牌总是 URL 的最后一个元素;
  • 第 23 行:将从 URL 获取的 CSRF 令牌与会话的 CSRF 令牌进行有效性校验。若无效,[validate_csrf] 函数将抛出 [ValidationError] 异常(第 27 行);
  • 第 41 行:将 CSRF 令牌包含在发送给客户端的响应中。JSON 和 XML 客户端需要该令牌。这是因为这些客户端收到的 HTML 页面中,链接内并不包含 CSRF 令牌。因此,它们将通过服务器发送的 JSON 或 XML 响应获取该令牌;

注意:第 23 行的 [validate_csrf] 函数并不检查是否完全匹配。CSRF 令牌存储在会话中,键名为 [csrf_token]。 测试结果表明,只要 CSRF 令牌是在当前会话期间生成的,它就是有效的。因此,如果你手动将浏览器中显示的 URL 中的 CSRF 令牌 [xyz](例如 (/lister-simulations/xyz))替换为之前在某次操作中接收到的另一个令牌 [abc],那么 [/lister-simulations] 操作将成功;

34.4. 浏览器测试

首先:

  • [with_csrftoken] 参数设为 [True] 的方式启动服务器;
  • 使用浏览器请求 URL [http://localhost:5000]

Image

  • [1] 中,CSRF 令牌;

让我们执行一些操作,直到获得一组模拟列表:

Image

现在,手动输入 URL [http://localhost:5000/supprimer-simulation/1/x] 以删除 id=1 的模拟。我们故意输入一个错误的 CSRF 令牌,以观察会发生什么。服务器的响应如下:

Image

注 1:不能确定此处使用的方法总能有效抵御 CSRF 攻击。让我们回到攻击示意图:

Image

如果在 [5] 处下载的 JavaScript 脚本能够读取爱丽丝使用的浏览器历史记录,它将能够获取浏览器执行过的 URL,例如 [/target/csrf_token]。随后,它便能获取会话令牌 [csrf_token] 并在 [6-7] 处实施攻击。然而,浏览器仅允许访问脚本运行所在的浏览器窗口的历史记录。 因此,如果爱丽丝没有使用同一个窗口与网站 A 进行交互 [1-2] 并阅读玛洛莉的消息 [3],CSRF 攻击将无法实现。

34.5. 控制台客户端

测试应用程序第 14 版另一种方法是复用第 12 版的测试用例,并将其适配到新服务器上。

Image

[impots/http-clients/09] 文件夹最初是通过复制 [impots/http-clients/07] 文件夹创建的,随后对其进行了修改。

让我们回到初始化会话的路由:

#  application root
@app.route('/', methods=['GET'])
def index() -> tuple:
    #  redirect to /init-session/html
    return redirect(url_for("init_session", type_response="html", csrf_token=generate_csrf()), status.HTTP_302_FOUND)

# init-session-with-csrf-token
@app.route('/init-session/<string:type_response>/<string:csrf_token>', methods=['GET'])
def init_session(type_response: str, csrf_token: str) -> tuple:
    #  execute the controller associated with the action
    return front_controller()

以下这些路由都不适合用于初始化 JSON 或 XML 会话:

  • 第 2–5 行:[/] 路由初始化的是 HTML 会话;
  • 第 8–11 行:[/init-session] 路由需要一个我们不知道的 CSRF 令牌;

我们决定在服务器上添加一个新路由:

1
2
3
4
5
#  init-session-without-csrftoken
@app.route('/init-session-without-csrftoken/<string:type_response>', methods=['GET'])
def init_session_without_csrftoken(type_response: str) -> tuple:
    #  redirect to /init-session/type_response
    return redirect(url_for("init_session", type_response=type_response, csrf_token=generate_csrf()), status.HTTP_302_FOUND)
  • 第 2 行:新的路由。它不期望 CSRF 令牌。因此,我们回到了上一版本的 [/init-session] 路由;
  • 第 4-5 行:我们将客户端(JSON、XML、HTML)重定向至 [/init-session] 路由,该路由的参数中包含 CSRF 令牌;

您可以在浏览器中测试此新路由:

Image

服务器的响应(配置为 [with_csrftoken=True])如下:

Image

  • [1] 中,服务器被重定向到 [/init-session] 路由,CSRF 令牌包含在 URL 中;
  • [2] 中,CSRF 令牌位于服务器发送的 JSON 字典中,与 [csrf_token] 键相关联;

让我们回到客户端代码:

Image

我们将 [config] 配置修改如下:


   config.update({
        # fichier des contribuables
        "taxpayersFilename"f"{script_dir}/../data/input/taxpayersdata.txt",
        # fichier des résultats
        "resultsFilename"f"{script_dir}/../data/output/résultats.json",
        # fichier des erreurs
        "errorsFilename"f"{script_dir}/../data/output/errors.txt",
        # fichier de logs
        "logsFilename"f"{script_dir}/../data/logs/logs.txt",
        # le serveur de calcul de l'impôt
        "server": {
            "urlServer""http://127.0.0.1:5000",
            "user": {
                "login""admin",
                "password""admin"
            },
            "url_services": {
                "calculate-tax""/calculer-impot",
                "get-admindata""/get-admindata",
                "calculate-tax-in-bulk-mode""/calculer-impots",
                "init-session""/init-session-without-csrftoken",
                "end-session""/fin-session",
                "authenticate-user""/authentifier-utilisateur",
                "get-simulations""/lister-simulations",
                "delete-simulation""/supprimer-simulation",
            }
        },
        # mode debug
        "debug"True,
        # csrf_token
        "with_csrftoken"True,
    }
    )

    # route init-session
    url_services = config['server']['url_services']
    if config['with_csrftoken']:
        url_services['init-session'] = '/init-session-without-csrftoken'
    else:
        url_services['init-session'] = '/init-session'
  • 第 31 行:一个布尔值将向客户端指示其所连接的服务器是否支持 CSRF 令牌;
  • 第 37–40 行:设置 [init-session] 操作的服务 URL:
    • 如果服务器使用 CSRF 令牌,则服务 URL 为 [/init-session-without-csrftoken]
    • 否则,服务 URL 为 [/init-session]

已引入路由 [/init-session-without-csrftoken]。它允许 JSON/XML 客户端在没有 CSRF 令牌的情况下与服务器建立会话。客户端将在服务器的响应中找到该令牌。

接下来,我们修改实现客户 [dao] 层的 [ImpôtsDaoWithHttpSession] 类:

Image

#  imports
import json

import requests
import xmltodict
from flask_api import status

from AbstractImpôtsDao import AbstractImpôtsDao
from AdminData import AdminData
from ImpôtsError import ImpôtsError
from InterfaceImpôtsDaoWithHttpSession import InterfaceImpôtsDaoWithHttpSession
from TaxPayer import TaxPayer

class ImpôtsDaoWithHttpSession(InterfaceImpôtsDaoWithHttpSession):

    #  manufacturer
    def __init__(self, config: dict):
        #  parent initialization
        AbstractImpôtsDao.__init__(self, config)
        #  saving configuration items
        #  general configuration
        self.__config = config
        #  server
        self.__config_server = config["server"]
        #  services
        self.__config_services = config["server"]['url_services']
        #  debug mode
        self.__debug = config["debug"]
        #  logger
        self.__logger = None
        #  cookies
        self.__cookies = None
        #  session type (json, xml)
        self.__session_type = None
        #  token CSRF
        self.__csrf_token = None

    # étape request / response
    def get_response(self, method: str, url_service: str, data_value: dict = None, json_value=None):
        #  [method]: HTTP GET or POST method
        #  [url_service] : URL of service
        #  [data]: POST parameters in x-www-form-urlencoded
        #  [json]: POST parameters in json
        #  [cookies]: cookies to include in the request

        #  you must have a XML or JSON session, otherwise you won't be able to handle the response
        if self.__session_type not in ['json', 'xml']:
            raise ImpôtsError(73, "il n'y a pas de session valide en cours")

        #  we add the CSRF token to the URL service token
        if self.__csrf_token:
            url_service = f"{url_service}/{self.__csrf_token}"

        #  query execution
        response = requests.request(method,
                                    url_service,
                                    data=data_value,
                                    json=json_value,
                                    cookies=self.__cookies,
                                    allow_redirects=True)

        #  debug mode?
        if self.__debug:
            #  logger
            if not self.__logger:
                self.__logger = self.__config['logger']
            #  log on
            self.__logger.write(f"{response.text}\n")

        #  result
        if self.__session_type == "json":
            résultat = json.loads(response.text)
        else:  #  xml
            résultat = xmltodict.parse(response.text[39:])['root']

        #  retrieve response cookies, if any
        if response.cookies:
            self.__cookies = response.cookies

        #  we retrieve the CSRF token
        if self.__config['with_csrftoken']:
            self.__csrf_token = résultat.get('csrf_token', None)

        #  status code
        status_code = response.status_code

        #  if status code other than 200 OK
        if status_code != status.HTTP_200_OK:
            raise ImpôtsError(35, résultat['réponse'])

        #  we return the result
        return résultat['réponse']


    def init_session(self, session_type: str):
        #  note the session type
        self.__session_type = session_type

        #  delete the CSRF token from previous calls
        self.__csrf_token = None

        #  the URL of the init-session action is requested
        url_service = f"{self.__config_server['urlServer']}{self.__config_services['init-session']}/{session_type}"

        #  request execution
        self.get_response("GET", url_service)

  • 第 38–92 行:CSRF 令牌的处理主要在 [get_response] 方法中进行;
  • 第 60 行:关键点在于 [allow_redirects=True] 参数。这是其默认值,但我们特意将其突出显示;

[with_csrftoken=True] 模式下:

  • 客户端通过调用路由 [/init-session_without_csftoken/type_response] 开始与服务器的交互;
  • 服务器对此请求的响应是重定向至 [/init-session/type_response/csrf_token] 路由;
  • 由于 [allow_redirects=True] 参数的作用,客户端将跟随此重定向 [requests]
  • 在第 72 行和第 74 行获取的结果中,将找到与键 [csrf_token] 关联的 CSRF 令牌;

[with_csrftoken=False] 模式下:

  • (待续)
    • 客户端通过调用路由 [/init-session/type_response] 开始与服务器的交互;
    • 服务器对此请求的响应是重定向至 [/init-session/type_response] 路由;
    • 由于 [allow_redirects=True] 参数,客户端 [requests] 会跟随此重定向;
    • 第 81–82 行中没有 CSRF 令牌可获取。因此,属性 [self.__csrf_token] 仍为 None(第 36 行);
  • 第 51–52 行:对于所有后续请求,如果存在 CSRF 令牌,则将其添加到初始路由中;
  • 第 81–82 行:服务器为每个新客户端请求生成的令牌会被本地存储,以便在第 52 行随下一次请求一并返回;

此外,[init_session] 方法也略有调整:

    def init_session(self, session_type: str):
        #  note the session type
        self.__session_type = session_type

        #  delete the CSRF token from previous calls
        self.__csrf_token = None

        #  the URL of the init-session action is requested
        url_service = f"{self.__config_server['urlServer']}{self.__config_services['init-session']}/{session_type}"

        #  request execution
        self.get_response("GET", url_service)

这里需要记住的是,我们创建了一个路由 [/init-session-without-csrftoken/<response-type>],用于在不使用 CSRF 令牌的情况下初始化客户端/服务器对话。 然而,我们可以看到,代码第 12 行调用的 [get_response] 方法会系统地将存储在 [self.__csrf_token] 中的 CSRF 令牌追加到服务 URL 的末尾。这就是为什么在代码第 6 行,如果存在该 CSRF 令牌,我们会将其移除。

就这样。为了测试,我们将运行:

  • the console clients [main, main2, main3];
  • 测试类 [Test1HttpClientDaoWithSession] [Test2HttpClientDaoWithSession]

通过依次将配置参数 [with_csrftoken] 设置为 True,然后设置为 False。

Image

以下是在 [with_csrftoken=True] 条件下运行 [main json] 客户端时获得的日志示例:


2020-08-08 16:33:23.317903, MainThread : début du calcul de l'impôt des contribuables
2020-08-08 16:33:23.317903, Thread-1 : début du calcul de l'impôt des 4 contribuables
2020-08-08 16:33:23.317903, Thread-2 : début du calcul de l'impôt des 2 contribuables
2020-08-08 16:33:23.317903, Thread-3 : début du calcul de l'impôt des 4 contribuables
2020-08-08 16:33:23.317903, Thread-4 : début du calcul de l'impôt des 1 contribuables
2020-08-08 16:33:23.379221, Thread-2 : {"action": "init-session", "état": 700, "réponse": ["session démarrée avec le type de réponse json"], "csrf_token": "ImFiZmZkYjZmMzFkZDc2YWRjNWYwOGM0NTBmMGM4ODJjYzViOWI4NGEi.Xy63sw.H5L0--yWsvfaWvggrGw78z5VnN0"}
2020-08-08 16:33:23.381073, Thread-4 : {"action": "init-session", "état": 700, "réponse": ["session démarrée avec le type de réponse json"], "csrf_token": "ImY5YzQyMjlkYzcyYmM4YmZiMGI0NWY5MjE4MzIzNDExZjc0MGQ3MWQi.Xy63sw.q6olg7IP_g2ro_RBFRCX1BX90g8"}
2020-08-08 16:33:23.386982, Thread-3 : {"action": "init-session", "état": 700, "réponse": ["session démarrée avec le type de réponse json"], "csrf_token": "IjkxZGNlN2YyMmUxMjQ0M2Y0MTdjNDQ4ZmQ1MDMxZjkwNjBhNzAzZjMi.Xy63sw.-6buL11No3UJBlElpW4tX4B-lp0"}
2020-08-08 16:33:23.390269, Thread-1 : {"action": "init-session", "état": 700, "réponse": ["session démarrée avec le type de réponse json"], "csrf_token": "IjIxNmU4MDQyZDFmZmIyZDlmZjE4MzNlNDUzYzFjMGYxMWYxYzEwNGYi.Xy63sw.fgs6Cm2owsJf4NjTm7gKrVESabI"}
2020-08-08 16:33:23.413206, Thread-2 : {"action": "authentifier-utilisateur", "état": 200, "réponse": "Authentification réussie", "csrf_token": "ImFiZmZkYjZmMzFkZDc2YWRjNWYwOGM0NTBmMGM4ODJjYzViOWI4NGEi.Xy63sw.H5L0--yWsvfaWvggrGw78z5VnN0"}
2020-08-08 16:33:23.422877, Thread-2 : {"action": "calculer-impots", "état": 1500, "réponse": [{"marié": "non", "enfants": 3, "salaire": 100000, "impôt": 16782, "surcôte": 7176, "taux": 0.41, "décôte": 0, "réduction": 0, "id": 1}, {"marié": "oui", "enfants": 3, "salaire": 100000, "impôt": 9200, "surcôte": 2180, "taux": 0.3, "décôte": 0, "réduction": 0, "id": 2}], "csrf_token": "ImFiZmZkYjZmMzFkZDc2YWRjNWYwOGM0NTBmMGM4ODJjYzViOWI4NGEi.Xy63sw.H5L0--yWsvfaWvggrGw78z5VnN0"}
2020-08-08 16:33:23.428622, Thread-4 : {"action": "authentifier-utilisateur", "état": 200, "réponse": "Authentification réussie", "csrf_token": "ImY5YzQyMjlkYzcyYmM4YmZiMGI0NWY5MjE4MzIzNDExZjc0MGQ3MWQi.Xy63sw.q6olg7IP_g2ro_RBFRCX1BX90g8"}
2020-08-08 16:33:23.429127, Thread-3 : {"action": "authentifier-utilisateur", "état": 200, "réponse": "Authentification réussie", "csrf_token": "IjkxZGNlN2YyMmUxMjQ0M2Y0MTdjNDQ4ZmQ1MDMxZjkwNjBhNzAzZjMi.Xy63sw.-6buL11No3UJBlElpW4tX4B-lp0"}
2020-08-08 16:33:23.429127, Thread-1 : {"action": "authentifier-utilisateur", "état": 200, "réponse": "Authentification réussie", "csrf_token": "IjIxNmU4MDQyZDFmZmIyZDlmZjE4MzNlNDUzYzFjMGYxMWYxYzEwNGYi.Xy63sw.fgs6Cm2owsJf4NjTm7gKrVESabI"}
2020-08-08 16:33:23.429127, Thread-2 : {"action": "fin-session", "état": 400, "réponse": "session réinitialisée", "csrf_token": "IjU1YjlmZDA0OWRhNTJlODFmYjgyYjlhM2ExYWNhZmUzNTk2NjA5NGIi.Xy63sw.nyNSvkcG6iG0oIMBjtYPo8ySgdw"}
2020-08-08 16:33:23.438519, Thread-2 : fin du calcul de l'impôt des 2 contribuables
2020-08-08 16:33:23.443033, Thread-4 : {"action": "calculer-impots", "état": 1500, "réponse": [{"marié": "oui", "enfants": 3, "salaire": 200000, "impôt": 42842, "surcôte": 17283, "taux": 0.41, "décôte": 0, "réduction": 0, "id": 1}], "csrf_token": "ImY5YzQyMjlkYzcyYmM4YmZiMGI0NWY5MjE4MzIzNDExZjc0MGQ3MWQi.Xy63sw.q6olg7IP_g2ro_RBFRCX1BX90g8"}
2020-08-08 16:33:23.446510, Thread-3 : {"action": "calculer-impots", "état": 1500, "réponse": [{"marié": "oui", "enfants": 5, "salaire": 100000, "impôt": 4230, "surcôte": 0, "taux": 0.14, "décôte": 0, "réduction": 0, "id": 1}, {"marié": "non", "enfants": 0, "salaire": 100000, "impôt": 22986, "surcôte": 0, "taux": 0.41, "décôte": 0, "réduction": 0, "id": 2}, {"marié": "oui", "enfants": 2, "salaire": 30000, "impôt": 0, "surcôte": 0, "taux": 0.0, "décôte": 0, "réduction": 0, "id": 3}, {"marié": "non", "enfants": 0, "salaire": 200000, "impôt": 64210, "surcôte": 7498, "taux": 0.45, "décôte": 0, "réduction": 0, "id": 4}], "csrf_token": "IjkxZGNlN2YyMmUxMjQ0M2Y0MTdjNDQ4ZmQ1MDMxZjkwNjBhNzAzZjMi.Xy63sw.-6buL11No3UJBlElpW4tX4B-lp0"}
2020-08-08 16:33:23.453477, Thread-1 : {"action": "calculer-impots", "état": 1500, "réponse": [{"marié": "oui", "enfants": 2, "salaire": 55555, "impôt": 2814, "surcôte": 0, "taux": 0.14, "décôte": 0, "réduction": 0, "id": 1}, {"marié": "oui", "enfants": 2, "salaire": 50000, "impôt": 1384, "surcôte": 0, "taux": 0.14, "décôte": 384, "réduction": 347, "id": 2}, {"marié": "oui", "enfants": 3, "salaire": 50000, "impôt": 0, "surcôte": 0, "taux": 0.14, "décôte": 720, "réduction": 0, "id": 3}, {"marié": "non", "enfants": 2, "salaire": 100000, "impôt": 19884, "surcôte": 4480, "taux": 0.41, "décôte": 0, "réduction": 0, "id": 4}], "csrf_token": "IjIxNmU4MDQyZDFmZmIyZDlmZjE4MzNlNDUzYzFjMGYxMWYxYzEwNGYi.Xy63sw.fgs6Cm2owsJf4NjTm7gKrVESabI"}
2020-08-08 16:33:23.457912, Thread-4 : {"action": "fin-session", "état": 400, "réponse": "session réinitialisée", "csrf_token": "IjQ0ZDQxODgzN2M5NjRiYWI0NjA2MTk5YWFkNGFhMzY1M2IxNWMyNDIi.Xy63sw.mOa5MKXvJ-EXf_qEok-OqC5j_mg"}
2020-08-08 16:33:23.458442, Thread-4 : fin du calcul de l'impôt des 1 contribuables
2020-08-08 16:33:23.459045, Thread-3 : {"action": "fin-session", "état": 400, "réponse": "session réinitialisée", "csrf_token": "ImQ0NDZlYmViYjY1ZDUxYzJhMTNmM2JiZTRkMjBjZGJkYzE0OGVkYzMi.Xy63sw.fviTJz4zFDqVLlVlkrosT_JRPww"}
2020-08-08 16:33:23.459700, Thread-3 : fin du calcul de l'impôt des 4 contribuables
2020-08-08 16:33:23.460492, Thread-1 : {"action": "fin-session", "état": 400, "réponse": "session réinitialisée", "csrf_token": "Ijg3MjQ1NGUyYTUyOGEyNTdmZmNmYWZkMmU2OTgyMzUwNjI1YTlhZjIi.Xy63sw.I0xBl9Q8DzsuXPSgOdeARc_VKBA"}
2020-08-08 16:33:23.460492, Thread-1 : fin du calcul de l'impôt des 4 contribuables
2020-08-08 16:33:23.460492, MainThread : fin du calcul de l'impôt des contribuables

如果我们观察依次接收到的CSRF令牌,会发现它们各不相同。