首页 > 解决方案 > SQLAlchemy:调用函数并将返回值始终保存在表中

问题描述

我有一个用户表,它有一个支付密码。我决定加密所有这些信息。每个 API 检索或插入新数据,它总是像这样调用一个函数来加密密码。

  from cryption import encrypt, decrypt
  enc_password = encrypt(password)

  data = User(id=id, password=enc_password)
  db.add(data)
  db.commit()

由于我在 API 中对此进行了加密,因此它看起来是多余的。此外,有时我忘记添加加密代码,这会导致严重错误。所以,我想知道我是否可以在模型上这样做而不是这样做。如果我可以在插入数据之前进行加密,并在查询级别返回数据时进行解密,那就太好了。

标签: python-3.xencryptionflasksqlalchemy

解决方案


首先,一个重要的警告:


警告永远不要存储用户的密码。从不。一旦您的服务器遭到入侵,黑客不仅会拥有您的服务器,他们还会拥有加密密钥和所有密码。

相反,存储密码哈希。请参阅为什么密码散列被认为如此重要?以及散列密码和加密密码之间的区别

在 Python 中,使用passlib为您处理密码散列。


这样一来,您就可以通过使用属性进行转换,将密码哈希自动存储在数据库中,或进行任何其他数据转换。我在我的用户模型中使用它。

在以下示例模型中,该password属性实际上将password_hash列设置为哈希值:

from passlib.context import CryptContext


PASSLIB_CONTEXT = CryptContext(
    # in a new application with no previous schemes, start with pbkdf2 SHA512
    schemes=["pbkdf2_sha512"],
    deprecated="auto",
)


class User(db.Model):
    __tablename__ = "users"

    id = db.Column(db.Integer, primary_key=True)

    # site-specific fields, like name, email, etc.

    # password hash handling
    # pbkdf2-sha512 is 130 characters short, but it never hurts to
    # leave space for future growth of hash lengths.
    password_hash = db.Column(db.String(256), nullable=False)

    def __init__(self, password=None, password_hash=None, **kwargs):
        if password_hash is None and password is not None:
            password_hash = self.generate_hash(password)
        super().__init__(password_hash=password_hash, **kwargs)

    @property
    def password(self):
        raise AttributeError("User.password is write-only")

    @password.setter
    def password(self, password):
        self.password_hash = self.generate_hash(password)

    def verify_password(self, password):
        return PASSLIB_CONTEXT.verify(password, self.password_hash)

    @staticmethod
    def generate_hash(password):
        """Generate a secure password hash from a new password"""
        return PASSLIB_CONTEXT.hash(password.encode("utf8"))

这让我可以使用User(..., password=password)它,它会自动为我散列密码。或者更新密码,if new_password == new_password_again and some_user.verify_password(old_password): some_user.password = new_password而无需记住如何再次对密码进行哈希处理。

您还可以使用“隐藏”列来存储您需要在存储时加密、在检索时解密的任何其他数据。使用前导下划线命名模型属性_以将它们标记为 API 私有,然后将真实列名Column()作为第一个参数传递给对象。然后使用一个属性对象来处理加密和解密:

class Foo(db.Model):
    __tablename__ = "foo"

    id = db.Column(db.Integer, primary_key=True)

    # encrypted column, named "bar" in the "foo" table
    _bar = db.Column("bar", db.String(256), nullable=False)

    @property
    def bar(self):
        """The bar value, decrypted automatically"""
        return decrypt(self._bar)

    @bar.setter
    def bar(self, value):
        """Set the bar value, encrypting it before storing"""
        self._bar = encrypt(bar)

这在 SQLAlchemy 手册的Using Descriptors and Hybrids中有介绍。请注意,在这种情况下使用 a 没有意义hybrid_property,因为您的数据库无法在服务器端加密和解密。

如果您确实加密,请将您的密钥与源代码分开,并确保您轮换密钥(保留旧密钥以解密尚未重新加密的旧数据),以便如果密钥被泄露,您至少可以代替它。

选择一个好的、值得信赖的加密配方。该cryptography软件包包括 Fernet 配方,请参阅我的另一个答案以获取有关如何使用它的建议,然后升级到MultiFernet()该类以管理密钥轮换。

此外,阅读密钥管理最佳实践。密码学很容易出错。


推荐阅读