【问题标题】:SQLAlchemy: call function and save returned value in table alwaysSQLAlchemy:调用函数并将返回值始终保存在表中
【发布时间】:2019-11-06 08:49:26
【问题描述】:

我有一个用户表,它有一个支付密码。我决定加密所有这些信息。每个 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.x encryption flask sqlalchemy


【解决方案1】:

首先,一个重要的警告:


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

相反,存储密码哈希。见Why is password hashing considered so important?Difference between Hashing a Password and Encrypting it

在 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)

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

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

选择一个好的、受信任的加密方案。 cryptography 包包含 Fernet 配方,请参阅 another answer of mine 获取有关如何使用它的建议,然后升级到 MultiFernet() class 以管理密钥轮换。

另外,请阅读key management best practices。密码学很容易出错。

【讨论】:

    猜你喜欢
    • 2017-01-02
    • 1970-01-01
    • 1970-01-01
    • 2011-10-19
    • 2016-07-30
    • 2013-02-03
    • 2015-11-13
    • 2012-09-12
    • 2020-06-12
    相关资源
    最近更新 更多