别再用 configparser 了:给本地配置加上加密、校验和版本迁移

先说一个我遇到过不止一次的事故

某个 Python 写的桌面工具,用户配置存在 config.json 里,其中有一项是第三方服务的 API key。

用户为了排查问题,把整个配置目录打包发给了客服。key 跟着压缩包一起出去了。

这类事故的根因不是「用户不小心」,而是我们把不该明文落盘的东西,用最方便的方式落盘了 :json.dump 一行代码,问题当场解决,密文一行代码都还没写。

如果你也在写带配置文件的 Python 工具,下面三个缺口大概率同时存在:

  1. 明文 。config.json、settings.ini 打开就能看,改一行就能生效。
  2. 没有类型和校验 。configparser 读出来永远是字符串,"true" 和 True 靠调用方自己猜;数值改成 99999 也照单全收,直到运行时才炸。
  3. 没有版本迁移。老用户机器上留着 v1 的字段,新代码只认 v2,于是「新装没问题、老用户升级就崩」。

python-dotenv、pydantic-settings 解决的是第 2 点的一部分,但对第 1、3 点基本不管;dynaconf 功能很全,代价是引入一套自己的运行时模型。

我想要的是另一种东西:配置结构用类和属性声明出来,读写在类型上收口,加密、校验、迁移都由这个结构自动完成。

这就是 conf_base 想做的事。下面是它解决同样问题时,代码长什么样。

三十秒版本的对比

你现在的做法 痛点 换成声明式之后
json.load + 字典 拼错 key 要到运行时才发现 拼错属性名当场 AttributeError
configparser 读字符串 每次都要 int() / == 'true' 类型由 default 决定,写错类型直接报错
明文文件 密钥、token 裸奔 一行 _cipher,落盘即密文
手改配置文件 越界值、非法枚举照单全收 validator 在写入前拦截
新增字段 老用户的旧文件没人管 _schema_version + _migrate 自动升级

从字典搬到类:五行

下面用到 AES-256-GCM,需要额外装加密依赖:pip install "conf-base[aes]"。 只用明文格式 + 校验的话,pip install conf-base 就够。

python 复制代码
from conf_base import ConfigBase, Section, AesGcmCipher

class ServerConfig(ConfigBase):
    _path = 'server.json.enc'
    _cipher = AesGcmCipher(key_env='APP_SECRET')

    server = Section('Server')
    secret = server.field(default='', read_only=True)
    port = server.field(default=8080, validator=lambda v: 1 <= v <= 65535)
    workers = server.field(default=4, validator=lambda v: 1 <= v <= 64)

类定义完成的那一刻,server.json.enc 就被创建好了,缺失的字段自动补默认值。之后所有读写都走属性:

python 复制代码
ServerConfig.port.value = 9090      # 校验通过 → 立刻落盘
ServerConfig.port.value             # 9090
ServerConfig.port.value = 70000     # ConfigValueError:校验失败

没有 load()、没有 save()、没有「改了忘了写回」这类 bug------赋值即持久化。

加密:磁盘上只有一个信封

_cipher = AesGcmCipher(key_env='APP_SECRET') 之后,文件里不再是 JSON 明文,而是这样一个信封:

json 复制代码
{
  "v": 1,
  "alg": "AES-256-GCM",
  "kid": "v1",
  "nonce": "...",
  "data": "..."
}

明文里只有算法、密钥编号和 nonce,配置值在 data 里,没有密钥读不出来。 密钥可以从环境变量取(示例里的 APP_SECRET),也可以代码里注入;AES-256-GCM 是 AEAD,篡改一个字节解密就会失败,不会悄悄返回错误数据。

如果只是防手改、不要求保密,用 SignedCipher 更轻:文件仍是明文,但末尾会附一行 HMAC-SHA256 签名,被改过就拒绝加载。

校验:把「运行时才炸」提前到「写入时就炸」

python 复制代码
    port = Section('Server').field(
        default=8080,
        validator=lambda v: isinstance(v, int) and 1 <= v <= 65535,
    )

validator 是任意 Callable[[Any], bool]。返回 False 抛 ConfigValueError,抛异常则会被包装成 ConfigValueError 并保留原始异常链。两种失败都会阻止写入,配置文件不会被污染。

类型这一层框架已经替你挡了:值的类型永远与 default 相同,True 不会被当成 1 存进去,"8080" 也不会被悄悄转换。

迁移:老用户的配置文件怎么活下来

python 复制代码
def migrate_v1_to_v2(cfg):
    server = dict(cfg['Server'])
    server['workers'] = server.pop('threads')
    return {**cfg, 'Server': server}


class AppConfig(ConfigBase):
    _path = 'app.json'
    _schema_version = 2
    _migrate = {1: migrate_v1_to_v2}

    server = Section('Server')
    workers = server.field(default=4)

文件里的 _schema_version 是 1,代码要求 2,读文件时自动调用 1 对应的迁移函数,结果写回并把版本号更新为 2。版本号比代码还新(用户装过更新版又回退)则直接抛 SchemaError,不会拿新格式喂给旧代码。

迁移函数签名就是 dict -> dict,可以随便拆函数、写测试,不需要理解库内部。

还有两个细节

环境变量覆盖 :field(env_key='APP_PORT') 之后,环境变量优先级最高,而且被覆盖的项禁止回写------防止你在代码里改了半天,落盘时把环境变量里的值写进文件。

批量写入 :with AppConfig.server.batch(): 里的多次赋值只在退出时落盘一次,中途异常整批回滚到进入前的状态。桌面应用里「改 10 个设置点一次确定」这种场景,省掉 9 次磁盘 IO。

什么时候别用它

  • 你只是读三个环境变量------python-dotenv 更合适;
  • 配置来自远程配置中心(Nacos、Apollo、etcd)------这个库只管本地文件;
  • 配置需要高频写入(每秒级)------它按「人改配置」的频率设计;
  • 配置结构复杂到需要嵌套模型校验------pydantic-settings 的表达力更强。

它解决的是「本地有一个配置文件,希望它类型安全、能加密、能被校验、能升级」这一类问题,仅此而已。

安装

bash 复制代码
pip install conf-base

不需要任何强制依赖;要 YAML/TOML 或加密再按需装扩展:

bash 复制代码
pip install "conf-base[yaml,toml,aes]"

仓库:GitHub | Gitee 三个可直接运行的示例在 examples/,中英双语文档在 README。

如果这个思路对你有用,点个 star 是最好的反馈;有具体场景想讨论,开 issue 就行。

相关推荐
砚底藏山河1 小时前
python量化:实时行情WebSocket断线重连状态机
java·python·websocket·金融
CodeGL1 小时前
16、Python Web开发
开发语言·前端·python
小嘉丶学长1 小时前
Day08 · 字符串(二)
服务器·开发语言·python·学习·1024程序员节
BD_Marathon2 小时前
【Python】列表推导式
python
2501_948122632 小时前
Qs 跨端三方库仓颉适配全流程
python·react native·django·tornado
萌动的小火苗2 小时前
MQTT的QoS0、QoS1、QoS2有什么区别?
开发语言·网络·python·php·信息与通信
forest_092 小时前
《大模型原理通俗讲》系列 00|开篇 大模型到底是怎么回事
人工智能·python·prompt·agi
工程师迪菲2 小时前
【基础篇 01】YOLO 到底在做什么?先从一张图片看明白
python
happylifetree2 小时前
Python72:核心语法-类型注解-函数的类型注解
python