基础篇 第二章 Flask与HTTP
前言
这一切开始于2010年4月1日,Armin Ronacher在网上发布了一篇关于“下一代Python微框架”的介绍文章,文章里称这个Denied框架不依赖Python标准库,只需要复制一份deny.py放到你的项目文件夹就可以开始编程。伴随着一本正经的介绍、名人推荐语、示例代码和演示视频,这个“虚假”的项目让不少人都信以为真。5天后,Flask(http://flask.pocoo.org/)就从这么一个愚人节玩笑诞生了。。
2. Flask与HTTP
当用户访问一个URL,浏览器便生成对应的HTTP请求,经由互联网发送到对应的Web服务器。Web服务器接收请求,通过WSGI将HTTP格式的请求数据转换成我们的Flask程序能够使用的Python数据。在程序中,Flask根据请求的URL执行对应的视图函数,获取返回值生成响应。响应依次经过WSGI转换生成HTTP响应,再经由Web服务器传递,最终被发出请求的客户端接收。浏览器渲染响应中包含的HTML和CSS代码,并执行JavaScript代码,最终把解析后的页面呈现在用户浏览器的窗口中。
2.1 请求响应循环
flask web 工作流程图如下:
2.2 HTTP请求
URL是一个请求的起源。不论服务器是运行在美国洛杉矶,还是运行在我们自己的电脑上,当我们输入指向服务器所在地址的URL,都会向服务器发送一个HTTP请求。一个标准的URL由很多部分组成,以下面这个URL为例:
http://helloflask.com/hello?name=Grey
2.2.1 请求报文
2.2.2 Request对象
requests 常用的属性和方法:
假设请求的URL是http://helloflask.com/hello?name=Grey
时,页面加载后会显示“Hello,Grey!”。这说明处理这个URL的视图函数从查询字符串中获取了查询参数name的值.
from flask import Flask, request
app = Flask(__name__)
@app.route('/hello')
def hello():
name = request.args.get('name', 'Flask') # 获取查询参数name的值
return '<h1>Hello, %s!<h1>' % name # 插入到返回值中
2.2.3 在Flask中处理请求
- 路由匹配:
$ flask routes
Endpoint Methods Rule
-------- ------- -----------------------
hello GET /hello
go_back GET /goback/<int:age>
hi GET /hi
...
static GET /static/<path:filename>
- 设置监听的HTTP方法
@app.route('/hello', methods=['GET', 'POST'])
def hello():
return '<h1>Hello, Flask!</h1>'
- URL处理
Flask内置的URL变量转换器
转换器通过特定的规则指定,即“<转换器:变量名>”
。<int:year>
把year的值转换为整数,因此我们可以在视图函数中直接对year变量进行数学计算:
@app.route('goback/<int:year>')
def go_back(year):
return '<p>Welcome to %d!</p>' % 2018
在用法上唯一特别的是any转换器,你需要在转换器后添加括号来给出可选值,即“<any(value1,value2,...):变量名>”
,比如:
@app.route('/colors/<any(blue, white, red):color>')
def three_colors(color):
return '<p>Love is patient and kind. Love is not jealous </p>' % color
当你在浏览器中访问http://localhost:5000/colors/
时,如果将<color>
部分替换为any
转换器中设置的可选值以外的任意字符,均会获得404错误响应。
如果想在any转换器中传入一个预先定义的列表,可以通过格式化字符串的方式(使用%或是format()函数)来构建URL规则字符串,比如:
colors = ['blue', 'white', 'red']
@app.route('/colors/<any(%s):color>' % str(colors)[1:-1])
...
2.2.4 请求钩子
有时我们需要对请求进行预处理(preprocessing)和后处理(postprocessing),这时可以使用Flask提供的一些请求钩子(Hook),它们可以用来注册在请求处理的不同阶段执行的处理函数(或称为回调函数,即Callback)。这些请求钩子使用装饰器实现,通过程序实例app调用,用法很简单:以before_request
钩子(请求之前)为例,当你对一个函数附加了app.before_request装饰器后,就会将这个函数注册为before_request处理函数,每次执行请求前都会触发所有before_request
处理函数。Flask默认实现的五种请求钩子如表
@app.before_request
def do_something():
pass # 这里的代码会在每个请求处理前执行
2.3 HTTP响应
在Flask程序中,客户端发出的请求触发相应的视图函数,获取返回值会作为响应的主体,最后生成完整的响应,即响应报文。
2.3.1 响应报文
常见响应状态码:
2.3.2 在Flask中生成响应
响应在Flask中使用Response
对象表示,响应报文中的大部分内容由服务器处理,大多数情况下,我们只负责返回主体内容。Flask会调用make_response()
方法将视图函数返回值转换为响应对象。
默认的状态码为200,下面指定了不同的状态码:
def hello():
...
return '<h1>Hello, Flask!</h1>', 201
- 重定向
我们可以使用Flask提供的redirect()
函数来生成重定向响应,重定向的目标URL作为第一个参数,使用redirect()
函数时,默认的状态码为302,即临时重定向。如果你想修改状态码,可以在redirect()
函数中作为第二个参数或使用code关键字传入。
from flask import Flask, redirect
# ...
@app.route('/hello')
def hello():
return redirect('http://www.example.com')
from flask import Flask, redirect, url_for
...
@app.route('/hi')
def hi():
...
return redierct(url_for('hello')) # 重定向到/hello
@app.route('/hello')
def hello():
...
2.3.3 响应格式
不同的响应数据格式需要设置不同的MIME类型,MIME类型在首部的Content-Type字段中定义,MIME类型(又称为media type或content type)是一种用来标识文件类型的机制,它与文件扩展名相对应,可以让客户端区分不同的内容类型,并执行不同的操作。以默认的HTML类型为例:
Content-Type: text/html; charset=utf-8
from flask import make_response
@app.route('/foo')
def foo():
response = make_response('Hello, World!')
response.mimetype = 'text/plain'
return response
-
1. 纯文本
MIME类型:text/plain -
2. HTML
MIME类型:text/html
<!DOCTYPE html>
<html>
<head></head>
<body>
<h1>Note</h1>
<p>to: Peter</p>
<p>from: Jane</p>
<p>heading: Reminder</p>
<p>body: <strong>Don't forget the party!</strong></p>
</body>
</html>
HTML(https://www.w3.org/html/)指Hypertext MarkupLanguage(超文本标记语言),是最常用的数据格式,也是Flask返回响应的默认数据类型。
- 3. XML
MIME类型:application/xml
<?xml version="1.0" encoding="UTF-8"?>
<note>
<to>Peter</to>
<from>Jane</from>
<heading>Reminder</heading>
<body>Don't forget the party!</body>
</note>
XML(https://www.w3.org/XML/)指Extensible MarkupLanguage(可扩展标记语言),它是一种简单灵活的文本格式,被设计用来存储和交换数据,XML中的标签只用于定义数据。一般作为AJAX请求的响应格式,或是Web API的响应格式。
- JSON
MIME类型:application/json
{
"note":{
"to":"Peter",
"from":"Jane",
"heading":"Remider",
"body":"Don't forget the party!"
}
}
JSON(http://json.org/)指JavaScript Object Notation(JavaScript对
象表示法),是一种流行的、轻量的数据交换格式。JSON轻量,简洁,容易阅读和解析,而且能和Web默认的客户端语言JavaScript更好地兼容。
from flask import Flask, make_response, json
...
@app.route('/foo')
def foo():
data = {
'name':'Grey Li',
'gender':'male'
}
response = make_response(json.dumps(data))
response.mimetype = 'application/json'
return response
from flask import jsonify
@app.route('/foo')
def foo():
return jsonify(name='Grey Li', gender='male')
from flask import jsonify
@app.route('/foo')
def foo():
return jsonify({name: 'Grey Li', gender: 'male'})
@app.route('/foo')
def foo():
return jsonify(message='Error!'), 500
2.3.4 Cookie
HTTP是无状态(stateless)协议。也就是说,在一次请求响应结束后,服务器不会留下任何关于对方状态的信息。但是对于某些Web程序来说,客户端的某些信息又必须被记住,比如用户的登录状态,这样才可以根据用户的状态来返回不同的响应。为了解决这类问题,就有了Cookie技术。Cookie技术通过在请求和响应报文中添加Cookie数据来保存客户端的状态信息。
在Flask中,如果想要在响应中添加一个cookie,最方便的方法是使用Response类提供的set_cookie()
方法。要使用这个方法,我们需要先使用make_response()方法手动生成一个响应对象,传入响应主体作为参数。这个响应对象默认实例化内置的Response类。Response类常用的属性和方法:
set_cookie()方法的参数:
from flask import Flask, make_response
...
@app.route('/set/<name>')
def set_cookie(name):
response = make_response(redirect(url_for('hello')))
response.set_cookie('name', name)
return response
在浏览器中访问 http://127.0.0.1:5000/set/Grey
查看浏览器中的Cookie,就会看到多了一块名为name的cookie,其值为我们设置的“Grey”,如图2-10所示。因为过期时间使用默认值,所以会在浏览会话结束时(关闭浏览器)过期。
在Flask中,Cookie可以通过请求对象的cookies属性读取。在修改后的hello视图中,如果没有从查询参数中获取到name的值,就从Cookie中寻找:
from flask import Flask, request
@app.route('/')
@app.route('/hello')
def hello():
name = request.args.get('name')
if name is None:
name = request.cookies.get('name', 'Human') # 从Cookie中获取name值
return '<h1>Hello, %s</h1>' % name
这时服务器就可以根据Cookie的内容来获得客户端的状态信息,并根据状态返回不同的响应。如果你访问http://localhost:5000/set/Grey
,那么就会将名为name的cookie设为Grey,重定向到/hello后,你会发现返回的内容变成了“Hello,Grey!”
。如果你再次通过访问http://localhost:5000/set/
修改name cookie的值,那么重定向后的页面返回的内容也会随之改变。
2.3.5 session:安全的Cookie
在浏览器中手动添加和修改Cookie是很容易的事,仅仅通过浏览器插件就可以实现。所以,如果直接把认证信息以明文的方式存储在Cookie里,那么恶意用户就可以通过伪造cookie的内容来获得对网站的权限,冒用别人的账户。为了避免这个问题,我们需要对敏感的Cookie内容进行加密。方便的是,Flask提供了session对象用来将Cookie数据加密储存。
- 设置程序密钥
程序的密钥可以通过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()
方法中添加第二个参数,作为没有获取到对应环境变量时使用的默认值。
- 模拟用户认证
下面我们会使用session模拟用户的认证功能。
from flask import redirect, session, url_for
@app.route('/login')
def login():
session['logged_in'] = True # 写入session
return redirect(url_for('hello'))
# get name value from query string and cookie
@app.route('/')
@app.route('/hello')
def hello():
name = request.args.get('name')
if name is None:
name = request.cookies.get('name', 'Human')
response = '<h1>Hello, %s!</h1>' % escape(name) # escape name to avoid XSS
# return different response according to the user's authentication status
if 'logged_in' in session:
response += '[Authenticated]'
else:
response += '[Not Authenticated]'
return response
这个登录视图只是简化的示例,在实际的登录中,我们需要在页面上提供登录表单,供用户填写账户和密码,然后在登录视图里验证账户和密码的有效性。session对象可以像字典一样操作,我们向session中添加一个logged-in cookie
,将它的值设为True,表示用户已认证。当我们使用session对象添加cookie时,数据会使用程序的密钥对其进行签名,加密后的数据存储在一块名为session的cookie里,使用session对象存储的Cookie,用户可以看到其加密后的值,但无法修改它。因为session中的内容使用密钥进行签名,一旦数据被修改,签名的值也会变化。这样在读取时,就会验证失败,对应的session值也会随之失效。所以,除非用户知道密钥,否则无法对session
cookie的值进行修改。
访问 http://localhost:5000/login
:
程序中的某些资源仅提供给登入的用户,比如管理后台,这时我们就可以通过判断session是否存在logged-in键来判断用户是否认证:
from flask import session, abort
@app.route('/admin')
def admin():
if 'logged_in' not in session:
abort(403)
return 'Welcome to admin page.'
@app.route('/logout')
def logout():
if 'logged_in' in session:
session.pop('logged_in')
return redirect(url_for('hello
访问http://localhost:5000/admin
:
现在访问http://localhost:5000/logout
则会登出用户,重定向后的/hello页面的认证状态信息会变为[Not authenticated]:
再次访问http://localhost:5000/ladmin
:
尽管session对象会对Cookie进行签名并加密,但这种方式仅能够确保session的内容不会被篡改,加密后的数据借助工具仍然可以轻易读取(即使不知道密钥)。因此,绝对不能在session中存储敏感信息,比如
用户密码。
2.4 Flask上下文
我们可以把编程中的上下文理解为当前环境(environment)的快照(snapshot),Flask中有两种上下文,程序上下文(application context)和请求上下文(request context)。程序上下文中存储了程序运行所必须的信息,请求上下文里包含了请求的各种信息,比如请求的URL,请求的HTTP方法等。
2.4.1 上下文全局变量
在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()方法进行操作。
2.4.2 激活上下文在下
面这些情况下,Flask会自动帮我们激活程序上下文:
- 当我们使用flask run命令启动程序时。
- 使用旧的app.run()方法启动程序时。
- 执行使用@app.cli.command()装饰器注册的flask命令时。
- 使用flask shell命令启动Python Shell时
当请求处理完毕后,请求上下文和程序上下文也会自动销毁。也就是说,在请求处理时这两者拥有相同的生命周期。
如果我们在没有激活相关上下文时使用这些变量,Flask就会抛出
RuntimeError异常:“RuntimeError:Working outside of application
context.”或是“RuntimeError:Working outside of request context.”。
>>> from app import app
>>> from flask import current_app
>>> with app.app_context():
current_app.name
'app'
>>> from app import app
>>> from flask import current_app
>>> app_ctx = app.app_context()
>>> app_ctx.push()
>>> current_app.name
>'app'
>>> app_ctx.pop()
>>> from app import app
>>> from flask import request
>>> with app.test_request_context('/hello'):
request.method
'GET'
同样的,这里也可以使用push()和pop()方法显式地推送和销毁请求上下文。
2.4.3 上下文钩子
,Flask也为上下文提供了一个teardown_appcontext钩子,使用它注册的回调函数会在程序上下文被销毁时调用,而且通常也会在请求上下文被销毁时调用。比如,你需要在每个请求处理结束后销毁数据库连接:
@app.teardown_appcontext
def teardown_db(exception):
...
db.close()
使用app.teardown_appcontext装饰器注册的回调函数需要接收异常对象作为参数,当请求被正常处理时这个参数值将是None,这个函数的返回值将被忽略。
2.5 HTTP进阶实践
2.5.1 重定向回上一个页面
在复杂的应用场景下,我们需要在用户访问某个URL后重定向到上一个页面。我们创建了两个视图函数foo和bar,分别显示一个Foo页面和一个Bar页面
- (1)HTTP referer
HTTP referer是一个用来记录请求发源地址的HTTP首部字段(HTTP_REFERER),即访问来源.这个值通常会用来追踪用户,比如记录用户进入程序的外部站点,以此来更有针对性地进行营销。在Flask中,referer的值可以通过请求对象的referrer属性获取,即request.referrer,但是在很多种情况下,referrer字段会是空值,比如用户在浏览器的地址栏输入URL,或是用户出于保护隐私的考虑使用了防火墙软件或使用浏览器设置自动清除或修改了referrer字段。我们需要添加一个备选项:
return redirect(request.referrer or url_for('hello'))
- (2) 查询参数
除了自动从referrer获取,另一种更常见的方式是在URL中手动加入包含当前页面URL的查询参数,这个查询参数一般命名为next。比如,下面在foo和bar视图的返回值中的URL后添加next参数:
from flask import request
@app.route('/foo')
def foo():
print("执行fool......")
return '<h1>Foo page</h1><a href="%s">Do something and redirect</a>' \
% url_for('do_something', next=request.full_path)
@app.route('/bar')
def bar():
print("执行bar......")
return '<h1>Bar page</h1><a href="%s">Do something and redirect</a>' \
% url_for('do_something', next=request.full_path)
@app.route('/do-something')
def do_something():
# do something here
return redirect(request.args.get('next', url_for('hello')))
在do_something视图中,我们获取这个next值,然后重定向到对应的路径:
return redirect(request.args.get('next'))
用户在浏览器的地址栏直接访问时可以轻易地修改查询参数,为了避免next参数为空的情况,我们也要添加备选项,如果为空就重定向到hello视图:
return redirect(request.args.get('next', url_for('hello')))
为了覆盖更全面,我们可以将这两种方式搭配起来一起使用:首先获取next参数,如果为空就尝试获取referer,如果仍然为空,那么就重定向到默认的hello视图。因为在不同视图执行这部分操作的代码完全相同,我们可以创建一个通用的redirect_back()
函数:
# redirect to last page
@app.route('/foo')
def foo():
print("执行fool......")
return '<h1>Foo page</h1><a href="%s">Do something and redirect</a>' \
% url_for('do_something', next=request.full_path)
@app.route('/bar')
def bar():
print("执行bar......")
return '<h1>Bar page</h1><a href="%s">Do something and redirect</a>' \
% url_for('do_something', next=request.full_path)
@app.route('/do-something')
def do_something():
# do something here
return redirect_back()
def is_safe_url(target):
ref_url = urlparse(request.host_url)
test_url = urlparse(urljoin(request.host_url, target))
return test_url.scheme in ('http', 'https') and \
ref_url.netloc == test_url.netloc
def redirect_back(default='hello', **kwargs):
for target in request.args.get('next'), request.referrer:
if not target:
continue
if is_safe_url(target):
return redirect(target)
return redirect(url_for(default, **kwargs))
- 对URL进行安全验证
虽然我们已经实现了重定向回上一个页面的功能,但安全问题不容小觑,鉴于referer和next容易被篡改的特性,如果我们不对这些值进行验证,则会形成开放重定向(Open Redirect)漏洞。
以URL中的next参数为例,next变量以查询字符串的方式写在URL里,因此任何人都可以发给某个用户一个包含next变量指向任何站点的链接。举个简单的例子,如果你访问下面的URL:
http://localhost:5000/do-something?next=http://helloflask.com
程序会被重定向到http://helloflask.com。也就是说,如果我们不验证next变量指向的URL地址是否属于我们的应用内,那么程序很容易就会被重定向到外部地址。攻击者就可以在重定向后的B网站诱导用户输入敏感信息,比如银行卡号及密码。
确保URL安全的关键就是判断URL是否属于程序内部:
try:
from urlparse import urlparse, urljoin
except ImportError: # Python3需要从urllib.parse导入
from urllib.parse import urlparse, urljoin
from flask import request
def is_safe_url(target):
ref_url = urlparse(request.host_url)
test_url = urlparse(urljoin(request.host_url, target))
return test_url.scheme in ('http', 'https') and \
ref_url.netloc == test_url.netloc
这个函数接收目标URL作为参数,并通过request.host_url
获取程序内的主机URL,然后使用urljoin()
函数将目标URL转换为绝对URL。接着,分别使用urlparse模块提供的urlparse()函数解析两个URL,最后对目标URL的URL模式和主机地址进行验证,确保只有属于程序内部的URL才会被返回。在执行重定向回上一个页面的redirect_back()函数中,我们使用is_safe_url()验证next和referer的值:
def redirect_back(default='hello', **kwargs):
for target in request.args.get('next'), request.referrer:
if not target:
continue
if is_safe_url(target):
return redirect(target)
return redirect(url_for(default, **kwargs))
2.5.2 使用AJAX技术发送异步请求
1.认识AJAX
AJAX指异步Javascript和XML(Asynchronous JavaScript AndXML),它不是编程语言或通信协议,而是一系列技术的组合体。简单来说,AJAX基于XMLHttpRequest(https://xhr.spec.whatwg.org/)让我们可以在不重载页面的情况下和服务器进行数据交换。加上JavaScript和DOM(Document Object Model,文档对象模型),我们就可以在接收到响应数据后局部更新页面。而XML指的则是数据的交互格式,也可以是纯文本(Plain Text)、HTML或JSON。顺便说一句,XMLHttpRequest不仅支持HTTP协议,还支持FILE和FTP协议。
以删除某个资源为例,在普通的程序中流程如下:
- 1)当“删除”按钮被单击时会发送一个请求,页面变空白,在接收
到响应前无法进行其他操作。 - 2)服务器端接收请求,执行删除操作,返回包含整个页面的响
应。 - 3)客户端接收到响应,重载整个页面。
使用AJAX技术时的流程如下:
- 1)当单击“删除”按钮时,客户端在后台发送一个异步请求,页面
不变,在接收响应前可以进行其他操作。 - 2)服务器端接收请求后执行删除操作,返回提示消息或是无内容
的204响应。 - 3)客户端接收到响应,使用JavaScript更新页面,移除资源对应的
页面元素。
2.使用jQuery发送AJAX请求
jQuery
是流行的JavaScript库,它包装了JavaScript
,让我们通过更简单的方式编写JavaScript
代码。对于AJAX
,它提供了多个相关的方法,使用它可以很方便地实现AJAX操作。更重要的是,jQuery
处理了不同浏览器的AJAX
兼容问题,我们只需要编写一套代码,就可以在所有主流的浏览器正常运行。
异步加载长文章示例:
在示例程序的对应页面中,我们将显示一篇很长的虚拟文章,文章正文下方有一个“加载更多”按钮,当加载按钮被单击时,会发送一个AJAX请求获取文章的更多内容并直接动态插入到文章下方。
from jinja2.utils import generate_lorem_ipsum
# AJAX
@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',
type: 'get',
success: function(data){
$('.body').append(data);
}
})
})
})
</script>''' % post_body
@app.route('/more')
def load_post():
return generate_lorem_ipsum(n=1)
文章的随机正文通过Jinja2提供的generate_lorem_ipsum()函数生成,n参数用来指定段落的数量,默认为5,它会返回由随机字符组成的虚拟文章。文章下面添加了一个“加载更多”按钮。按钮下面是两个<script></script>
代码块,第一个script从CDN加载jQuery资源。
在第二个script标签中,我们在代码的最外层创建了一个$(function(){...})
函数,这个函数是常见的$(document).ready(function(){...})
函数的简写形式。这个函数用来在页面DOM加载完毕后执行代码,类似传统JavaScript中的window.onload方法,所以我们通常会将代码包装在这个函数中。美元符号是jQuery的简写,我们通过它来调用jQuery提供的多个方法,所以$.ajax()
等同于jQuery.ajax()
。
在$(function(){...})
中,$('#load')
被称为选择器,我们在括号中传入目标元素的id、class或是其他属性来定位到对应的元素,将其创建为jQuery对象。我们传入了“加载更多”按钮的id值以定位到加载按钮。在这个选择器上,我们附加了.click(function(){...})
,这会为加载按钮注册一个单击事件处理函数,当加载按钮被单击时就会执行单击事件回调函数。在这个回调函数中,我们使用
.
a
j
a
x
(
)
方
法
发
送
一
个
A
J
A
X
请
求
到
服
务
器
,
通
过
u
r
l
将
目
标
U
R
L
设
为
‘
‘
‘
“
/
m
o
r
e
”
‘
‘
‘
,
通
过
t
y
p
e
参
数
将
请
求
的
类
型
设
为
G
E
T
。
当
请
求
成
功
处
理
并
返
回
2
X
X
响
应
时
(
另
外
还
包
括
304
响
应
)
,
会
触
发
s
u
c
c
e
s
s
回
调
函
数
。
s
u
c
c
e
s
s
回
调
函
数
接
收
的
第
一
个
参
数
为
服
务
器
端
返
回
的
响
应
主
体
,
在
这
个
回
调
函
数
中
,
我
们
在
文
章
正
文
(
通
过
.ajax()方法发送一个AJAX请求到服务器,通过url将目标URL设为```“/more”```,通过type参数将请求的类型设为GET。当请求成功处理并返回2XX响应时(另外还包括304响应),会触发success回调函数。success回调函数接收的第一个参数为服务器端返回的响应主体,在这个回调函数中,我们在文章正文(通过
.ajax()方法发送一个AJAX请求到服务器,通过url将目标URL设为‘‘‘“/more”‘‘‘,通过type参数将请求的类型设为GET。当请求成功处理并返回2XX响应时(另外还包括304响应),会触发success回调函数。success回调函数接收的第一个参数为服务器端返回的响应主体,在这个回调函数中,我们在文章正文(通过(’.body’)选择)底部使用append()方法插入返回的data数据。
2.5.3 HTTP服务器端推送
在某些场景下,我们需要的通信模式是服务器端的主动推送(server push)。比如,一个聊天室有很多个用户,当某个用户发送消息后,服务器接收到这个请求,然后把消息推送给聊天室的所有用户。类似这种关注实时性的情况还有很多,比如社交网站在导航栏实时显示新提醒和私信的数量,用户的在线状态更新,股价行情监控、显示商品库存信息、多人游戏、文档协作等。
常用推送技术:
2.5.4 Web安全防范
无论是简单的博客,还是大型的社交网站,Web安全都应该放在首位。Web安全问题涉及广泛,我们在这里介绍其中常见的几种攻击(attack)和其他常见的漏洞(vulnerability)。
1、注入攻击
在OWASP(Open Web Application Security Project,开放式Web程序安全项目)发布的最危险的Web程序安全风险Top 10中,注入攻击(Injection)都位列第一。注入攻击包括系统命令(OS Command)注入、SQL(Structured Query Language,结构化查询语言)注入(SQLInjection)、NoSQL注入、ORM(Object Relational Mapper,对象关系映射)注入等。我们这里重点介绍的是SQL注入。
2.6 本章代码
本章全部代码:
import os
try:
from urlparse import urlparse, urljoin
except ImportError:
from urllib.parse import urlparse, urljoin
from jinja2 import escape
from jinja2.utils import generate_lorem_ipsum
from flask import Flask, make_response, request, redirect, url_for, abort, session, jsonify
app = Flask(__name__)
app.secret_key = os.getenv('SECRET_KEY', 'secret string')
# get name value from query string and cookie
@app.route('/')
@app.route('/hello')
def hello():
name = request.args.get('name')
if name is None:
name = request.cookies.get('name', 'Human')
response = '<h1>Hello, %s!</h1>' % escape(name) # escape name to avoid XSS
# return different response according to the user's authentication status
if 'logged_in' in session:
response += '[Authenticated]'
else:
response += '[Not Authenticated]'
return response
# redirect
@app.route('/hi')
def hi():
return redirect(url_for('hello'))
# use int URL converter
@app.route('/goback/<int:year>')
def go_back(year):
return 'Welcome to %d!' % (2018 - year)
# use any URL converter
@app.route('/colors/<any(blue, white, red):color>')
def three_colors(color):
return '<p>Love is patient and kind. Love is not jealous or boastful or proud or rude.</p>'
# return error response
@app.route('/brew/<drink>')
def teapot(drink):
if drink == 'coffee':
abort(418)
else:
return 'A drop of tea.'
# 404
@app.route('/404')
def not_found():
abort(404)
# return response with different formats
@app.route('/note', defaults={'content_type': 'text'})
@app.route('/note/<content_type>')
def note(content_type):
content_type = content_type.lower()
if content_type == 'text':
body = '''Note
to: Peter
from: Jane
heading: Reminder
body: Don't forget the party!
'''
response = make_response(body)
response.mimetype = 'text/plain'
elif content_type == 'html':
body = '''<!DOCTYPE html>
<html>
<head></head>
<body>
<h1>Note</h1>
<p>to: Peter</p>
<p>from: Jane</p>
<p>heading: Reminder</p>
<p>body: <strong>Don't forget the party!</strong></p>
</body>
</html>
'''
response = make_response(body)
response.mimetype = 'text/html'
elif content_type == 'xml':
body = '''<?xml version="1.0" encoding="UTF-8"?>
<note>
<to>Peter</to>
<from>Jane</from>
<heading>Reminder</heading>
<body>Don't forget the party!</body>
</note>
'''
response = make_response(body)
response.mimetype = 'application/xml'
elif content_type == 'json':
body = {"note": {
"to": "Peter",
"from": "Jane",
"heading": "Remider",
"body": "Don't forget the party!"
}
}
response = jsonify(body)
# equal to:
# response = make_response(json.dumps(body))
# response.mimetype = "application/json"
else:
abort(400)
return response
# set cookie
@app.route('/set/<name>')
def set_cookie(name):
response = make_response(redirect(url_for('hello')))
response.set_cookie('name', name)
return response
# log in user
@app.route('/login')
def login():
session['logged_in'] = True
return redirect(url_for('hello'))
# protect view
@app.route('/admin')
def admin():
if 'logged_in' not in session:
abort(403)
return 'Welcome to admin page.'
# log out user
@app.route('/logout')
def logout():
if 'logged_in' in session:
session.pop('logged_in')
return redirect(url_for('hello'))
# AJAX
@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',
type: 'get',
success: function(data){
$('.body').append(data);
}
})
})
})
</script>''' % post_body
@app.route('/more')
def load_post():
return generate_lorem_ipsum(n=1)
# redirect to last page
@app.route('/foo')
def foo():
return '<h1>Foo page</h1><a href="%s">Do something and redirect</a>' \
% url_for('do_something', next=request.full_path)
@app.route('/bar')
def bar():
return '<h1>Bar page</h1><a href="%s">Do something and redirect</a>' \
% url_for('do_something', next=request.full_path)
@app.route('/do-something')
def do_something():
# do something here
print(1,2,3,4)
return redirect_back()
def is_safe_url(target):
ref_url = urlparse(request.host_url)
test_url = urlparse(urljoin(request.host_url, target))
return test_url.scheme in ('http', 'https') and \
ref_url.netloc == test_url.netloc
def redirect_back(default='hello', **kwargs):
for target in request.args.get('next'), request.referrer:
if not target:
continue
if is_safe_url(target):
return redirect(target)
return redirect(url_for(default, **kwargs))