前言



由于浏览器无状态的特性,cookie 技术应运而生,cookie 是一个会话级的存储,大小 4KB 左右,用于浏览器将服务器设置的信息重新带给服务器进行验证,不支持跨域,在浏览器清空缓存或超过有效期后失效,不能存放敏感信息,session 是专门用于存储最初设置给浏览器 cookie 数据的地方,我们本篇就来讨论一下 cookiesession 在 NodeJS 中的使用方式。




cookie 的基本使用

1、NodeJS 原生操作 cookie

下面是 cookie 在 Node 原生中的读取和写入方法。



原生中操作 cookie



1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
复制代码
const http = require("http");

// 创建服务
http.createServer((req, res) => {
    if (req.url === "/read") {
        // 读取 cookie
        console.log(req.headers.cookie);
        res.end(req.headers.cookie);
    } else if (req.url === "/write") {
        // 设置 cookie
        res.setHeader("Set-Cookie", [
            "name=panda; domain=panda.com; path=/write; httpOnly=true",
            `age=28; Expires=${new Date(Date.now() + 1000 * 10).toGMTString()}`,
            `address=${encodeURIComponent("回龙观")}; max-age=10`
        ]);
        res.end("Write ok");
    } else {
        res.end("Not Found");
    }
}).listen(3000);
复制代码



上面代码创建了一个 http 服务器,可以通过读取 cookie 请求头的值来获取浏览器发来的 cookie,服务器可以通过给浏览器设置响应头 Set-Cookie 实现对浏览器 cookie 的设置,多个 cookie 参数为数组,在数组内可以规定每一条 cookie 的规则,中间使用一个分号和一个空格隔开。

  • domain 用来设置允许访问 cookie 的域;
  • path 用来设置允许访问 cookie 的路径;
  • httpOnly 用来设置是否允许浏览器中修改 cookie,如果通过浏览器修改设置过 httpOnly=truecookie,则会增加一条同名 cookie,原来的 cookie 不会被修改;
  • Expires 用来设置过期时间,绝对时间,值为一个 GMTUTC 格式的时间;
  • max-age 同样用来设置过期时间,相对时间,值为一个正整数,单位 s



cookie 默认不支持存储中文,如果存储中文需先使用 encodeURIComponent 方法进行转译,将转译后的结果存入 cookie,在浏览器获取 cookie 需使用 decodeURIComponent 方法转回中文。




2、Koa 中操作 cookie

Koa 是当下流行的 NodeJS 框架,是对原生 Node 的一个轻量的封装,但是内部实现了快捷操作 cookie 的方法,下面是原生中对 cookie 的操作在 Koa 中的写法。



Koa 中操作 cookie



1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
复制代码
const Koa = require("koa");
const Router = require("koa-router");

// 创建服务和路由
const app = new Koa();
const router = new Router();

// 签名需要设置 key
app.keys = ["shen"];

router.get("/read", (ctx, next) => {
    // 获取 cookie
    let name = ctx.cookies.get(name) || "No name";
    let name = ctx.cookies.get(age) || "No age";
    ctx.body = `${name}-${age}`;
});

router.get("/write", (ctx, next) => {
    // 设置 cookie
    ctx.cookies.set("name", "panda", { domain: "panda.com" });
    ctx.cookies.set("age", 28, { maxAge: 10 * 1000, signed: true });
});

// 使用路由
app.use(router.routes());
app.listen(3000);
复制代码



Koa 中将获取和设置 cookie 的方法都挂在了 ctx 上下文对象的 cookies 属性上,分别为 getset

cookies.get 的参数为获取 cookie 的键名,返回值为键对应的值,cookies.set 的第一个参数同样为 cookie 的键名,第二个参数为键对应的值,第三个参数为一个对象,用来配置该条 cookie 的规则,如 domainpath 和过期时间等,这里 maxAge 值为毫秒数。




注意:Koa 中设置的 cookie 默认不允许浏览器端通过 document.cookie 获取,但是服务器也可以被欺骗,比如使用 postman 发送一个带 Cookie 请求头的请求,服务器可以通过设置签名来预防,即添加 signed 选项并将值设置为 true




3、Koa 操作 cookie 方法的原理

cookies 对象都是挂在 ctx 上来实现的,使用过 Koa 都知道如果要操作 ctx 就会用到中间件的思想,我们这就看看这两个方法使用原生封装的过程。



Koa 中 ctx.cookies 对象 get 和 set 方法的原理



1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
复制代码
const Koa = require("koa");
const querystring = require("querystring");

const app = new Koa();

