我们来聊一聊综合楼的看门大爷
综合楼有一个门卫大爷,综合楼里面有各个房间,207,211等等,每个想进入楼内的人都需要在看门大爷那做一个身份的验证,登记之后才能进到综合楼里,而进到综合楼里的话还分各种权限。如学生能进实验室,老师能进实验室和自己的办公室。
大爷要做的第一件事就是要知道谁是这个楼里的老师或者是学生,那么我们首先需要创建一张USER表,存储能进这栋楼的用户信息,所以在登录之时,也就是你刚进入这栋大楼的时候,大爷会看一下User表,看看上面有没有你的名字,如果有你的名字,再对你的密码啥的进行验证。
那么我们现在说一下,如何对密码进行存储和校验
我们说一下加密算法,也就是Hash哈希算法。哈希算法的细节我们再次不深究,我们只需要知道,Hash算法有一个特点:输入的值对应一个唯一的输出。这样我们就可以实现:注册的时候密码用Hash算法做不可逆的加密,登录的时候也用Hash加密。数据库中不存真实的密码,而是存加密运算过的密码。这时候,一样的密码对应着相同的加密后的密文,我们只需要对比密文是否相等就可以知道密码是否相等了。常见的Hash算法有MD5,SHA256,SHA1等等。
那么我们如何保密地跟门卫大爷交流呢
我们现在知道了我们的信息是如何存储在门卫大爷手上的USER表中,也知道了门卫大爷如何对我们的身份进行验证,那么问题来了,我们告诉门卫大爷我们的信息的时候,万一旁边偷偷站着一个人在偷听我和门卫大爷的对话,偷听了我用POST请求告诉门卫大爷的信息怎么办呢。首先,我们肯定需要使用HTTPS,确保在传输过程中的数据信息安全性。 可是HTTPS的证书也是可以伪造的,HTTPS也可能会失效。所以,在HTTPS的情况下,我们还需要自己在前端对重要信息进行加密,再发往后端。
其实很多小的系统不需要前端加密,国外的一些大公司也不加密,包括github,微软,谷歌都不做前端加密。加密要求很高的系统才需要,小系统就信任https就好了。不过话虽这么说,github此前就是不使用前端加密的,去年翻车了,然后给用户群发邮件改密码,然后乖乖前端加密了。github这次翻车的原因是:你前端不加密,而且https没被攻破,但是https在nginx就解开了啊,nginx解开加密后,在后台,也就是说在公司内部机器上是明文了,明文随便一个运维人员都能看见,在后端打个log日志信息就看到了明文密码了,所以这样很危险啊。于是外国一些公司也开始跟着国内的大厂一起在前端加密了。
我们实施加密的具体方法可以有很多种,下面列出几种常用的(对密码加密的方法,往后还有对账号和手机号,身份证号进行加密的方法,但是不一样,因为身份证号,手机号涉及到短信验证码的发送或者是像公安机关部门申请认证,这样子手机号和身份证等信息一定要有解密的过程,密码可以不需要解密,所以对需要明文的信息和对不需要明文的信息我们采用不同的方法)。
1.前端进行MD5加密,发往后端,后端加salt加密
2.(出于更高安全考虑,使用SHA加密代替MD5)前端进行SHA加密,然后发往后端,后端再进行SHA加salt加密
3.后端先生成一组RSA的公钥和私钥(将HTTPS自己额外做一遍),前端拿着公钥加密,后端拿着私钥解密,然后后端把解密出来的密码再做不可逆的加密如SHA1和MD5等重新进行加salt加密。
4.用AES对称加密的方法,在前端访问登录页的时候,后端生成一个秘钥返回给前端,然后前端使用秘钥加密,后端使用相同的秘钥解密就好了。
5.使用uuid(通用唯一识别码)加密就好了(服务器为了对上验证码和你这次会话会用uuid来标识验证码),既然uuid是现成的,那我们直接拿uuid进行一次对称加密就好了,或者服务器生成需要uuid的时候就生成一个秘钥做成uuid,uuid就作为你秘钥。然后后端解密加密,继续md5加盐。
数据库存储账号密码的格式如
我们在上面都提到了一种东西,叫做salt,盐是一串随机数,因为md5在当今计算机的算力之下暴力破解是可能的,所以md5,实际上没有那么安全了,这时候,我们就要增加密码的位数,然后再进行md5,当密码位数增加的时候,暴力破解的难度也在几何增长,所以增加密码位数的过程我们就叫加盐。当用户注册的时候,我们会随机生成一串字符,加在用户密码字符串的后面或是前面,然后再进行md5加密。(为了更高安全性,在更新的架构中通常使用SHA256甚至SHA512代替MD5)(还可以使用账号创建时间,创建人ip等等信息作为盐,盐的生成方法很多,具体方法看自己的需求)
当门卫大爷验证了我们的身份之后,我们想进入每个屋子怎么办呢(调用各个Api时的鉴权)
现在我们进入了综合楼,可是,有的人能进所有的屋子,有的人能进207,我们如何识别鉴权呢?
此前,我们见过将token存储在数据库中的鉴权(无车承运系统),也见过将token存在服务器缓存中,放置于redis中等方法,这些方法无一例外是有状态的(需要服务器的信息记录)。这样的一种情况对于单体应用来说当然没有问题,但是随着现在所谓的微服务拆分之后,每个微服务服务器都去调用数据库或者共用redis获取相关的信息会使系统又高耦合了起来,甚至如果存在本机的缓存中,微服务将很难方法其他主机的缓存,这样会很消耗服务器资源甚至不易做到跨服务统一鉴权,因为每一次的请求都会额外地多访问一次存储设备,这样是很消耗资源的。
同时,我们也需要明确一个架构概念,即架构越简单越好,组件越多风险越大。所以,session,redis啥的有状态服务我们尽可能避免(将状态维持在不论是服务器还是redis还是数据库中,都会增加服务器的负载压力,且服务器的架构复杂化之后会难以维护)。
这时候我们就要介绍一个协议:JWT(JSON Web Token),可以从名字看出来,这个token是可以从json里传到前端的(在cookie中也有,在web端尽量使用cookie来存储,因为localstorage不安全)。jwt特殊情况下在不支持Cookie的地方也能使用(如微信小程序和app端的登录请求),同时这样也能在跨域的时候对token处理得游刃有余,跨域时就从body取出存入浏览器内存或是自行setCookie即可。jwt可以说是现在应用最广泛的一种鉴权手段了。
token我们叫做令牌,是持于用户手上的,就是综合楼大楼门卫颁发给我们的,上面写着权限信息等信息,服务器不进行存储,存在用户的cookie中,用户每次使用,就从cookie中取出置于head中。
(下面是登录之后返回的body信息,很标准的jwt,在cookie中也有一份同样的token)
{ "msg":"操作成功", "code":200, "token":"eyJhbGciOiJIUzUxMiJ9.eyJsb2dpbl91c2VyX2tleSI6IjM0MDRlNDg3LTk2YTUtNGI1My1hOGFhLTE4NjE3ZTM5M2VkNSJ9.DIGs1_9gR4DIiZZKNkzodFYdy0O_vasQ-9x-Jdk4j95WXjbnnljoDZ7sDHVGtxHJYGO8dadnnF4ARA8Yj0YG3A" }
//发送请求时拦截请求加上head // request拦截器 service.interceptors.request.use( config => { if (getToken()) { config.headers['Authorization'] = 'Bearer ' + getToken() // 让每个请求携带自定义token 请根据实际情况自行修改 } return config }, error => { console.log(error) Promise.reject(error) } ) ------------------------------------------------------------------------------------------- //下面是getToken()方法 export function getToken() { return Cookies.get(TokenKey) }
token的信息有分成明文和加密两部分,一部分存用户信息并进行base64压缩(这一部分相当于明文),一部分存服务器私钥加密过后的密文字符串,每次访问接口,后端会用公钥对私钥加密的部分进行解密,解密之后与明文部分进行比对相等,那就说明token有效。
因为有了token就相当于有了用户权限,这样我们就必须安全地存储token,那么将token存储在cookie中是安全的,所以我们在cookie存token,并在请求时将其置入head即可。
这会有个问题,后端给用户发布了token之后,如果想撤回用户的token怎么办,因为我们说在服务器不存储相关信息,有关登录信息全存储在了用户手上,那么撤回就是无法完成的,只能通过设置超时时间如30分钟,15分钟来达到。(如果实在想实现,就维护一个redis记录一下退出登陆的账号,下次相同账号的请求就拒绝就好了)
结论
生成一对公私钥,服务器放置私钥,前端放置公钥(相当于自己实现一遍简化版单向的HTTPS,但是优点是不会被伪造的证书颁发机构攻破)
刚进入注册或者登录页面,向服务器请求验证码图片,服务器返回uuid(用于标识验证码)和验证码图片的base64码
将用户输入的密码用md5加密,然后对用户名明文和密码密文统一使用我们自己生成的公钥加密。
整个http通讯用https加密
后端用私钥解密,获得用户名明文和前端加密过的密码
后端生成一个随机的盐salt(如果是登录则从数据库查询salt值),前端加密过的密码字符串后加上salt字符串,再进行一次md5加密
将salt和两次md5加密的密码和明文用户名存入数据库(如果是登录则与数据库中的加密后的密码比较)。实现登录注册。
登录成功后,后端将用户权限有关的信息打包成jwt(下面会说jwt),存在cookie中发送给前端(如果是微信小程序,或者手机app不能拿到cookie,可以在body中也传一份,让他们取出来存在自己的localstorage中,毕竟最后验证的不是cookie中的,而是head中的token)
用户每次请求都在head中带上token进行验证。
其他问题
当我们实现了登录注册之后,事情真的这么简单吗,毕竟验证登录是最最重要的安全措施了,在做验证登录的时候我们有很多细节需要解决。比如,为了防止有人疯狂试错密码,可以使用一个redis(为了安全,系统复杂就复杂了)记录记录每天该用户的错误登录次数,如果超过5次禁止登录。还或者同账号只能同时登录一次(顶号)(还是使用redis来实现)。还有,用手机验证码代替密码(就像好食堂一样,直接放弃密码)。还可以,进行异地登录控制认证,如果使用的ip地址所属的区域和常见登录区域不一致时,调用身份验证服务多重验证,比如强制使用手机验证码验证等。还可以使用第三方登录接口,如微信登录,qq登录。