我在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
答案 0 :(得分:0)
在您的hybrid_property
email
中,如果self.f.decrypt(self.email_hash.encode('utf-8'))
是self.email_hash
类型,则行str
很好,但是email
是hybrid_property
,当SQLAlchemy使用它来生成SQL self.email_hash
时实际上是sqlalchemy.orm.attributes.InstrumentedAttribute
类型。
来自docs的关于混合属性的信息:
在许多情况下,Python中的函数和 SQLAlchemy SQL表达式有足够的区别,以至于两个分开 应该定义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>
不幸的是,上面的代码仅能工作,因为模拟Fernet.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_time
和iv
的静态值,然后直接调用Fermat._encrypt_from_parts
。或者,您可以使用hash
和just set a fixed seed内置的python来确定性。然后,您可以对要查询的电子邮件字符串进行哈希处理,然后首先直接查询Users.email_hash
。只要您没有对密码字段执行上述任何操作!