序
很多系统在登录的时候会使用明文密码的方式往后端进行传参,即使传输过程中使用的是HTTPS,但是如果在中间人攻击中,或者钓鱼网站。攻击者过于轻松的从中截取到明文的密码。
用户登录后的权限的Token或者Cookie会用明文的方式在HTTPS上进行传输,其实如果出现上述场景太容易出现问题。
-
输入密码场景,在Web 前端进行明文加密后进行传输是否有意义?
- 必须要做,虽然不是决定性的保护措施,但却是很有意义的低成本安全增强方案。
-
在登录后的有权限的接口上建立在HTTPS上是否需要在应用上层实现加密是否有意义?
- 如果有开发资源的情况下,建议需要做。
- 在开发资源不宽裕的团队中在无权限接口场景中,不建议使用密文传输,但是建议加入签名机制防止传输过程中篡改。主要原因是如果是密文传输,中间内容不可见。会直接影响到调试过程,导致开发成本的提升。另外如果接入一些三方系统时,比如WAF,日志,路由等,因为内容不可见,可能会导致早期开发成本较高。所以退一步,加入签名机制,传输过程中虽然明文可见的,但是可以保证数据的完整性,签名过程中加入过期时间,也可以在一些场景下防止请求被重放,在一些要求限制请求延迟的接口中还可以。
-
在所有接口上建立加密传输,或者签名是否需要做?
- 如果开发资源丰富的情况下,建议需要做。
- 在开发资源不宽裕的团队中在公开接口场景中,基于HTTPS时不暂时做过多的开发。
所有场景,加密总会比不加密要安全,但是必须考虑的是开发成本和收益的问题。
敏感操作需要使用,2FA是一个比较有效的解决方案,一定要注意防重复的问题。当然如果在中间人攻击中如果中间人篡改的源码,那么这些防御基本都会失效,所以搭配App扫码验证的的方式就有很好的效果。
-
在权限接口中肯能会出现的风险
- 窃听风险(eavesdropping):第三方可以获知通信内容。
- 篡改风险(tampering):第三方可以修改通信内容。
- 冒充风险(pretending):第三方可以冒充他人身份参与通信。
实践
这里提供一个解决思路,可以适用于一些场景,需要按照具体场景进行适配。还有一个备选的方案,在 https 上做一层类似https的协议,https比较安全的原因很大一部分是证书有效性的验证依赖于操作系统,但是如果没有的操作系统的验证,如果单纯的类似https的协议,收益不大,所以这个实践中就融入了业务的流程。
服务端信息存储的设计
- 用户名/邮箱/手机号存储:关于用户信息的保存,这里可能有些做法中会选择使用对称加密的方式保存,这里需要注意的是开发成本和收益,建议使用明文保存全文(应对业务的变动)。业务稳定后可以选择明文保存隐藏部分内容的,(便于明文搜索),密文保存整体内容。
-
密码的存储:必须使用单向的Hash算法,并且加盐。推荐使用Bcrypt。前端发送值为 Bcrypt( Raw ) 得到 Val_0,后端保存值为 提取Val_0的salt值为( Val_1 ),Val_1 + (Delimiter) + Hash256( Val_0 )
- 要求1:后端是不能直接接收明文的密码的,所以前端发送的时候就必须要进行加密
- 要求2:后端需要进行加密保存。所以这里保存之前的salt和加密后的值,这里使用了Hash256的方式进行加密,这里也可以再次使用Bcrypt进行加密,但是在Secret设计中需要获取这次Bcrypt中使用的Salt值。
请求签名的设计
这里使用Hmac的思路进行请求签名的方式。Hash( Raw + Salt ),可以看到如果按照这个思路中,前端需要保存Salt,并且后端需要持有相同的Salt,这里因为 原始的明文需要传输所以Salt尽可能不在相同的渠道进行传输,或者不传输。
技术选型:
这里使用JWT组件来做签名组件,Algorithm采用Hash256,需要加入过期时间字段。这个组件在多个平台都有成熟的实现。组件中的 Secret 可以类比成salt,基本原理和Hmac类似。这里为了防止整个请求参数被篡改,那么签名中需要有请求参数的hash值,所以在这个思路中,需要有一个比较合适的通用请求方式,所以最好是Post请求,并且所有的请求参数都放到请求体中,这样会更加便利于这个思路的实现。
signature = JWT(payload={
"digest": hash256(RequestBody),
"otherFields": "otherFieldsValues",
"exp": expireTime
}, key=secretKey, algorithm='HS256')
SecretKey 的设计
现在来看可以看到,SecretKey 的安全性,成了关键,这里使用办法是将用户输入值作为SecretKey相关的生成方式。这里可以适配两个比较普遍的场景,密码登录,验证码登录。
这里主要的思路在登录过程中将用户的输入在前端计算出和注册流程中一样的值,然后将对称的值当做SecretKey使用,这样在登录流程中SecretKey就会这个值就不会出现在传输中并且一边密码的强度又比较高,所以登录过程相对会比较安全。
注册流程中, 由上述流程中可以看出如果可以在可以通过别的渠道获取到和后端一样的对称密钥,那么整个流程会安全很多,这里提供一个方式采用验证的URL的方式,这里需要注意的是密钥需要是用URL hash的方式存放到URL上,用户在注册验证的时候,点开URL,默认情况下浏览器是不会发送Url Hash # 后的部分的,只有客户端可以获取到。
流程参考:
注册流程
- 前端输入用户名/邮箱/手机号和密码(验证码)
- 前端收集到密码后
- 然后再次使用Bcrypt对密码进行加密得到值 Val_0
- 后端如果验证通过,将 Salt( Val_0 ) + (Delimiter) + Hash256( Val_0 ) 进行持久化的存储,返回成功。
- 前端跳回登录,如果不登录的场景,前端使用登录相同的流程进行登录。(这里最好不要由后端自动登录权限)
登录流程
前端输入用户名/邮箱/手机号和密码/验证码(需要加入人机验证,所有公开的写类型操作的接口必须要做人机验证,类似的发送短信、邮箱验证,登录,注册,等等,验证码的场景下验证码的强度不足,可能会被破解,可以采用字母+数字的验证,验证通过后使用当前密钥换取强度较大的密钥,但可能会被跟踪,验证码和密码登录有同等的做法)
前端请求接口得到后端保存的密码的Salt的值
-
前端收集到密码后
- 使用Bcrypt( Raw ) 和后端取得的Salt得到值 Val_0,在进行Val_1 + (Delimiter) + Hash256 ( Val_0 ) 得到 Val_1,这样前端可以计算出和后端存储一样的值,
- 然后 Hash256( Val_1 ) 得到 Val_2
- 然后再次使用Bcrypt对密码使用新的Salt值进行加密得到值 Val_3
- 使用Val_1作为Key对Val_3进行AES加密,前端需要保存Val_1这个值,如果后端验证通过,那么就Val_1 当做 Secret 来使用,
- 后端如果验证通过,把存储中的密码Hash值作为身份的验证的SecretKey,然后把 Val_3 更新原有的密码的Hash值,并返回通过,进行后续的操作。(这里需要注意密钥的有效期,需要在上一个密钥未过期的时候来更换密钥,更换方式使用当前密钥做key然后使用对称加密的方式即可)
如有问题欢迎留言或者私信