将SQLAlchemy hybrid_property与原生属性构建相结合



我在SQLAlchemy中有一个User类。我希望能够在数据库中加密用户的电子邮件地址属性,但仍然可以通过过滤器查询进行搜索。

我的问题是,如果我使用@hybrid_property,我的查询理论上是有效的,但我的构造不起作用;如果我使用@property,我的构造起作用但我的查询没有

from cryptography.fernet import Fernet  # <- pip install cryptography
from werkzeug.security import generate_password_hash
class User(db.Model):
__tablename__ = 'users'
id = db.Column(db.Integer, primary_key=True)
email_hash = db.Column(db.String(184), unique=True, nullable=False)
password_hash = db.Column(db.String(128))
# @property       # <- Consider this as option 2...
@hybrid_property  # <- Consider this as option 1...
def email(self):
f = Fernet('SOME_ENC_KEY')
value = f.decrypt(self.email_hash.encode('utf-8'))
return value
@email.setter
def email(self, email):
f = Fernet('SOME_ENC_KEY')
self.email_hash = f.encrypt(email.encode('utf-8'))
@property
def password(self):
raise AttributeError('password is not a readable attribute.')
@password.setter
def password(self, password):
self.password_hash = generate_password_hash(password)
def __init__(self, **kwargs):
super(User, self).__init__(**kwargs)
# other checks and modifiers

对于选项1:当我试图用User(email='a@example.com',password='secret')构建用户时,我会收到回溯,

~/models.py in __init__(self, **kwargs)
431     # Established role assignment by default class initiation
432     def __init__(self, **kwargs):
--> 433         super(User, self).__init__(**kwargs)
434         if self.role is None:
435             _default_role = Role.query.filter_by(default=True).first()
~/lib/python3.6/site-packages/sqlalchemy/ext/declarative/base.py in _declarative_constructor(self, **kwargs)
697             raise TypeError(
698                 "%r is an invalid keyword argument for %s" %
--> 699                 (k, cls_.__name__))
700         setattr(self, k, kwargs[k])
701 _declarative_constructor.__name__ = '__init__'
TypeError: 'email' is an invalid keyword argument for User

对于选项2:如果我将@hybrid_property改为@property,那么构造是可以的,但查询User.query.filter_by(email=form.email.data.lower()).first()失败并返回None

我应该更改什么以使其按要求工作

===============

注意,我应该说,我已经尽量避免使用双重属性,因为我不想对底层代码库进行大量编辑。因此,我明确地试图避免在User(email_input='a@a.com', password='secret')User.query.filter_by(email='a@a.com').first():方面将创建与查询分离

class User(db.Model):
__tablename__ = 'users'
id = db.Column(db.Integer, primary_key=True)
email_hash = db.Column(db.String(184), unique=True, nullable=False)
password_hash = db.Column(db.String(128))
@hybrid_property
def email(self):
f = Fernet('SOME_ENC_KEY')
value = f.decrypt(self.email_hash.encode('utf-8'))
return value
@property
def email_input(self):
raise AttributeError('email_input is not a readable attribute.')
@email_input.setter
def email_input(self, email):
f = Fernet('SOME_ENC_KEY')
self.email_hash = f.encrypt(email.encode('utf-8'))
@property
def password(self):
raise AttributeError('password is not a readable attribute.')
@password.setter
def password(self, password):
self.password_hash = generate_password_hash(password)
def __init__(self, **kwargs):
super(User, self).__init__(**kwargs)
# other checks and modifiers

hybrid_propertyemail中,如果self.email_hashstr类型,则行self.f.decrypt(self.email_hash.encode('utf-8'))是可以的,但是,由于emailhybrid_property,当SQLAlchemy使用它生成SQL时,self.email_hash实际上是sqlalchemy.orm.attributes.InstrumentedAttribute类型。

来自关于混合属性的文档:

在许多情况下,Python函数和SQLAlchemySQL表达式有足够的差异应该定义Python表达式。

因此,您可以定义一个hybrid_property.expression方法,这是SQLAlchemy将用于生成sql的方法,允许您在hybrid_property方法中保持字符串处理的完整性。

以下是我最终得到的代码,在你的例子中对我有效。为了简单起见,我从您的User模型中去掉了很多,但所有重要的部分都在那里。我还必须为代码中调用但未提供的其他函数/类编写实现(请参阅MCVE(:

class Fernet:
def __init__(self, k):
self.k = k
def encrypt(self, s):
return s
def decrypt(self, s):
return s
def get_env_variable(s):
return s
def generate_password_hash(s):
return s
class User(db.Model):
__tablename__ = 'users'
id = db.Column(db.Integer, primary_key=True)
email_hash = db.Column(db.String(184), unique=True, nullable=False)
f = Fernet(get_env_variable('FERNET_KEY'))
@hybrid_property
def email(self):
return self.f.decrypt(self.email_hash.encode('utf-8'))
@email.expression
def email(cls):
return cls.f.decrypt(cls.email_hash)
@email.setter
def email(self, email):
self.email_hash = self.f.encrypt(email.encode('utf-8'))

if __name__ == '__main__':
db.drop_all()
db.create_all()
u = User(email='a@example.com')
db.session.add(u)
db.session.commit()
print(User.query.filter_by(email='a@example.com').first())
# <User 1> 

不幸的是,上面的代码之所以有效,是因为mockFernet.decrypt方法返回了传入的确切对象。存储用户电子邮件地址的Fernet编码哈希的问题是,即使使用相同的密钥,Fernet.encrypt也不会从一次执行到下一次执行返回相同的fernet token。例如:

>>> from cryptography.fernet import Fernet
>>> f = Fernet(Fernet.generate_key())
>>> f.encrypt('a@example.com'.encode('utf-8')) == f.encrypt('a@example.com'.encode('utf-8'))
False

所以,您想在数据库中查询一条记录,但无法知道在查询时实际查询的字段的存储值是多少。您可以构建一个classmethod,查询整个users表,并循环遍历每个记录,解密其存储的哈希,并将其与明文电子邮件进行比较。或者,您可以构建一个始终返回相同值的哈希函数,使用该函数对新用户的电子邮件进行哈希,并使用电子邮件字符串的哈希直接查询email_hash字段。其中,考虑到大量用户,第一种将是非常低效的。

Fernet.encrypt的功能是:

def encrypt(self, data):
current_time = int(time.time())
iv = os.urandom(16)
return self._encrypt_from_parts(data, current_time, iv)

因此,您可以定义current_timeiv的静态值,并直接自己调用Fermat._encrypt_from_parts。或者,您可以使用hash中内置的python,只需设置一个固定的种子,使其具有确定性。然后,您可以对要查询的电子邮件字符串进行散列,然后首先直接查询Users.email_hash。只要你没有为密码字段做任何上述操作!

相关内容

  • 没有找到相关文章

最新更新