零基础入门Python18|密码、认证、权限与接口测试基础

零基础入门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())
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值