功能说明
ProductRepository 演示如何把用户输入安全地送入 SQLite:所有值都通过 ? 占位符绑定,绝不拼接进 SQL 字符串;排序列不能直接使用用户输入,而是先与 ALLOWED_SORTS 白名单匹配,再取出固定列名;动态 IN 子句只根据参数个数生成占位符,值仍然逐个绑定。这样可以避免 SQL 注入,即使输入中包含引号、分号或 DROP TABLE 也不会被当作语句执行。
仓储封装了建表、插入和查询,使用 sqlite3.Row 让结果可以按列名访问。插入操作在事务中执行,数据库错误统一转换为 RepositoryError,调用方可以区分输入校验错误和底层存储错误。
关键代码
动态占位符只由列表长度决定,列表内容全部进入参数数组:
cleaned = [name for name in names if isinstance(name, str) and name]
placeholders = ", ".join("?" for _ in cleaned)
sql += f" AND name IN ({placeholders})"
parameters.extend(cleaned)
排序方向也只从 ASC 和 DESC 两个常量中选择,不接收任意字符串。
使用注意
白名单只能保护标识符,不能保护值;任何值都必须继续使用参数绑定。表名、列名、排序方向无法用占位符替代,必须采用白名单或引用函数。LIKE 模式中的 % 和 _ 需要额外转义,否则用户仍可扩大匹配范围,但这不属于注入。
limit 被限制在 1 到 100,避免一次拉取过多数据;生产环境还应结合游标分页。names 为空序列会抛错,而不是生成 IN () 这种无效 SQL。SQLite 的连接不是线程安全的,多线程应使用独立连接或连接池。示例使用内存数据库,进程结束即销毁;真实应用需要迁移脚本、唯一约束和更细的事务边界。
验证方式
示例向内存库插入四件商品,按名称过滤并按价格降序查询,得到 keyboard 和 mouse 两行;按最低价格 100 查询得到 monitor。传入 price; DROP TABLE products 作为排序列应被白名单拒绝并输出 unsupported sort column,随后统计行数仍为 4,证明表没有被删除。可以再构造带单引号的商品名,确认插入和查询都按字面值工作。