Flask与HTTP

请求响应循环

当用户访问一个URL,浏览器便生成对应的HTTP请求,经由互联网发送到对应的Web服务器。Web服务器接收请求,通过WSGI将HTTP格式的请求数据转换成我们的Flask程序能够使用的Python数据。在程序中,Flask根据请求的URL执行对应的视图函数,获取返回值生成响应。响应依次经过WSGI转换生成HTTP响应,再经由Web服务器传递,最终被发出请求的客户端接收。浏览器渲染响应中包含的HTML和CSS代码,并执行Java Script代码,最终把解析后的页面呈现在用户浏览器的窗口中。

HTTP请求

http://javami.com/hello?name=kevin这个URL后面的?name=Kevin部分是查询字符串(query string)。

URL中的查询字符串用来向指定的资源传递参数。查询字符串从问号?开始,以键值对的形式写出,多个键值对之间使用&分隔。

请求报文

一个HTTP请求报文由请求行(request line)、请求头部(header)、空行和请求数据4个部分组成,下图给出了请求报文的一般格式。

Request对象

使用request的属性获取请求URL

除了URL,请求报文中的其他信息都可以通过request对象提供的属性和方法获取,其中常用的部分:

在Flask中处理请求

路由匹配

使用flask routes命令可以查看程序中定义的所有路由

flask routes

设置监听的HTTP方法

可以在app.route()装饰器中使用methods参数传入一个包含监听的HTTP方法的可迭代对象。比如,下面的视图函数同时监听GET请求和POST请求:

@app.route('/hello',methods=['GET','POST'])
def hello():
    return 'Hello,World!'

URL处理

@app.route('/hello/<string:name>')
def hello(name):
    return 'Hello,'+name

请求钩子

需要对请求进行预处理(preprocessing)和后处理(postprocessing),这时可以使用Flask提供的一些请求钩子(Hook),它们可以用来注册在请求处理的不同阶段执行的处理函数(或称为回调函数,即Callback)。

before_request钩子(请求之前)为例,当你对一个函数附加了app.before_request装饰器后,就会将这个函数注册为before_request处理函数,每次执行请求前都会触发所有before_request处理函数。

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

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

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

HTTP响应

响应报文主要由协议版本、状态码(status code)、原因短语(reasonphrase)、响应首部和响应主体组成。

常见的几种状态码和相应的原因短语。

在Flask中生成响应

重定向

如果要在程序内重定向到其他视图,那么只需在redirect()函数中使用url_for()函数生成目标URL即可

http/app.py:重定向到其他视图

from flask import Flask,redirect,url_for

app = Flask(__name__)

@app.route('/hi')
def hi():
    return redirect(url_for('hello'))


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

http/app.py:返回404错误响应

from flask import Flask,abort

app = Flask(__name__)

@app.route('/hi')
def hi():
    return redirect(url_for('hello'))


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

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

响应格式

from flask import Flask,make_response

app = Flask(__name__)

@app.route('/foo')
def foo():
    response = make_response('Hello,world')
    response.mimetype = 'text/plain'
    return response

纯文本

MIME类型:text/plain

HTML

MIME类型:text/html

XML

MIME类型:application/xml

JSON

MIME类型:application/json

from flask import Flask,make_response,json

app = Flask(__name__)

@app.route('/foo')
def foo():
    data = {
        'name':'kevin',
        'age':24,
    }
    response = make_response(json.dumps(data))
    response.mimetype = 'application/json'
    return response

使用jsonify函数可以将前面的例子简化为这种形式:

from flask import Flask,jsonify

app = Flask(__name__)

@app.route('/foo')
def foo():
    data = {
        'name':'kevin',
        'age':24,
    }
    return jsonify(data)

jsonify()函数接收多种形式的参数。你既可以传入普通参数,也可以传入关键字参数。如果你想要更直观一点,也可以像使用dumps()方法一样传入字典、列表或元组。

来一块Cookie

在Flask中,如果想要在响应中添加一个cookie,最方便的方法是使用Response类提供的set_cookie()方法。要使用这个方法,我们需要先使用make_response()方法手动生成一个响应对象,传入响应主体作为参数。这个响应对象默认实例化内置的Response类。

Response类的常用属性和方法

set_cookie()方法的参数

set_cookie视图用来设置cookie,它会将URL中的name变量的值设置到名为namecookie里:

http/app.py:设置cookie

from flask import Flask,redirect,url_for,make_response

app = Flask(__name__=)

@app.route('/set/<name>')
def set_cookies(name):
    #url_for('字符串')
    response = make_response(redirect(url_for('foo')))
    response.set_cookie('name',name)
    return response

