多应用+插件架构,代码干净,二开方便,首家独创一键云编译技术,文档视频完善,免费商用码云13.8K 广告
众所周知,HTTP是一种无状态的协议,也就是说:当客户端每次发出请求时,下一次请求无法得知上一次请求的状态(所包含的状态数据)。一旦数据交换完毕,客户端与服务器端的连接就会关闭,再次交换数据需要建立新的连接。那么如何才能把一个用户的状态数据保存或者关联起来?由此产生了cookie和session这两门技术来解决这一问题。 会话(Session)跟踪是Web程序中常用的技术,用来跟踪用户的整个会话。常用的会话跟踪技术是Cookie与Session。Cookie通过在客户端记录信息确定用户身份,Session通过在服务器端记录信息确定用户身份。 * * * * * # cookie 首先产生了 cookie 这门技术来解决这个问题,由于HTTP是一种无状态的协议,服务器单从网络连接上无从知道客户身份。怎么办呢?就给客户端们颁发一个通行证吧,每人一个,无论谁访问都必须携带自己通行证。这样服务器就能从通行证上确认客户身份了。这就是Cookie的工作原理。 cookie是一个实际存在的东西,它是 http 协议的一部分。 ## 处理流程(B代表浏览器,S代表服务器): 1. B首次请求S 2. S将cookie放在响应头中发送给B,具体是response头中的`Set-Cookie` 3. B接收到cookie之后,将cookie进行存储 4. 之后每次B请求S时,将cookie放在请求头中发送给S,具体是request头中的`Cookie` ## 使用场景 1. 非重要的用户信息 2. 浏览记录(例如京东、天猫等都有浏览记录的功能) 3. 猜你喜欢 S通过分析cookie里面存储的用户的浏览记录,来给用户推“猜你喜欢”的商品 ## 参数 其他可选的 cookie 参数会影响将 cookie 发送给服务器端的过程,主要有以下几种: * path:表示 cookie 影响到的路径,匹配该路径才发送这个 cookie。 * expires 和 maxAge:告诉浏览器这个 cookie 什么时候过期,expires 是 UTC 格式时间,maxAge 是 cookie 多久后过期的相对时间。当不设置这两个选项时,会产生 session cookie,session cookie 是 transient 的,当用户关闭浏览器时,就被清除。一般用来保存 session 的 session_id。 * secure:当 secure 值为 true 时,cookie 在 HTTP 中是无效,在 HTTPS 中才有效。 * httpOnly:浏览器不允许脚本操作 document.cookie 去更改 cookie。一般情况下都应该设置这个为 true,这样可以避免被 xss 攻击拿到 cookie。 ## cookie在node.js(express)中的应用 express 在 4.x 版本之后,session管理和cookies等许多模块都不再直接包含在express中,而是需要单独添加相应模块。 ### 下载安装 express4 中操作cookie需要使用 `cookie-parser`模块(https://github.com/expressjs/cookie-parser )。 ``` cnpm i cookie-parser -S ``` ### 使用步骤 1. 加载express模块 2. 创建express实例app 3. 加载cookie-parser模块 4. app使用cookieParser 5. `res.cookie()`设置cookie,`req.cookie `就可以访问浏览器携带的cookie了,其中`req.signedCookies`是访问的是加密模式下的cookie。 ``` let express = require('express') // 首先引入 cookie-parser 这个模块 let cookieParser = require('cookie-parser') let app = express() app.listen(3000) /* 使用 cookieParser 中间件,cookieParser(secret, options) 其中 secret 用来加密 cookie 字符串(下面会提到 signedCookies) options 传入上面介绍的 cookie 可选参数 */ app.use(cookieParser()) app.get('/', (req, res) => { // Cookies that have not been signed console.log('Cookies: ', req.cookies) // Cookies that have been signed console.log('Signed Cookies: ', req.signedCookies) }) ``` # session 除了使用cookie,Web应用程序中还经常使用session来记录客户端状态。session是服务器端使用的一种记录客户端状态的机制,使用上比cookie简单一些,相应的也增加了服务器的存储压力。 ## 什么是session? Session是另一种记录客户状态的机制,不同的是Cookie保存在客户端浏览器中,而Session保存在服务器上。客户端浏览器访问服务器的时候,服务器把客户端信息以某种形式记录在服务器上。这就是Session。客户端浏览器再次访问时只需要从该Session中查找该客户的状态就可以了 ## session的产生背景? cookie 虽然很方便,但是使用 cookie 有一个很大的弊端,cookie 中的所有数据在客户端就可以被修改,数据非常容易被伪造,那么一些重要的数据就不能存放在 cookie 中了,而且如果 cookie 中数据字段太多会影响传输效率。为了解决这些问题,就产生了 session,session 中的数据是保留在服务器端的。 session 的运作通过一个 session_id 来进行。session_id 通常是存放在客户端的 cookie 中,比如在 express 中,默认是 connect.sid 这个字段,当请求到来时,服务端检查 cookie 中保存的 session_id 并通过这个 session_id 与服务器端的 session data 关联起来,进行数据的保存和修改。 ## 存储位置 session 可以存放在 : 1. 内存 2. cookie本身 3. redis 或 memcached 等缓存中 4. 数据库 5. 文件 6. 集群 线上来说,缓存的方案比较常见,存数据库的话,查询效率相比前三者都太低,不推荐。cookie session 有安全性问题,下面会提到。 ## session在node.js(express)中的应用 ### 下载安装 express 中操作 session 要用到`express-session`模块 (https://github.com/expressjs/session ) ``` cnpm i express-session -S ``` 这个模块,主要的方法就是 session(options),其中 options 中包含可选参数,主要有: * name: 设置 cookie 中,保存 session 的字段名称,默认为 connect.sid 。 * store: session 的存储方式,默认存放在内存中,也可以使用 redis,mongodb 等。* express 生态中都有相应模块的支持。 * secret: 通过设置的 secret 字符串,来计算 hash 值并放在 cookie 中,使产生的 signedCookie 防篡改。 * cookie: 设置存放 session id 的 cookie 的相关选项,默认为: `default: { path: '/', httpOnly: true, secure: false, maxAge: null }` * genid: 产生一个新的 session_id 时,所使用的函数, 默认使用 uid2 这个 npm 包。 * rolling: 每个请求都重新设置一个 cookie,默认为 false。 * resave: 即使 session 没有被修改,也保存 session 值,默认为 true。 ## session存储举例 ### 在内存中存储 session express-session 默认使用内存来存 session,对于开发调试来说很方便。 ``` var express = require('express'); // 首先引入 express-session 这个模块 var session = require('express-session'); var app = express(); app.listen(5000); // 按照上面的解释,设置 session 的可选参数 app.use(session({ secret: 'recommand 128 bytes random string', // 建议使用 128 个字符的随机字符串 cookie: { maxAge: 60 * 1000 } })); app.get('/', function (req, res) { // 检查 session 中的 isVisit 字段 // 如果存在则增加一次,否则为 session 设置 isVisit 字段,并初始化为 1。 if(req.session.isVisit) { req.session.isVisit++; res.send('<p>第 ' + req.session.isVisit + '次来此页面</p>'); } else { req.session.isVisit = 1; res.send("欢迎第一次来这里"); console.log(req.session); } }); ``` ### 在 redis 中存储 session session 存放在内存中不方便进程间共享,因此可以使用 redis 等缓存来存储 session。 假设你的机器是 4 核的,你使用了 4 个进程在跑同一个 node web 服务,当用户访问进程1时,他被设置了一些数据当做 session 存在内存中。而下一次访问时,他被负载均衡到了进程2,则此时进程2的内存中没有他的信息,认为他是个新用户。这就会导致用户在我们服务中的状态不一致。 使用 redis 作为缓存,可以使用`connect-redis`模块(https://github.com/tj/connect-redis )来得到 redis 连接实例,然后在 session 中设置存储方式为该实例。 ``` var express = require('express'); var session = require('express-session'); var redisStore = require('connect-redis')(session); var app = express(); app.listen(5000); app.use(session({ // 假如你不想使用 redis 而想要使用 memcached 的话,代码改动也不会超过 5 行。 // 这些 store 都遵循着统一的接口,凡是实现了那些接口的库,都可以作为 session 的 store 使用,比如都需要实现 .get(keyString) 和 .set(keyString, value) 方法。 // 编写自己的 store 也很简单 store: new redisStore(), secret: 'somesecrettoken' })); app.get('/', function (req, res) { if(req.session.isVisit) { req.session.isVisit++; res.send('<p>第 ' + req.session.isVisit + '次来到此页面</p>'); } else { req.session.isVisit = 1; res.send('欢迎第一次来这里'); } }); ``` 我们可以运行 redis-cli 查看结果,如图可以看到 redis 中缓存结果。 ## 各种存储的利弊 上面我们说到,session 的 store 有四个常用选项:1)内存 2)cookie 3)缓存 4)数据库 其中,开发环境存内存就好了。一般的小程序为了省事,如果不涉及状态共享的问题,用内存 session 也没问题。但内存 session 除了省事之外,没有别的好处。 cookie session 我们下面会提到,现在说说利弊。用 cookie session 的话,是不用担心状态共享问题的,因为 session 的 data 不是由服务器来保存,而是保存在用户浏览器端,每次用户访问时,都会主动带上他自己的信息。当然在这里,安全性之类的,只要遵照最佳实践来,也是有保证的。它的弊端是增大了数据量传输,利端是方便。 缓存方式是最常用的方式了,即快,又能共享状态。相比 cookie session 来说,当 session data 比较大的时候,可以节省网络传输。推荐使用。 数据库 session。除非你很熟悉这一块,知道自己要什么,否则还是老老实实用缓存吧。 ### signedCookie 上面都是讲基础,现在讲一些专业点的。 上面有提到 cookie 虽然很方便,但是使用 cookie 有一个很大的弊端,cookie 中的所有数据在客户端就可以被修改,数据非常容易被伪造 其实不是这样的,那只是为了方便理解才那么写。要知道,计算机领域有个名词叫 签名,专业点说,叫 信息摘要算法。 比如我们现在面临着一个菜鸟开发的网站,他用 cookie 来记录登陆的用户凭证。相应的 cookie 长这样:dotcom_user=alsotang,它说明现在的用户是 alsotang 这个用户。如果我在浏览器中装个插件,把它改成 dotcom_user=ricardo,服务器一读取,就会误认为我是 ricardo。然后我就可以进行 ricardo 才能进行的操作了。之前 web 开发不成熟的时候,用这招甚至可以黑个网站下来,把 cookie 改成 dotcom_user=admin 就行了,唉,那是个玩黑客的黄金年代啊。 OK,现在我有一些数据,不想存在 session 中,想存在 cookie 中,怎么保证不被篡改呢?答案很简单,签个名。 假设我的服务器有个秘密字符串,是 this_is_my_secret_and_fuck_you_all,我为用户 cookie 的 dotcom_user 字段设置了个值 alsotang。cookie 本应是 {dotcom_user: 'alsotang'} 这样的。 而如果我们签个名,比如把 dotcom_user 的值跟我的 secret_string 做个 sha1 sha1('this_is_my_secret_and_fuck_you_all' + 'alsotang') === '4850a42e3bc0d39c978770392cbd8dc2923e3d1d' 然后把 cookie 变成这样 ``` { dotcom_user: 'alsotang', 'dotcom_user.sig': '4850a42e3bc0d39c978770392cbd8dc2923e3d1d', } ``` 这样一来,用户就没法伪造信息了。一旦它更改了 cookie 中的信息,则服务器会发现 hash 校验的不一致。 毕竟他不懂我们的 secret_string 是什么,而暴力破解哈希值的成本太高。 ### cookie-session 上面一直提到 session 可以存在 cookie 中,现在来讲讲具体的思路。这里所涉及的专业名词叫做 对称加密。 假设我们想在用户的 cookie 中存 session data,使用一个名为 session_data 的字段。 存 ``` var sessionData = {username: 'alsotang', age: 22, company: 'alibaba', location: 'hangzhou'} ``` 这段信息的话,可以将 sessionData 与我们的 secret_string 一起做个对称加密,存到 cookie 的 session_data 字段中,只要你的 secret_string 足够长,那么攻击者也是无法获取实际 session 内容的。对称加密之后的内容对于攻击者来说相当于一段乱码。 而当用户下次访问时,我们就可以用 secret_string 来解密 sessionData,得到我们需要的 session data。 signedCookies 跟 cookie-session 还是有区别的: - 是前者信息可见不可篡改,后者不可见也不可篡改 - 是前者一般是长期保存,而后者是 session cookie cookie-session 的实现跟 signedCookies 差不多。 不过 cookie-session 我个人建议不要使用,有受到回放攻击的危险。 回放攻击指的是,比如一个用户,它现在有 100 积分,积分存在 session 中,session 保存在 cookie 中。他先复制下现在的这段 cookie,然后去发个帖子,扣掉了 20 积分,于是他就只有 80 积分了。而他现在可以将之前复制下的那段 cookie 再粘贴回去浏览器中,于是服务器在一些场景下会认为他又有了 100 积分。 如果避免这种攻击呢?这就需要引入一个第三方的手段来验证 cookie session,而验证所需的信息,一定不能存在 cookie 中。这么一来,避免了这种攻击后,使用 cookie session 的好处就荡然无存了。如果为了避免攻击而引入了缓存使用的话,那不如把 cookie session 也一起放进缓存中。 ### session cookie 初学者容易犯的一个错误是,忘记了 session_id 在 cookie 中的存储方式是 session cookie。即,当用户一关闭浏览器,浏览器 cookie 中的 session_id 字段就会消失。 常见的场景就是在开发用户登陆状态保持时。 假如用户在之前登陆了你的网站,你在他对应的 session 中存了信息,当他关闭浏览器再次访问时,你还是不懂他是谁。所以我们要在 cookie 中,也保存一份关于用户身份的信息。 比如有这样一个用户 ``` {username: 'alsotang', age: 22, company: 'alibaba', location: 'hangzhou'} ``` 我们可以考虑把这四个字段的信息都存在 session 中,而在 cookie,我们用 signedCookies 来存个 username。 登陆的检验过程伪代码如下: ``` if (req.session.user) { // 获取 user 并进行下一步 next() } else if (req.signedCookies['username']) { // 如果存在则从数据库中获取这个 username 的信息,并保存到 session 中 getuser(function (err, user) { req.session.user = user; next(); }); } else { // 当做为登陆用户处理 next(); } ```