前言
几乎每个应用程序都需要某种形式的身份验证、密码处理或使用安全凭据,例如 API 密钥。您可能不是安全专家,但您应该知道如何安全地处理所有这些密码和凭据,以保护应用程序用户的凭据和数据以及您自己的 API 密钥和各种令牌。
确保这些安全元素的安全包括生成、验证它们、安全地存储它们并保护它们免受对手的侵害。因此,在本文中,我们将探索 Python 库、工具和概念,它们将对此有所帮助!
Python 安全密码处理涉及输入获取、随机生成、加密哈希存储及密钥管理。本文详解 getpass 安全输入、secrets 强密码生成、bcrypt/argon2 哈希方案、salt 盐值作用、dotenv 环境变量及系统密钥环存储方法。同时补充传输层安全、JWT 令牌处理、防时序攻击及审计日志规范,提供完整的凭据安全最佳实践指南,帮助开发者构建安全的身份验证系统。

几乎每个应用程序都需要某种形式的身份验证、密码处理或使用安全凭据,例如 API 密钥。您可能不是安全专家,但您应该知道如何安全地处理所有这些密码和凭据,以保护应用程序用户的凭据和数据以及您自己的 API 密钥和各种令牌。
确保这些安全元素的安全包括生成、验证它们、安全地存储它们并保护它们免受对手的侵害。因此,在本文中,我们将探索 Python 库、工具和概念,它们将对此有所帮助!
让我们从简单开始 - 你有基本的 Python 应用程序与命令行界面。您需要向用户询问密码。您可以使用 input(),但这会在终端中显示密码,以避免您应该使用相反:getpass 是一个非常简单的包,允许您提示用户输入密码以及通过提取当前用户的登录名来获取他们的用户名。请注意,并非每个系统都支持隐藏密码。Python 会尝试警告你这一点,所以只需在命令行中阅读警告。
import getpass
user = getpass.getuser()
password = getpass.getpass(prompt='Enter password: ')
# Do Stuff...
注意: 在生产环境中,应防止时序攻击(Timing Attacks)。如果验证逻辑过早返回,攻击者可以通过测量响应时间来推断密码字符。建议使用恒定时间比较函数,如 hmac.compare_digest。
有时,生成密码而不是提示用户输入密码可能更可取。例如,如果要设置首次登录时更改的初始密码。
没有任何用于生成密码的库,但实现它并不困难:使用上述代码生成的密码会很强大,但很难记住。如果它只是一个初始的临时密码或短期令牌,那么它很好,但如果用户应该使用更长时间,那么使用密码短语更合适。
import string
import secrets
length = 15
# Choose wide set of characters, but consider what your system can handle
alphabet = string.ascii_letters + string.digits + string.punctuation
password = ''.join(secrets.choice(alphabet) for i in range(length))
print(password)
我们可以像上面使用简单的密码一样构建一个密码生成器,但是当有可用的库时,为什么要打扰。这个库被称为著名的 XKCD,关于密码强度,它完全按照漫画的描述 - 生成由单词组成的强密码短语:此片段首先在您的系统上查找单词/字典文件,例如 /usr/share/dict/words 并选择指定长度的所有单词,然后从中生成用于生成密码短语的单词列表。生成器本身有一些参数,我们可以用来自定义密码短语。除了明显的单词数和长度外,它还具有离合参数,这是一个单词,其字符将用作密码短语中单词的首字母。
# pip install xkcdpass
from xkcdpass import xkcd_password as xp
word_file = xp.locate_wordfile()
words = xp.generate_wordlist(wordfile=word_file, min_length=5, max_length=10)
for i in range(4):
print(xp.generate_xkcdpassword(words, acrostic="python", numwords=6, delimiter="*"))
如果你真的想自己构建这个,而不是在你的项目中添加依赖项,你可以在 Python 文档中使用这个配方。成
现在我们要求用户输入密码或为他们生成密码,我们该怎么办?我们可能希望将其存储在数据库中的某个位置,但正如您可能(希望)知道的那样,您永远不应该以明文格式存储密码。为什么?
好吧,密码永远不应该以可恢复的格式存储,无论是纯文本还是加密。它们应该使用加密强度高的单向函数进行哈希处理。这样,如果有人掌握了数据库中的密码,他们将很难恢复任何实际密码,因为从哈希中恢复任何密码的唯一方法是暴力破解它 - 也就是说 - 获取可能的明文密码,使用相同的算法对其进行哈希处理并将结果与数据库中的条目进行比较。
为了使暴力破解更加困难,应另外使用盐。Salt 是与散列密码一起存储的随机字符串。它在散列之前附加到密码中,使其更加随机,因此更难猜测(使用彩虹表)。
但是,对于每秒可以尝试数十亿个哈希的现代硬件,使密码难以猜测是不够的,因此使用慢速哈希函数进行密码哈希,使攻击者暴力破解密码的效率要低得多。
市面上有很多库和单独的哈希算法,但上述要求大大缩小了我们的选择范围。在 Python 中进行哈希处理的首选解决方案应该是 passlib,因为它提供了适当的算法,以及即使不精通密码学的人也可以使用的高级接口。
在此代码段中,我们使用我们选择的算法 bcrypt,因为它是最流行和经过充分测试的哈希算法之一。首先,我们检查其可能的设置并检查算法使用的默认轮数是多少。然后,我们修改哈希器以使用更高的轮数(成本因素),使哈希更慢,因此哈希更难破解。此数字应是不会对用户造成无法容忍的延迟的最大可能数字(~300ms)。定期更新默认舍入值,因此您不一定需要更改此值。
# pip install passlib
from passlib.hash import bcrypt
from getpass import getpass
print(bcrypt.setting_kwds)
# ('salt', 'rounds', 'ident', 'truncate_error')
print(bcrypt.default_rounds)
# 12
hasher = bcrypt.using(rounds=13) # Make it slower
password = getpass()
hashed_password = hasher.hash(password)
print(hashed_password)
# $2b$13$H9.qdcodBFCYOWDVMrjx/uT.fbKzYloMYD7Hj2ItDmEOnX5lw.BX.
# \__/\/__ \____________________/\_____________________________/
# Alg Rounds Salt (22 char) Hash (31 char)
print(hasher.verify(password, hashed_password))
# True
print(hasher.verify("not-the-password", hashed_password))
# False
准备好哈希器后,我们提示用户输入密码并对其进行哈希处理。此时我们可以将其存储在数据库中,出于演示目的,我们继续根据原始明文密码对其进行验证。
从上面的代码中,我们可以看到整个用法归结为我们选择的算法的 to 和 verify 方法。但是,如果您想更好地控制方案、回合等,那么您可以使用 CryptContext class:此上下文对象允许我们使用多个方案,设置默认值或配置成本因素。如果您的应用程序身份验证很简单,那么这可能不是必需的,但如果您需要能够使用多种哈希算法、弃用它们、重新哈希哈希或类似的高级任务,那么您可能需要查看完全集成教程。
from passlib.context import CryptContext
ctx = CryptContext(schemes=["bcrypt", "argon2", "scrypt"],
default="bcrypt",
bcrypt__rounds=14)
password = getpass()
hashed_password = ctx.hash(password)
print(hashed_password)
# $2b$14$pFTXqnHjn91C8k8ehbuM.uSJM.H5S0l7vkxE8NxgAiS2LiMWMziAe
print(ctx.verify(password, hashed_password))
print(ctx.verify("not-the-password", hashed_password))
您可能想要使用的另一个原因是,如果您需要处理操作系统密码,例如 /etc/shadow 中的密码。为此,您可以使用 passlib.hosts 中可用的预配置上下文,有关更多详细信息,请参阅相关文档。
为了完整起见,我还列出了其他几个可用的库,包括它们的(不同的)用例:
小旁注:有了所有新获得的有关正确存储密码方法的知识,让我们想象一下您忘记了某些服务的密码。您点击'忘记密码?'在网站上,他们向您发送您的实际密码,而不是恢复链接。这意味着他们以明文形式存储您的密码,这也意味着您应该逃离该服务(如果您在其他地方使用相同的密码,请更改它)。
在上一节中,我们假设目的是存储其他用户的凭据,但是您自己用于登录远程系统的密码呢?
将密码留在代码中显然是一个糟糕的选择,因为它以明文形式躺在那里供任何人查看,并且您还冒着意外将其推送到 git repo 的风险。更好的选择是将其存储在环境变量中。您可以创建 .env 文件,将其添加到当前项目所需的凭据,并填充它。然后,您可以使用 python-dotenv package 将所有这些变量放入您的应用程序中,如下所示:此代码段首先构建 .env 文件的路径 using os.path 函数,然后用于加载环境变量。如果您的 .env 文件位于当前目录中,如上例所示,那么您可以简化代码并仅调用 load_dotenv() 自动查找环境文件。加载文件后,剩下的就是使用 os.environ 检索各个变量。
# pip install python-dotenv
import os
from os.path import join, dirname
from dotenv import load_dotenv
dotenv_path = join(dirname(__file__), ".env")
load_dotenv(dotenv_path)
API_KEY = os.environ.get("API_KEY", "default")
print(API_KEY)
# a3491fb2-000f-4d9f-943e-127cfe29c39c
或者,如果您不想用应用程序变量和机密污染您的环境,您可以像这样直接加载它们:
from dotenv import dotenv_values
config = dotenv_values(".env")
print(config)
# OrderedDict([('API_KEY', 'a3491fb2-000f-4d9f-943e-127cfe29c39c')])
上述解决方案很好,但我们可以做得更好。我们可以改用系统的密钥环,而不是将密码存储在未受保护的文件中,这是一个可以将安全凭据存储在主目录的加密文件中的应用程序。默认情况下,此文件使用您的用户帐户登录密码进行加密,因此当您登录时它会自动解锁,因此您不必担心额外的密码。
要在 Python 应用程序中使用密钥环凭据,我们可以使用 library called keyring:在上面的代码中,我们首先检查密钥环配置文件的位置,如果需要,您可以在其中进行一些配置调整。然后,我们检查活动的密钥环并继续向其添加密码。每个条目都有 3 个属性 - 服务、用户名和密码,其中服务充当命名空间,在本例中为应用程序的名称。要创建和检索条目,我们可以分别使用 set_password 和 get_password。除此之外,还可以使用 get_credential - 它返回一个凭据对象,该对象具有用户名和密码的属性。
# pip install keyring
import keyring
import keyring.util.platform_ as keyring_platform
print(keyring_platform.config_root())
# /home/username/.config/python_keyring # Might be different for you
print(keyring.get_keyring())
# keyring.backends.SecretService.Keyring (priority: 5)
NAMESPACE = "my-app"
ENTRY = "API_KEY"
keyring.set_password(NAMESPACE, ENTRY, "a3491fb2-000f-4d9f-943e-127cfe29c39c")
print(keyring.get_password(NAMESPACE, ENTRY))
# a3491fb2-000f-4d9f-943e-127cfe29c39c
cred = keyring.get_credential(NAMESPACE, ENTRY)
print(f"Password for username {cred.username} in namespace {NAMESPACE} is {cred.password}")
# Password for username API_KEY in namespace my-app is a3491fb2-000f-4d9f-943e-127cfe29c39c
除了密码,现代应用常涉及访问令牌(Access Tokens)和刷新令牌(Refresh Tokens)。
JSON Web Token (JWT) 常用于无状态认证。务必使用强签名算法(如 RS256 或 ES256),避免使用 none 算法。私钥绝不可硬编码在客户端代码中。
import jwt
secret_key = "your-secret-key" # 应从环境变量或密钥管理服务获取
payload = {"user_id": 123, "exp": 1735689600} # 过期时间
token = jwt.encode(payload, secret_key, algorithm="HS256")
所有凭据传输必须通过 HTTPS/TLS 加密。切勿在 HTTP 请求头中明文传递密码。使用 requests 库时,确保启用证书验证:
import requests
response = requests.post("https://api.example.com/login", json={"password": pwd}, verify=True)
在记录日志时,严禁打印密码、令牌或敏感信息。使用占位符代替:
import logging
logger = logging.getLogger(__name__)
logger.info(f"User login attempt for user: {username}") # 不要记录密码
定期检查日志文件,确保没有敏感数据泄露。使用结构化日志格式有助于过滤和监控。
即使您不是安全专家,您仍然负责您构建的应用程序的基本安全功能。这包括妥善保管用户的数据,尤其是密码,因此希望其中一些示例和食谱可以帮助您做到这一点。安全是一个持续的过程,随着威胁环境的演变,您需要不断更新您的知识和工具。
— END —

微信公众号「极客日志」,在微信中扫描左侧二维码关注。展示文案:极客日志 zeeklog
使用加密算法(如AES、TripleDES、Rabbit或RC4)加密和解密文本明文。 在线工具,加密/解密文本在线工具,online
解析常见 curl 参数并生成 fetch、axios、PHP curl 或 Python requests 示例代码。 在线工具,curl 转代码在线工具,online
将字符串编码和解码为其 Base64 格式表示形式即可。 在线工具,Base64 字符串编码/解码在线工具,online
将字符串、文件或图像转换为其 Base64 表示形式。 在线工具,Base64 文件转换器在线工具,online
将 Markdown(GFM)转为 HTML 片段,浏览器内 marked 解析;与 HTML转Markdown 互为补充。 在线工具,Markdown转HTML在线工具,online
将 HTML 片段转为 GitHub Flavored Markdown,支持标题、列表、链接、代码块与表格等;浏览器内处理,可链接预填。 在线工具,HTML转Markdown在线工具,online