1. 背景
随着 AI 技术的发展和普及,越来越多的企业开始使用 Python 进行项目开发。Python 以其简洁的语法,强大的库支持,以及优秀的可读性,受到了广大开发者的喜爱。然而,Python作为一种解释型语言,其源代码是公开可读的,这在一定程度上增加了代码被非法篡改或窃取的风险。
当企业将 Python 项目部署到客户服务器后,由于源代码的可读性,客户有可能获取到项目的完整源代码,这对企业的技术秘密保护构成了巨大的挑战。此外,如果客户在没有得到授权的情况下对源代码进行修改,还可能导致项目运行出现问题,影响服务质量。
那么,如何保护 Python 项目的源代码,防止在部署到客户服务器后被客户获取呢?
针对这一问题,本文将对 Python 项目的加密方案进行技术调研,希望通过对现有的技术方案的分析和比较,为企业选择合适的源代码保护方案提供参考。
本文将首先介绍 Python 源代码的保护原理和方法,然后深入探讨各种源代码保护技术的优缺点,最后根据企业的具体需求,提出适合的源代码保护方案。
2. 方案介绍
目前业界存在以下4种常见的软件解决方案:
方案 | 优点 | 缺点 |
---|---|---|
编译成字节码 (.pyc文件) | - 提供基本的代码保护,防止直接阅读源码。 - Python自身就支持生成.pyc文件,操作简单。 | - 字节码可以被反编译,安全性相对较低。 - 不会隐藏程序的逻辑结构和算法。 |
源代码混淆 | - 使代码难以阅读和理解,增加了代码保护的复杂性。 - 可以结合其他方法使用,如编译成.pyc后再混淆。 | - 混淆后的代码可能会影响运行效率。 - 完全的安全性仍无法保证,有可能被专业人士破解。 |
打包为可执行文件 | - 用户只能看到一个可执行文件,而不是一系列的Python源文件。 - 适合分发给最终用户使用,使用起来更加简单直接。 | - 打包后的应用体积相对较大。 - 有可能被专业工具分析和提取原始字节码。 |
Cython 或 C 扩展 | - 将Python代码转换成C代码,从而编译成机器码执行,提高了保护级别和执行效率。 - 可以与Python无缝集成,部分代码转换成C后,其余代码仍然可以使用Python编写。 | - 需要C语言的知识,增加了开发的复杂性。 - 不是所有Python代码都适合或需要转换成C。 |
由于笔者不是太擅长 C 语言,而且将项目转换成 C 代码,成本较大,感兴趣的朋友可以自行研究,本文主要介绍前面三种方式。 项目 demo 源码地址:github.com/yugasun/pyp...
2.1 编译成字节码(.pyc文件)
克隆项目后,git checkout 分支 pycompile
在项目根目录下添加 compile.py
文件,内容如下:
python
import compileall
import re
exclude_dir_pattern = re.compile(r"[\\/](venv|build|dist)[\\/]")
compileall.compile_dir("./", force=True, rx=exclude_dir_pattern)
然后执行该文件:
bash
python compile.py
执行成功后,就会在项目根目录下生成文件夹 __pycache__
,内容如下:
bash
tree __pycache__
__pycache__
├── compile.cpython-310.pyc
└── main.cpython-310.pyc
之后只需要执行命令 PYTHONPATH=__pycache__ python -m main
即可。
2.2 打包为可执行文件 - pyinstaller
官方简介: PyInstaller 读取您编写的 Python 脚本。它分析您的代码,以发现您的脚本执行所需的所有其他模块和库。然后,它会收集所有这些文件的副本,包括活动的 Python 解释器!-- 并将它们与您的脚本放在一个文件夹中,或者可以选择放在一个可执行文件中。
通过定义编译脚本,pyinstaller 可以将 python 脚本编译生成可执行文件,使最终用户不需要安装Python环境就可以运行程序。这在一定程度上隐藏了源代码。
这里以生成可执行文件为例:
2.2.1 定义 pyinstaller 打包需要的 spec 文件
pyprotect.spec
内容如下:
python
# -*- mode: python ; coding: utf-8 -*-
from PyInstaller.utils.hooks import collect_submodules
import argparse
parser = argparse.ArgumentParser()
parser.add_argument("--debug", action="store_true")
options = parser.parse_args()
include_libraries = [
"fastapi",
"uvicorn",
]
hidden_imports = []
for module in include_libraries:
hidden_imports += collect_submodules(module)
project_name = "pyprotect"
project_version = "0.1.0"
a = Analysis(
["main.py"],
pathex=[],
binaries=[],
datas=[],
hiddenimports=hidden_imports,
hookspath=[],
hooksconfig={},
runtime_hooks=[],
excludes=[],
noarchive=False,
optimize=0,
)
pyz = PYZ(a.pure)
if options.debug:
print("####### Debug mode #######")
exe = EXE(
pyz,
a.scripts,
a.binaries,
a.zipfiles,
a.datas,
name=project_name,
debug=True,
strip=False,
upx=False,
console=True,
)
else:
exe = EXE(
pyz,
a.scripts,
a.binaries,
a.datas,
[],
name=project_name + "_" + project_version,
debug=False,
bootloader_ignore_signals=False,
strip=False,
upx=True,
upx_exclude=[],
runtime_tmpdir=None,
console=True,
disable_windowed_traceback=False,
argv_emulation=False,
target_arch=None,
codesign_identity=None,
entitlements_file=None,
icon=["ico\\example.ico"],
)
2.2.2 执行构建命令
项目根目录下添加 gen.sh
文件,内容如下:
bash
#!/bin/sh
rm -rf ./dist
# get --debug option
if [ "$1" = "--debug" ]; then
DEBUG="--debug"
else
DEBUG=""
fi
# Pyinstaller options
PYINSTALLER="pyinstaller -y"
SPEC_FILE="pyprotect.spec"
${PYINSTALLER} ${SPEC_FILE} -- ${DEBUG}
上面的
--debug
参数并不是pyinstaller
命令参数,我这里只是为了调试,而自定义的。
通过定义项目入口文件 main.py
,执行 pyinstaller 命令后,就可以在项目根目录下生成可执行文件 dist/pyprotect_0.1.0
,执行 ./dist/pyprotect_0.1.0
就可以启动项目了。
当然以上脚本还定义了一些额外参数,这里就不一一介绍,可以参考官方文档:pyinstaller.org/en/stable/u...
2.3 代码混淆 - pyarmor
克隆项目后,git checkout 分支
pyarmor
注意:请在具有相同 Python 版本和相同平台的机器中运行这个混淆,否则它不起作用。因为pyarmor_runtime_000000 有一个扩展模块(用C或C++编写的一个模块,使用 Python 的 CAPI 与核心和用户代码进行交互),它依赖于平台并绑定到Python版本。
安装 pyarmor:
bash
pip install pyarmor
2.3.1 混淆单一文件
bash
pyarmor gen demo.py
此命令生成一个混淆脚本 dist/demo.py,这是一个有效的Python脚本,通过Python解释器可以运行它:
bash
python dist/demo.py
检查默认输出路径中的所有生成文件:
bash
ls dist/
... demo.py... pyarmor_runtime_000000
有一个额外的Python包 pyarmor_runtime_000000,这是运行混淆脚本所必需的。
2.3.2 混淆包/文件夹内
它还可以将整个路径 dist 复制到另一台机器上。但这并不方便,更好的方法是使用-i在包路径中生成所有必需的文件:
bash
pyarmor gen -O dist app
检查输出:
bash
tree dist
dist
├── app
│ ├── __init__.py
│ ├── libs
│ │ ├── __init__.py
│ │ └── moduleA.py
│ └── main.py
├── demo.py
└── pyarmor_runtime_000000
├── __init__.py
├── __pycache__
│ └── __init__.cpython-311.pyc
└── pyarmor_runtime.so
现在所有内容都在包路径 dist 中,只需将整个路径复制到任何目标机器即可。
2.3.3 支持过期时间的混淆
可以很容易地通过 -e
来设置混淆脚本的到期日期。例如,生成过期日期为 10
天的混淆脚本:
bash
pyarmor gen -O dist -e 10 demo.py
运行混淆脚本 dist/demo.py
以验证它:
bash
python dist/demo.py
让我们使用另一个表单来设置过去的日期 2023-05-30
:
bash
pyarmor gen -O dist -e 2023-05-30 demo.py
现在 dist/demo.py 应该不起作用:
bash
python dist/demo.py
通过给混淆后的代码设置过期时间,可以有效控制目标机器的使用权限,可以用作基于时间的 license 机制。
2.3.4 将混淆脚本绑定到设备
从 Pyarmor 8.4.6 开始,通过 python -m pyarmor.cli.hdinfo
获取目标机器硬件信息:
bash
python -m pyarmor.cli.hdinfo
Machine ID: 'abcedkldjkldjdkldjkd'
Default Mac address: '9c:3e:53:7f:44:5a'
Default IPv4 address: '192.168.0.46'
然后可以用 -b 将硬件信息绑定到混淆脚本。例如,绑定到 Mac 地址:
bash
pyarmor gen -O dist -b 9c:3e:53:7f:44:5a demo.py
所以 dist/demo.py 只能在目标机器中运行。
3. 总结
在对比了本文提到的几种方案之后,我个人更倾向于推荐使用 打包为可执行文件
的方法来有效防止源代码泄露。采用这种方式,当应用部署后,用户将只能接触到一个可执行文件,而不是众多散落的 Python 源文件。这不仅能在一定程度上保护代码不被轻易获取,还因其直接性和易用性,特别适合向最终用户分发。而且也可以根据实际需求,定制化 spec 文件,来优化打包后的可执行文件。
当然,实际情况因人而异,不同的项目可能会有不同的需求,比如需要更高的安全性,或者需要更好的运行效率。在选择源代码保护方案时,需要根据项目的具体情况,综合考虑各种因素,选择最适合的方案。