Flask学习记录2

HTTP

请求响应循环

    当我们在浏览器的地址栏输入URL,按下回车,稍等片刻,浏览器会显示一个页面。事实上,每一个Web应用都包含这样的处理模式,“请求-响应循环(Request-Response Cycle)”:客户端发出请求,服务器端处理请求并返回响应,如下图所示:
在这里插入图片描述
    这是一个Web程序的基本工作模式,如果在进一步,这个模式又包含着更多的工作单元,如下图所示,展示了一个Flask程序工作的实际流程:
在这里插入图片描述
    从上图可以看出,HTTP在整个流程中起到了至关重要的作用,它是客户端和服务器端之间沟通的桥梁。当用户访问一个URL,浏览器便随之生成对应的HTTP请求,经由互联网发送到对应的Web服务器。Web服务器接收请求,通过WSGI将HTTP格式的请求数据转换成Flask程序能够使用的Python数据。在程序中,Flask根据请求的URL执行对于的视图函数,获取返回值生成响应。响应以此经过WSGI转换生成HTTP响应,再经由Web服务器传递,最终被发出请求的客户端接收。浏览器渲染响应中包含的HTML和CSS代码,并执行JS代码,最终把解析后的页面呈现在用户浏览器中。

HTTP请求

    URL是一个请求的的起源。URL由指定协议、服务器地址(域名)、获取的资源路径构成。

请求报文

    当在浏览器访问URL时,随之产生的事一个发向其所在服务器的请求。请求的实质是发送到服务器上的一些数据,这种浏览器与服务器之间的数据被称为报文(message),请求时浏览器发送的数据被称为请求报文(request message),而服务器返回的数据被称为响应报文(response message)。请求报文由请求的方法、URL、协议版本、首部字段(header)以及内容实体组成。
    HTTP通过方法来区分不同的请求类型,如下表所示:
在这里插入图片描述

Request对象

    Flask的请求对象request封装了从客户端发来的请求报文,可以通过它获取请求报文的所有数据。请求解析和响应封装实际上大部分是有Werkzeug完成的,Flask子类化Werkzeug的请求和响应对象并添加了和程序相关的特定功能。
    假设请求的URL是http://helloflask.com/hello?name=CLOUDS,当Flask接受到请求后,请求对象户会提供多个属性来获取URL的各个部分,常用的属性如下表:

属性
pathu’/hello’
full_pathu’/hello?name=CLOUDS’
hostu’/helloflask’
host_urlu’http://helloflask.com/’
base_urlu’http://helloflask.com/hello’
urlu’http://helloflask.com/hello?name=CLOUDS’
url_rootu’http://helloflask.com/’

    除了URL,请求报文中的其他信息都可以通过request对象提供的属性和方法获取,其中常用的部分如下表所示:
在这里插入图片描述
    Werkzeug的MutliDict类是字典的子类,它主要实现了同一个键对应多个值的情况。比如一个文件上传字段可能会接受多个文件。这时就可以通过getlist()方法来获取文件对象列表。而ImmutableMultiDict类继承了MutliDict类,但其值不可更改。
    需要注意的是,和普通的字典类型不同,当从request对象类型为MutliDict或ImmutableMultiDict的属性(比如files、from、args)中直接使用键作为索引获取数据时(比如request.args[‘name’]),如果没有对应的键,那么会返回HTTP400错误响应(Bad Requst,表示请求无效),而不是抛出KeyError异常。为了避免这个错误,应该使用get()方法获取数据,如果没有对应的值则返回None;get()方法的第二个参数可以设置默认值,比如request.args.get(‘name’,‘Human’)。如果开启了调试模式,那么会抛出BadRequestKeyError异常并显示对应的错误堆栈信息,而不是常规的400响应。

在Flask中处理请求

    URL是指网络上资源的地址。在Flaks中,需要让请求的URL匹配对应的视图函数,视图函数返回值就是URL对应的资源。

1. 路由匹配
  为了便于将请求分发到对应的视图函数,程序实例中存储了一个路由表(app.url_map),其中定义了URL规则和视图函数的映射关系。当请求发来后,Flask会根据请求报文中的URL(path部分)来尝试与这个表中的所有URL规则进行匹配,调用匹配成功的视图函数。如果没有找到匹配的URL规则,说明程序中没有处理这个URL的视图函数,Flask会自动返回404错误响应(Not Found,表示资源未找到)。
  当请求的URL与某个视图函数的URL规则匹配成功时,对应的视图函数就会被调用。使用flask routes命令可以查看程序中定义的所有路由,这个列表有app.url_map解析得到:
在这里插入图片描述
  在输出的文本中,可以看到每个路由对应的端点(Endpoint)、HTTP方法(Methods)和URL规则,其中static端点是Flask添加的特殊路由,用来访问静态文件。

2. 设置监听的HTTP方法
  从上面的路由列表可以发现,每一个路由除了包含URL规则外,还设置了监听的HTTP方法。GET是常用的HTTP方法,所以视图函数默认监听的方法类型就是GET,HEAD、OPTINONS方法的请求由Flask处理,而像DELETE、PUT等方法一般不会再程序中实现,在之后的构建Web API时才会用到这些方法。
  在app.route()装饰器中使用methods参数传入一个包含监听的HTTP方法的可迭代对象。如下图所示,视图函数同时监听GET请求和POST请求:

@app.route('/1', methods={'GET', 'POST'})
def index():
    return 'Hello, Flask'

  当某个请求的方法不符合要求时,请求将无法被正常处理。比如,在提交表单时通常会使用POST方法,而如果提交的目标URL对应的视图函数只允许GET方法,这时Flask会自动返回一个405错误响应(Method Not Allowed,表示请求方法不允许)。
  通过定义方法列表,可以为同一个URL规则定义多个视图函数,分别处理不同HTTP方法的请求。

3. URL处理
  URL规则中若有变量,在没有特别声明的情况下,默认类型为字符串。Flask提供了一些转换器可以在URL规则里使用,如下图所示:
在这里插入图片描述
  转换器通过特定的规则指定,即“<转换器:变量名>”。默认的行为不仅仅是转换变量类型,还包括URL匹配。如下例子:

@app.route('/back/<int:year>')
def index2(year):
    age=2021-year
    return 'Welcome to %s'%age

  在这个例子中,如果不使用转换器,默认的year变量会被转成字符串,为了能够在Python中计算天数,需要使用int()将year强转成整型。但是如果用户输入的是英文字母,就会出现转换错误,抛出ValueError异常,这就需要手动验证;使用转换器后,如果URL中传入的变量不是数字,那么会直接返回404错误响应。
  在用法上唯一特别的是any转换器,需要在转换器后添加括号来给出可选值,即“<any(value1,value2,…):变量名>”,比如:

@app.route('/colors/<any(black,blue,red):color>')
def index3(color):
    return '<p>Peace in your mind. Love in your heart</p>'

   当在浏览器访问http://localhost:5000/colors/时,如果将部分替换为any转换器中设置的可选值以为的任意字符,均会得到404错误响应。如果想在any转换器中传入一个预先定义的列表,可以通过格式化字符串的方式(使用%或是format()函数)来构建URL规则字符串,比如:

colors = ['black', 'blur', 'red']

@app.route('/colors/<any(%s):color>' % str(colors)[1:-1])
def index3(color):
    return '<p>Peace in your mind. Love in your heart</p>'

请求钩子

    在需要对请求进行预处理(preprocessing)和后处理(postprocessing),这时可以使用Flask提供的一些请求钩子(Hook),它们可以用来注册在请求处理的不同阶段执行的处理函数(或称为回调函数,即Callback)。这些请求钩子使用装饰器实现,通过程序实例app调用,例如:当对一个函数附加了app.before_request装饰器后,就会将这个函数注册为before_request处理函数,每次执行请求前都会触发所有before_request处理函数。Flask默认实现的5种请求钩子如下表所示:
在这里插入图片描述
    这些钩子和app.route()装饰器使用方法基本相同,每个钩子可以注册多个处理函数,函数名并不是必须和钩子名称相同,实例如下:

@app.before_request
def do_something():
    pass  # 这里的代码会在每个请求处理前执行

    假如已经创建了三个视图函数A、B、C,其中视图C使用了after_this_request钩子,那么当请求A进入后,整个请求处理周期的请求处理函数调用流程如下图所示:
在这里插入图片描述

    下面是请求钩子的一些常见应用场景:

  • before_first_request:在玩具程序中,运行程序前我们需要进行一些程序的初始化操作,比如创建数据库表,添加管理员用户。这些工作可以放到使用before_first_request装饰器注册的函数中。
  • before_request:比如网站上要记录用户最后在线的时间,可以通过用户最后发送的请求时间来实现。为了避免在每个视图函数都添加更新在线时间的代码,可以仅在使用before_request钩子注册的函数中调用这段代码。
  • after_request:在视图函数中进行数据库操作,比如更新、插入等,之后需要将更改提交到数据库中。提交更改的代码就可以放到after_request钩子注册的函数中。

    另外一种常见的应用是建立数据库连接,通常会有多个视图函数需要建立和关闭数据库连接,这些操作基本相同。一个理想的解决方法就是在请求之前(before_request)建立连接,在请求之后(teardown_request)关闭连接。通过在使用相应的请求钩子注册的函数中添加代码就可以实现。

    after_request钩子和after_this_request钩子必须接受一个响应类对象作为参数,并且返回同一个或更新后的响应对象。

HTTP响应

    在Flask程序中,客户端发出的请求触发相应的视图函数,获取返回值作为响应的主题,最后生成完整的响应,即响应报文。

响应报文

    响应报文主要有协议版本、状态码(status code)、原因短语(reason phrase)、响应首部和响应主体组成。响应报文的首部包含一些关于响应和服务器的信息,这些内容有Flask生成,而在视图函数中返回的内容即为响应报文中的主题内容。浏览器接受到响应后,会把返回的响应主题解析并显示在浏览器窗口上。
    HTTP状态码用来表示请求处理的结果,下表示常见的几种状态码和相应的原因短语。
在这里插入图片描述
    当关闭调试模式时,即FLASK_ENV使用默认值production,如果程序出错,Flask会自动返回500错误响应;而调试模式下则会显示调试信息和错误堆栈。

