Flask 入门系列教程(二)

  • A+
所属分类:安全开发

本节,我们先从一道经典的面试题目开始:当你在浏览器中输入一个 URL 并按下 Enter 后,都发生了什么?

其实这个问题还是蛮大的,网上也有很多解读,今天我们就从 HTTP 来入手,看看这背后究竟发生了什么。

请求响应循环

其实大家对于 HTTP 协议应该是再熟悉不过了,它是超文本传输协议,定义了服务器和客户端之间信息交流的格式和传递方式。

那么对于上面的问题,我们其实也可以大致的说出一个简易流程:

  1. 按下 Enter 之后,浏览器会向 URL 地址发送一个 HTTP 请求

  2. 在浏览器的背后,有一个后台程序,用于接收相关请求,并返回处理的结果

  3. 浏览器接收结果,并渲染给终端用户查看

事实上,每一个 Web 应用都包含这种处理模式,即“请求-响应循环(Request-Response Cycle)”:客户端(浏览器等)发出请求,服务端处理请求并响应。

我们再把上面的流程扩展到 Flask 服务器上,就是由浏览器生成的 HTTP 请求发送至 Web 服务器。Web 服务器接收到请求后,经由 WSGI 协议把数据转换成 Flask 程序能够识别的数据后,传递给 Flask 程序。然后 Flask 程序再根据视图函数等处理相关请求,最后再返回响应给 Web 服务器。最终交由浏览器来渲染结果,比如加载 CSS,执行 JavaScript 代码等等操作。

我们可以看下下面的图片

Flask 入门系列教程(二)


这里有两个概念我们要先明确下

Web 服务器:Web 服务器是一类特殊的服务器,其作用是主要是接收 HTTP 请求并返回响应。我们常用的 Web 服务器有 Nginx,tomcat 等,相信大家都非常熟悉或多少听说些。

WSGI:它确切来说应该是一种协议,或者接口规范。定义了 web 服务器和 web 应用(Flask 等)之间的接口规范。只有 Web 服务器和 Web 应用都遵守了 WSGI 协议,那么他们才能正常通信。

比如说在上一节我们使用 app.run() 启动测试服务器时,就是使用了 Flask 自带的 Web 服务器,当然这种服务器只能用来开发测试时使用,在生成环境,我们需要部署到 Nginx 等 Web 服务器上。

在了解了 Web 程序的整体运行流程之后,我们再来深入的探究下 Flask 的工作原理。

Flask 上下文

HTTP 请求

当 Flask 接收到客户端的请求后(后面的章节中我们都会直接省略 Web 服务器和 WSGI 的转换步骤),就会产生一些视图函数可以访问的对象,通过这些对象来处理请求,这就是请求对象--request。

request 对象包含了 HTTP 请求中的 URL 信息和相关的报文信息

URL 信息

例如请求 URL 为:http://www.luobodazahui.top/hello?name=zhouluobo

属性
path '/hello'
full_path '/hello?name=zhouluobo'
host 'www.luobodazahui.top'
host_url 'http://www.luobodazahui.top'
base_url 'http://www.luobodazahui.top/hello'
url 'http://www.luobodazahui.top/hello?name=zhouluobo'

报文信息

属性或方法 说明
args 查询字符串信息
cookies cookies 信息字典
data 字符串形式的请求数据
form 表单数据
get_json() 获取 json 类型的请求数据
method 请求的 HTTP 方法

下面我们通过一个简单的例子来具体查看下

@app.route('/test/')
def test_view():
    query = 'Flask'
    if request.args:
        query = request.args.get('name''Flask')
    host = request.host
    path = request.full_path
    cookie = request.cookies
    method = request.method
    return """
    <h1>
    <p>query string: %s</p>
    <p>host: %s</p>
    <p>path: %s</p>
    <p>cookies: %s</p>
    <p>method: %s</p>
    </h1>
    """
 % (query, host, path, cookie, method)

当我们在浏览器输入:http://127.0.0.1:5000/test/,可以得到

Flask 入门系列教程(二)


当我们在浏览器输入:http://127.0.0.1:5000/test/?name=luobo,可以得到

Flask 入门系列教程(二)


在这里,request 是一个全局的变量,我们可以在任何的视图函数中去使用它。当然,这仅仅局限在当前线程中,对于多线程服务器中,不同线程服务器的请求对象是不同的。

两种上下文

在 Flask 中,有两种上下文:程序上下文和请求上下文。主要包括下面四种

变量名 上下文类型 说明
request 请求上下文 请求对象,封装了 HTTP 请求中的内容
session 请求上下文
请求上下文用户会话,存储请求之间需要保留的值
g 程序上下文 处理请求时的临时存储对象,仅在当前请求有效
current_app 程序上下文 当前的程序实例

对于 request,我们已经了解了,下面再来看看 session。

session

session 最常用的就是确认用户状态了,比如检查用户是否登陆等。下面我们就简单实现一个基于浏览器的用户认证功能,来理解下 session 的强大功效。

普通的认证系统,用户在页面表单中输入用户名和密码后,后台程序进行确认,如果认证通过,则返回响应,并在浏览器的 Cookie 中设入标记,例如“loginID:User1”。但是因为浏览器 Cookie 是很容易被修改的,所以如果使用名称存储这些信息就会非常不安全,此时就需要 session 登场了。

在 Flask 中 session 通过密钥对数据进行签名从而加密数据,所以我们需要先设置一个密钥。

app.secret_key = 'Very Hard Secret'

