会话、401 与 403 · 后端开发
服务器怎么知道你是谁,又怎么知道你能做什么?
课程目录为什么需要它
在最有名的 Web 安全风险榜 OWASP Top 10(2021 版)里,「访问控制失效」排第一:用户读到或改掉了别人的东西。这通常不是什么高明的入侵,只是某个处理函数里少了一个检查。学会在每个请求上问两个问题——这是谁?以及他可以这样做吗?——就能防住大部分问题。
直觉理解
两个不同的问题,两个不同的状态码:
- 认证:你是谁?没有有效的登录 → 401 Unauthorized。
- 授权:你登录了,但你可以做这件事吗?不可以 → 403 Forbidden。
记住登录状态的常见办法是会话(session):
- 用户用密码登录。服务器检查密码,生成一个随机的会话 id,存在服务器上:
SESSIONS["s-8f3a…"] = "ada"。 - 响应里设置一个 cookie:
Set-Cookie: session=s-8f3a…; HttpOnly; Secure。 - 浏览器在之后每个请求里都把它带回来:
Cookie: session=s-8f3a…。服务器查表就知道是哪个用户。
然后每个处理函数检查规则:管理员可以删任何帖子;普通成员只能删自己的。
永远不要从请求体或 URL 里取用户身份({"user": "ada"}):谁都能这么写。只相信服务器自己存下的东西。
定义
认证
Finding out who is making the request. Failing it means 401.确认是谁发出的请求。认证失败对应 401。
授权
Deciding whether that user may do this action on this thing. Failing it means 403.决定这个用户能不能对这个东西做这个操作。不允许对应 403。
会话
A random id the server stores next to a user, sent to the browser in a cookie and returned on every request.服务器和某个用户对应存下的随机 id,通过 cookie 发给浏览器,浏览器在之后每个请求里都带回来。
示例
这是谁?
动手试试读出会话 cookie,查表。
SESSIONS = {"s-ada": "ada", "s-bo": "bo"}
ROLES = {"ada": "admin", "bo": "member"}
POSTS = {1: {"author": "bo", "text": "Hello"}, 2: {"author": "ada", "text": "Welcome"}}
def current_user(headers):
cookie = headers.get("Cookie", "")
for part in cookie.split(";"):
name, _, value = part.strip().partition("=")
if name == "session" and value in SESSIONS:
return SESSIONS[value]
return None
print(current_user({"Cookie": "theme=dark; session=s-bo"}))
print(current_user({"Cookie": "session=s-fake"}))
print(current_user({}))bo None None
- L6没有 Cookie 头就是空字符串,而不是报错。
- L8"session=s-bo" 拆成名字 "session" 和值 "s-bo"。
- L9只有服务器自己生成的 id 才算数。
- L14不认识的 id:谁都不是。
None
401 和 403
动手试试先查是谁,再查能做什么。
SESSIONS = {"s-ada": "ada", "s-bo": "bo"}
ROLES = {"ada": "admin", "bo": "member"}
POSTS = {1: {"author": "bo", "text": "Hello"}, 2: {"author": "ada", "text": "Welcome"}}
def current_user(headers):
cookie = headers.get("Cookie", "")
for part in cookie.split(";"):
name, _, value = part.strip().partition("=")
if name == "session" and value in SESSIONS:
return SESSIONS[value]
return None
def edit_post(headers, post_id, text):
user = current_user(headers)
if user is None:
return 401, {"error": "log in first"}
post = POSTS[post_id]
if user != post["author"] and ROLES[user] != "admin":
return 403, {"error": "not your post"}
post["text"] = text
return 200, post
print(edit_post({}, 1, "Hi"))
print(edit_post({"Cookie": "session=s-bo"}, 2, "Hi"))
print(edit_post({"Cookie": "session=s-bo"}, 1, "Hi"))
print(edit_post({"Cookie": "session=s-ada"}, 1, "Edited"))(401, {'error': 'log in first'})
(403, {'error': 'not your post'})
(200, {'author': 'bo', 'text': 'Hi'})
(200, {'author': 'bo', 'text': 'Edited'})
- L16没登录:401。
- L19以 bo 登录,但帖子 2 是 ada 的,而 bo 不是管理员:403。
- L26ada 是管理员,所以可以编辑任何人的帖子。
常见错误
相信客户端说的身份
放在请求体、URL 里的用户 id,或者 user=ada 这样的普通 cookie,谁都能改。要在服务器上通过会话(或签名的令牌)查出用户。
只检查有没有登录
登录了不等于每个帖子都是你的。每个修改数据的处理函数里,都要检查所有权或角色。
把 401 和 403 弄混
401 表示「请先登录」(客户端登录后就能解决)。403 表示「我们知道你是谁,但不行」。
小测验
1. Bo 已经登录,试图删除 Ada 的账号。Bo 不是管理员。返回哪个状态码?
查看答案
答案:A
我们知道 Bo 是谁(已认证),但他没有权限(未授权)。
2. 处理函数应该从哪里得到当前用户?
查看答案
答案:A
只有服务器自己存的数据才可信;其他都是客户端随便填的。
动手练习
3 道练习,分三层:先预测程序的输出,再按新需求改程序,最后从零写一个。代码在浏览器里运行,提交马上判题。
- 预测输出四个请求
准确写出它打印的内容。
- 改需求只能删自己的帖子
给
delete_post加上缺少的授权检查:作者或管理员可以删除帖子;其他人得到403{"error": "not your post"}。401 和 404 保持不变。 - 从零写访问策略
写一个
check(user, action, doc),返回这个请求的状态码。user是None或者形如{"name": "bo", "role": "member"}的字典;doc形如{"owner": "ada", "public": True};action是"read"、"edit"或"delete"。规则按顺序:
- 读公开文档,人人都可以,不登录也行 →
200 - 没有用户 →
401 - 管理员什么都能做 →
200 - 所有者可以读、改、删 →
200 - 其他成员:(文档不是公开的,所以连读也不行)一律 →
403
- 读公开文档,人人都可以,不登录也行 →
本课术语
| 术语 | 中文 | 英文定义 |
|---|---|---|
| Authentication | 认证 | Who are you? Failing it: 401. |
| Authorization | 授权 | May you do this? Failing it: 403. |
| Session | 会话 | A server-side record of a login, found through a cookie. |
| Cookie | Cookie | A small value the browser stores and sends back to the same site. |