在Flask中生成响应

    响应在Flask中使用Response对象表示,响应报文中的大部分内容由服务器处理,大多数请求下,我们只负责返回的主体内容,
    根据之前的记录,Flask会先判断是否可以找到与请求URL向匹配的路由,如果没有则返回404响应。如果找到,则调用对应的视图函数,视图函数的返回值构成了响应报文的主题内容,正确返回状态码默认为200。Flask会调用make_response()方法将视图函数返回值转换为响应对象。
    完整地说,视图函数可以返回最多由三个元素组成的元祖:响应主题、状态码、首部字段。其中首部字段可以为字典,或是两元素元组组成的列表。
    例如,普通的响应可以只包含主体内容,默认的状态码为200:

@app.route('/hello')
def demo():
        return 'hello'

    下面指定了不同的状态码:

@app.route('/hello')
def demo():
    return 'hello', 201  # 指定状态码

1. 重定向
    Flask提供了redirect()函数来生成重定向响应,重定向的目标URL作为第一个参数,如下所示:

@app.route('/hello2')
def demo2():
    return redirect('http://www.baidu.com')  

    redirect()函数默认的状态码为302,即临时重定向。如果要修改状态码,可以在redirect()函数中作为第二个参数或使用code关键字传入。若要在程序内重定向到其它视图,只需在redirect()中使用url_for()函数生成目标URL。

2. 错误响应
    大多数情况下,Flask会自动处理常见的错误响应。HTTP错误对应的异常类在Werkzeug的werkzeug.exceptions模块中定义,抛出这些异常即可返回对应的错误响应。如果想手动返回错误响应,更便利的方法是使用Flask提供的abort()函数。
    在abort()函数中传入状态码即可返回对应的错误响应,abort()函数的前面不需要写return语句,一旦abort()函数被调用,abort()函数之后的代码将不被执行。

@app.route('/404')
def not_found():
    abort(404)

响应格式

    在HTTP响应中,数据可以通过多种格式传输。大多数情况下,一般会使用HTML格式,这也是Flask中的默认设置。在特定情况下,我们也会使用到其它格式。不同的响应数据格式需要设置不同的MIME类型,MIME类型在首部的Content-Type字段中定义。
    MIME类型(又称media type或content type)是一种用来标识文件类型的机制,它与文件扩展名相对应,可以让客户端区分不同的内容类型,并执行不同的操作。一般的格式为“类型名/子类型名”,其中的子类型名一般为文件扩展名。如HTML的MIME类型为“text/html”,png图片的MIME类型为“image/png”。
    如果想使用其它MIME类型,可以通过Flask提供的make_response()方法生成响应对象,传入响应的主题作为参数,然后使用响应对象的mimetype属性设置MIME类型,比如:

@app.route('/demo3')
def demo3():
    response = make_response('Hello, World!')
    response.mimetype = 'text/plain'
    return response

在这里插入图片描述
    也可以直接设置首部字段,如response.heaer[‘Content-Type’] = ‘text/xml; charset=utf-8’。但操作mimetype属性更加方便,且不需要设置字符集(charset)选项。
    常用的数据格式有纯文本、HTML、XML和JSON,下面对这几种数据类型进行简单的介绍和分析。

1. 纯文本
  MIME类型:text/plain。事实上,其它几种格式本质上都是纯文本。如同样是一行包含HTML标签的文本“<h1>Hello, World!</h1>”,当MIME类型设置为纯文本时,浏览器会以文本形式显示“<h1>Hello, World!</h1>”;当MIME类型声明为text/html时,浏览器则会将其作为标题1样式的HTML代码渲染。

2. HTML
  MIME类型:text/html。HTML值Hypertex Markup Language (超文本标记语言),是最常用的数据格式。也是Flask返回响应的默认数据类型。当数据类型为HTML时,浏览器会自动根据HTML标签以及样式类定义渲染对应的样式。

3. XML
  MIME类型:application/xml。XML指Extensible Markup Language(可扩展标记语言),它是一种简单灵活的文本格式,被设计用来存储和交换数据。XML的出现主要是为了弥补HTML的不足:对于仅仅需要数据的请求来说,HTML提供的数据过于丰富,且不易重用。XML和HTML一样都是标记性语言,使用标签来定义文本,但HTML中的标签用于显示内容,而XML中的标签只用于定义数据。XML一般作为AJAX请求的响应格式,或是Web API的响应格式。

4. JSON
  MIME类型:application/json。JSON值JavaScripts Object Notation(JavaScript对象表示法),是一种流行的、轻量的数据交换格式。它的出现又弥补了XML的诸多不足:XML又较高的重用性,但XML相对于其他文档格式来说体积太大,处理和解析的速度较慢。JSON轻量,简洁,容易阅读和解析,而且能和Web默认的客户端语言JavaScript更好地兼容。JSON的结构机遇:“键值对的集合”和“有序的值列表”,这两种数据结构类似Python中的字典和列表。正式因为这种通用的数据结构,使得JSON在同样基于这些结构的编程语言之间交换称为可能。

    Flask通过引入Python标准库中的json模块(或simplejson)为程序提供了JSON支持。可以直接从Flask中导入json对象,然后调用dumps()方法将字典、列表或元组序列化(serialize)为JSON字符串,再使用上面记录的方法修改MIME类型,即可返回JSON响应,如下所示:

