用户登录 之 进阶

明文密码,是错误的!

之前实现用户登录,密码都是明文。现实中,不允许出现这种错误,涉及到密码,一般都要转码!
最简单的转码,可以将密码转为 MD5 值 (什么是 MD5 可以查搜索引擎);但也有潜在的问题,比如说数据库被别人拿到了,虽然不能直接提取到账户的明文密码,但是可以先构造几亿个非复杂密码(比如说简单的数字、字母)(这个也叫彩虹表),提取其 MD5,然后反向匹配,就可以很大概率获得账户的明文密码了。
为了防范彩虹表的硬破解,一般实际代码过程中,会有个加盐的逻辑,别被这个术语吓到,就是混淆的意思。比如说我们简单地在每次取 MD5 值时,原本是 password 直接取 MD5 的,可以在后面加上一个特殊的字符串 (比如4776b1e273这样固定的随机值)。

我们在介绍『用户登录』的时候,也说了一个思路,将 username + password 合起来,当做一个 钥匙。下面的 Demo,我们将 username + password 合起来取 MD5 值后,作为账户的 ID。如此一来,没有引入数据库,也能在 Demo 中快速匹配客户端提交的登录信息。

如何获得 MD5 值?参考如下的 Python 代码:

>>> import hashlib
>>> hashlib.md5('hepo-123456').hexdigest()
'f18dd8bb3a1f2a2205d66dcc3fc03921'
>>> hashlib.md5('hepochen-12345678').hexdigest()
'30f427e0c9f43c451e1b58ae7280590e'

我们另外构造了一个 users_db.json 这个文件,数据如下:

{
    "f18dd8bb3a1f2a2205d66dcc3fc03921":{
        "username": "hepo",  "password": "123456", "from": "Earth"
    },
    "30f427e0c9f43c451e1b58ae7280590e":{
        "username": "hepochen",  "password": "12345678", "from": "China"
    }
}

剩下具体的,直接在 FirstWeb.app 内参考 real.jade 这个文件,对应可访问 Demo 的 URL 为 http://127.0.0.1:9765/real

Cookie 与 Session

在写后端的时候,有一个基本的准则: 不要相信用户的输入
比如说 Cookie,它在客户端(浏览器),用户是可以自行修改其值的,如果重要的数据交互是来自于 Cookie 的,则务必要进行校验。

我们在前面,也介绍过 Cookie 与 Session,以及两者间不算有大的差别,常见的使用场景,是服务端上存 session 的具体内容,而客户端(浏览器)上存一个 session_id 到 Cookie 中。但这种使用 Session 的方式,并不在我们的介绍范围内,毕竟加密后的 Session 整个存在 Cookie 也是一个非常不错 (甚至更好) 的解决方案。
基本的思路很简单:

  1. 使用 itsdangerous 对数据进行加密,并通过 Cookie 的方式写入浏览器;
  2. 需要的时候,服务器端再对这个来自 Cookie 的 session 进行解密。

需要注意的: session 存储的内容宜小不宜多,因为存在 Cookie 内了,意味着每次页面访问,这个内容会混在请求的头部;理论上,过大的 Cookie 其实会降低页面的响应速度(就是感觉网速受影响了);另外一方面,多数时候是可以忽略不计,不要被『理论上』的东西吓到,『过大』是多大?只要不滥用,Cookie 才那么一点大,不值一提。

在『用户登录』的处理逻辑中,相关 Cookie & Session 保存登录状态的逻辑,请参考 FirstWeb.app 中 real.jade 内的源码。
Demo 为了方便期间,对 Cookie 进行加密校验的,只是呈现了相应技术的使用逻辑。此扩展开来的知识,已经离前端很远了,后续在全端系列下的其它课程中,会有更多的说明。

小的思考:real.jade 中,我们会发现已经登录、退出登录的时候,或者需要手工刷新一次 (没有自动刷新,这样可以看到交互的数据呈现),或者使用了 Javascript 脚本进行自动刷新。可为什么要刷新页面呢?因为 FirstWeb 的 Web 框架,在一个 Jade 文件内同时揉和了前端与后端,这里就产生了一个问题: 客户端拿回 response 后新的 cookie 才会有效,要删除 cookie 也同样需要客户端拿回 response;所以,同一时间内 request.cookiesresponse.set_cookieresponse.delete_cookie 其实并不会同步。有点费解?虽然实际场景不会遇到这种情况,但我想以后我们总是会遇到类似 费解 的现象,其实都是有据可循的:request 是当下浏览器旧的 cookies,resposne 则是尚未回到浏览器未生效的新的 cookies,它们处于不同的时间段内,也处于不同的变量空间之内。

更安全的 session

如果一个 Cookie 被别人获取了,那么就很容易登录你的账户了,因为程序上实现 登录 行为的最终结果,不过就是为了获得一个合法的 Cookie。
假设记录用户登录信息的 Cookie 泄露了,怎么办?如果是安全系数非常高的产品中,必然会加入其它参数的校验,比如浏览器的头部信息、当前访问 IP 对应的城市,如果跟当前的 Cookie/Session 无法匹配,则认为 已登录 的状态无效。
如果安全系数要求并高,为了考虑提供更友好的体验,一般可以将这个操作交给用户自己,这个操作的名称一般叫 登出所有设备,当然,一般我们也需要记录账户最近登录的基本信息。
登出所有设备,这个怎么实现?也很简单呀,在 account 这个数据对象中(一般是存在数据库中),增加一个 cookie_token,每次写入/读取 Cookie 的时候,必定使用 cookie_token 进行进一步的校验,如果不匹配,则校验失败;这个逻辑跟本文开头提到的 加盐 很像?那么,只要更新这个 cookie_token,之前旧的 Cookie 就全部会校验失败,自然,所有登录过的设备,都将自动退出登录。

当然,解决方案,从来不是唯一的。
不唯一,只是提醒我们不要被一些常规束缚了。但另一方面,被常规束缚也算是一种惯性,它本身是高效的表现,是有积极意义的。
所以,最终需要,自己平衡、掌控。