本节,我们先从一道经典的面试题目开始:当你在浏览器中输入一个 URL 并按下 Enter 后,都发生了什么?
其实这个问题还是蛮大的,网上也有很多解读,今天我们就从 HTTP 来入手,看看这背后究竟发生了什么。
请求响应循环
其实大家对于 HTTP 协议应该是再熟悉不过了,它是超文本传输协议,定义了服务器和客户端之间信息交流的格式和传递方式。
那么对于上面的问题,我们其实也可以大致的说出一个简易流程:
-
按下 Enter 之后,浏览器会向 URL 地址发送一个 HTTP 请求
-
在浏览器的背后,有一个后台程序,用于接收相关请求,并返回处理的结果
-
浏览器接收结果,并渲染给终端用户查看
事实上,每一个 Web 应用都包含这种处理模式,即“请求-响应循环(Request-Response Cycle)”:客户端(浏览器等)发出请求,服务端处理请求并响应。
我们再把上面的流程扩展到 Flask 服务器上,就是由浏览器生成的 HTTP 请求发送至 Web 服务器。Web 服务器接收到请求后,经由 WSGI 协议把数据转换成 Flask 程序能够识别的数据后,传递给 Flask 程序。然后 Flask 程序再根据视图函数等处理相关请求,最后再返回响应给 Web 服务器。最终交由浏览器来渲染结果,比如加载 CSS,执行 JavaScript 代码等等操作。
这里有两个概念我们要先明确下
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 信息
属性 | 值 |
---|---|
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/,可以得到
当我们在浏览器输入:http://127.0.0.1:5000/test/?name=luobo,可以得到
在这里,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 页面
可以看到,插入的 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 上下文的使用,请求钩子,重定向等知识点。