零基础入门Python18|密码、认证、权限与接口测试基础
本篇图解:程序执行顺序

这张图对应代码的执行顺序。先说清输入、处理和输出,再解释语法细节,初学时不容易迷路。
一、上一篇课后练习讲解
订单状态应保留历史,不建议直接删除。模型增加status,取消函数先检查状态:
def cancel_order(db, order_id):
order = db.get(Order, order_id)
if order is None:
raise ValueError("订单不存在")
if order.status == "cancelled":
raise ValueError("订单已经取消")
try:
for item in order.items:
item.product.stock += item.quantity
order.status = "cancelled"
db.commit()
db.refresh(order)
return order
except Exception:
db.rollback()
raise
测试要在每个失败案例后重新查询数据库,证明状态和库存没有被错误修改。
可执行验收答案
下面用 pytest 风格把三个边界固定下来:
def test_cancel_success_restores_stock(client, db):
order = create_order(db, status="paid", items=[(1, 2)])
response = client.post(f"/orders/{order.id}/cancel")
assert response.status_code == 200
assert db.scalar("select status from orders where id=?", (order.id,)) == "cancelled"
assert db.scalar("select stock from products where id=1") == order.stock_before + 2
def test_cancel_twice_does_not_add_stock(client, db):
order = create_order(db, status="cancelled", items=[(1, 2)])
before = db.scalar("select stock from products where id=1")
response = client.post(f"/orders/{order.id}/cancel")
assert response.status_code in (400, 409)
assert db.scalar("select stock from products where id=1") == before
def test_password_never_in_response(client):
response = client.post("/register", json={"email":"a@example.com", "password":"secret123"})
assert "password" not in response.json()
运行 pytest -q 时三个测试都通过;如果取消接口返回 200 但库存未恢复,检查事务是否在修改状态前读取了明细;如果密码出现在响应或日志,立即删除该字段并轮换测试凭据。
二、本篇成果
理解密码哈希、认证、授权、会话、Token和常见Web风险;完成密码哈希与资源权限函数,并用pytest验证成功和失败路径。安全不是最后上线前加一个开关,而是每层明确不信任什么数据。
三、密码绝不能明文保存
错误:
user.password = "password123"
正确做法使用专门密码哈希算法。安装:
python -m pip install "pwdlib[argon2]"
from pwdlib import PasswordHash
password_hash = PasswordHash.recommended()
stored_hash = password_hash.hash("password123")
assert stored_hash != "password123"
assert password_hash.verify("password123", stored_hash)
assert not password_hash.verify("wrong", stored_hash)
哈希不可逆验证,不是加密后再解密。每个密码还会使用随机盐,相同密码产生不同哈希。不要自己用普通SHA256保存密码,它计算太快,容易被暴力破解。
四、认证与授权不同
- 认证:确认当前请求是谁;
- 授权:确认这个用户能否执行某操作。
用户登录成功不等于可以修改所有文章。资源必须检查owner_id。
def ensure_owner(current_user_id, resource_owner_id):
if current_user_id != resource_owner_id:
raise PermissionError("无权操作此资源")
不要只在前端隐藏按钮,攻击者可以直接发送HTTP请求,后端必须检查。
五、会话和Token
Cookie会话常在服务端保存登录状态,浏览器携带会话Cookie。JWT Token把签名声明交给客户端携带,服务器验证签名和过期时间。
无论采用哪种方式,都要考虑:
- HTTPS传输;
- 过期时间;
- 退出和撤销;
- 密钥管理;
- Cookie的HttpOnly、Secure和SameSite;
- Token不能放进URL或日志。
后续Flask项目使用会话,FastAPI项目使用OAuth2和JWT,便于比较。
六、参数化查询、XSS、CSRF和CORS
- SQL注入:使用参数化查询或ORM,不拼接输入;
- XSS:输出到HTML时转义,富文本需要白名单清洗;
- CSRF:Cookie自动携带时,写操作使用CSRF Token或SameSite策略;
- CORS:限制哪些浏览器来源可跨域调用,不是身份认证;
- 限流:限制登录和高成本接口请求频率;
- 错误提示:登录失败统一提示“邮箱或密码错误”,避免枚举账号。
不同风险需要不同措施,不能用JWT解决所有安全问题。
七、可测试的权限服务
security.py:
from dataclasses import dataclass
from pwdlib import PasswordHash
password_hash = PasswordHash.recommended()
@dataclass
class User:
id: int
email: str
password_hash: str
@dataclass
class Article:
id: int
title: str
author_id: int
def register_user(user_id, email, password):
email = email.strip().lower()
if "@" not in email:
raise ValueError("邮箱格式不正确")
if len(password) < 8:
raise ValueError("密码至少8位")
return User(
id=user_id,
email=email,
password_hash=password_hash.hash(password),
)
def authenticate(user, password):
return password_hash.verify(password, user.password_hash)
def update_article(article, current_user, new_title):
if article.author_id != current_user.id:
raise PermissionError("只能修改自己的文章")
new_title = new_title.strip()
if not new_title:
raise ValueError("标题不能为空")
article.title = new_title
return article
test_security.py:
import pytest
from security import Article, authenticate, register_user, update_article
def test_password_is_hashed_and_can_be_verified():
user = register_user(1, "Alice@example.com", "password123")
assert user.email == "alice@example.com"
assert user.password_hash != "password123"
assert authenticate(user, "password123")
assert not authenticate(user, "wrong")
def test_only_author_can_update_article():
author = register_user(1, "a@example.com", "password123")
other = register_user(2, "b@example.com", "password123")
article = Article(id=1, title="旧标题", author_id=author.id)
updated = update_article(article, author, "新标题")
assert updated.title == "新标题"
with pytest.raises(PermissionError, match="只能修改自己的文章"):
update_article(article, other, "恶意修改")
assert article.title == "新标题"
def test_invalid_registration():
with pytest.raises(ValueError, match="邮箱"):
register_user(1, "invalid", "password123")
with pytest.raises(ValueError, match="至少8位"):
register_user(1, "a@example.com", "short")
运行:
pytest -q
测试不是只覆盖成功路径。权限测试还要确认失败后原文章没有被修改。
八、项目进入框架前的安全清单
- 密码只保存专用哈希;
- 密钥和数据库密码来自环境变量;
- 每个资源操作检查当前用户;
- 查询参数化;
- 错误信息不泄露账号状态和堆栈;
- 登录、上传和AI接口有限流计划;
- 自动化测试包含未登录、无权限、重复数据和非法输入。
九、本篇验收
- 能解释哈希与加密区别;
- 能区分认证和授权;
- 密码验证代码真实运行;
- 非作者修改被拒绝且数据不变;
- 能说明XSS、CSRF、CORS处理不同风险;
- pytest覆盖成功和失败路径。
十、课后练习
给权限服务增加delete_article和管理员角色:admin可删除任意文章,普通用户只能删除自己的文章。编写作者删除成功、其他用户失败、管理员成功三个测试。下一篇开始Flask完整记账项目,并把会话认证、数据库和测试串联起来。
实战补充:第一个可回归的后端测试集
测试应该描述用户能观察到的行为,而不是 ORM 私有属性。最小测试集包含成功、输入错误、资源不存在、越权和事务回滚。
def test_create_task(client):
response = client.post('/tasks', json={'title': '学习 SQL'})
assert response.status_code == 201
assert response.json()['data']['title'] == '学习 SQL'
def test_empty_title(client):
response = client.post('/tasks', json={'title': ''})
assert response.status_code == 422
数据库使用临时库或事务回滚,外部 Redis 用假实现;测试结束后清理文件和依赖替换。下一篇开始 Flask 账本项目,先创建应用工厂和健康检查。
本篇结束:完整模块文件
本节不是代码片段,而是本篇结束时该模块的完整版本。请先备份旧文件,再整体替换;替换后重新运行本篇命令和测试。阅读时重点看本篇新增的函数、事务边界和错误处理,未涉及的代码先不要自行删减。
本篇完整示例
from getpass import getpass
from hashlib import scrypt
from os import urandom
salt = urandom(16)
digest = scrypt(getpass('密码:').encode(), salt=salt, n=2**14, r=8, p=1)
print(digest.hex())

722

被折叠的 条评论
为什么被折叠?