app.use(async (ctx, next) => {
    // 获取 cookie
    const get = key => {
        let cookies = ctx.get("cookie") || "";
        return querystring.parse(result, "; ")[key];
    };

    // 设置 cookie,存储所有的 cookie,等于 setHeader 中的第二个参数
    let cookies = [];
    const set = (key, val, options = {}) => {
        // 用于构造单条 cookie 和权限等设置的数组,默认存放这条 cookie 的键和值
        let single = [`${key}=${encodeURIComponent(val)}`];

        // 下面是配置
        if (options.domain) {
            arr.push(`domain=${options.domain}`);
        }

        if (options.maxAge) {
            arr.push(`Max-Age=${options.maxAge}`);
        }

        if (options.path) {
            arr.push(`path=${options.path}`);
        }

        if (options.httpOnly) {
            arr.push(`HttpOnly=true`);
        }

        // 将配置组合到 single 中后转为字符串存入 cookies
        cookies.push(single.join("; "));

        // 设置给浏览器
        ctx.set("Set-Cookie", cookies);
    }

    // 将获取和设置 cookie 的方法挂在 cookies 对象上
    ctx.cookies = { get, set };

    await next();
});
复制代码



get 方法内部获取 cookie 请求头的值并根据传入的 key 获取值,set 方法内,将传入的键值和选项拼接成符合 cookie 的字符串,通过 Set-Cookie 响应头设置给浏览器。




session 的基本使用

1、NodeJS 原生使用 session

正常 session 是存放在数据库中的,我们这里为了方便就用一个名为 session 的对象来代替。



原生中使用 session



1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
复制代码
const http = require("http");
const uuid = require('uuid/v1'); // 生成随字符串
const querystring = require("querystring");

// 存放 session
const session = {};

// 创建服务
http.createServer((req, res) => {
    if (req.url === "/user") {
        // 取出 cookie 存储的用户 ID
        let userId = querystring.parse(req.headers["cookie"], "; ")["study"];

        if (userId) {
            if (session[userId].studyCount === 0) res.end("您的学习次数已用完");
            session[userId].studyCount--;
        } else {
            // 生成 userId
            userId = uuid();

            // 将用户信息存入 session
            session[userId] = { studyCount: 30 };

            // 设置 cookie
            req.setHeader("Set-Cookie", [`study=${userId}`]);
        }

        // 响应信息
        res.end(`
            您的用户 ID 为 ${userId},
            剩余学习次数为:${session[userId].studyCount}
        `);
    } else {
        res.end("Not Found");
    }
}).listen(3000);
复制代码



上面写的案例是一个网校的场景,一个新用户默认有 30 次学习机会,以后每次访问服务器学习次数减 1,如果 studyCount 值为 0,则提示学习次数用完,否则提示当前用户的 ID 和剩余学习次数,session 中存储的是每一个用户 ID 对应的剩余学习次数,这样就不会轻易的被修改学习剩余次数,因为服务器只认用户 ID,再通过 ID 去更改对应的剩余次数(当然忽略了别人冒充这个 ID 的情况,只能减,不能加),这样就不会因为篡改 cookie 而篡改用户存在 session 中的数据,除非连整个数据库都拖走。

2、Koa 中使用 session

我们接下来使用 Koa 实现和上面一摸一样的场景,在 Koa 的社区中提供了专门操作 session 的中间件 koa-session,使用前需安装。



Koa 中使用 session



1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
复制代码
const Koa = require("koa");
const Router = require("koa-router");
const session = requier("koa-session");
const uuid = require("uuid/v1");

// 创建服务和路由
const app = new Koa();
const router = new Router();

// cookie 的签名
app.keys = ["panda"];

// 使用 koa-session 中间件
app.use(session({
    key: "shen",
    maxAge: 10 * 1000
}, app));

router.get("/user", (ctx, next) => {
    // 取出 cookie 存储的用户 ID
    let userId = ctx.cookie("study");

    if (ctx.session.userId) {
        if (ctx.session[userId].studyCount === 0) res.end("您的学习次数已用完");
        ctx.session[userId].studyCount--;
    } else {
        // 生成 userId
        userId = uuid();

        // 将用户信息存入 session
        ctx.session[userId] = { studyCount: 30 };

        // 设置 cookie
        ctx.cookies.set("study", userId);
    }

    // 响应信息
    ctx.body = `
        您的用户 ID 为 ${userId},
        剩余学习次数为:${session[userId].studyCount}
    `;
});

// 使用路由
app.use(router.routes());
app.listen(3000);
复制代码



使用 Koakoa-session 以后,不再需要我们创建 session 对象进行存储,并且 cookie-session 中间件帮我们封装了 API 可以直接操作 mongoMySQL 数据库,上面代码中与用原生相比还增加了 cookiesession 的签名和过期时间,比原生写起来要方便很多。




总结



本篇内容更偏向于 cookiesession 在 NodeJS 中的使用,没有过多的叙述理论性的内容,cookiesession 是相互依存的,也就是说共同使用的,现在已经有 JWT 的方案来替代,因为相比较下有很多优点,但某些项目和特殊场景还在使用 cookiesession,所以还是写了这一篇,如果对 JWT 感兴趣可以看 通过一个案例理解 JWT。