当然,更加安全的做法是把该密钥写到部署服务器的环境变量中,对于这种写法,我们在后面部署程序时再详细讲解。
接下来我们做模拟用户认证的情况,写两个视图函数,分别模拟登陆和登出场景。

@app.route('/login/')
def login():
    session['loginID'] = 'admin'
    return redirect(url_for('welcome'))@app.route('/logout/')
def logout():
    if 'loginID' in session:
        session.pop('loginID')
    return redirect(url_for('welcome'))

再修改 welcome 视图函数,用于展示是否登陆

@app.route('/user/', defaults={'name''陌生人'})
@app.route('/user/<name>')
def welcome(name):
    res = '<h1>Hello, %s!</h1>' % name
    if 'loginID' in session:
        res += 'Authenticated'
    else:
        res += 'UnAuthenticated'
    return res

这里我们使用了 redirect 函数,是一个重定向方法。只需要传入目标的 URL 地址,就可以在视图函数处理结束后跳转至目标的页面。

当我在浏览器输入:http://127.0.0.1:5000/login/的时候,就会在浏览器中插入一个加密的 cookie 并跳转至 welcome 页面

Flask 入门系列教程(二)


可以看到,插入的 cookie 是加密的,这样就加大了攻击者的攻击难度,从而在一定程度上保护了我们系统的安全。

g 和 current_app

其实你应该会有个疑惑,我们已经有了一个 app 程序实例了,为什么还需要定义一个 current_app 变量呢?在不同的视图函数中,request 对象都表示和视图函数对应的请求,也就是当前请求(current request)。而程序会有多个程序实例的情况,为了能获取对应的程序实例,而不是固定的某一个程序实例,我们就需要使用 current_app 变量。当然对于多个程序实例的情况,我们留待后面的章节详细介绍。

g 存储在程序上下文中,而程序上下文会随着每一个请求的进入而激活,随着每一个请求的处理完毕而销毁,所以每次请求都会重设这个值。比如说如果对于某个请求,我们几个视图函数都需要用到一个前端传递过来的变量,那么就可以把它保存到 g 变量当中

g.name = request.args.get('name')

这样,其他的视图函数就可以在同一个请求中直接使用 g.name 来访问,而不用每次都调用 request 了。
对于 current_app 和 g 的更多使用方式,在后面的学习中我们会慢慢接触的更多。

请求钩子

在处理请求之前或之后执行的代码,就称为请求钩子。比如在请求之前,我们需要初始化数据库,创建 admin 用户等等,就需要在请求之前调用请求钩子来做这件事情。

在 Flask 中提供了四种请求钩子,以装饰器的形式注册到函数,使得我们可以方便的应用该功能

钩子名称 作用
before_first_request 在处理第一个请求之前运行
before_request 在每次请求之前运行
after_request 如果没有未处理的异常抛出,则在每次请求之后运行
teardown_request 即使有未处理的异常抛出,也在每次请求之后运行

在请求钩子函数和视图函数之间共享数据一般使用上下文全局变量 g,比如上面的例子我们就可以写成

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

重定向回上一个页面

功能实现

重定向回上一个页面,这应该是一个非常常见的应用场景,那么该如何通过 Flask 来实现呢。

首先我们修改下 login 视图函数,在请求参数中查找 next 参数,如果存在则重定向到 next 参数对应的地址,否则重定向到 hello 视图函数对应的地址

@app.route('/login/')
def login():
    session['loginID'] = 'admin'
    return redirect(request.args.get('next'or url_for('hello'))

这里所谓的 next 参数,其实只是一种约定俗成的命名方式

再修改 needpage1 视图函数,如果用户未登陆则展示登陆链接,并保存 next 参数

@app.route('/needlogin1/')
def needLogin1():
    if 'loginID' in session:
        return '<h1>Hello, needLogin1!</h1>'
    else:
        return """
            <h1>Login</h1><a href="%s">Go To Login</a>
                """
 % url_for('login', next=request.url)

这样,当用户处于未登录状态时,就可以点击 Go To Login 链接进行登陆,登陆成功之后会自动跳转回当前页面了。

安全处理

现在我们虽然完成了功能,但是却还遗留了相关的安全问题。因为我们的 next 参数是以查询字符串的方式写在 URL 里的,所以如果有人拦截了我们的请求,就可以随便修改 next 的指向,此时我们就需要验证 next 变量是否属于我们的应用,否则很容易被指向外部链接,从而造成安全隐患。

我们先创建一个检查 URL 正确性的函数

from urllib.parse import urlparse
def check_next(target):
    ref_url = urlparse(request.host_url)
    test_url = urlparse(target)
    return ref_url.netloc == test_url.netloc

该函数接收目标地址为参数,并比较本应用的 host_url 和目标地址的 host_url 是否相同
改写 login 视图函数

@app.route('/login/')
def login():
    session['loginID'] = 'admin'
    target = request.args.get('next')
    if check_next(target):
        return redirect(target)
    return redirect(url_for('hello'))

只有当 check_next 函数返回 True 时才重定向到 next 变量对应的地址,否则重定向到 hello 对应的地址。

本节所以代码可以查看本教程的 GitHub 代码仓库的 2a tag 版本代码

总结

本章着重介绍了 Flask 中的 HTTP 相关知识,包括 Web 服务器的运行方式,Flask 上下文的使用,请求钩子,重定向等知识点。

Flask 入门系列教程(二)


发表评论

:?: :razz: :sad: :evil: :!: :smile: :oops: :grin: :eek: :shock: :???: :cool: :lol: :mad: :twisted: :roll: :wink: :idea: :arrow: :neutral: :cry: :mrgreen: