前面我们已经知道,HTTP协议是一种通用机制。客户端使用HTTP向服务器请求文档,而服务器通过HTTP向客户端提供文档。
然而,HTTP——超文本传输协议的”超文本“如何体现?
其实HTTP的设计初衷并非只是将其作为一种用于传输文件的新方法,也不是将其作为旧式文件传输协议(如FTP)的一个更复杂的提供缓存功能的替代品。当然HTTP能够传输书籍、图片以及视频这些独立的文件,但是尽管如此,HTTP的目的其实远不止于此,它还允许师姐各地的服务器发送文档,并通过相互之间的交叉引用形成一张互相连接的信息网————HTTP就是为万维网(World Wide Web)设计的。
万维网——可以直接成为Web——的工作所实现的梦想就是把寻找引用的任务交给机器来负责。
假设有一段文字”第九章关于cookie的讨论“,这段文字本来是孤立的,与外界没有联系,但是如果它出现在电脑屏幕上,加了下划线,并且被点击之后可以转到所引用的文本,那么这段文字就成为了一个超链接(hyperlink)。文本中包含内嵌超链接的整个文档就叫做超文本文档。如果文档中又加入了图片、声音以及视频,该文档就成了超媒体(hypermedia)。
其中前缀hyper表示后面的媒介能够理解文档之间相互引用的机制,并且能够为用户生成链接。为了操作超媒体,人们发明了统一资源定位符URL。它不仅为现代的超文本文档提供了一个统一的机制,还能够供以前的FTP文件和telnet服务器使用。在网络浏览器的地址栏可以看到很多类似下面这样的例子。
https://www,python.org http://en.wikipedia.org/wiki/Python_(programming_language) ftp://ssd.jpl.nasa.gov/pub/eph/planets/README.txt
第一个标记(如https、http)即为所使用的机制(scheme),它指明了获取文档所使用的协议。后面跟着一个冒号和两个斜杠,然后是主机名,接着可能还有端口号。URL最后是一个路径,用于在可用服务的所有文档中指明要获取的特定文档。
解析与构造URL:关于URL以及urllib的主要介绍,参见Python核心编程3-Web开发部分 1.Web客户端和服务器的内容,本篇会对链接中没有提到的部分进行一些补充。
现在介绍推动Web发展的核心文档样式的书籍很多。此外还有一些现行的标准也对超文本文档的格式、使用层级样式表(CSS,Cascading Style Sheets)确定超文本文档样式的机制以及Javascript等浏览器内嵌语言的API做了描述。其中,JavaScript等浏览器内核语言可以在用户与页面交互或浏览器从服务器我获取更多信息时对文档进行实时的修改。下面是集合核心标准与资源的链接:http://www.w3.org/TR/html5/ 、 http://www.w3.org/TR/CSS/ 、https://developer.mozilla.org/en-US/docs/Web/JavaScript 、 https://developer.mozilla.org/en-US/docs/Web/API/Document_Object_Model
HTML是一种使用大量尖括号(<...>)来装饰纯文本的机制。每对尖括号都创建了一个标签(tag),如果标签开头没有斜杠的话,就表示文档中某个新元素(element)的开始,否则就表示元素的结尾。下面的例子展示了一个简单的段落,该段落中包含了一个加粗的单词和一个斜体的单词。
This is a paragraph with bold and italic words.
某些标签是自包含的,不需要之后再使用对应的结束标记。最有名的例子就是
标签,它创建了段落中的一个空行。有些更为一丝不苟的开发者会把
写为
,这是从扩展标记语言(XML)中学习过来的,但在HTML中这并不是必须的。比如,并不一定要为所有开始标签提供对应的结束标签。当一个用
从上面的实例段落中,可以清楚地认识到,HTML的标签是可以层层嵌套的。设计者在构建完整的Web页面时可以不断地在HTMl元素内部嵌入其他HTML元素。在构建页面的过程中,设计者大多会不可避免地不断重复使用HTML定义的有限元素集合中的元素。这些元素用于表示页面上不同类型的内容。监管最新的HTML5标准允许设计者直接在页面中创建新元素,但是设计者们还是会倾向于使用标准元素。
一个大型的页面可能会处于各种不同的原因使用
Provo
61°F
这样,对应的CSS和Javascript就可以通过.city和.temperature这样的选择器来引用特定的元素了。如果想要更细粒度一点,可以使用h5.city和p.temperature。最简单形式的CSS选择器只需要一个标签的名称,后面加上以句点作为前缀的class名称即可。这两者都不是必须的。的目的都是唯一的,因此他们选择只为外层元素指定class的值
Provo
61°F
此时,要在CSS或Javascript中引用该,就需要使用更为复杂的模式了。我们使用空格来连接外层标签的class值与内层标签的名称:.weather h5 \ .weather p。
import os,pprint,sqlite3
from collections import namedtuple
def open_database(path='bank.db'):
new = not os.path.exists(path)
db = sqlite3.connect(path)
if new:
c = db.cursor()
c.execute('CREATE TABLE payment (id INTEGER PRIMARY KEY,'
'debit TEXT,credit TEXT,dollars INTEGER,memo TEXT)')
add_payment(db,'brandon','psf',125,'Registration for PyCon')
add_payment(db,'brandon','liz',200,'Payment for writing that code')
add_payment(db,'sam','brandon',25,'Gas money-thanks for the ride!')
db.commit()
return db
def add_payment(db,debit,credit,dollars,memo):
db.cursor().execute('INSERT INTO payment (debit,credit,dollars,memo)'
'VALUES (?,?,?,?)',(debit,credit,dollars,memo))
def get_payments_of(db,account):
c = db.cursor()
c.execute('SELECT * FROM payment WHERE credit = ? or debit = ?'
'ORDER BY id',(account,account))
Row = namedtuple('Row',[tup[0] for tup in c.description])
return [Row(*row) for row in c.fetchall()]
if __name__ == '__main__':
db = open_database()
pprint.pprint(get_payments_of(db,'brandon'))
import bank
from flask import Flask,redirect,request,url_for
from jinja2 import Environment,PackageLoader
app = Flask(__name__)
get = Environment(loader=PackageLoader(__name__,'templates')).get_template
@app.route('/login',methods=['GET','POST'])
def login():
username = request.form.get('username','')
password = request.form.get('password','')
if request.method == 'POST':
if (username,password) in [('brandon','atigdng'),('sam','wyzzy'),('gjy','330219')]:
response = redirect(url_for('index'))
response.set_cookie('username',username)
return response
return get('login.html').render(username=username)
@app.route('/logout')
def logout():
response = redirect(url_for('login'))
response.set_cookie('username','')
return response
@app.route('/')
def index():
username = request.cookies.get('username')
if not username:
return redirect(url_for('login'))
payments = bank.get_payments_of(bank.open_database(),username)
return get('index.html').render(payments=payments,username=username,
flash_messages= request.args.getlist('flash'))
@app.route('/pay',methods=['GET','POST'])
def pay():
username = request.cookies.get('username')
if not username:
return redirect(url_for('login'))
account = request.form.get('account','').strip()
dollars = request.form.get('dollars','').strip()
memo = request.form.get('memo','').strip()
complaint = None
if request.method == 'POST':
if account and dollars and dollars.isdigit() and memo:
db = bank.open_database()
bank.add_payment(db,username,account,dollars,memo)
db.commit()
return redirect(url_for('index',flash = 'Payment Successful.'))
complaint = ('Dollars must be an integer' if not dollars.isdigit()
else 'Please fill in all three fields')
return get('pay.html').render(complaint=complaint,account=account,
dollars=dollars,memo=memo)
if __name__ == '__main__':
app.debug = True #打开调试模式。一旦对运行中的代码做了修改,Flask就会自动重启并重新载入应用程序。这样就能在对代码进行微调时快速看到修
#改的效果
app.run()
我们会在本章接下来的几节中学习上述代码的弱点,并以此来了解一个应用程序要抵御网络攻击所需要采取的最基本的操作。代码中的这些弱点都来自于数据处理过程中发生的错误,与网站是否合理采用了TLS防止网络窃听无关。读者可以假设该网站已经采取了加密保护,比如在前端使用了一个反向代理服务器。我们只考虑攻击者在无法获取特定用户与应用程序之间传递的数据时所能进行的恶意行为。
本书不会讨论表单的设计,因为它也是个很复杂的问题,有很多技术上的选择。关于Web设计的书籍和网站会详细讨论这些问题,本书仅讨论表单对于网络的意义。
GET /search?q=python+network+programming HTTP/1.1
Host:example.com
这意味着,GET的参数是浏览历史中的一部分,任何人只要站在我们后面看着浏览器的地址栏就可以知道我们输入的字段。因此,我们绝对不能使用GET来传输密码或整数这样的敏感信息。当我们填写一个GET表单时,其实就是在指定接下来要访问的地址。最终,浏览器会根据表单信息构造一个URL,指向我们希望服务器生成的页面。填写之前的搜索表单中的3个不同的字段会生成3个独立的页面、浏览器的3条浏览历史以及3个URL。后期可以重新访问这三条浏览历史。如果希望好友也能查看同样的页面可以分享个这些URL给好友。
在提交上面这个表单时,浏览器会把所有数据都放入请求消息体中,而请求路径本身是没有变化的。因为我们POST提交的东西并不是我们想要访问的地址,所以表单参数不会被放在URL中。一个哲学家把这种动作称为“言语行为”,表示会在世界上创建一个新状态的言语。
另外,浏览器上传大型负载(如整个文件)时还可以使用一种基于MIME标准的表单编码multipart/forms。不过,无论使用哪种编码,POST表单的语义都是一样的。
Web浏览器知道POST请求是一个会造成状态变化的动作,因此它们在处理POST请求时是非常小心的。如果用户在浏览一个由POST请求返回的页面时重新加载网页,那么浏览器会弹出一个对话框,大致内容:此网页需要使用您之前输入的数据才能正常显示,返回至该页面可能会重新发送这些数据,是否继续操作?
为了防止用户在浏览POST返回的页面上进行重新加载,或在前进后退操作时不断收到浏览器弹出的对话框,有两种技术可供网站采用。
app.secret_key = 'saiGeij8AiS2ahMo5dahveixuV3'
#有了签名密钥后,Flask就会通过Session对象来使用该密钥,设置cookie
session['username'] = username
session]['csrf_token'] = uuid.uuid4().hex
#收到请求并提取出cookie之后,Flask会检查签名密钥,缺人密钥正确后才会信任此次请求。
# 如果cookie的签名不正确,就认为该cookie是伪造的,因此尽管请求中提供了cookie,但是该cookie无效。
username = session.get('username')
我们将会在后续的代码中进行上述改进。
用户在成功登录账单应用程序后,如果页面中包含这段代码,那么代码中描述的POST请求就会自动发送并以受害用户的身份支付账单。因为用户无法在最终生成的网页上看到