基本解决方案
绝大多数Web应用里面都有用户登录的功能。
由于HTTP(S)协议的特点,服务器没有办法锁定客户端,因为客户端对服务器发送HTTP(S)请求的时候,往往没有携带自身的信息,于是服务器想确认客户端的身份是有心无力的。
于是出现了在HTTP(S)报头中附带Cookie的一种做法,来帮助服务器识别客户端:
这就是Web应用处理用户登录的解决方案。
Cookie是储存在用户的浏览器中的,通常还会带有一个域名:这个Cookie是哪个网站要求设置的。
比如百度设定Cookie就有存储在www.baidu.com这个域名中,当你访问这个域名的时候,浏览器就会自动在HTTP(S)的请求报头中附上对应的Cookie,避免了不同网站之间Cookie的命名空间冲突。
安全与性能
现在问题来了,怎么在Cookie中存储一些信息比较好呢?
不妨先给出几个分析:
- Cookie 不宜过大。
由于Cookie会附到每一次HTTP(S)请求中,若Cookie过多,会明显增重网络I/O的负担。 - Cookie 不宜可读
由于Cookie在客户端本地可以很方便地被JavaScript修改,若Cookie可读,那么也很容易被恶意用户篡改(伪造) - Cookie 不宜与用户信息有关
如果Cookie的生成与用户的信息有关,即便Cookie加密到不可读,一旦算法被识破,就有一定几率被反推伪造,或者泄漏个人信息,有隐患。
由此我们可以有这样一个解决方案:
- 当用户访问时,随机生成一个足够强的Hash值作为Cookie,跟用户信息无关、且不可读、不易伪造。
- 在后端服务器维护一个以上述Hash值作为索引的集合。
- 在上述集合中缓存用户的个人信息。
- 当用户访问需要验证身份的资源时,服务器从集合中取出对应的Cookie,并检查缓存中的个人信息来确认身份。
- 当用户修改了个人信息时,更新缓存。
这个Hash值就相当于一个口令,通常我们称它为Session ID(会话ID)。意思就是维护一个客户端与服务器之间的会话,用一个足够离散的Hash值来防止撞表,通常来说,一个站点同时有效的会话数不超过其用户数的20%,而若Hash值的字符集是64,长度为40,Hash值有 6440=2240=1.8∗1074 种可能,撞表成功的期望代价很高,可以认为是安全的。
万一这个人运气很好,撞表成功了,这意味着他能伪装成某个用户了,这跟暴力破解密码的行为是一致的,但这个SessionID是临时性的,随时可能失效,至少不会造成密码泄露(当然你不能在你的数据库中明文存储用户的密码或者提供API查询密码),当用户选择登出或者Session过期的时候,这个SessionID就会失效,撞表的结果就失去了意义。
关于中间人攻击,一般的做法是加密Cookie,最好使用HTTPS整个加密,这个不属于本文讨论范畴。
另外,这个Session ID确实足够短又足够好用,既不会给网络I/O造成什么严重的负担,也足够服务器来识别用户的身份。
困难的地方在于,设计一个安全的Hash算法是不容易的,没有足够好的数学功底就不要自己写Hash了。直接用现成的算法即可。除此之外,管理Session集合的内存也是不容易的,要正确地增删改Session对象,及时地释放空间……这其实是一个数据结构问题,考虑到查询性能等种种原因,也用别人已经开发好的比较好。
成熟的解决方案
Java Servlet 有 JSessionID 的设置,PHP也有PHPSessionID,NodeJS比较开放,Express有一个插件叫做express-session
可以解决这个问题。
网上学 Session 的资料很多,在此不一一列举,只是想说明为什么要这样实现登录功能。怎么做是安全高效的以及为什么。