我有一个朋友,姑且就先称呼他为小王吧,前几日,小王去面试;
面试官问:如何在数据库中存储密码?
场景: 小王是应聘者,张总是面试官,面试主要围绕密码存储和相关的安全技术展开。
张总:“你好,小王。我看到你在简历中提到对密码安全有一些了解。你能简单说说,当我们要存储用户密码时,应该采取哪些措施吗?”
小王:“当然,密码是敏感信息,所以我们需要对它进行加密,以确保它在数据库中被保护好。”
张总:“加密?你是指密码存储时需要加密吗?能解释一下吗?”
小王:“是的,我们可以使用加密算法,比如AES,把密码加密后存储在数据库中。”
张总:“你确定是要加密吗?如果我们加密了密码,系统在验证用户登录时,需要解密密码来做对比。这样安全吗?”
小王:“嗯……我想也许不应该解密密码。可能是哈希处理更合适?”
张总:“对的。我们通常不会加密密码,而是进行哈希处理,因为哈希是不可逆的。你知道为什么这样做吗?”
小王:“我想是为了防止密码泄露。即使数据库被入侵,黑客也无法直接获取明文密码。”
张总:“没错。不过单单哈希处理是不够的。你知道彩虹表攻击吗?”
小王:“彩虹表?听过一些,好像是与破解哈希值相关的?”
张总:“对,彩虹表是预计算的哈希值表,攻击者可以用它来匹配数据库中的哈希值,找到对应的明文密码。所以,仅仅依赖哈希值是不够的。你知道我们还能做些什么来防止这种攻击吗?”
小王:“加盐?我们可以为每个密码生成一个随机的盐值,然后一起哈希处理。”
张总:“没错,加盐是防止彩虹表攻击的重要措施。通过添加独特的随机盐,我们可以大大增加破解的难度。你能举例说明你会用什么哈希算法吗?”
小王:“我们公司之前使用了SHA-256来哈希密码。我听说它比MD5更安全。”
张总:“SHA-256确实比MD5安全很多,但实际上对于密码哈希,还有更合适的选择。你听说过Argon2吗?”
小王:“Argon2是专为密码哈希设计的算法,获得了2015年的密码学竞赛大奖。它可以设置内存使用和迭代次数,这让它在应对暴力破解时更加有效。相比于SHA-256,Argon2能够抵御针对现代硬件的并行攻击。”
张总:“听起来很强大。那我们为什么不直接用SHA-256呢?它不是很常用吗?”
小王:“SHA-256是一种通用的哈希算法,主要用于数据完整性验证,比如区块链和数字签名。但是,它在密码学上的应用不如像Argon2这样的专门密码哈希算法。密码哈希需要应对暴力破解和时间复杂度的问题,而Argon2能够提供更好的防护。”
张总内心:“小伙子还不错...是个人才。”
今天我们就结合我这位小王朋友的面试经验来深入的聊一聊:如何在数据库中存储密码?
你是否也曾有过这样的困惑:为什么当我们忘记一个账号的登录密码并点击“忘记密码”时,系统总是让我们创建一个新密码,而不是告诉我们原来的密码呢?
你可能觉得这有些不便,因为有时你只想知道原来的密码,而不想再想一个新密码。然而,当你深入学习编程后,你会发现这里面有非常合理的安全考量。
在这篇文章中,我们将仔细讨论这个问题,帮助那些曾经或现在对这一问题有同样困惑的同学们理解背后的原因。
让我们假设一个用户在你的网站上注册了一个账号,例如 xw@qq.com
,并设置了密码为 abc654321
。最直接的方式是将用户的密码以明文形式存储在数据库中:
username password
xw@qq.com abc654321
这种方法虽然简单易懂,但存在巨大的安全隐患。如果黑客获取了你的数据库访问权限,他不仅能看到这个用户的密码,还能轻易猜到用户在其他网站上使用的相同账号和密码。事实上,很多用户在多个网站上会使用相同的邮箱和密码组合,这使得黑客可以通过攻破一个网站,获得多个网站的用户信息。因此,存储明文密码几乎没有任何保障。
为了解决明文存储带来的风险,开发人员通常会将密码转换为不可逆的哈希值,然后将哈希值存储在数据库中。例如:
username password
xw@qq.com 5baa61e4c9b93f3f0682250b6cf8331b7ee68fd8
在这个例子中,密码 abc654321
被通过哈希算法(如 SHA-1 或 SHA-256)转换成了一串不可逆的字符。即使黑客获取了这个哈希值,他们也不能直接通过哈希值反推原始密码。
为什么这样做?
保护用户隐私:如果系统能够恢复或查看原始密码,这样一来,系统本身就会有用户密码的明文副本。这将极大地增加密码泄露的风险。
防止数据泄露后滥用:即使黑客侵入数据库,获得了密码的哈希值,也无法通过这些哈希值反向计算出原始密码。这种设计大大降低了密码被盗后的风险。
然而,哈希算法并不是完全防御性的。虽然哈希是单向的,但黑客可以通过暴力破解或使用预先计算的哈希值表(例如彩虹表)进行反推。这就是为什么哈希算法的计算速度越快,越不适合密码存储。例如,SHA-1 计算速度非常快,因此不适合直接用于密码哈希。
使用 SHA-256 进行密码哈希
import hashlib
def hash_password_sha256(password: str) -> str:
# 使用 SHA-256 哈希算法
sha_signature = hashlib.sha256(password.encode()).hexdigest()
return sha_signature
# 示例
password = "abc654321"
hashed_password = hash_password_sha256(password)
print("SHA-256 哈希值:", hashed_password)
这个方法虽然比存储明文密码要安全,但仍然不足以抵御现代的暴力破解攻击。
什么是彩虹攻击?
彩虹表攻击是一种通过预先计算大量常见密码及其哈希值的方式,试图快速破解哈希密码的技术。攻击者可以利用这些表进行快速查找,匹配数据库中的哈希值,从而获得密码。
彩虹表攻击模拟
import hashlib
# 简单模拟常见密码字典
common_passwords = ["123456", "password", "abc123", "qwerty", "letmein"]
# 用来存储常见密码的哈希值(即模拟彩虹表)
rainbow_table = {}
# 创建彩虹表 (使用 SHA-256 哈希算法)
def create_rainbow_table():
for password in common_passwords:
hashed_password = hashlib.sha256(password.encode()).hexdigest()
rainbow_table[hashed_password] = password
print("彩虹表生成成功!")
# 模拟攻击者通过彩虹表破解密码哈希
def crack_password(hashed_password):
# 尝试使用彩虹表查找原始密码
if hashed_password in rainbow_table:
return rainbow_table[hashed_password]
else:
return None
# 创建彩虹表
create_rainbow_table()
# 模拟数据库中存储的哈希密码
stored_hashed_password = hashlib.sha256("abc123".encode()).hexdigest()
print("存储的哈希密码:", stored_hashed_password)
# 尝试通过彩虹表破解存储的哈希密码
cracked_password = crack_password(stored_hashed_password)
if cracked_password:
print(f"密码已破解: {cracked_password}")
else:
print("未能破解密码")
彩虹表生成:我们首先创建了一个简单的彩虹表,包含常见密码的哈希值。实际的彩虹表会非常庞大,包含数百万甚至更多的常见密码及其哈希值。
攻击模拟:我们尝试通过彩虹表匹配数据库中存储的哈希密码。如果找到了对应的哈希值,我们就可以还原出原始密码。
彩虹表攻击的限制:
彩虹表虽然有效,但也有局限性,特别是当密码存储中使用了加盐技术时:
加盐防御:每个密码都有独立的随机盐,即使彩虹表中包含了相同的密码,也无法匹配到哈希值。这样,即便攻击者获取了数据库,他们也需要逐个破解每个密码。
bcrypt 和其他“慢”哈希算法:像 bcrypt、PBKDF2 这样的密码哈希算法不仅会自动使用盐,还会通过增加计算时间来进一步增加破解难度。
什么是加盐?
这里的加盐,可不是我们吃的食用盐,加盐,其实是编程中的一个概念,用来让密码更安全。要理解它,你可以想象一下我们日常生活中的一个情景。
假设你喜欢喝咖啡,大家也都喜欢喝咖啡。有的人喜欢加糖,有的人喜欢不加糖。现在,如果我给你一杯完全相同的黑咖啡,不加糖,你一口就能尝出来这就是纯咖啡,跟别人杯子里的咖啡味道一模一样。黑客破解密码也是这样,如果所有人的密码存储方式都是直接拿密码去做哈希,黑客就可以通过匹配很多“常见的黑咖啡”(简单密码)来破解你的密码。
加盐,就像往咖啡里加上一点“独特的调料”,比如糖、奶油,甚至其他你喜欢的配料。这样,即使两个人的咖啡原料是一样的(比如密码相同),但每个人往里面加了不同的配料,结果喝起来味道就完全不同了。
在密码存储中,“盐”就是这份独特的调料。每次你设置密码,系统会给你的密码加一点“盐”(一串随机生成的字符串)。当系统保存你的密码时,它保存的是密码加上盐后的一串哈希值(类似你加了调料后咖啡的味道)。这样,即使黑客知道别人的密码是“123456”,但因为你加了不同的盐,他破解的时候还是搞不清你到底用了什么密码。
举个更简单的例子:
假设你和朋友都设置了相同的密码“password”。如果不加盐,黑客拿到了一个人的密码哈希值,马上就能推断出你和其他人的密码都是“password”。
但如果加了盐,相当于每个人的密码不再是简单的“password”,而是变成了“salt1+password”(你加了一点盐1)和“salt2+password”(你的朋友加了点不同的盐2)。这样,即使密码一样,黑客看到的东西却完全不同——他们就没法通过一个哈希值破解出所有人的密码了。
所以,“加盐”就是密码里的独特调味料,让黑客破解起来更费劲,让你的密码更安全。
username salt password
xw@qq.com 2dc7fcc... sha256("2dc7fcc..." + password_1)
john@163.com afadb2f... sha256("afadb2f..." + password_2)
所以,“加盐”就是密码里的独特调味料,每个用户的密码都会有一个唯一的盐值,即使黑客得到了数据库,也无法通过彩虹表轻易破解密码,让你的密码更安全。
使用随机盐的 SHA-256 哈希
import os
import hashlib
def generate_salt() -> str:
# 生成16字节的随机盐值
return os.urandom(16).hex()
def hash_password_with_salt(password: str, salt: str) -> str:
# 使用 SHA-256 和随机盐哈希密码
salted_password = salt + password
sha_signature = hashlib.sha256(salted_password.encode()).hexdigest()
return sha_signature
# 示例
password = "abc654321"
salt = generate_salt()
hashed_password = hash_password_with_salt(password, salt)
print("随机盐:", salt)
print("哈希后的密码:", hashed_password)
虽然 SHA-256 加盐哈希增强了密码的安全性,但依然存在一定的破解风险。bcrypt 和 PBKDF2 这样的算法专为密码存储设计,它们的计算速度比常规哈希算法要慢得多,从而增加破解难度。这些算法内置了随机盐,并且可以根据需要调整计算成本。
使用 bcrypt 进行密码哈希
import bcrypt
def hash_password_bcrypt(password: str) -> str:
# 生成盐并哈希密码
salt = bcrypt.gensalt() # 自动生成盐
hashed_password = bcrypt.hashpw(password.encode(), salt)
return hashed_password
def check_password_bcrypt(password: str, hashed_password: str) -> bool:
# 验证密码
return bcrypt.checkpw(password.encode(), hashed_password)
# 示例
password = "abc654321"
hashed_password = hash_password_bcrypt(password)
print("bcrypt 哈希后的密码:", hashed_password)
# 验证密码
is_valid = check_password_bcrypt(password, hashed_password)
print("密码验证结果:", is_valid)
正因为密码是以不可逆的方式存储的,当用户忘记密码时,系统无法直接告诉用户原来的密码。相反,系统会通过重置密码的方式来确保安全性。通过发送验证码或其他身份验证方式,确保只有合法用户能够重置密码。这种方式能有效防止恶意用户通过系统获取密码。
存储密码的正确方式至关重要。无论是使用哈希算法、加盐技术,还是采用更安全的密码哈希算法(如 bcrypt 和 PBKDF2),最终目的都是为了保护用户数据免受攻击。而允许用户重置密码而不是查看原始密码,则是确保密码安全存储的必要手段。通过本文,希望你对密码存储背后的原理和安全性考量有了更深的理解。
- HMAC(哈希消息认证码):可以进一步加强密码的安全性,尤其是在服务器和数据库分离时。
- 密码管理工具:使用密码管理工具生成和保存复杂的密码,也是对用户教育的一部分,减少了用户重复使用简单密码的风险。