前面我们用三篇教程详述了一个企业级用户认证体系的设计与实现,其中主要功能:用户Token的生成、验证和刷新都已经实现了,现在是时候把Token认证和我们的用户结合到一起啦。
用户和Token有以下结合点:
- 用户登录成功后,生成Token给用户客户端下发Token串。
- 用户退出登录,把用户登出平台(Platform) 对应的Token和UserSession主动清除掉。
- 用户修改密码,用户在服务端的所有Token和UserSession全部清除掉,强制用户在每个平台重新登录。
这些用户行为的逻辑实现都对应着对Token和UserSession的不同操作,所以这也是为什么我在专栏的章节安排和代码开发进度上先建设用户认证体系再来做用户注册登录等功能的原因。
本节我们来实现用户注册、登录、登出的功能。
图片
注册功能
即然要把用户登录相关的行为与Token体系整合到一起,我们得先有用户才行,我们先来把用户注册的功能搞定。其实注册功能的逻辑没有什么值得大说特说的,唯一一个值得探究的是怎么保证用户的密码安全。
用户密码安全怎么保证
保证密码安全有两个方向
- 用户的密码输入不能太随意、太容易让别人蒙对,必须对其长度、复杂度进行限制。
- 用户的密码在服务端必须是加密存储的。
第一点比较好理解,我们对用户密码的长度、构成元素(大小写、数字、特殊符号)都要有一定的要求。 第二点关于用户密码的加密存储,我早期工作的几家公司,有用md5的sha1的,还有他俩一起用的,一般还会再加个盐(salt) 再进行md5、sha1。
而最近工作的两家公司,用户密码是用的 bcryt 。 那这里我们探讨一下到底应该用哪种?这里先说答案哈,用bcrypt。
md5,sha1,bcrypt 它们都叫做哈希算法,就是把明文变成哈希字符串的算法,不过他们还有小分类。md5、sha1 这些是快速哈希算法,而bcrypt是慢速哈希算法。 什么意思呢?
意思是如果你不停地堆CPU,快速哈希算法的哈希速度也会成倍增长,可以简单地理解成以前双核CPU执行哈希一个字符串的任务要花费一秒,变成四核CPU后就要花0.5s了。
而对慢速哈希来说,如果你不停地堆CPU,它执行哈希的速度也会变快,但快的非常有限,双核变四核,速度也就从 1s 变0.9s这种级别的提升。
上面举例的数据是我为了大家好理解自己编的,快速哈希和慢速哈希大概就是上面这个意思。所以数据库中使用bcrypt这种慢速哈希的密码,即使是数据库数据被盗,想要通过撞库的方式破解用户的密码,比使用md5、sha1哈希后的密码难度和成本要高很多。
Bcypt 哈希后的字符串构成如下:
图片
- Prefix说明了使用的bcrypt的版本
- Cost是进行哈希的次数-数字越大生成bcrypt的速度越慢,成本越大。同样也意味着如果密码库被盗,攻击者想通过暴力破解的方法猜测出用户密码的成本变得越昂贵。
- Salt是添加到要进行哈希的字符串中的随机字符(21.25个字符),所以使用bcrypt时不需要我们在表里单独存储Salt。
- Hashed Text是明文字符串最终被bcrypt应用这些设置哈希后的哈希文本。
搞清楚用户密码使用bcrypt加密的原因后,我们先把会用到的工具函数写好,在 common/util 目录下新建 password.go。 搞清楚用户密码使用bcrypt加密的原因后,我们先把会用到的工具函数写好,在 common/util 目录下新建 password.go。
Go语言里通过 "golang.org/x/crypto/bcrypt"支持了bcrypt算法的操作,我们把用到的方法封装到下面的工具函数中。
package util
import (
"golang.org/x/crypto/bcrypt"
"unicode"
)
func BcryptPassword(plainPassword string) (string, error) {
bytes, err := bcrypt.GenerateFromPassword([]byte(plainPassword), 11)
return string(bytes), err
}
func BcryptCompare(passwordHash, plainPassword string) bool {
err := bcrypt.CompareHashAndPassword([]byte(passwordHash), []byte(plainPassword))
return err == nil
}
bcrypt.GenerateFromPassword中第二个参数 Cost 越大,速度越慢,在这里我设成了11,大家可以自己调整。
bcrypt.CompareHashAndPassword 这个方法能帮我们比对哈希字符串的原串和给定的明文字符串是否相等。如果不相等会报一个类似这样的错误:
crypto/bcrypt: hashedPassword is not the hash of the given password
我们登录功能时直接用这个方法就能验证用户密码对不对。
继续在password.go里添加验证用户密码复杂度的工具函数
func PasswordComplexityVerify(s string) bool {
var (
hasMinLen = false
hasUpper = false
hasLower = false
hasNumber = false
hasSpecial = false
)
if len(s) >= 8 {
hasMinLen = true
}
for _, char := range s {
switch {
case unicode.IsUpper(char):
hasUpper = true
case unicode.IsLower(char):
hasLower = true
case unicode.IsNumber(char):
hasNumber = true
case unicode.IsPunct(char) || unicode.IsSymbol(char):
hasSpecial = true
}
}
return hasMinLen && hasUpper && hasLower && hasNumber && hasSpecial
}
这个函数会检查密码的长度、大小写、数字、特殊符号这些元素是不是都符合要求,实现注册逻辑的时候我们直接调用即可。
下面我们开始实现注册功能啦,在实现前我们再默念一遍逻辑分层的口诀
请求验证和数据绑定逻辑 --- Controller
外围业务逻辑 --- 应用服务
核心业务逻辑 --- 领域服务
数据访问逻辑 --- 数据访问层
第三方对接 -- Library(这个本节用不到)
本节剩余内容和详细的代码实现,可在加入项目后访问 https://github.com/go-study-lab/go-mall/compare/c11...c12 就能看本章节的详细代码。
图片