对于登录大家并不陌生,访问系统几乎都需要登录。因为系统也不知道是谁在访问,所以需要你告诉系统你是谁,还需要证明你真的是你,如何证明?给系统展示你的密码,因为密码只有你才拥有,你有这个密码,你就能证明你真的是你,这就是一个登录。
看似简单的几个步骤,但里面涉及的安全问题却有很多。
密码储存安全
首先我们看关于密码存储安全的问题。系统服务器需要存储用户密码,才能在用户登录时验证密码的正确性,但存储就会有泄露的风险,比如数据库被偷,服务器被入侵,内部员工泄露数据,被撞库等风险。因此我们需要认真地考虑如何安全存储用户密码。
我认为作为一名软件开发工程师, 严禁明文存储密码是common sense。那该如何解决不能明文存储密码的问题?也许看官你会说,md5 ?没错,md5 可以。那么md5属于什么?它是一种单向散列算法,单向散列算法主要就是通过算法生成一个摘要,来解决密码不能明文化问题。比如:
- md5(123456) = e10adc3949ba59abbe56e057f20f883e
这样虽然能解决密码不明文化,但是它还是会存在安全问题。因为现代计算机的计算能力实在太强了,一秒可以计算十亿次 ,可以通过穷举对比的方式破解密码。这也就是所谓的彩虹表攻击。
解决被彩虹表攻击的问题对密码也有一定的要求,比如要求密码的复杂度,需要不同类型的字符进行组合,在生成摘要时加点盐来防止穷举破解密码。但这就安全了吗?还不够。一次算法远远不够满足安全要求,如md5(md5(md5(password+salt))),现在往往采用自适应的方式来存储密码,可以设置重复计算一万次,盐使用随机生成的16+位字符串。
这种方式虽然性能不会很好,但对于密码生成摘要存储来说,性能不好往往是好事,毕竟用户注册或修改密码只是一次操作,用户是可以接受的,但对于黑客来说,这是致命的,黑客从原来的一秒产生几百万甚至上千万的md5值,变成了一秒只能产生一个,黑客想要破解一个密码,从现代的计算机算力来看,需要上千年的时间。目前推荐的使用密码存储算法已不再推荐md5了,推荐采用Bcrypt Scrypt pdkdf2算法。
(很多可以通过MD5/SHA值进行反向查询,都是已经存储了大量的彩虹表)
密码传输安全
解决了密码存储安全,再来看密码传输安全。有人会说使用https就能解决网络传输的安全问题,但这还是不够。浏览器到认证服务器之间请求可能会经过一层或多层gateway,如nginx,zuul,spring cloud gateway等。很多系统都是在gateway处安装证书设置https协议,浏览器到gateway处是加密传输的,但gateway到认证服务器还是http协议。
攻击人可以在这条链路上窃取明文密码,那全链路https不就可以解决问题了?还不够,这些gateway内部都可能会接触到明文密码,都有密码泄露的风险,有些gateway不是我们负责的,无法保证他人会不会开个后门拿出明文密码,或者安全意识较低的程序员打印日志不小心把明文密码打印出来。那如何解决这个问题?我们可以采用浏览器传输密码之前就对密码先加密的方法。
加密方式分为对称加密与非对称加密。
对称加密:加密与解密用的是同一个密钥。如DES,AES非对称加密:加密与解密用的是不同的密钥,一个叫公钥,一个叫私钥,公钥加密的数据只能由对应的私钥才能解,如RSA。
如果采用对称加密方式,需要浏览器在调登录api之前,先获取认证服务器的密钥,拿到密钥后对密码进行加密传输,经过的gateway都只能获取密文,密码到了认证服务器,认证服务器再通过密钥对密文进行解密,获取到密码明文,就可以进行密码验证。但有一个安全问题,浏览器获取密钥也会经过gateway,如果gateway把密钥也打印到了日志中,密文也打印到了日志中,那攻击人同样可以通过日志获取明文密码。
既然对称加密不可取,我们来看看非对称加密。浏览器登录前经过gateway获取认证服务器的公钥,使用公钥进行加密,最终密文到认证服务器,再通过私钥解密拿到明文密码进行密码认证。这种方式gateway只能拿到公钥和密文,无法解密,就算打印到日志中,攻击人无法拿到明文密码了。
但这样就安全了吗?
如果攻击人拿gateway中的密文直接去调认证服务器中的登录api,认证服务器一样可以通过私钥进行解密,并登录成功。所以我们还需要加一些限制:保证密文有过期时间,并且是只能使用一次。
浏览器获取认证服务器公钥时,携带用户名到认证服务器,认证服务器生成随机数并与用户名关联,随机数只保存5分钟,随机数与公钥一起返回给浏览器。浏览器使用随机数加密码通过公钥一起加密调登录api,认证服务器通过私钥解密,获取到明文密码与随机数,验证随机数的有效性与合法性,都正常就进行正常登录,比较完随机数后立刻删除随机数,如不正常拒绝登录。
攻击人就算获取到了密码密文,公钥,随机数,也只能在5分钟之内赶在gateway正常请求登录之前,发起登录攻击,但这个难度太大。还有就是认证服务器保证客户端是gateway或可信的服务发起的请求,认证服务器可以对客户端做白名单限制,方式有很多种,在这就不一一赘述了。
但现在就安全了吗?还真不一定。如果攻击人攻破了gateway,在浏览器请求认证服务器获取公钥时,gateway返回攻击人颁发的公钥,待用户输入完账号密码后,浏览器虽然进行了加密,数据到了gateway,攻击人再通过自己的私钥进行解密拿到明文密码,再通过明文密码在登录页进行正常的登录,同样可以登录成功。因此浏览器也需要做安全验证,验证公钥的合法性。
认证服务器可以采用CA机构颁发的公钥,认证服务器与浏览器都相信CA机构(做安全总得相信点东西,如果什么都不信任就没法做安全了,有永无止境的安全问题),通过CA机构的方式验证公钥的合法性来避免中间人篡改公钥的问题(讲得不是很清楚,比如CA机构是个啥,为什么CA机构可信?这里面可聊的话题太多,有兴趣可以查看《密码学与网络安全》等书籍或一起探讨研究)。
那密码安全了吗?还是远远不够。比如黑客知道了你密码的长度,可以不断地调登录或修改密码的接口来试错,总会试出来正确的密码,因此需要对任何会验证密码合法性的接口都需要加频率限制。如登录连续错了5次锁5分钟,再错5次锁半小时,防止黑客试出密码。但这种方式也有问题。如竞争对手公司不断地使用用户的账号和错误的密码去登录,导致用户的账号一直处于被锁状态,正常用户也没法使用,这就违背了安全中的可用性。那就需要加ip限制和验证码机制了。为了用户的体验性,可以做成第一次登录用户可以正常登录,错误之后,就需要使用验证码的方式登录,超过5次锁定账号,同一ip登录错误次数过多,将ip加入黑名单中。
无密码安全
密码有很多安全问题,复杂密码对于用户来说也挺麻烦的,那采用无密码技术。没有密码是不是就安全了呢?虽然现在可以采用指纹登录与刷脸登录,但新的安全问题也随之而来。密码是需要私密性的,但指纹可以从照片中获取,美国国防部某个官员因在拍照时露出了大拇指,随后就有了这个大拇指的清晰指纹图(拍照的时候不要剪刀手或点赞了,最好指纹对准自己吧,手动狗头)。
还有就是存在不确定性,刷脸登录时,如果灯光太暗或太亮,脸部受伤了,化妆了,那登录能保证成功吗?脸部相似的人,登录时能保证区分开来吗?如果不能就违背了账号唯一性,日后审计也是个问题。还有一个问题就是不可修改。当密码泄露了可以修改密码,但你的指纹已经作为登录凭据了,换个手指头就好了,当十个手指头都用过了,那是不是该用脚指头了?当然无密码肯定是比有密码使用上更方便快捷,随着技术的发展,这些问题也都会解决,只是也会有更多的安全问题。
我们再来看会话安全(密码安全还有各种各样的问题,篇幅有限,不再聊了)。
会话标识储存安全
登录完成后,用户不可能每一次操作都需要输入密码。因此系统需要记录用户的登录状态,又称会话状态。常见的做法是系统保存session。session存入用户信息,生成随机数sessionId,将sessionId返回浏览器,并存入浏览器的cookie中,下一次用户访问系统,携带cookie,系统通过cookie找到session,就可以知道用户是谁
对于集群服务,用户首次登陆,访问的A服务器,A服务器存入session,下次访问到了B服务器,B服务没有session,认为用户没有登陆,提示用户需登陆,这是一个bug。我们将每台服务器都识别到有session就可以解决这个问题了。session存入redis,登陆时往redis存session,之后都从redis取session。或者每台服务器都有session,每台服务器的session同步也能解决这个问题。
不管采用哪种方式,都有一个安全风险,sessionId给出去了,不论sessionId是随机数生成还是加密算出来的字符串,黑客并不关心,黑客只关心这个字符串代表了用户的会话状态。黑客也不需要拿到密码只需要拿到这个字符串,就可以模拟用户进行诈骗,转账,发表非法政治评论等非法活动。
保护sessionId不被非法利用与保护密码同等重要。大多数情况下sessionId存储在cookie中,我们先了解cookie。
这是登录okta后生成的其中一个cookie,有name,value,domain,path,Expires/Max-Age,Httponly,Secure等属性,这里重点介绍其中几个。
- Domain:cookie对于哪个域有效。这个cookie的域是thoughtworks.okta.com,则只有访问thoughtworks.okta.com下的api,浏览器才会将该cookie发送至后端服务器。这个值可以包含子域,如设置domain为okta.com时,访问thoughtworks.okta.com也会带上该cookie。
- HttpOnly:当值为true时,告诉浏览器不能通过js访问到该cookie,只有在发送请求到后端时,才会携带该cookie。
- Secure:当值为true时,告诉浏览器,只有访问协议问https的api时,才会携带该cookie。
- Expires/Max-Age:cookie有两种,本地cookie与session cookie。如果设置了cookie的过期时间则为本地cookie,不设置为session cookie。session cookie的特点是没有具体的过期时间,随着浏览器关闭而清除。本地cookie即使浏览器关闭也不会清除,而是到了时间自动清除。这也是为什么关闭浏览器后再次打开浏览器有些系统需要重新登录,而有些不需要的原因。
知道cookie的几个特性后我们再来看看攻击人常用的几种攻击方式:XSS攻击,CSRF攻击。
会话标识传输安全
(1) XSS攻击叫做跨站脚本攻击,指用户的输入拼接了正常的html+js+css,变成了带有攻击性的html+js+css。浏览器可能无法识别具有攻击性的html+js+css,按照正常的逻辑执行代码,这可能会导致攻击人偷走cookie(XSS还有其他的危害,但这里仅讨论与会话标识相关)。如果黑客在html中插入隐藏的form表单,通过document.cookie()获取到浏览器中cookie,作为参数并自动发送post请求到攻击人的后端api中,攻击人就可以拿到用户的cookie,也就可以拿到sessionId了。这种方式可以通过设置cookie的HttpOnly为true来防止js获取cookie值。从而避免通过XSS攻击获取sessionId。
(2) CSRF攻击叫做跨站请求伪造。XSS攻击是指本网站的代码执行攻击脚本造成了对本网站的影响。CSRF攻击则是用户打开了其他网站,浏览器执行了其他网站的攻击脚本,却对本网站造成了伤害。举个例子,当我在浏览器中登录了某银行的网站,进行了转账操作,浏览器调用了
https://www.xxx.com/transfer?toBankId=123456&money=100,我的账户少了100块,收到短信扣了100块。这时来了一封邮件,标题为你想得到力量吗?内容是一个链接,我点击这个链接,看到url是www.yyy.com/index.htm,立马又收到一个短信,我账号又少了1000块,我刷新下页面,又少1000块。打开页面查看源码,发现有个隐藏的标签,
src=https://www.xxx.com/transfer?toBankId=123456&money=1000。也就意味着每次刷新页面,浏览器都会执行一次
https://www.xxx.com/transfer?toBankId=123456&money=1000 GET请求。大多数浏览器有同源策略(协议\主机\端口组成源),其中一个限制是同源的网页才会共享cookie。但浏览器对html标签有白名单,img就是其中之一,通过img标签的src就可以发送get 请求,因访问的是xxx(银行)的域名,携带了cookie,银行认为是合法请求,转账成功。因img是get请求,那把转账等高危操作改成post接口不就可以了? 也不行,因为form表单的post请求也在白名单中。
CSRF攻击之所以成功,是因为攻击人可以完全伪造用户的请求,那让攻击人无法伪造就可以解决这个问题了。在转账时,要求用户再次输入密码或输入验证码,就可以解决CSRF攻击。转账操作可以这么做,发表评论这类的操作,每次都要求用户输入密码或验证码用户体验就很很差了。
(3) 还有Referer check,浏览器发送请求时,携带Referer header,值为网站url中的域名,异常转账时,虽然调用的www.xxx.com的api,但referer 值为www.yyy.com。在服务端只要验证Referer值就可以判断这是不是一个CSRF攻击。这种方式也有问题,就是完全相信了第三方(浏览器)。对于低版本的浏览器已经有办法可以篡改Referer值,高版本的浏览器目前无法篡改,如用户使用低版本的浏览器,Referer check将无法保证安全性。
那还有什么办法可以解决CSRF攻击的问题? 我们来看下okta是如何做到解决这个问题的。我们登陆okta成功后,打开网页源代码查看html,搜索token可以看到
在span中保存了一个token值 我们再创建一个tab页
打开浏览器的f12,查看网络请求,可以看到request header中有x-okta-xcrftoken这个header。
这就是为了解决CSRF攻击的方式:CSRF Token方式。
csrf token工作原理就是在用户登录成功后,服务端生成token并保存一段时间,返回给浏览器,浏览器保存在html标签中。当用户操作访问后端api时,将该token放入request header中。后端验证该token 的合法性即可判断是否是CSRF攻击。这种方式能生效的重点在于攻击人无法拿到目标网站的html。
最近在思考一个问题,就是如果黑客同时发起XSS攻击和CSRF攻击,这种方式是不是也失效了?黑客通过XSS攻击,获取到了CSRF token,攻击人立马发送钓鱼邮件给目标用户,目标用户点击了链接,网站打开时,先从黑客处获取CSRF Token,并携带CSRF Token发起了CSRF攻击,还有个前提是浏览器版本太低没有Referer,那不就可以攻击成功了?(我杞人忧天了吗?) cookie+session有这么多安全需要考虑,那不要cookie+session不就没这么多问题吗?现在流行的jwt就可以做到无session的登录认证,但jwt也有各种各样的安全问题。
【本文是51CTO专栏作者“ThoughtWorks”的原创稿件,微信公众号:思特沃克,转载请联系原作者】