Python Flask结合PostgreSQL开发时密码哈希验证的字节与字符串类型冲突问题
嘿,这个问题我之前也碰到过,核心原因是你把二进制字节数据直接存入了字符串类型的数据库字段,导致数据在存储和读取过程中被破坏了,咱们一步步来解决:
问题根源拆解
你注册时生成的salt是os.urandom(16)返回的二进制bytes类型,password_hash也是salt + hash_value拼接出的bytes。但PostgreSQL的character varying是字符串类型,当psycopg2把bytes插入这个字段时,会默认尝试用UTF-8编码把它转成字符串——但随机生成的二进制字节大概率不是合法的UTF-8序列,这个转换过程要么直接报错,要么悄悄破坏了原始数据。
等到登录时,你从数据库取出的db_salt已经是被破坏后的字符串了:哪怕你用bytes(db_salt, 'utf-8')转回去,得到的也不是当初生成的那个salt,自然没法生成和注册时一致的哈希值,还会先触发TypeError,因为pbkdf2_hmac要求salt必须是bytes类型。
两种可行的解决方案
方案一:用Base64编码把二进制转成安全字符串存储(推荐)
Base64是专门用来把二进制数据转成可安全存储/传输的字符串的编码方式,不会丢失任何数据,而且兼容性极好。
修改注册代码:
import base64 import os import hashlib # 生成salt和哈希值 salt = os.urandom(16) iterations = 100000 hash_value = hashlib.pbkdf2_hmac( 'sha256', password.encode('utf-8') + app_config['PEPPER'].encode('utf-8'), salt, iterations ) # 把二进制数据转成Base64字符串 salt_b64 = base64.b64encode(salt).decode('utf-8') # 这里可以直接存salt+hash的Base64,或者分开存,看你需求 password_hash_b64 = base64.b64encode(salt + hash_value).decode('utf-8') # 插入数据库 query = "INSERT INTO mytable (password_hash, salt) VALUES (%s, %s);" params = (password_hash_b64, salt_b64) db.query(query, params)
修改登录验证代码:
import base64 import hashlib # 查询用户数据 query = "SELECT password_hash, salt FROM mytable WHERE email = %s;" params = (email,) users = db.query(query, params) db.close() if not users: return False db_password_hash_b64, db_salt_b64 = users[0] # 把Base64字符串解码回二进制bytes db_salt = base64.b64decode(db_salt_b64) # 重新计算哈希值 iterations = 100000 hash_value = hashlib.pbkdf2_hmac( 'sha256', password.encode('utf-8') + app_config['PEPPER'].encode('utf-8'), db_salt, iterations ) # 验证哈希是否一致 expected_hash = db_salt + hash_value if base64.b64decode(db_password_hash_b64) == expected_hash: return True else: return False
方案二:改用PostgreSQL的二进制类型(bytea)存储
如果你的数据库可以修改表结构,直接把字段类型改成bytea(PostgreSQL的二进制存储类型),这样psycopg2会直接把bytes存入字段,读取时也直接返回bytes,完全不用处理编码转换。
首先修改表结构:
CREATE TABLE IF NOT EXISTS myschema.mytable ( -- 其他无关字段... password_hash bytea NOT NULL, salt bytea NOT NULL, -- 其他无关字段... )
然后你的注册和登录代码几乎不用改——注册时直接传bytes参数,登录时取出的db_salt已经是bytes类型,直接传给pbkdf2_hmac就行,完美适配!
为什么之前的尝试没用?
你试过bytes(db_salt, 'utf-8')转类型,但因为存储时原始二进制已经被错误转成了字符串(比如某些无法解码的字节被替换成了�或者其他占位符),转回来的bytes和最初生成的salt完全不一样,所以哈希验证肯定失败。
选上面任一方案都能解决问题,我个人更推荐Base64方案,因为字符串类型在各种场景下的兼容性都更好,后续如果要导出数据或者做其他操作也更方便。
备注:内容来源于stack exchange,提问作者jazzmasterkc