@app.route('/demo4')
def demo4():
    data = {
        'name': '老王',
        'tel': '1234567890'
    }
    response = make_response(json.dumps(data))
    response.mimetype = 'application/json'
    return response

    但一般并不直接使用json模块的dumps()、load()等方法,因为Flask通过包装这些方法提供了更方便的jsonify()函数。借助jsonify()函数,我们仅需要传入数据或参数,它会对传入的参数进行序列化,转化成JSON字符串作为响应的主体,然后生成一个响应对象,同时设置正确的MIME类型。把上面例子通过jsonify函数实现如下:

@app.route('/demo5')
def demo5():
    return jsonify(name='老王',tel='1234567890')

    另外,jsonify()函数默认生成200响应,也可以通过附加状态码来自定义响应类型,比如:

@app.route('/demo6')
def demo6():
    return jsonify(message='Error'),500

Cookie

    HTTP是无状态协议。也就是说,在一次请求响应结束后,服务器不会留下任何对方状态的信息。但是对于某些Web程序来说,客户端的某些信息又必须被记住,如用户的登录状态,这样可以根据用户的状态来返回不同的响应。为了解决这类问题,就有了Cookie技术。Cookie技术通过在请求和响应报文中添加Cookie数据来保存客户端的状态信息。
    在Flask中,如果想要在响应中添加一个cookie,最方便的方式是使用Response类提供的set_cookie()方法。使用这个方法,需要先使用make_response()方法手动生成一个响应对象,传入响应主体作为参数。这个响应对象默认实例化内置的Response类。下表是内置的Response类常用的属性和方法:
在这里插入图片描述
    除表中列出的方法和属性外,Response类同样拥有和Request类相同的get_json()方法、is_json()方法以及json属性。
    set_cookie()方法支持多个参数来设置Cookie的选项,如下表所示:
在这里插入图片描述
    set_cookie使用用来设置cookie,它会将URL中的name变量的值设置名为name的cookie里,如下所示:

@app.route('/demo7/<name>')
def demo7(name):
    response = make_response(redirect(url_for('demo')))
    response.set_cookie('name',name)
    return response

    在这个make_response()函数中,传入的是redirect()函数生成的重定向响应。set_cookie视图会在生成的响应报文首部创建一个Set-Cookie字段,如下所示:
在这里插入图片描述
    这时去查看浏览器的Cookie,就会看到多了一块名为name的cookie。因为过期时间使用的默认值,所以会在浏览器会话结束时过期。
在这里插入图片描述
    当浏览器保存了服务器端设置的Cookie后,浏览器再次发送到该服务器的请求会自动携带设置的Cookie信息,Cookie的内容存储在请求首部的Cookie字段中,整个由上而下的交互过程如下图所示:
在这里插入图片描述
    在Flask中,Cookie可以通过请求对象的cookie属性读取。在修改后的hello视图中,如果没有从查询参数中获取到name的值,就从Cookie中寻找:

@app.route('/')
@app.route('/demo8')
def demo8():
    name = request.args.get('name')
    if name is None:
        name = request.cookies.get('name', 'Human')  # 从Cookie中获取name值
    return 'Hello, %s' % name

    这时服务器就可以根据Cookie的内容来获得客户端的状态信息,并根据状态返回不同的响应。

session:安全的Cookie

    Cookie在Web程序中发挥了很大的作用,其中最重要的功能是存储用户的认证信息。但这会有很大的安全隐患,在浏览器中手动添加和修改Cookie是很容易的事,通过插件就可以实现。为了避免这个问题,可以通过对敏感的Cookie内容进行加密。Flask提供了session对象用来将Cookie数据加密储存。
    session指用户会话(user session),又称为对话(dialogue),即服务器和客户端/浏览器之间或桌面程序和用户之间建立的交互活动。在Flask中,session对象用来加密Cookie。默认情况下,它会把数据存储在浏览器上一个名为session的cookie里。

1. 设置程序秘钥
  session通过对数据进行签名以加密数据,因此,需要先设置一个秘钥。这里的秘钥就是一个具有一定复杂度和随机性的字符串。程序的秘钥可以通过Flask.secret_key属性或配置变量SECRET_KEY设置,如下:

app.secret_key = 'secret string'

  更安全的做法是把秘钥写进系统环境变量(在命令行中使用export或set命令),或是保存在.env文件中:

SECRET_KEY = secret string

  然后再程序脚本中使用os模块提供的getenv()方法获取:

import os
app.secret_key = os.getenv(‘SECRET_KEY’,'secret string'

  可以在getenv()方法中添加第二个参数,作为没有获取到对应环境变量时使用的默认值

2. 模拟用户认证
  下面代码是使用session模拟用户的认证功能

@app.route('/login')
def login():
    session['logged_in'] = True  # 写入session
    return redirect(url_for('demo'))

  这是一个简单的登录视图,在实际登录中,需要在页面提供登录表单,提供用户填写的账户和密码,之后在登录视图里验证账户和密码的有效性。session对象可以像字典一样操作,向其添加一个logged-in cookie,将它的值设为True,表示用户已认证。
  使用session对象添加cookie时,数据会使用程序的秘钥对其进行签名,加密后的数据存储在一块叫session的cookie里,如下图所示:
在这里插入图片描述
  使用session对象存储的Cookie,用户可以看到起加密后的值,但无法修改。因为session中的内容使用秘钥进行签名,一旦数据被修改,签名的值也会变化。之后再读取,就会验证失败,对应的session值也会随之失效。所以,除了用户知道秘钥,否则无法对session cookie的值进行修改。
  当支持用户登录后,就可以根据用户的认证状态显示不同的内容。如下所示,修改之前的demo8()视图函数,将程序重定向到hello视图:

@app.route('/')
@app.route('/demo8')
def demo8():
    name = request.args.get('name')
    if name is None:
        name = request.cookies.get('name', 'Human')  # 从Cookie中获取name值
        response = '<h1>Hello, %s</h1>' % name
        if 'logged_in' in session:  # 根据用户认证状态返回不同的内容
            response += '[Authenticated]'
        else:
            response += '[Not Authenticated]'
    return response

  session中的数据可以通过键值对读取,或是使用get()方法。这里判断了session中是否包含logged_in键,如果有则表示用户已经登录。通过判断用户的认证状态,响应不同的数据。
  登出用户的视图,登出账号对应的实际操作就是把代表用户认证的logged-in cookie删除,通过session对象的pop方法实现,如下所示:

@app.route('/logout')
def logout():
    if 'logged_in' in session:
        session.pop('logged_in')
    return redirect(url_for('demo'))

  默认情况下,session cookie会在用户关闭浏览器时删除。通过将session.permanent属性设为True,将session的有效期延长为Flask.permanent_session_lifetime属性值对应的datetime.timedelta对象,也可以通过配置变量PERMANENT_SESSION_LIFETIME,默认为31天。

Flask上下文

    Flask有两种上下文,程序上下文(application context)和请求上下文(request context)。在Flask中,对一个请求进行处理时,视图函数一般都会需要请求参数、配置等对象。为了避免大量可有可无的参数把视图函数弄得一团糟,Flask使用上下文临时把某些对象变为全局访问。程序上下文存储了程序运行所必须的信息,请求上下文包含了请求的各种信息。

上下文全局变量

    每一个视图函数都需要上下文信息。Flask中上下文对象:相当于一个容器,保存了Flask程序运行过程中的一些信息。请求对象只在各自的线程内是全局的。Flask通过本地线程(thread local)技术将请求对象在特定的线程和请求中全局可访问。为了方便获取这两种上下文环境存储的信息,Flask提供了四个上下文全局变量,下表:

变量名上下文类别说明
current_app程序上下文指向处理请求的当前程序实例
g程序上下文替代Pyhon的全局变量用法,确保仅在当前请求中可用。用于存储全局数据,每次请求都会重设
request请求上下文封装客户端发出的请求报文数据
session请求上下文用于记住请求之前的数据,通过签名的Cookie实现

    在不同的视图函数中,request对象都表示和视图函数对应的请求,也就是当前请求(current request)。而程序也会有多个程序示例的情况,为了能获取对应的程序实例,而不是固定的某一个程序实例,就需要current_app变量。
    因为g存储在程序上下文中,而程序上下文会随着每一个请求的进入而激活,随着每一个请求的处理完毕而销毁,所以每次请求都会重设这个值。通常会使用它结合请求钩子来保存每个请求处理前所需的全局变量,如当前登入的用户对象,数据库连接等。例如,某个视图中查询字符串获得某个值,如果每一个视图都需要这个值,那么就要在每个视图重复这行代码,借助g可以将这个操作移动到before_request处理函数中执行,然后保存到g的任意属性上,如下:

@app.before_request
def get_name():
    g.name = request.args.get('name')

    设置这个函数后,在其他视图中可直接使用g.name获取对应的值。另外,g也支持使用类似字典的get()、pop()以及setdefault()方法进行操作。

激活上下文

以下情况,Flask会自动激活程序上下文:

  • 使用flask run命令启动程序时
  • 使用旧的app.run()方法启动程序时
  • 执行使用@app.cli.command()装饰器注册的flask命令时
  • 使用flask shell命令启动Python Shell时

    当请求进入时,Flask会自动激活请求上下文,这时可以使用request和session变量。另外,当请求上下文被激活时,程序上下文也被自动激活。当请求处理完毕后,请求上下文和程序上下文也会自动销毁。也就是说,在请求处理时这两者拥有相同的生命周期。结合Python的代码执行机制,这也就意味着,可以在视图函数中或视图函数内调用的函数/方法中使用所有上下文全局变量。在使用flask shell命令打开的Python shell中,或是自定义的flask命令函数中,可以使用current_app和g变量,也可以手动激活请求上下文来使用reuqest和session。
    如果在没有激活相关上下文时使用这些变量,Flask就会抛出RuntimeError异常。
    同样依赖上下文的还有url_for()、jsonify()等函数,所以也只能在视图函数中使用它们。其中jsonify()函数内部调用中使用了curren_app变量,而url_for()则需要依赖请求上下文才可以正常运行。

上下文钩子

    Flask为上下文提供了一个teardown_appcontext钩子,使用它注册的回调函数会在程序上下文被销毁时调用,而且通常也会在请求上下文被销毁时调用。例如,需要在每个请求处理结束后销毁数据库连接。使用@app.teardown_appcontext装饰器注册的回调函数需要接受异常对象作为参数,当请求被正常处理时这个参数值将是None,这个函数的返回值将被忽略。

HTTP进阶实践

重定向回上一个页面

1. 获取上一个页面的URL
要重定向回上一个页面,最关键的是获取上一个页面的URL。上一个页面的URL一般可以通过两种方式获取:
    (1)HTTP referer
      HTTP referer(起源为referrer在HTTP规范中的错误拼写)是一个用来记录请求发源地址的HTTP首部字段(HTTP_PEFERER),即访问来源。当用户在某个站点点击链接,浏览器向新链接所在的服务器发起请求,请求的数据中包含的HTTP_REFERER字段记录了用户所在的原站点URL。
      这个值通常会用来追踪用户,比如记录用户进入程序的外部站点,以此来更有针对性地进行营销。在Flask中,referer的值可以通过请求对象的referrer属性获取,即request.referrer(正确拼写形式)。如下:
      创建两个视图函数,一个显示foo页面一个显示bar页面

@app.route('/foo')
def foo():
    return '<h1> Foo page</h1><a href="%s">Fuck something</a>' % url_for('fuck_something')

@app.route('/bar')
def bar():
    return '<h1> Bar page</h1><a href="%s">Fuck something</a>' % url_for('fuck_something')

      在这两个页面,添加了一个指向fuck_something视图的链接,fuck_something视图如下所示:

@app.route('/fuck_something')
def fuck_something():
    # fuck something
    return redirect(url_for('demo'))  # 重定向到demo

      现在,do_something视图的返回值修改至如下:

return redirect(request.referrer)

      但是在很多情况下,referrer字段会是空值,比如用户在浏览器的地址栏输入URL,或是用户出于保护隐私的考虑使用了防火墙软件或者浏览器设置了自动清除或修改referrer字段。这时需要添加一个备用项:

return redirect(request.referrer or url_for('demo'))

    (2)查询参数
      除了自动从referrer获取,另一种更常见的方式是在URL中手动加入包含当前页面URL的查询参数,这个参数一般命名为next。比如,在foo和bar视图的返回值中的URL后添加next参数:

@app.route('/foo')
def foo():
    return '<h1> Foo page</h1><a href="%s">Fuck something</a>' % url_for('fuck_something', next=request.full_path)

@app.route('/bar')
def bar():
    return '<h1> Bar page</h1><a href="%s">Fuck something</a>' % url_for('fuck_something', next=request.full_path)

      在程序内部只需要使用相对URL,所以这里使用request.full_path获取当前页面的完整路径。在fuck_something视图中,获取到next值,然后重定向到对应的路径:

return redirect(request.args.get('next'))

      用户在浏览器的地址栏直接访问时可以轻易地修改查询参数,为了米面next参数为空的情况,同样也学要添加备选项,如果为空就重定向到demo视图:

return redirect(request.args.get('next',url_for('demo')))

      为了覆盖更全面,可以将这两种方式搭配一起使用:首先获取next参数,如果为空就尝试获取referer,如果任然为空,那么久重定向到指定的默认视图。因为在不同视图执行这部分操作的代码完全相同,可以创建一个通用的redirect_back()函数,如下所示:

def redirect_back(default='demo', **kwargs):
    for target in request.args.get('next'), request.referrer:
        if target:
            return redirect(target)
    return redirect(url_for(default, **kwargs))

      通过设置默认值,使其在referer和next为空的情况下重定向到默认的视图。在fuck_something视图中使用这个函数的实例如下:

@app.route('/fuck_something')
def fuck_something():
    # fuck something
    return redirect_back()

2. 对URL进行安全验证
鉴于referer和next容易被篡改,如果不对这些值进行验证,则会形成开放重定向(Open Redirect)漏洞。确保URL安全的关键就是判断URL是否属于程序内部,以下创建了一个URL验证函数 is_safe_url(),用来验证next变量值是否属于程序内部URL

 def is_safe_url(target):
    ref_url = urlparse(request.host_url) #获取程序内的主机URL
    test_url = urlparse(urljoin(request.host_url, target)) #使用urljoin将目标URL转换为绝对URL
    return test_url.scheme in ('http', 'https') and ref_url.netloc == test_url.netloc #检验目标URL的URL模式及与主机地址对比,只有属于程序内部的URL才会返回

      在执行重定向回上一个页面的redirect_back()函数中,使用is_safe_url()验证next和referer的值:

def redirect_back(default='demo', **kwargs):
    for target in request.args.get('next'), request.referrer:
        if target:
            continue
        if is_safe_url(target):
            return redirect(target)
    return redirect(url_for(default, **kwargs))

使用AJAX技术发送异步请求

    在传统的Web应用中,程序的操作都是基于请求响应循环来实现的。每当页面状态需要变动,或是需要更新数据时,都伴随着一个发现服务器的请求。当服务器返回响应时,整个页面会重载,并渲染新页面。
    这种模式会带来有些问题。首先,频繁更新页面会牺牲性能,浪费服务器资源,同时降低用户体验。另外,对于一些操作性很强的程序来说,页面重载会显得很不合理。

    1.认识AJAX
    AJAX指异步Javascript和XML(Asynchronous JavaScript And XML),它不是编程语言或通信协议,而是一系列技术的组合体。简单来说,AJAX基于XMLHttpRequest让我们可以在不加载页面的情况下和服务器进行数据交换。加上JavaScript和DOM,使其可以在接受到响应数据后局部更新页面。而XML指的是数据的交互格式,也可以是纯文本、HTML或JSON。XMLHttpRequest不仅支持HTPP协议,还支持FILE和FTP协议。
    在Web程序中,很多加载数据的操作都可以在客户端使用AJAX实现。在这种模式下,可以在客户端实现大部分页面逻辑,而服务器则主要负责处理数据。这样可以避免每次请求都渲染整个页面,不仅加强了用户体验,也降低了服务器的负载。
    2.使用jQuery发送AJAX请求
    jQuery是流行的JavaScript库,它包装了JavaScript,让我们通过更简单的方式编写JavaScript代码。对于AJAX,它提供了多个相关的方法,使用它可以很方便地实现AJAX操作。更重要的是,jQuery处理了不同浏览器的AJAX兼容问题,只需要编写一套代码,就可以在所有主流的浏览器正常运行。
    ajax()函数是底层函数,有丰富的自定义配置,支持的主要参数如下所示:
在这里插入图片描述
    jQuery还提供了其他快捷方法(shorthand method):用于发送GET请求的get()方法和用于发送POST请求的post()方法,还有直接用于获取json数据的getjson()以及获取脚本的getscript()方法。这些方法都是基于ajax()方法实现的。
    3.返回“局部数据”
    对于处理AJAX请求的视图函数来说,不会返回完整的HTML响应,这时一般会返回局部数据,常见的三种类型如下所示:

  1. 纯文本或局部HTML模板
    纯文本可以在JavaScript用来直接替换页面中的文本值,而局部HTML则可以直接到插入页面中,例如返回评论列表。
  2. JSON数据
    JSON数据可以在JavaScript中直接操作,在jQuery中的ajax()方法的success回调中,响应主体中的JSON字符串会被解析为JSON对象,可以直接获取并进行操作。
  3. 空值
    在某些场景中,一些程序接受AJAX请求的视图并不需要返回数据给客户端,如用来删除文章的视图。这时可以直接返回空值,并将状态码指定为205(表示无内容)。

    4.异步加载长文章实例
    在实例程序页面,将会显示一篇长的虚拟文章,文章下方有一个“加载更多”按钮,当点击加载按钮时,会发送一个AJAX请求获取文章的更多内容并直接动态插入到文章下方。

@app.route('/post')
def show_post():
    post_body = generate_lorem_ipsum(n=2)  # 随机生成两段文字
    return '''
<h1> long post </h1>
<div class="body">%s</div>
<button id="load">加载</button>
<script src="https://code.jquery.com/jquery-3.3.1.min.js"></script>
<script type="text/javascript">
$(function(){
    $('#load').click(function(){
        $.ajax({
            url: '/more',                  //目标URL
            type: 'get',                   //请求方法
            success: function(data){       //返回2XX响应后触发的回调函数
                $('.body').append(data);   //将返回的响应插入到页面中
            }
        })
    })
})
</script>''' % post_body

    文章的随机正文通过Jinja2提供的generate_lorem_ipsum()函数生成,n参数指定段落的数量,默认为5,它会返回由随机字符组成的虚拟文章。文章下面添加了一个“加载”按钮。按钮下面是两个<script></script> 代码块,第一个script从CDN加载jQuery资源。在第二个script标签中,在最外层创建了一个$(function(){...})函数。这个函数用来在页面DOM加载完毕后执行代码。美元符号是jQuery的简写,通过它来调用jQuery提供的多个方法,所以$.ajax()等同于jQuery.ajax()。
    在$(function(){...})中,$('#load')称为选择器,在括号中传入目标的id、class或是其它属性来定位到对应的元素,将其创建为jQuery对象。传入“加载”按钮的id值以定位到按钮。在这个选择器上,附加了.click(function(){...}),这会为加载按钮注册一个点击事件处理函数,当加载按钮被点击时就会执行点击事件回调函数。在这个回调函数中,使用$.ajax()方法发送一个AJAX请求到服务器,通过url将目标URL设为“/more”,通过type参数将请求的类型设为GET。当请求成功处理并返回2XX响应时(也包括304响应),会触发success回调函数。success回调函数接受的第一个参数为服务器端返回的响应主题,在这个回调函数中,通过$('.body')来选择文章正文,使用append()方法插入返回的data数据。
    处理/more的视图函数会随机返回文章,如下所示:

@app.route('/more')
def load_post():
    return generate_lorem_ipsum(n=2)

HTTP服务器推送

    无论是传统的HTTP请求-响应式的通信模式,还是异步的AJAX请求,服务器端始终处于被动的应答状态,只有在客户端发出请求的情况下,服务器端才会返回响应。这种通信模式称为客户端拉取(client pull)。这种模式下,用户只能通过刷新页面或主动点击按钮来拉取新数据。在某些场景中是需要的通信模式时服务器端的主动推送(server push)。例如,在聊天室中多个用户,当某个用户发送消息后,服务器接收到这个请求,然后把消息推送给聊天室的所有用户。类似这种关注实时性的情况还有很多。
    实现服务器端推送的一系列技术被合称为HTTP Server Push(HTTP服务器端推送),目前常用的推送技术如下所示:
在这里插入图片描述
    按照列出顺序,这几种方式对实时通信的实现越来越完善,每种技术都有各自的优缺点,要根据面向的用户群及程序自身的特点来分析选择合适的方式。
    轮询(polling)这类使用AJAX技术模拟服务器端推送的方法实现起来比较简单,但通常会造成服务器资源上的浪费,增加服务器的负担,而且会让用户的设备耗费更多的电量。SSE效率更高,基本支持所有主流浏览器,但浏览器一般会限制标签页的连接数量。
    除了这些推送技术,在HTML5的API中还包含了一个WebSocket协议,和HTTP不通,它是基于TCP协议的全双工通信协议(full-duplex communication protocol)。和服务器推送技术相比,WebSocket实时性更强,而且可以实现双向通信(bidirectional communication)。此外,WebSocket的浏览器兼容性强于SSE。

Web安全防范

注入攻击

    注入攻击包含系统命令(OS Command)注入、SQL注入、NoSQL注入、ORM注入等。主要简单总结SQL注入。

  1. 攻击原理
    在编写SQL语句时,如果直接将用户传入的数据作为参数使用字符串拼接的方式插入到SQL查询中,那么攻击者可以通过注入其他语句来执行攻击操作,如获取敏感数据、修改数据、删除数据库表等。

  2. 攻击实例
    假设有一个学生信息查询程序,其中某个视图函数接受用户输入的密码,根据密码查询对应的数据再返回。数据库又一个db对象表示,SQL通过execute()方法执行,如下:

 def bobby_table():
    password = request.args.get('password')
    cur = db.execute("SELECT * FROM students WHERE password='$s';" %password)
    results = cur.fetchall()
    return  results

    通过查询字符串获取用户输入的查询参数,且没有经过任何处理就使用字符串格式化的方法拼接到SQL语句中。在这种情况下,如果攻击者输入的password参数值为“or 1=1 --”,那么最终视图函数将执行如下SQL语句:

SELECT * FROM students WHERE password='' or 1=1 --;'

    这时会把students表中的所有记录全部查询并返回。如果攻击者将password参数的值设为“'; drop table users;--”,那么查询语句就变成:

SELECT * FROM students WHERE password='' ; drop table students;--;'

    执行这个语句会把students表中所有的记录全部删掉。
3. 主要防范方法
1)使用ORM可以一定程度避免SQL注入问题
2)验证输入类型。如在URL规则中限制URL变量为整形
3)参数化查询。在构造SQL语句时避免使用拼接字符串或字符串格式化(使用白分化或format()方法)的方式来构建SQL语句。使用各类接口库提供的参数化查询方法。
4)转义特殊字符,比如引号、分号及横线等。一般使用参数化查询,各种接口库会自动进行转义工作。