当浏览器保存了服务器端设置的Cookie后,浏览器再次发送到该服务器的请求会自动携带设置的Cookie信息,Cookie的内容存储在请求首部的Cookie字段中,整个交互过程由上到下:

在Flask中,Cookie可以通过请求对象的cookies属性读取。在修改后的hello视图中,如果没有从查询参数中获取到name的值,就从Cookie中寻找

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

session:安全的Cookie

在编程中,session指用户会话(user session),又称为对话(dialogue),即服务器和客户端/浏览器之间或桌面程序和用户之间建立的交互活动。在Flask中,session对象用来加密Cookie。默认情况下,它会把数据存储在浏览器上一个名为session的cookie里。

设置程序密钥

session通过密钥对数据进行签名以加密数据,所以我们先设置一个密钥。

程序的密钥可以通过Flask.secret_key属性或配置变量SECRET_KEY设置,比如:

app.secret_key = 'hellokevin'

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

SECRET_KEY = 'hellokevin'

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

import os
#第一个参数写上key,第二个参数,作为没有获取到对应环境变量时使用的默认值。
app.secret_key = os.getenv('SECRET_KEY','helloworld')

模拟用户认证

http/app.py:登入用户
会使用session模拟用户的认证功能,登入用户的login视图。

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

session中添加一个logged-in cookie,将它的值设为True,表示用户已认证。

当我们使用session对象添加cookie时,数据会使用程序的密钥对其进行签名,加密后的数据存储在一块名为session的cookie里。

使用session对象存储的Cookie,用户可以看到其加密后的值,但无法修改它。因为session中的内容使用密钥进行签名,一旦数据被修改,签名的值也会变化。这样在读取时,就会验证失败,对应的session值也会随之失效。所以,除非用户知道密钥,否则无法对session cookie的值进行修改。

当支持用户登录后,可以根据用户的认证状态分别显示不同的内容。在login视图的最后,将程序重定向到hello视图,下面是修改后的hello视图:

@app.route('/')
@app.route('/hello')
def hello():
    name = request.args.get('name')
    if name is None:
        #从Cookie中获取name值
        name = request.cookies.get('name','Human')
        response = name
        # 根据用户认证状态返回不同的内容
        if 'logged_in' in session:
            response += '认证成功'
        else:
            response += '认证不通过'

    return response

http/app.py:模拟管理后台

@app.route('/admin')
def admin():
    if 'logged_in' not in session:
        abort(403)
    return 'Welcome to admin page.'

登出账户对应的实际操作其实就是把代表用户认证的logged-in cookie删除,这通过session对象的pop方法实现:
http/app.py:登出用户

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

Flask上下文

Flask中有两种上下文,程序上下文(application context)和请求上下文(request context)。

程序上下文中存储了程序运行所必须的信息,请求上下文里包含了请求的各种信息,比如请求的URL,请求的HTTP方法等。

上下文全局变量

Flask会在每个请求产生后自动激活当前请求的上下文,激活请求上下文后,request被临时设为全局可访问。而当每个请求结束后,Flask就销毁对应的请求上下文。

在不同的视图函数中,request对象都表示和视图函数对应的请求,也就是当前请求(current request)。而程序也会有多个程序实例的情况,为了能获取对应的程序实例,而不是固定的某一个程序实例,我们就需要使用current_app变量。

因为g存储在程序上下文中,而程序上下文会随着每一个请求的进入而激活,随着每一个请求的处理完毕而销毁,所以每次请求都会重设这个值。我们通常会使用它结合请求钩子来保存每个请求处理前所需要的全局变量,比如当前登入的用户对象,数据库连接等。

在hello视图中从查询字符串获取name的值,如果每一个视图都需要这个值,那么就要在每个视图重复这行代码。借助g我们可以将这个操作移动到before_request处理函数中执行,然后保存到g的任意属性上:

from flask import 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变量。另外,当请求上下文被激活时,程序上下文也被自动激活。当请求处理完毕后,请求上下文和程序上下文也会自动销毁。也就是说,在请求处理时这两者拥有相同的生命周期。

同样依赖于上下文的还有url_for()jsonify()等函数,所以你也只能在视图函数中使用它们。其中jsonify()函数内部调用中使用了current_app变量,而url_for()则需要依赖请求上下文才可以正常运行。

上下文钩子

Flask也为上下文提供了一个teardown_appcontext钩子,使用它注册的回调函数会在程序上下文被销毁时调用,而且通常也会在请求上下文被销毁时调用。比如,你需要在每个请求处理结束后销毁数据库连接:

@app.teardown_appcontext
def teardown_db(exception):
    db.close()

使用app.teardown_appcontext装饰器注册的回调函数需要接收异常对象作为参数,当请求被正常处理时这个参数值将是None,这个函数的返回值将被忽略。

