为了账号安全,请及时绑定邮箱和手机立即绑定

Flask 入门之旅(二)

标签:
Python Flask

本节,我们先从一道经典的面试题目开始:当你在浏览器中输入一个 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 代码等等操作。

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

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 上下文的使用,请求钩子,重定向等知识点。

图片描述

点击查看更多内容
TA 点赞

若觉得本文不错,就分享一下吧!

评论

作者其他优质文章

正在加载中
  • 推荐
  • 评论
  • 收藏
  • 共同学习,写下你的评论
感谢您的支持,我会继续努力的~
扫码打赏,你说多少就多少
赞赏金额会直接到老师账户
支付方式
打开微信扫一扫,即可进行扫码打赏哦
今天注册有机会得

100积分直接送

付费专栏免费学

大额优惠券免费领

立即参与 放弃机会
意见反馈 帮助中心 APP下载
官方微信

举报

0/150
提交
取消