本节主要目录如下:
一、请求响应循环
二、HTTP请求
2.1、请求报文
2.2、Request对象
2.3、在Flask中处理请求
2.4、请求钩子
三、HTTP响应
3.1、响应报文
3.2、在Flask中生成响应
3.3、响应格式
3.4、Cookie
3.5、session:安全的Cookie
四、Flask上下文
4.1、上下文全局变量
4.2、激活上下文*
4.3、上下文钩子
五、HTTP进阶实践
5.1、重定向回上一个页面
5.2、使用AJAX技术发送异步请求
5.3、HTTP服务器端推送
5.4、Web安全防范
一、请求响应循环
“请求-响应循环”:客户端发出请求,服务器处理请求并返回响应。
Flask Web程序的工作流程:
当用户访问一个URL,浏览器便生成对应的HTTP请求,经由互联网发送到对应的Web服务器。Web服务器接收请求,通过WSGI将HTTP格式的请求数据转换为成我们的Flask程序能够使用的Python数据。
在程序中,Flask根据请求的URL执行对应的视图函数,获取返回值生成响应。响应依此经过WSGI转换生成HTTP响应,再经由Web服务器传递,最终被发出请求的客户端接收。浏览器渲染响应中包含的HTML和CSS代码,并执行JavaScript代码,最终把解析后的页面呈现在用户浏览器的窗口中。
二、HTTP请求
一个标准的URL由很多部分组成,以下面这个URL为例:
http://helloflask.com/hello?name=Grey
URL组成部分:
信息 | 说明 |
---|---|
http:// | 协议字符串,指定要使用的协议 |
helloflask.com | 服务器的地址(域名) |
/hello?name=Grey | 要获取的资源路径(path),类似UNIX的文件目录结构 |
2.1、请求报文
请求的实质是发送到服务器上的一些数据,这种浏览器与服务器之间交互的数据被称为报文,请求时浏览器发送的数据被称为请求报文,而服务器返回的数据被称为响应报文。
请求的报文由请求的方法、URL、协议版本、首部字段以及内容实体组成。
请求报文示意表:
组成说明 | 请求报文内容 |
---|---|
报文首部:请求行(方法、URL、协议) | GET/hello HTTP/1.1 |
报文首部:各种首部字段 | Host:helloflask.com Connection:keep-alive Cache-Control:max-age=0 User-Agent:... |
空行 | |
报文主体 | name=Grey |
常见的HTTP方法:
方法 | 说明 |
---|---|
GET | 获取资源 |
POST | 传输数据 |
PUT | 传输文件 |
DELETE | 删除资源 |
HEAD | 获得报文首部 |
OPTIONS | 询问支持的方法 |
2.2、Request对象
Flask的请求对象requests封装了从客户端发来的请求报文,当收到请求后,请求对象会提供多个属性来获取URL(http://helloflask.com/hello?name=Grey)的各个部分:
属性 | 值 | 属性 | 值 |
---|---|---|---|
path | u'/hello' | base_url | u'http://helloflask.com/hello' |
full_path | u'/hello?name=Grey' | url | u'http://helloflask.com/hello?name=Grey' |
host | u'/helloflask.com' | url_root | u'HelloFlask' |
host_url | u'/HelloFlask' |
除了URL,请求报文中的其他信息都可以通过request对象提供的属性和方法获取:
属性/方法 | 说明 |
---|---|
args | 存储解析后的查询字符串,可通过字典方式获取键值。 |
value | Werkze CombinedMultiDict 对象,结合了 args 和form 属性的值 |
headers | 一个 Werkzeug EnvironHeaders 象,包含首部字段, 可以以字典的形式操作 |
user_agent | 用户代理( User Agent,)UA,包含了用户的客户端类型,操作系统类型等信息 |
from flask import Flask
app = Flask(__name__)
@app.route('/hello')
def hello():
name = request.args.get('name','Flask') # 获取查询参数name的值
return '<h1>hello,%s</h1>' % name
2.3、在Flask中处理请求
URL是指向网络上资源的地址。在Flask中,我们需要让请求的URL匹配对应的视图函数,视图函数返回值就是URL对应的资源。
2.3.1、路由匹配
为了便于将请求分发到对应的视图函数,程序实例中存储了一个路由表(app.url_map),其中定义了URL规则和视图函数的映射关系。
当请求的URL与某个视图函数的URL规则匹配成功时,对应的视图函数就会被调用。使用flask routes命令可以查看程序中定义的所有路由,这个列表由app.url_map解析得到:
$ flask routes
Endpoint Methods Rule
----------- ------- -----------------------
greet GET /greet/<name>
greet GET /greet
hello_world GET /
static GET /static/<path:filename>
在输出的文本中,我们可以看到每个路由对应的端点、HTTP方法和URL规则,其中static端点是Flask添加的特殊路由,用来访问静态文件。
2.3.2、设置监听的HTTP方法
在app.route()装饰器中使用methods参数传入一个包含监听的HTTP方法的可迭代对象。比如,下面的视图函数同时监听GET和POST请求:
@app.route('/hello',methods=['GET','POST'])
def hello():
return '<h1>Hello, Flask!</h1>'
当某个请求的方法不合符要求时,请求将无法被正常处理。返回405错误响应(表示请求方法不允许)。
2.3.3、URL处理
Flask内置的URL变量转换器:
转换器 | 说明 |
---|---|
string | 不包含斜线的字符串(默认值) |
int | 整型 |
float | 浮点数 |
path | 包含斜线的字符串。static路由的URL规则中的filename变量就使用了这个转换器 |
any | 匹配一系列给定值中的一个元素 |
uuid | UUID字符串 |
转换器通过特定的规则指定,即“<转换器:变量名>”。int:year把year的值转换为整数,因此我们可以在视图函数中直接对year变量进行数学计算:
@app.route('goback/<int:year>')
def go_back(year):
return '<p>Welcome to %d</p>' % (2018 - year)
在用法上唯一特别的是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 or boastful or proud or rude.</p>'
还可以在any转换器中传入一个预先定义的列表,可通过格式化字符串的方式(使用%或是format()函数)来构建URL规则字符串:
colors = ['blue','white','red']
@app.route('/colors/any(%S):color>' %s str(colors)[1:-1])
...
2.4、请求钩子
有时候需要对请求进行预处理和后处理,这时可以使用Flask提供的一些请求钩子,它们可以用来注册在请求处理的不同阶段执行的处理函数(或称为回调函数,即Callback)。
这些请求钩子使用装饰器实现,通过程序实例app调用:以before_request钩子(请求之前)为例,当对一个函数附加了app.before_request装饰器后,就会将这个函数注册为before_request处理函数,每次执行请求前都会触发所有before_request处理函数。
Flask默认实现的五种请求钩子:
钩子 | 说明 |
---|---|
before_first_request | 注册一个函数,在处理第一个请求前运行 |
before_request | 注册一个函数,在处理每个请求前运行 |
after_request | 注册一个函数,如果没有未处理的异常抛出,会在每个请求结束后运行 |
teardown_request | 注册一个函数,即使有未处理的异常抛出,会在每个请求结束后运行。如果发生异常,会传入异常对象作为参数到注册的函数中 |
after_this_request | 在视图内注册一个函数,会在这个请求结束后运行 |
使用和app.route()装饰器基本相同,每个钩子可以注册任意多个处理函数,函数名并不是必须和钩子函数名称相同,示例:
@app.before_request
def do_something()
pass # 这里的代码会在每个请求处理前执行
使用情况示例:
- before_first_request:在完整程序中,运行程序前我们需要进行一些程序的初始化操作,比如创建数据库表,添加管理员用户。
- before_request:网站上要记录用户最后在线时间,可以通过用户最后发送请求时间来实现。
-
after_request:在视图函数中进行数据库操作,比如更新、插入等,之后需要将更改提交到数据库中。
另一种常见的应用是建立数据库连接,通常会有多个视图函数需要建立和关闭数据库连接,这些操作基本相同。一个理想的方法是在强求之前(before_request)建立连接,在请求之后(teardown_request)关闭连接。
三、HTTP响应
在Flask程序中,客户端发出的请求触发相应的视图函数,获取返回值会作为响应的主体,最后生成完整的响应,即响应报文。
3.1、响应报文
响应报文主要由协议版本、状态码、原因短语、响应首部和响应主体组成。以向localhost:5000/hello的请求为例,服务器生成的响应报文示意:
组成说明 | 响应报文内容 |
---|---|
报文首部:状态行(协议、状态码、原因短语) | HTTP/1.1 200 OK |
报文首部:各种首部字段 | Content-Type:text/html;charset=utf-8 ... |
空行 | |
报文主体 | <h1>Hello Human!</h1> |
常见状态码和相应的原因短语:
类型 | 状态码 | 原因短语 | 说明 |
---|---|---|---|
成功 | 200 | OK | 请求被正常处理 |
201 | Created | 请求被处理,并创建了一个新资源 | |
204 | No Content | 请求处理成功,但无内容返回 | |
重定向 | 301 | Moved Permanently | 永久重定向 |
302 | Found | 临时性重定向 | |
304 | Not Modified | 请求的资源未被修改,重定向到缓存的资源 | |
客户端错误 | 400 | Bad Request | 表示请求无效,即请求报文中存在错误 |
401 | Unauthorized | 类似403,表示请求的资源需要获取授权信息,在浏览器会弹出认证弹窗 | |
403 | Forbidden | 表示请求的资源被服务器拒绝访问 | |
404 | Not Found | 表示服务器上无法找到请求的资源或URL无效 | |
服务器端错误 | 500 | Internal Server Error | 服务器内部发生错误 |
3.2、在Flask中生成响应
响应在Flask中用Response对象表示,大部分情况,我们只负责返回主体内容。
视图函数可以返回最多由三个元素组成的元组:响应主体、状态码、首部字段(可以为字典或是两元素元组组成的列表)。
# 普通的响应可以只包含主体
@app.route('/hello')
def hello():
...
return '<h1>Hello,Flask!</h1>'
# 默认状态码为200,下面指定不同的状态码
@app.route('/hello')
def hello():
...
return '<h1>Hello,Flask!</h1>',201
# 要生成状态码为3XX的重定向响应:
@app.route('/hello')
def hello():
...
return '',302,{'Location','http://www.example.com'}
3.2.1、重定向
当某个用户在没有经过认证的情况下访问需要登录后才能访问的资源,程序通常会重定向到登录页面。
除了上一节手动生成302响应,我们可以使用Flask提供的redirect()函数来生成重定向响应,重定向的目标URL作为第一个参数:
from flask import Flask,redirect
# ...
@app.route('/hello')
def hello():
return redirect('http://www.example.com')
# 使用redirect()函数时,默认的状态码为302,即临时重定向。若要修改则在函数中第二个参数
若要在程序内重定向到其他视图,只需要在redirect()函数中使用url_for()函数生成目标URL即可:
# http/app.py重定向到其他视图
from flask import Flask,url_for
...
@app.route('/hi')
def hi():
...
return redirect(url_for('/hello')) # 重定向到/hello
@app.route('/hello')
def hello():
...
3.2.2、错误响应
使用Flask提供的abort()函数手动返回错误响应,在abort()函数中传入状态码即可返回对应的错误响应:
from flask import Flask,abort
..
@app.route('/404')
def not_found():
abort(404)
3.3、响应格式
Flask默认使用HTML格式返回响应,在Content-Type字段中定义设置不同的MIME类型以返回不同的响应数据格式。以默认的HTML为例:
Content-Type:text/html;charset=utf-8
若要使用其他MIME类型,通过Flask提供的make_response()方法生成响应对象,传入响应的主体作为参考,然后使用响应对象的mimetype属性设置MIMW类型:
from flask import make_response
@app.route('/foo')
def foo():
response = make_response('Hello,World!')
response.mimetype = 'text/plain'
return response
常见的数据格式有纯文本、HTML、XML和JSON:
3.3.1、纯文本
MIME类型:text/plain
# 示例
Note
to:Peter
from:Jane
heading:Reminder
body:Don't forget the party!
3.3.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>
3.3.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一般作为AJAX请求的响应格式,或是Web API的响应格式。
3.3.4、JSON
MIME类型:application/json
# 示例
{
"note":{
"to":"Peter",
"from":"Jane",
"heading":"Reminder",
"body":"Don't forget the party!"
}
}
可以直接从Flask中导入json对象,然后调用dumps()方法将字典、列表或元组序列化为JSON字符串,再使用前面介绍的方法修改MIME类型,即可返回JSON响应,例如:
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
除此Flask提供更方便的jsonify()函数,仅需要传入数据或参数,它会对我们传入的参数进行序列化,转化成JSON字符串作为响应的主体,然后生成一个响应对象,并且设置正确的MIME类型。
# 上述简化版(jsonify()函数)
from flask import jsonify
@app.route('/foo')
def foo():
return jsonify({name:'Grey Li',gender:'male'})
# jsonify()函数默认生成200响应
3.4、Cookie
HTTP是无状态协议。就是说在一次请求响应结束后,服务器不会留下任何关于对方状态的信息。
Cookie技术通过在请求和响应报文中添加Cookie数据来保存客户端的状态信息。
在Flask中使用Response类提供的set_cookie()方法在响应中添加一个cookie。使用方法:先使用make_response()方法手动生成一个响应对象,传入响应主体作为参数。这个响应对象默认实例化内置的Response类。
Response类的常用属性和方法:
方法/属性 | 说明 |
---|---|
headers | 一个Werkzeug的Headers对象,表示响应首部,可以像字典一样操作 |
status | 状态码,文本类型 |
status_code | 状态码,整型 |
mimetype | MIME类型 |
set_cookie() | 用来设置一个cookie |
set_cookie() 方法支持多个参数来设置Cookie的选项:
属性 | 说明 |
---|---|
key | cookie的键 |
value | cookie的值 |
max_age | cookie被保存的时间数,单位为秒;默认在用户会话结束时过期 |
expires | 具体的过期时间,一个datetime对象或UNIX时间戳 |
path | 限制cookie只在给定的路径可用,默认为整个域名 |
domain | 设置cookie可用的域名 |
secure | 如果为True,只有通过HTTPS才可以使用 |
httponly | 如果为True,进制客户端JavaScript获取cookie |
set_cookei视图用来设置cookie,它会将URL中的name变量的值设置到名为name的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
# 查看浏览器的Cookie会看到多了一块名为name的cookie
# 在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
3.5、session:安全的Cookie
Flask提供session对象将Cookie数据加密存储。
附注:在编程中,session指用户会话,又称对话,即服务器和客户端/浏览器之间或桌面程序和用户之间的交互活动、在Flask中,session对象用来加密Cookie。默认情况下,它会把数据存储在浏览器上一个名为session的cookie里。
3.5.1、设置程序密钥
session通过密钥对数据进行签名以加密数据,通过Flask.secret_key属性或配置变量SECRET_KEY设置,比如:
app.secret_key = 'secret string'
# 更安全的做法是把密钥写进环境变量中或保存在.env文件值:
SECRET_KEY=secret string
# 然后在程序脚本中使用os模块提供的getenv()方法获取:
import os
# ...
app.secre_key = os.getenv('SECRET_KEY','secret string')
# getenv()方法中的第二个参数作为没有获取到对应环境变量时使用的默认值。
3.5.2、模拟用户认证
# 使用session模拟用户的认证功能
from flask import redirect,session,url_for
@app.route('/login')
def login():
session['logged_in'] = True # 写入session
return redirect(url_for('hello'))
当支持用户登录后,我们就可以根据用户的认证状态分别显示不同的内容。在login视图的最后,我们将程序重定向到hello视图:
from flask import request,session
@app.route('/')
@app.route('/hello')
def hello():
name = request.args.get('name','Human')
response = '<h1>Hello,%s</h1>'% name
# 根据用户认证状态返回不同的内容
if 'logged_in' in session:
response += '[Authenticated]'
else:
response += '[Not Authenticated]'
return response
程序中的某些资源仅提供给登入的用户,比如管理后台,这时我们就可以通过判断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.'
# 通过判断logged_in是否存在session中,可以实现:如果用户已经认证,会返回一个提示文字,否则返回403错误响应。
登出用户的logout视图实际操作就是把代表用户认证的logged_in cookie删除,这通过session对象的pop方法实现:
from flask import session
@app.route('/logout')
def logout():
if 'logged_in' in session:
session.pop('logged_in')
return redirect(url_for('hello'))
提示:默认session cookie会在用户关闭浏览器时删除。通过将session.permanent属性设置为True可以将session的有效期延长。Flask.permanent_session_lifetime属性值对应的datetime.timedelta对象,也可以通过配置变量PERMANENT_SESSION_LIFETIME设置,默认为31天。
注意:加密仅能保证session的内容不被篡改,借助工具仍可以读取,因此不能在session中存储敏感信息,比如用户密码。
四、Flask上下文
我们可以把编程中的上下文理解为当前环境的快照。Flask中有两种上下文,程序上下文和请求上下文。
4.1、上下文全局变量
每一个视图都需要上下文信息。前面实例中直接从Flask导入一个全局的request对象,然后在视图函数里直接调用request的属性获取数据。为了方便获取这两种上下文环境中存储的信息,Flask提供了四个上下文全局变量:
变量名 | 上下文类别 | 说明 |
---|---|---|
current_app | 程序上下文 | 指向处理请求的当前程序实例 |
g | 程序上下文 | 替代Python的全局变量用法,确保仅在当前请求中可用。用于存储全局数据,每次请求都会重设 |
request | 请求上下文 | 封装客户端发出的请求报文数据 |
session | 请求上下文 | 用于记住请求间的数据,通过前面的Cookie实现 |
在不同的视图函数中,request对象都表示和视图函数对应的请求,也就是当前请求。而程序也会有多个程序实例的情况,为了能获取对应的程序实例,而不是固定的某一个程序实例,我们就需要current_app变量。
g存储在程序上下文中,而程序上下文会随着每一个请求的进入而激活,随着每一个请求的处理完毕而销毁,所以每次请求都会重设这个值。通常结合钩子来保存每个请求处理前所需要的全局变量,比如当前登入的用户对象,数据库连接等。
from flask import g
@app.before_request
def get_name():
g.name = request.args.get('name')
设置这个函数后,在其他视图中可以直接使用g.name获取对应的值。另外,g 也支持使用类似字典的get()、pop()以及setdefault()方法进行操作。
4.2、激活上下文*
Flask自动激活程序上下文的情况:
- 使用flask run命令启动程序时
- 旧方法app.run()方法启动程序时
- 执行使用@app.cli.command()装饰器注册的flask命令时
- 使用flask shell命令启动Python Shell时
当请求进入时,Flask会自动激活请求上下文(程序上下文也自动激活),这时我们可以使用request和session变量。请求处理完毕后两个上下文都自动销毁(拥有相同的生命周期)。
如果我们在没有激活上下文时使用这些变量,Flask就会抛出RuntimeRrror异常:
"RuntimeError:Working outside of application context."或是"RuntimeError:Working outside of request context."
手动激活上下文:
# Python Shell
# 程序上下文对象使用app.app_context()获取
>>> from app import app
>>> from flask import current_app
>>> with app.app_context():
... current_app.name
'app'
# 或是显式地使用push()方法推送(激活)上下文,在执行完相关操作时使用pop()方法销毁上下文
>>> from app import app
>>> from flask import current_app
>>> app_ctx = app.app_context()
>>> app_ctx.push()
>>> current_app.name
'app'
>>> app_ctx.pop()
# 而请求上下文可以通过test_request_context()方法临时创建:
>>> from app import app
>>> from flask import request
>>> with app.test_request_context('/hello'):
... request.method
'GET'
# 同样的,这里也可以使用push()和pop()方法显式地推送和销毁请求上下文
4.3、上下文钩子
Flask为上下文提供了一个teardown_appcontext钩子,使用它注册的回调函数会在程序上下文被销毁时调用,而且通常也会在请求上下文被销毁时调用。
# 比如在每个请求处理结束后销毁数据库连接
@app.teardown_appcontext
def teardown_db(exception):
...
db.close()
五、HTTP进阶实践
5.1、重定向回上一个页面
# 创建两个视图函数foo和bar,分别显示一个Foo页面和一个Bar页面
@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')
# 这两个页面都添加了一个指向do_something视图的链接:
@app.route('/do_something')
def do_something():
return redirect(url_for('hello'))
要完成的操作:在Foo页面上单击链接,我们希望被重定向回Foo页面;Bar页面同理:
5.1.1、获取上一个页面的URL
要重定向回上一个页面,最关键的是获取上一个页面的URL。上一个页面的URL一般可以通过两种方式获取:
(1)HTTP referer
HTTP referer是一个用来记录请求发起地址的HTTP首部字段,即访问来源。当用户在某个站点单击链接,浏览器向新链接所在的服务器发起请求,请求的数据中包含HTTP_REFERER字段记录了用户所在原站点URL。
这个值通常用来追踪用户,在Flask中,referer的值可以通过请求对象的referrer属性获取,即request.referrer。现在可改写do_something视图的返回值:
return redirect(request.referrer)
但在多种情况下,referrer字段会是空值,比如在浏览器的地址栏输入URL,或是用户出于保护隐私的考虑使用了防火墙软件等修改了referrer字段。我们需要加一个备选项:
return redirect(request.referrer or url_for('hello'))
(2)查询参数
在URL中手动加入包含当前页面URL的查询参数,这个参数一般命名为next。
# 在foo和bar视图的返回值中的URL后添加next参数:
@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)
在程序内部只需要使用相对URL,所以这里使用request.full_path获取当前页面的完整路径。在do_something视图中,我们获取这个next值,然后重定向到对应的路径:
return redirect(request.args.get('next'))
为了避免next参数为空的情况,添加备选项,如果为空就重定向到hello视图:
return redirect(request.args.get('next',url_for('hello')))
(3)整合
为了覆盖更全面,我们将这两种方式搭配起来一起使用:首先获取next参数,如果为空就尝试获取referer,如果仍为空,就重定向到hello视图。因为在不同视图执行这部分操作的代码完全先那个塔,可以创建一个通用的redirect_back()函数:
# 重定向回上一个页面
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))
# 在do_something视图中使用这个函数的示例:
@app.route('/do_something_and_redirect')
def do_something():
return redirect_back()
5.1.2、对URL进行安全验证
鉴于referer和next容易被篡改的特性,如果我们不对这些值进行验证,则会形成开发重定向(Open Redirect)漏洞。如果我们不验证next变量指向的URL地址是否属于我们的应用内,那么程序很容易就会被重定向到外部地址。
# 创建一个URL验证函数is_safe_url(),用来验证next变量值是否属于程序内部URL
from urllib.parse import urlparse, urljoin
def is_safe_url(target):
ref_url = urlparse(request.host_url) # 获取程序内的主机URL
test_url = urlparse(urljoin(request.host_url, target)) # 将目标URL转换为绝对URL,使用urlparse()函数解析两个URL
return test_url.scheme in ('http', 'https') and ref_url.netloc == test_url.netloc # 验证,只有属于程序内部的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))
5.2、使用AJAX技术发送异步请求
5.2.1、AJAX
AJAX指异步Javascript和XML,它不是编程语言或通信协议,而是一些列技术的组合体。ajax让我们在不重载页面的情况下和服务器进行数据交换。加上JavaScript和DOM(文档对象模型),我们就可以在接收到数据后局部更新页面。XML指数据的交互模式,也可以是纯文本、HTML或JSON。
使用AJAX加载数据的情况:用户鼠标向下滚动到底部时在后台发送请求获取数据,然后插入文章;
以删除某个资源为例,AJAX实现步骤:
- 当单击“删除”按钮时,客户端在后台发送一个异步请求,页面不变,在接收响应前可以进行其他操作。
- 服务器端接收请求后执行删除操作,返回提示消息或是无内容的 204 响应
- 客户端接收到响应 ,使用 JavaScript 更新页面,移除资源对应的页面元素
5.2.2、使用jQuery发送AJAX请求
jQuery 是流行的 JavaScript 库,它包装了 JavaScript 。对于AJAX,它提供了多个相关的方法,使用它可以很方便地实现AJAX操作。
使用jQuery的ajax()函数发送AJAX请求。其所支持的参数:
参数 | 参数值类型及默认值 | 说明 |
---|---|---|
url | 字符串;默认为当前页地址 | 请求的地址 |
type | 字符串;默认为“GET“ | 请求的方式,即HTTP方法,比如GET、POST、DELETE等 |
data | 字符串;无默认值 | 发送到服务器的数据。会被 jQuery 自动转换为查询字符串 |
dataType | 字符串;默认由jQuery自动判断 | 期待服务器返回的数据类型,可用的值如下:“xml".html" "script"”json" "jsonp””text” |
contentTypr | 字符串;默认为‘application/x-www-form-urlencoded;charset=UTF-8' | 发送请求时使用的内容类型,即请求首部放Content-Type字段内容 |
complete | 函数;无默认值 | 请求完成后调用的回调函数 |
suceess | 函数;无默认值 | 请求成功后的调用的回调函数 |
error | 函数;无默认值 | 请求失败后调用的回调函数 |
5.2.3、返回“局部数据”
对于处理AJAX请求的视图函数来说,不会返回完整的HTM响应,而是局部数据,常见三种类型:
1、纯文本或局部HTML模板
纯文本可以在JavaScript用来直接替换页面中的文本值,而局部HTML则可以直接插入到页面中:
# 返回评论列表
@app.route('/comments/<int:post_id>')
def get_comments(post_id):
...
return render_template('comments.html')
2、JSON数据
JSON数据可以直接在JavaScript中直接操作
@app.route('/profile/<int:user_id>')
def get_profile(user_id):
...
return jsonify(username=username,bio=bio)
3、空值
有时程序中的某些接收AJAX请求的视图并不需要返回数据给客户端,比如用来删除文章的视图。返回空值,并将状态码指定为204(表示无内容):
@app.route('/post/delete/<int:post_id>',method=['DELETE'])
def delete_post(post_id):
...
return '', 204
4、异步加载长文章
当加载文章按钮被点击时,会发送一个AJAX请求获取文章的更多内容并直接动态插入到文章下方。
5.3、HTTP服务器端推送
社交网站在导航栏实时显示新提醒和私信的数量,用户的在线状态更新,股价行情监控,显示商品库存信息、多人游戏、文档协作等。
实现服务器端推送的一系列技术被合称为HTTP Server Push,目前常用的推送技术:
名称 | 说明 |
---|---|
传统轮询 | 在特定的时间间隔内,客户端使用 AJAX 技术不断向服务器发起 HTTP 请求,然后获取新的数据并更新页面 |
长轮询 | 和传统轮询类似,但是如果服务器端没有返回数据,那就保持连接一直开启, 直到有数据才返回。取回数据后再次发送另一个请求 |
Server-Sent Events(SSE) | SSE通过HTML中的EventSource API实现。SSE 会在客户端和服务器端建立 一个单向的通道,客户端监听来自服务器端的数据,而服务器端可以在任意时间发送数据,两者建立类似订阅/发布的通信模式 |
在HTML5的API中还包含一个WebSocket协议,它是一种基于TCP协议的全双工通信协议。实时性更强,而且还可以实现双向通信。
5.4、Web安全防范
下面介绍常见几种攻击和其他常见漏洞
5.4.1、注入攻击
注入攻击包括系统命令注入、SQL注入、NoSQL注入、ORM注入等。重点介绍SQL注入:
(1)攻击原理
在编写SQL语句时,如果直接将用户传入的数据作为参数使用字符串拼接的方式插入到SQL查询中,那么攻击者可以通过注入其他语句来执行攻击操作(获取敏感数据、修改数据、删除数据库表...)
(2)攻击示例
假设我们程序是一个学生信息查询程序,其中某个视图函数接收用户输入的密码,返回查询结果对应的数据。
@app.route('/students')
def body_table():
password = request.args.get('password')
cur = db.excute("SELECT * FROM students WHERE password='%s';" % password)
results = cur.fetchall()
return results
如果攻击者输入的password参数值为"'or 1=1 --",那么最终视图函数中被执行的SQL语句将变为:
SELECT * FROM students WHERE password='' or 1=1 --;'
这会吧students表中的所有记录全部查询并返回。若设为"'; drop table students; ---",那么查询语句变为:
SELECT * FROM students WHERE password=''; drop table students; --;
这个语句会把students表中的所有记录全部删掉。
(3)主要防范方法
- 使用ORM可以一定程度上避免SQL注入问题
- 验证输入类型。
- 参数化查询
- 转义特殊字符
5.4.2、XSS攻击
XSS(Cross-Site Scripting,跨站脚本)攻击历史悠久
(1)攻击原理
XSS是注入攻击的一种,攻击者通过将代码注入被攻击者网站中,用户一旦访问网页便会执行注入的恶意脚本。XSS攻击主要分为反射型XSS攻击和存储型XSS攻击:
(2)攻击示例
反射型XSS又称为非持久型XSS。当某个站点存在CSS漏洞时,这种攻击会通过URL注入攻击脚本,只有当用户访问这个URL时才会执行攻击脚本。
# 包含反射型XSS漏洞
@app.route('/hello')
def hello():
name = request.args.get('name')
response = '<h1>Hello,%s!</h1>' % name
这里未对字符串做任何处理就插入到返回的响应主体中,返回给客户端。若干某个用户输入了一段JavaScript代码作为查询参数的值:
http://example.com/hello?name=<script>alert('Bingo!');</script>
访问便会弹出相应内容。
存储型XSS也被称为持久性XSS,这种类型的XSS攻击更常见,危害也更大。它和上述类似,不过会把攻击代码储存到数据库中,任何用户访问包含攻击代码的页面都会被殃及。比如,某个网站通过表单接收用户的留言,如果服务器接收数据后未经处理就存储到数据库中,那么用户可以在留言中插入任意Javascript代码。比如一行重定向代码:
<script>window.location.href="http://attacker.com";</script>
其他用户一旦访问留言板页面,就会执行其中的JavaScript脚本。被重定向到攻击者写入的站点
(3)主要防范措施
-
HTML转义(对用户输入的内容进行HTML转义,转义后可以确保用户输入的内容在浏览中作为文本显示,而不是作为代码解析)
# 使用Jinja2提供的escape()函数对用处传入的数据进行转义: from jinja2 import escape @app.route('/hello') def hello(): name = request.args.get('name') response = '<h1>Hello,%s!</h1>' % escape(name)
-
验证用户输入 XSS攻击可以在任何用户可定制内容的地方进行,例如图片引用、自定义链接。在某些HTML属性中,使用普通的字符也可以插入JavaScript代码。所以需要做好验证工作
# 1、转义无法避免的XSS攻击情况,有下(链接): <a href="{{ url }}">Website</a> # 如果不对url验证,用户写入代码:"javascript:alert('Bingo!');",最终的代码就会变为: <a href="javascript:alert('Bingo!');">Website</a> # 2、图片 <img src="{{ url }}" # 类似,用户写入"123" onerror="alert('Bingo!')",最终的<img>标签就会变为: <img src="123" onerror="alert('Bingo!')">
5.4.3、CSRF攻击
CSRF(Cross Site Request Forgery,跨站请求伪造),又被称为One-Click Attack或Session Riding。
(1)攻击原理
攻击者利用用户在浏览器中保存的认证信息,想对应的站点发送伪造请求。
(2)攻击示例
假设我们是一个社交网站(A);攻击者可以是任意类型网站(B)。在A网站中,删除账户操作通过GET请求执行:
@app.route('/account/delete')
def delete_account():
if not current_user.authenticated:
abort(401)
current_user.delete()
return 'Deleted!'
用户登录后,访问http://example.com/account/delete就会删除账户。那么在攻击者的网站上,只需要创建一个显示图片的img标签,其中的src属性加入账户的URL:
<img src="http://example.com/account/delete">
当用户访问B网站时,浏览器在解析网页时会自动向img标签的src属性中的地址发起请求。吸取教训,改用POST提交删除账户的请求。尽管如此,攻击者只需要在B网站中内嵌一个隐藏表单,然后设置在页面加载后执行提交表单的JavaScript函数,仍会被执行。
(3)主要防范措施
-
正确使用HTTP方法(遵循原则)
将这些按钮内嵌在使用了POST方法的form元素中。
- GET方法属于安全方法,不会改变字眼状态,仅用于获取资源,因此又称幂等方法。页面中所有可以通过链接发起的请求都属于GET请求。
- POST方法用于创建、修改和删除资源。
-
CSRF令牌校验 判断请求是否来源自己的网站。通过在客户端加入伪随机数来防御CSRF攻击,这个伪随机数通常被称为CSRF令牌(token)。对于AJAX请求,我们可以在 XMLHttpRequest请求首部添加一个自定义字段X-CSRFToken来保存CSRF令牌。 通常使用扩展来实现CSRF令牌的创建和验证工作,比如Flask-SeaSurf、Flask-WTF内置的CSRFProtect等。