(十一)把身份、会话更换与记录权限分开
1. 登录和退出都更换编号
继续加入密码核对与会话更换:
# 续写同一文件:web_project/src/webbook/security.py
# 先查用户,再把输入密码与已保存的哈希比较;返回相同错误信息处理登录失败。
def authenticate(connection, username, password):
user = connection.execute(
"SELECT * FROM users WHERE username=?", (username,)
).fetchone()
if user is None or not check_password_hash(user["password_hash"], password):
raise Problem(401, "账号或密码不正确")
return user# 续写同一文件:web_project/src/webbook/security.py
# 登录和退出都先作废旧会话,再生成新编号,旧 Cookie 就不能继续表示当前身份。
def rotate_session(connection, previous, now, user=None):
connection.execute("DELETE FROM sessions WHERE sid=?", (previous.sid,))
return new_session(connection, now, user)登录成功时删除旧匿名会话,创建关联用户的新会话;退出时删除旧登录会话,换成匿名会话。旧编号因此不能继续代表刚才的身份,新的 csrf 令牌也随会话更换。
2. 找到账号后,再判断这一条记录
身份检查回答“这个请求由谁操作”,记录权限回答“这个人能不能操作这本书”。bob 即使知道 alice 的书籍编号,也要经过 owned_book;数据库里那条书籍的 user_id 不匹配,就返回 403。
页面隐藏按钮可以让界面更清楚,服务器的查询与更新分支仍然会核对会话和归属。新增数据的 owner 参数由会话取得,JSON 里额外写一个 user_id 不会替换它。
# 项目文件:demos/session_rotation.py
# 登录与退出都更换会话编号;旧编号和已经过期的会话不能继续代表登录身份。
from webbook.lab import lab_client, login
with lab_client() as (client, app, _database):
client.get("/api/session")
old_sid = client.cookies.get("reading_sid")
token = login(client)
print("登录换编号", old_sid != client.cookies.get("reading_sid"))
print(
"旧编号",
client.get(
"/api/books", headers={"Cookie": f"reading_sid={old_sid}"}
).status_code,
)
client.post("/logout", data={"csrf_token": token})
print("退出后", client.get("/api/books").status_code)
login(client)
now = app.now()
app.now = lambda: now + 3601
print("过期后", client.get("/api/books").status_code)输出中,登录前后编号不同,旧编号、退出后的会话、超过过期时间的会话都不能继续读取书籍,状态为 401。已登录但操作其他账号的记录则为 403。