密码加密存储
服务端注册登陆流程
概述
密码加密,之前的做法是使用 md5 散列的方式,因为 md5 不可逆,无法从密文推出原文。但md5并不安全,如果拖库则可使用彩虹表,也很容易批量还原出密码明文来
加盐,使用hash(password + salt)的结果存入数据库中,就算拿到数据库的密码反查,也没有用。如果是固定加盐,还是容易被找到规律,或者从源码中泄露。随机加盐,每一次盐都变,就增加了破解的难度
暴力破解,什么密码都不能保证不被暴力破解,例如穷举。所以要使用慢哈希算法(指执行这个哈希函数非常慢,这样暴力破解需要枚举遍历所有可能结果时,就需要花上非常非常长的时间),如bcrypt,就会让每一次计算都很慢,都是秒级的,这样穷举的时间就会很长
bcrypy 安装及文档
- 安装
pip install bcrypt
使用简介
import bcrypt
salt1 = bcrypt.gensalt()
salt2 = bcrypt.gensalt()
print(salt1, salt2) # 每次拿到盐都不一样
password = 'monkeyjerry'
salt = bcrypt.gensalt()
print(salt)
pw1 = bcrypt.hashpw(password.encode(), salt)
pw2 = bcrypt.hashpw(password.encode(), salt)
print(pw1, pw2) # 相同的盐,计算得到的密文相同
pw1 = bcrypt.hashpw(password.encode(), bcrypt.gensalt())
pw2 = bcrypt.hashpw(password.encode(), bcrypt.gensalt())
print(pw1, pw2) # 不同的盐,计算得到的密文不一样
# 校验
print(bcrypt.checkpw(password.encode(), pw1))
print(bcrypt.checkpw(password.encode(), pw2))
print(bcrypt.checkpw(password.encode() + b'changed password', pw2))
--------------------输出结果------------------------
b'$2b$12$CzALusCtV53OuqqdCM.XjO' b'$2b$12$d5o1eWCkSsfKIC/9e9MBc.'
b'$2b$12$2/WMWJfZcDLrq2JDlTZfqu'
b'$2b$12$2/WMWJfZcDLrq2JDlTZfquwu4E4Avg.1LzrtAkJAV4Ch6dYD4ioZ6' b'$2b$12$2/WMWJfZcDLrq2JDlTZfquwu4E4Avg.1LzrtAkJAV4Ch6dYD4ioZ6'
b'$2b$12$cMbj0PYsfd1ibwntu5lUiuwn/mIAMBHM22SH10XdlaxYYfTGevWOa' b'$2b$12$1CQfH.fOhHUS8V5idDR0aeZS/Qc2P1nsRxwm9VsjGbveHs06Cwq9a'
True
True
False
- bcrypt.gensalt()每次生成的盐都不一样
- 相同的盐,计算得到的密文相同
- 不同的盐,计算得到的密文不一样
salt = b'$2b$12$2/WMWJfZcDLrq2JDlTZfqu'
pw = b'$2b$12$2/WMWJfZcDLrq2JDlTZfquwu4E4Avg.1LzrtAkJAV4Ch6dYD4ioZ6'
$ 是分隔符
prefix 2b(默认):an adjustable prefix 2a or 2b(the default) to let you define what libraries you'll remain compatible with
rounds 12(默认):"log_rounds" defines the complexity of the hashing, increasing the cost as 2**log_rounds
盐2/WMWJfZcDLrq2JDlTZfqu
22个字符,base64
密文wu4E4Avg.1LzrtAkJAV4Ch6dYD4ioZ6
31个字符,base64
import datetime
password = 'monkeyjerry'
start = datetime.datetime.now()
pw = bcrypt.hashpw(password.encode(), bcrypt.gensalt())
delta = (datetime.datetime.now() - start).total_seconds()
print('加密需要的时间是{}'.format(delta))
check_pw = bcrypt.checkpw(password.encode(), pw)
delta = (datetime.datetime.now() - start).total_seconds()
print(check_pw)
print('校验需要的时间是{}'.format(delta))
- bcrypt加密、验证非常耗时
Bcrypt 有两个特点:每一次 HASH 出来的值不一样;计算非常缓慢。因此使用 Bcrypt 进行加密后,攻击者想要破解成本变得不可接受。但代价是应用自身也会性能受到影响,不过登录行为并不是随时在发生,因此能够忍受。对于攻击者来说,需要不断计算,让攻击变得不太可能,因此推荐使用 Bcrypt 进行密码加密
- KDF
- 请详细参考github或源码
服务端注册登陆服务
我们在之前的文章介绍了JSON Web Token,用于签名,防数据篡改,可点击参看JWT
- 注册:用户名密码post提交到服务端,服务端使用
bcrypt.hashpw(password.encode(), bcrypt.gensalt())
来加密密码,并和用户名一起插入数据库,如果事务能正常提交则存储并给出20x的响应,否则给出其他响应错误码 - 登陆:用户名和密码post提交到服务端,服务端拿到用户名到用户表中去查找,找不到直接返回错误如用户名密码错误,用户名存在则拿到密码使用
bcrypt.checkpw(password.encode(), pw)
(这里的pw是数据库中存储的加密后的密码)来校验密码是否正确,如果错误则给出错误码响应,否则返回必要的信息,如{"user":{"userid": xxx, "username": yyy}, "token": get_token()}
(一般同时会set-cookie),这里的token采用jwt生成,生成的示例代码如下:
import datetime
import jwt
KEY = "thisisthekey" # 实际项目替换成强度更高的key
def get_token(user_id):
payload = {
"user_id": user_id,
"timestamp": int(datetime.datetime.now().timestamp()) # 时间戳可用于判断是否过期,以便重发token或重新登陆
}
return jwt.encode(payload, KEY, algorithm="HS256")
jwt.decode(token, key, algorithms=["HS256"])
- 用户登陆成功后的其他请求会带上token,服务端采用
jwt.decode(token, KEY, algorithms=["HS256"])
来校验token来完成其他请求(decode后能拿到payload信息,如果token被篡改,则会decode失败,抛出异常jwt.exceptions.InvalidSignatureError: Signature verification failed
),且根据decode出来的payload中timestamp来判断是否过期,若过期会要求重新登陆