HTTP进阶实践

重定向回上一个页面

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


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

@app.route('/do_something')
def do_something():
    return redirect(url_for('hello'))

获取上一个页面的URL

上一个页面的URL一般可以通过两种方式获取:

  • HTTP referer
  • 查询参数

HTTP referer获取:

@app.route('/do_something')
def do_something():
    return redirect(request.referrer)

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

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

查询参数获取,添加备选项,如果为空就重定向到hello视图:

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

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

@app.route('/do_something')
def do_something():
    return redirect(request.args.get('next',url_for('hello')))

http/app.py:重定向回上一个页面

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

通过设置默认值,我们可以在referer和next为空的情况下重定向到默认的视图。在do_something视图中使用这个函数:

@app.route('/do_something_and_redirect')
def do_something_and_redirect():
    return redirect_back()

对URL进行安全验证

我们创建了一个URL验证函数is_safe_url(),用来验证next变量值是否属于程序内部URL。
http/app.py:验证URL安全性

from urllib.parse import urlparse,urljoin
from flask import request

def is_safe_url(target):
    ref_url = urlparse(request.host_url)
    #urljoin()函数将目标URL转换为绝对URL
    test_url = urlparse(urljoin(request.host_url, target))
    return test_url.scheme in ('http', 'https') and \
           ref_url.netloc == test_url.netloc

使用AJAX技术发送异步请求

认识AJAX

AJAX指异步Javascript和XML(Asynchronous Java Script And XML),它不是编程语言或通信协议,而是一系列技术的组合体。AJAX基于XMLHttpRequest(https://xhr.spec.whatwg.org/)让我们可以在不重载页面的情况下和服务器进行数据交换。加上Java Script和DOM(Document Object Model,文档对象模型),我们就可以在接收到响应数据后局部更新页面。而XML指的则是数据的交互格式,也可以是纯文本(Plain Text)、HTML或JSON。

以删除某个资源为例,在普通的程序中流程如下

  • 当“删除”按钮被单击时会发送一个请求,页面变空白,在接收到响应前无法进行其他操作。
  • 服务器端接收请求,执行删除操作,返回包含整个页面的响应。
  • 客户端接收到响应,重载整个页面。

使用AJAX技术时的流程如下

  • 当单击“删除”按钮时,客户端在后台发送一个异步请求,页面不变,在接收响应前可以进行其他操作。
  • 服务器端接收请求后执行删除操作,返回提示消息或是无内容的204响应。
  • 客户端接收到响应,使用Java Script更新页面,移除资源对应的页面元素。

使用jQuery发送AJAX请求

ajax()函数支持的参数

返回“局部数据

纯文本或局部HTML模板

from flask import render_template
@app.route('/comments/<int:post_id>')
def do_something_and_redirect():
    return render_template('comments.html')

JSON数据

@app.route('/profile/<int:user_id>')
def do_something_and_redirect():
    ...
    return jsonify(username=username,bin=bin)

空值

@app.route('/comments/<int:post_id>',methods=['DELETE'])
def delete_post(post_id):
    ...
    return '',204

异步加载长文章示例
http/app.py:显示虚拟文章

from jinja2.utils import generate_lorem_ipsum

@app.route('/post')
def show_post():
    post_body = generate_lorem_ipsum(n=2)   #生成两段随机文本
    return '''
<h1>A very long post</h1>
<div class="body">%s</div>
<button id="load">Load More</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

$(function(){ ... })函数,这个函数用来在页面DOM加载完毕后执行代码,类似传统Java Script中的window.onload方法。

处理/more的视图函数会返回随机文章正文,如下所示:

from jinja2.utils import generate_lorem_ipsum

@app.route('/more')
def lost_post():
    return generate_lorem_ipsum(n=1)

HTTP服务器端推送

实现服务器端推送的一系列技术被合称为HTTP Server Push(HTTP服务器端推送),目前常用的推送技术:

Web安全防范

注入攻击

  • 使用ORM可以一定程度上避免SQL注入问题;
  • 验证输入类型。比如某个视图函数接收整型id来查询,那么就在URL规则中限制URL变量为整型;
  • 参数化查询。在构造SQL语句时避免使用拼接字符串或字符串格式化(使用百分号或format()方法)的方式来构建SQL语句。

XSS攻击

防范XSS攻击最主要的方法是对用户输入的内容进行HTML转义,转义后可以确保用户输入的内容在浏览器中作为文本显示,而不是作为代码解析。

CSRF攻击

通过在客户端页面中加入伪随机数来防御CSRF攻击,这个伪随机数通常被称为CSRF令牌(token)。


本博客所有文章除特别声明外,均采用 CC BY-SA 3.0协议 。转载请注明出处!