概述
Flask以及Django内置的密文生成方式均采用PDKDF2(Password-Based Key Derivation Function),是一种基于迭代复杂度保证密码安全的密文生成方式,PBKDF2通过指定伪随机函数(伪随机数生成器是指通过特定算法生成一系列的数字,使得这一系列的数字看起来是随机的,但是实际是确定的如信息摘要算法MD5、SHA-256等)以及随机盐值处理输入值,并进行该过程的有限次迭代生成最终的密文。
PBKDF2需要以下输入值:
1.哈希函数,建议使用SHA-256或者更加安全的算法。
2.用户输入的密码(明文)。
3.盐,至少8位,应使用安全的随机数(Flask默认长度为8,Django默认长度为12)。
4.迭代的次数,因为需要消耗cpu,具体视情况而定,不少于100000次(Flask以及Django默认的次数均为150000次)。
PBKDF2通过将输入值以及随机产生的盐先进行哈希,再将输入值和该产生的哈希值进行哈希,将该过程迭代有限次生成最终的密文,迭代过程大大增加了破解难度。有效防止了通过碰撞以及彩虹表破解密码。
Python实现
Flask
Flask内部的密文生成函数为from werkzeug.security import generate_password_hash
,generate_password_hash(password, method="pbkdf2:sha256", salt_length=8)
接受用户输入的密码,默认使用的哈希函数是SHA-256,盐的长度默认是8。
源码分析
1 2 3 4 def generate_password_hash (password, method="pbkdf2:sha256" , salt_length=8 ): salt = gen_salt(salt_length) if method != "plain" else "" h, actual_method = _hash_internal(method, salt, password) return "%s$%s$%s" % (actual_method, salt, h)
gen_salt
函数用于生成指定长度的随机盐值:
1 2 3 4 5 6 7 SALT_CHARS = "abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ0123456789" def gen_salt (length ): if length <= 0 : raise ValueError("Salt length must be positive" ) return "" .join(_sys_rng.choice(SALT_CHARS) for _ in range_type(length))
起主要作用函数是_hash_internal(method, salt, password)
,该函数内部通过分析参数method
的值,决定是否生成密文、是否使用PBKDF2以及选用的哈希函数。下面看代码注释:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 def _hash_internal (method, salt, password ): if method == "plain" : return password, method if isinstance (password, text_type): password = password.encode("utf-8" ) if method.startswith("pbkdf2:" ): args = method[7 :].split(":" ) if len (args) not in (1 , 2 ): raise ValueError("Invalid number of arguments for PBKDF2" ) method = args.pop(0 ) iterations = args and int (args[0 ] or 0 ) or DEFAULT_PBKDF2_ITERATIONS is_pbkdf2 = True actual_method = "pbkdf2:%s:%d" % (method, iterations) else : is_pbkdf2 = False actual_method = method if is_pbkdf2: if not salt: raise ValueError("Salt is required for PBKDF2" ) rv = pbkdf2_hex(password, salt, iterations, hashfunc=method) elif salt: if isinstance (salt, text_type): salt = salt.encode("utf-8" ) mac = _create_mac(salt, password, method) rv = mac.hexdigest() else : rv = hashlib.new(method, password).hexdigest() return rv, actual_method
从_hash_internal(method, salt, password)
的实现可以看到,进行PBKDF2的主要函数是pbkdf2_hex(password, salt, iterations, hashfunc=method)
,继续深入,发现最终产生值的函数是使用的Pythonn内置模块hashlib提供的pbkdf2_hmac(hash_name, password, salt, iterations, dklen=None)
函数。该函数完成了PBKDF2的运算过程:将密码以及随机产生的盐先进行哈希,再将输入值和该产生的哈希值进行哈希,将该过程迭代有限次生成最终的密文,这里对于代码的实现不做深究(代码的实现很简单,主要是原理的理解)。
产生值以及验证
通过源码分析可知:generate_password_hash
生成的密文组成部分为:使用哈希算法名(15000表示的是迭代的次数),盐值,SHA-256值,通过$分隔:
1 pbkdf2:sha256:150000 $2b8nQyP1$d6eb56ab9813aa4300809bea2e6b7a22143969fdae6818ce14cbdd158bd8436e
这就是实际存储在数据库中的用户密码的密文,此时即使Flask也无法知晓用户的实际密码,需要对用户输入的密码进行验证时,只能再次进行一次该过程,动态演算后,将获得的密文和数据库的密文进行比较,判断是否一致。生成的密文之所以需要加上pbkdf2:sha256:150000$2b8nQyP1$
就是为了在验证的时候能够使用相同的运算环境,尤其是盐值,因为是动态生成的,每次都不一样,所以必须保存起来。
Flask提供了对应的验证函数check_password_hash(pwhash, password)
,该函数需要传递存储的密文以及待验证的明文密码,验证通过返回True
,反之返回False
,代码如下:
1 2 3 4 5 6 werkzeug/security.py def check_password_hash (pwhash, password ): if pwhash.count("$" ) < 2 : return False method, salt, hashval = pwhash.split("$" , 2 ) return safe_str_cmp(_hash_internal(method, salt, password)[0 ], hashval)
从代码的实现可以看到,内部也是使用的_hash_internal
来生成密文的。
用户认证模型
Flask中不提供数据库功能,这里使用Flask-SQLAlchemy
作为数据库扩展,使用Flask-Login
作为用户模型扩展,创建用户模型类:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 from werkzeug.security import check_password_hash, generate_password_hashfrom flask_login import UserMixinclass User (db.Model, UserMixin ): __tablename__ = 'users' id = db.Column(db.Integer, primary_key=True ) email = db.Column(db.String(64 ), unique=True , index=True ) username = db.Column(db.String(64 ), unique=True , index=True ) password_hash = db.Column(db.String(128 )) def __init__ (self, email=None , username=None , password=None ): self.email = email self.username = username self.password = password @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 validate_password (self, password ): return check_password_hash(self.password_hash, password) def __repr__ (self ): return f'<User {self.username} >'
Django
Django内的密文生成功能由from django.contrib.auth.hashers import make_password
提供,该方法通过获取Django settings中的PASSWORD_HASHERS
中配置的hasher进行密文的生成。
1 2 3 4 5 6 PASSWORD_HASHERS = [ 'django.contrib.auth.hashers.PBKDF2PasswordHasher' , 'django.contrib.auth.hashers.PBKDF2SHA1PasswordHasher' , 'django.contrib.auth.hashers.Argon2PasswordHasher' , 'django.contrib.auth.hashers.BCryptSHA256PasswordHasher' , ]
默认使用django.contrib.auth.hashers.PBKDF2PasswordHasher
进行计算,该类的不是实现如下:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 class PBKDF2PasswordHasher (BasePasswordHasher ): algorithm = "pbkdf2_sha256" iterations = 150000 digest = hashlib.sha256 def encode (self, password, salt, iterations=None ): assert password is not None assert salt and '$' not in salt iterations = iterations or self.iterations hash = pbkdf2(password, salt, iterations, digest=self.digest) hash = base64.b64encode(hash ).decode('ascii' ).strip() return "%s$%d$%s$%s" % (self.algorithm, iterations, salt, hash ) def pbkdf2 (password, salt, iterations, dklen=0 , digest=None ): """Return the hash of password using pbkdf2.""" if digest is None : digest = hashlib.sha256 dklen = dklen or None password = force_bytes(password) salt = force_bytes(salt) return hashlib.pbkdf2_hmac(digest().name, password, salt, iterations, dklen)
从结果来看,django中的默认使用的密文生成方式和Flask类似,默认使用的哈希函数为SHA-256,迭代次数为150000,随机产生的盐值默认长度为12(Flask的默认长度为8),在PBKDF2PasswordHasher
的基类BasePasswordHasher
中定义。都是通过Pythonn内置模块hashlib提供的pbkdf2_hmac(hash_name, password, salt, iterations, dklen=None)
函数完成PBKDF2的运算过程。唯一的不同是Django中对运算的结果进行了base64编码后再返回,这点从最终得到的密文值也可以发现:
1 pbkdf2_sha256$150000 $sYt5dCzmVAXv$bbPcDOWeuIXIQjA/5eoCuI0C1mwvaIevfiUCh2hUolA=
SHA-256产生的是64个十六进制数,不可能出现=,而=在base64中常见,用来填补缺少的位数,可以猜测使用了base64编码。关于base64的问题参考Base64编码 。
此外,也提供了对应的check_password(password, encoded, setter=None, preferred='default')
用于验证用户密码,作用的原理和Flaske的check_password(pwhash, password)
基本相似。
用户认证
Django中一般不直接调用make_password()
函数,其内置的用户模型类AbstractUser
的基类AbstractBaseUser
提供了两个方法set_password(self, raw_password)
和check_password(self, raw_pasword)
用于保存和验证用户密码:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 class RegisterView (View ): def get (self, request, *args, **kwargs ): register_get_form = RegisterGetForm() return render(request, "register.html" , { "register_get_form" : register_get_form, }) def post (self, request, *args, **kwargs ): register_post_form = RegisterPostForm(request.POST) if register_post_form.is_valid(): mobile = register_post_form.cleaned_data["mobile" ] password = register_post_form.cleaned_data["password" ] user = UserProfile(username=mobile) user.set_password(password) user.mobile = mobile user.save() login(request, user) return HttpResponseRedirect(reverse("index" )) else : register_get_form = RegisterGetForm() return render(request, "register.html" , { "register_get_form" : register_get_form, "register_post_form" : register_post_form, })
在创建用户的时候,直接调用用户对象的set_password()
方法传递获取的明文密码,即可完成对应密文的获取。
此外,Django提供了完善的用户认证系统,提供django.contrib.auth.authenticate()
方法用于用户的身份验证,默认使用的认证类django.contrib.auth.backends.ModelBackend
中就是通过调用user.check_password(password)
来验证用户的。
关于Django详细的认证流程参考Django自定义用户认证系统 。
小结
Flask以及Django默认的密文生成方式均采用PDKDF2,该函数通过增加迭代次数的方式增加的被强行破解的难度,此外Flask以及Django在实现的时候都采用的不固定式的随机盐,大大增强了安全性,这已经能够胜任绝大多数的使用场景了。
如果需要更安全的密码机制,两者都提供了采用Bcrypt生成密文的方法,Django中通过在PASSWORD_HASHERS
中设置:
1 2 3 4 5 6 PASSWORD_HASHERS = [ 'django.contrib.auth.hashers.BCryptSHA256PasswordHasher' , 'django.contrib.auth.hashers.BCryptPasswordHasher' , 'django.contrib.auth.hashers.PBKDF2PasswordHasher' , 'django.contrib.auth.hashers.PBKDF2SHA1PasswordHasher' , ]
Flask中也提供了第三方扩展Flask-Bcrypt
来实现,具体使用参考官方文档 。
需要注意的是,安全性越高的算法需要的计算量越大,相应的耗时越长,在使用的时候,注意取舍。