GET 是「我要看什么」,POST 是「我要你改什么」。 把删除操作写成 GET,
等于让任何一张图片链接都能删你的数据。
一次请求带过来的东西全在 request 对象里:路径、查询参数、表单、请求头、来源 IP。
args 是问号后面的,form 是请求体里的,两个别搞混。
把表单字段写进地址栏再读 args,会永远读到空。
前两篇的页面都是「只读」的。这一篇让页面能收东西。 教材第 9 章最后两节是【处理表单】和【认识 HTTP 请求消息】。 演示里的表单流程是真跑的:GET 拿到表单页、POST 提交成功、POST 错数据拿 400,三个都实测。 最后那个「为什么刷新会重复添加」是个真问题,标准解法叫 POST-重定向-GET, 第 12 章的留言板会用。
一、一个能用的表单页
<form method="post" action="/add"> 两个属性决定一切:method 是用哪种方式提交,
action 是提交到哪个地址。
里面每个 <input> 的 name 就是服务端收到时的字段名。
<input name="qty"> 提交之后,服务端能拿到 qty 这个字段。
name 忘了写,那个字段就压根不会提交。 这个坑很常见——输入框显示正常,用户也填了,
服务端就是收不到。
<button type="submit"> 是提交按钮。
type 不写的话,在某些浏览器里会变成普通按钮,点了没反应。
表单里不写 method,默认是 GET。
这时候你在输入框里填的东西会出现在网址上,被人截图、被人收藏、留在浏览器历史里。
二、GET 和 POST 差在哪
| GET | POST | |
|---|---|---|
| 参数在哪 | 网址问号后面 | 请求体里 |
| 能被收藏/分享吗 | 能 | 不能 |
| 会留在浏览器历史里吗 | 会 | 不会 |
| 有长度限制吗 | 有,看浏览器和服务器 | 基本没有 |
| 该用来干什么 | 查询、翻页、搜索 | 提交、修改、上传 |
一句话记法:GET 是「我要看什么」,POST 是「我要你改什么」。
这条规矩不是风格问题,是安全问题。把删除操作写成 GET,等于让任何一张图片链接都能删你的数据。
怎么做到?
别人在论坛里发一个 <img src="https://你的站/delete?id=7">,你登录着那个站、又打开了这个帖子,
浏览器就替你把那个 GET 发了。你什么都没点,数据没了。
这件事的正式名字叫 CSRF(跨站请求伪造)。防它的第一道就是:改数据的操作不许用 GET。
三、在服务端把东西收下来
request.method 区分两种请求。
演示里 GET /add 返回「这个地址只接受表单提交」,POST /add 返回「已收下 六角螺栓 M8 × 120,
当前共 1 条」。
表单字段从 request.form 取。注意它是字典,取不到给 None,不是抛异常。
数量那个字段要转成整数,用户可能填「很多」。
演示里 POST 一个 qty=很多,服务端返回 400,正文是「数量必须是数字,收到的是 '很多'」。
400 是「你发来的东西不对」。 跟 404(地址不存在)、500(我坏了)都不一样,
它是「请求本身有问题」,责任在发请求的那一方。
关键的一句:校验一定要在服务端做。 前端那点检查只能挡住手滑,
挡不住直接发请求的人——上面 qty=很多 这一条就是绕过页面直接 POST 进来的。
同一段代码可以既显示表单又处理提交(判断 request.method),但更常见的写法是拆成两个函数,
各自只管一件事。
四、一次请求里能拿到什么
演示第四段请求了 /info?page=2&kw=螺栓,然后打出来七项。
| 取的写法 | 是什么 |
|---|---|
request.method |
'GET' |
request.path |
'/info'(不含问号后面) |
request.args |
查询参数,{'kw': '螺栓', 'page': '2'} |
request.form |
表单字段,GET 请求时是空的 |
request.headers |
请求头,User-Agent、Host 都在里面 |
request.remote_addr |
来源 IP |
args 是问号后面的,form 是请求体里的,两个别搞混。 把表单字段写进地址栏再读 args,
会永远读到空。
做接口、做鉴权、做统计,都是从这个对象里取。
比如按 remote_addr 统计访问来源,按 User-Agent 判断是手机还是电脑。
request.args 和 request.form 都是「多值字典」——同一个名字可以出现好几次,
要用全部得 getlist('name')。普通 ['name'] 只给第一个。
五、为什么刷新会重复添加
表单提交成功,页面显示「已添加」。你按 F5 刷新,又加了一条。 原因是:浏览器的刷新是「重发上一次的请求」。上一次是 POST,它就再 POST 一遍。 服务器不知道这是「同一个提交」还是「用户又想加一条」。 标准解法叫 POST-重定向-GET:处理完表单,不直接返回页面,返回一个 302 让浏览器跳到结果页。
这样浏览器最后停在的是一个 GET 页面,刷新就是重发那个 GET——只是重新看一眼,不会重复提交。
代码上就是一句 return redirect(url_for('list_page')),不难。
难的是记住要写。 忘了写,用户每刷新一次数据就多一条。
更严格的系统还会给表单加一个一次性令牌(CSRF token),同一个令牌只能用一次。
第 12 章的留言板会用前一种做法(重定向),因为简单够用。
六、动手 Demo:表单和它背后的那次请求
起一个带表单的应用,先 GET 表单页,再 POST 提交,最后 POST 一个错数据。 然后把一次请求里能拿到的东西全打印出来。 最后看「刷新重复添加」是怎么被重定向解决的。
import logging, threading
from flask import Flask, request, redirect, url_for
from werkzeug.serving import make_server
import requests
logging.getLogger('werkzeug').setLevel(logging.ERROR)
app = Flask(__name__)
DB = []
@app.route('/form')
def form():
return ('<form method="post" action="/add">'
'品名 <input name="name" value="">'
'数量 <input name="qty" value="">'
'<button type="submit">提交</button></form>')
@app.route('/add', methods=['GET', 'POST'])
def add():
if request.method == 'GET':
return '这个地址只接受表单提交', 200
name = (request.form.get('name') or '').strip()
qty = (request.form.get('qty') or '').strip()
if not qty.isdigit():
return '数量必须是数字,收到的是 %r' % qty, 400
DB.append((name, int(qty)))
return '已收下 %s × %d,当前共 %d 条' % (name, int(qty), len(DB)), 200
@app.route('/info')
def info():
return ('UA前40 %r\n主机 %r\n方法 %r\n来源IP %r\n'
'查询参数 %r\n表单 %r\n路径 %r'
% (request.headers.get('User-Agent', '')[:40], request.host,
request.method, request.remote_addr,
dict(request.args), dict(request.form), request.path))
@app.route('/add2', methods=['POST'])
def add2():
DB.append(((request.form.get('name') or '').strip(), 1))
return redirect(url_for('form'))
srv = make_server('127.0.0.1', 0, app)
B = 'http://127.0.0.1:%d' % srv.server_port
threading.Thread(target=srv.serve_forever, daemon=True).start()
print('① 一个能用的表单页')
r = requests.get(B + '/form', timeout=5)
print(' 表单 HTML:')
for ln in r.text.replace('><', '>\n<').splitlines():
print(' ' + ln)
print(' method="post" 决定用哪种方式提交,action 决定提交到哪个地址。')
print(' 每个 input 的 name 就是收到时的字段名。')
print('② GET 和 POST 差在哪')
print('| | GET | POST |')
print('|---|---|---|')
for a, b_, c in (('参数在哪', '网址问号后面', '请求体里'),
('能被收藏/分享吗', '能', '不能'),
('会留在浏览器历史里吗', '会', '不会'),
('有长度限制吗', '有,看浏览器和服务器', '基本没有'),
('该用来干什么', '查询、翻页、搜索', '提交、修改、上传')):
print('| %s | %s | %s |' % (a, b_, c))
print(' 一句话记法:GET 是「我要看什么」,POST 是「我要你改什么」。')
print('③ 在服务端把东西收下来')
r = requests.get(B + '/add', timeout=5)
print(' GET /add → %d %s' % (r.status_code, r.text))
r = requests.post(B + '/add', data={'name': '六角螺栓 M8', 'qty': '120'}, timeout=5)
print(' POST /add → %d %s' % (r.status_code, r.text))
r = requests.post(B + '/add', data={'name': '膨胀螺栓', 'qty': '很多'}, timeout=5)
print(' POST 错数据 → %d %s' % (r.status_code, r.text))
r = requests.get(B + '/form', timeout=5)
print(' GET /form → %d,正文 %d 字节' % (r.status_code, len(r.content)))
print(' 校验一定要在服务端做。前端那点检查只能挡住手滑,挡不住直接发请求的人。')
print('④ 一次请求里能拿到什么')
r = requests.get(B + '/info', params={'page': '2', 'kw': '螺栓'},
headers={'User-Agent': 'demo-client/1.0'}, timeout=5)
print(' GET /info?page=2&kw=螺栓')
for ln in r.text.splitlines():
print(' ' + ln)
print(' args 是问号后面的,form 是请求体里的,两个别搞混。')
print('⑤ FAQ:为什么表单提交之后刷新会重复添加')
r = requests.post(B + '/add2', data={'name': '不该重发的'}, allow_redirects=False, timeout=5)
print(' POST /add2 → %d,Location: %s' % (r.status_code, r.headers.get('Location')))
print(' 浏览器刷新时,会把上一次的 POST 原样再发一遍')
print(' 标准解法是 POST-重定向-GET:处理完表单,返回一个 302 让浏览器跳到结果页。')
print(' 这样刷新的是结果页(GET),不会重复提交。当前 DB 有 %d 条。' % len(DB))
srv.shutdown()
实跑输出:
--------------------------------------------------------------------
表单和它背后的那次请求
--------------------------------------------------------------------
① 一个能用的表单页
--------------------------------------------------------------------
表单 HTML:
<form method="post" action="/add">
品名 <input name="name" value="">
数量 <input name="qty" value="">
<button type="submit">提交</button>
</form>
method="post" 决定用哪种方式提交,action 决定提交到哪个地址。
每个 input 的 name 就是收到时的字段名。
② GET 和 POST 差在哪
--------------------------------------------------------------------
| | GET | POST |
|---|---|---|
| 参数在哪 | 网址问号后面 | 请求体里 |
| 能被收藏/分享吗 | 能 | 不能 |
| 会留在浏览器历史里吗 | 会 | 不会 |
| 有长度限制吗 | 有,看浏览器和服务器 | 基本没有 |
| 该用来干什么 | 查询、翻页、搜索 | 提交、修改、上传 |
一句话记法:GET 是「我要看什么」,POST 是「我要你改什么」。
把删除操作写成 GET,等于让任何一张图片链接都能删你的数据。
③ 在服务端把东西收下来
--------------------------------------------------------------------
GET /add → 200 这个地址只接受表单提交
POST /add → 200 已收下 六角螺栓 M8 × 120,当前共 1 条
POST 错数据 → 400 数量必须是数字,收到的是 '很多'
GET /form → 200,正文 158 字节
request.method 区分这两种请求。
同一段代码可以既显示表单又处理提交,但更常见的写法是拆成两个函数,各自只管一件事。
校验一定要在服务端做。
前端那点检查只能挡住手滑,挡不住直接发请求的人——上面 qty=很多 这一条,
就是绕过页面直接 POST 进来的。
④ 一次请求里能拿到什么
--------------------------------------------------------------------
GET /info?page=2&kw=螺栓
UA前40 'demo-client/1.0'
主机 '127.0.0.1:62819'
方法 'GET'
来源IP '127.0.0.1'
查询参数 {'kw': '螺栓', 'page': '2'}
表单 {}
路径 '/info'
一次请求带过来的东西全在这个 request 对象里:路径、查询参数、表单、请求头、来源 IP。
做接口、做鉴权、做统计,都是从这里取。
args 是问号后面的,form 是请求体里的,两个别搞混。把表单字段写进地址栏再读 args,会永远读到空。
⑤ FAQ:为什么表单提交之后刷新会重复添加
--------------------------------------------------------------------
浏览器刷新时,会把上一次的 POST 原样再发一遍
服务器不知道这是「同一个提交」还是「用户又想加一条」
标准解法是 POST-重定向-GET:处理完表单,返回一个 302 让浏览器跳到结果页。
这样刷新的是结果页(GET),不会重复提交。
更严格的系统还会给表单加一个一次性令牌(CSRF token),同一个令牌只能用一次。
第 12 章的留言板会用前一种做法。
第一段把表单 HTML 拆开打印了,方便看清三个 input 和一个 button 的写法。 注意每个 input 都有 name,没有 name 的字段压根不会提交。 第三段三个请求连起来看:GET 被拒、正常 POST 成功、错数据返回 400。 那个 400 是服务端校验的结果,前端校验挡不住它——因为这次请求是直接发的,没经过页面。 第四段七行里,args 和 form 最容易搞混。这次是 GET 请求,所以 form 是空的 {}。 第五段那个 302 是重定向的状态码,Location 头告诉你跳到哪。 浏览器收到 302 就会自动再发一个 GET,所以用户看到的是结果页。 最后那句 DB 有 1 条,是 add2 加进去的那一条,说明重定向前的 POST 确实处理了, 只是处理完之后不让浏览器停在 POST 上。
30 秒实验
- 把演示第三段那个错数据的
qty: '很多'改成qty: '-5',看服务端还能不能拦住(提示:isdigit()对负号返回 False,所以也能拦住,但它拦不住'12.5'这类带小数点的)。 - 把演示第五段的
allow_redirects=False去掉,看status_code变成几、r.text是什么。 - 在演示第四段加一个
print(request.get_data()),再把请求改成 POST,看请求体长什么样。
常见误区
- 「表单用 GET 也能提交,简单点。 」填的内容会出现在网址上,被人截图、被收藏、留在浏览器历史里。 更重要的是: 改数据用 GET 会招来 CSRF。
- 「前端已经校验过了,服务端不用再校验。」前端校验挡得住手滑,挡不住直接发请求的人。 服务端校验是不能省的那一道。
- 「
args和form拿到的东西差不多。」一个是问号后面的,一个是请求体里的。 把表单字段写进地址栏再读args,会永远读到空。
自测三问(点开对答案)
1. 为什么「删除」这种操作不能用 GET? —— 因为 GET 可以被任何链接、图片、脚本触发,不需要用户点。 别人在论坛发一个 `第 9 章结束。下一篇进第 10 章:把这个网站搬到云上,让别人真的能访问。
- ☐ (本篇完,下一篇 10.1 虚拟环境与内网穿透)