XSS攻击

    

  1. 攻击原理
    XSS(Cross-Site Scripting,跨站脚本)是一种注入攻击,攻击者通过将代码注入到被攻击者的网站中,用户一旦访问网页便会执行注入的恶意脚本。XSS攻击主要分为反射性XSS攻击和存储型XSS攻击两类。

  2. 攻击分类
    反射性XSS又称非持久性XSS。当某个站点存在XSS漏洞时,这种攻击会通过URL注入攻击脚本,只有当用户访问这个URL时才会执行攻击脚本。存储性XSS也被称为持久性XSS,这种类型的XSS攻击更常见,危害也更大。它与反射性XSS类似,只不过会把攻击代码存储到数据库中,任意用户访问包含攻击代码的页面都会被殃及。

  3. 主要防护措施
    1)HTML转义:防范XSS攻击最主要的方法是对用户输入的内容进行HTML转义,转移后可以确保用户输入的内容在浏览器作为文本显示,而不是作为代码解析,如Jinjia2提供的escape()函数可以对用户传入的数据进行转义。
    2)验证用户输入:XSS攻击可以在任何用户可定制内容的地方进行,例如图片引用、自定义连接。仅仅通过转义HTML中的特殊字符并不能完全规避XSS攻击,因为在某些HTML属性中,使用普通的字符也可以插入JavaScript代码。除了转义用户输入外,还需要对用户的输入数据进行类型验证。

CSRF攻击

  1. 攻击原理
    CSRF(Cross Site Request Forgery,跨站请求伪造),是一种挟制用户在当前已登录的Web应用程序上执行非本意的操作的攻击方法。跟跨网站脚本(XSS)相比,XSS 利用的是用户对指定网站的信任,CSRF 利用的是网站对用户网页浏览器的信任。
  2. 主要防范措施
    1)正确使用HTTP方法:防范CSRF的基础就是正确使用HTTP方法。
    2)CSRF令牌校验:通过在客户端页面中假如伪随机数来防御CSRF攻击,这个伪随机数通常被称为CSRF令牌(token)。对于AJAX请求,可以再XMLHttpRequest请求首部添加一个自定义字段X-CSRFToken来保存CSRF令牌。Flask程序通常使用扩展来实现CSRF令牌的创建和验证工作,如Flask-SeaSurf、Flask-WTF内置的CSRFProtect等。
  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值