密码加密存储
服务端注册登陆流程

概述

密码加密,之前的做法是使用 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/WMWJfZcDLrq2JDlTZfqu22个字符,base64
密文wu4E4Avg.1LzrtAkJAV4Ch6dYD4ioZ631个字符,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来判断是否过期,若过期会要求重新登陆

扩展阅读