专栏首页YuanXin[干货]NodeJS实战 - cookie、session与token

[干货]NodeJS实战 - cookie、session与token

前言:无状态的 HTTP

众所周知,HTTP 协议是无状态的。但是随着 web 应用的发展,越来越多的场景需要标识用户身份。例如:单点登陆、购物车等等。

而 cookie、session 与 token,就是为了实现带有状态的“会话控制”。曾经我也傻傻搞不清他们的区别,只知道他们是为了解决 http 协议无状态的技术方案。

cookie 是以 K-V 形式,存储在浏览器中一种数据。它可以在服务端设置,也可以在浏览器端用 js 代码设置。它拥有 maxAge、domain、path 等属性,借助这些属性,可以实现父子域名之间的数据传递。

虽然 cookie 是 K-V 形式存储的,但是在设置 cookie 的值的时候,是直接给定形如key1=value1; key2=value2的字符串。它在服务器/浏览器端均可以设置:

  • 浏览器端:通过 js 代码来设置,例如 document.cookie = "firstName=dongyuanxin; path=/
  • 服务器端:通过给 Http Response Headers 中的Set-Cookie字段赋值,来设置 cookie。客户端接收到Set-Cookie字段后,将其存储在浏览器中。

在服务端,以 koajs 为例,设置 key 为id,value 为xxoo521.com的 cookie。代码如下:

const Koa = require("koa");
const Router = require("koa-router");
const serve = require("koa-static");

const port = 3333;

const app = new Koa();
const router = new Router();

router.get("/api", async (ctx, next) => {
    const cookie = ctx.cookies.get("id");
    if (!cookie) {
        // 设置 id = xxoo521.com
        ctx.cookies.set("id", "xxoo521.com");
    }
    ctx.response.body = "原文地址:xxoo521.com";
});

app.use(serve("."))
    .use(router.routes())
    .listen(port, () => {
        console.log("listen port:", port);
    });

代码调试

在启动上面代码,并且请求/api接口后。在 Chrome Dev Tools 中,能看到服务器返回的 Headers 的信息,如下图:

按照协议,浏览器应该成功保存了 cookies 的值。此时,找到Application => Storage => Cookies => 当前域名,即可验证 cookie 是否设置成功,如下图:

整体流程

以用户购买商品为例,整体流程如下:

  • 监测到浏览器客户端没有标识用户的 cookie,跳转到登陆界面
  • 用户账号密码登陆,后端验证,成功后,在Set-cookie中设置标识用户的 cookie
  • 登陆成功,保存用户标识的 cookie
  • 购买商品,自动携带用户身份的 cookie,后端验证无误后,购买成功

总结

由此可见,单纯的使用 cookie,需要将用户的身份信息保存在客户端,并不安全。除此之外,cookie 还有大小限制,以及只能使用字符串类型作为 value 值。

Session

认识 Session

Session 机制准确来说,也是通过 K-V 数据格式来保存状态。其中:

  • Key:也称 SessionID,保存在客户端浏览器。
  • Value:也称“Session”,保存在服务端。

可以看到,客户端只需要存储 SessionID。具体映射的数据结构放在了服务端,因此跳出了仅仅浏览器 cookie 只可以存储 string 类型的限制。

而客户端存储 SessionID,还是需要借助 cookie 来实现。

整体流程

假设/login接口登陆成功后,服务器可以生成 sessionId 和 session。其中,session 中保存了过期时间,一些冗余信息等。代码如下:

router.get("/login", async (ctx, next) => {
    const { user, pwd } = querystring.parse(ctx.request.search.slice(1));
    // mock数据,模拟一下登陆过程
    if (user === "test" && pwd === "123456") {
        // 生成客户端存储的sessinId
        const sessionId = crypto
            .createHash("md5")
            .update(user + pwd)
            .digest("hex");
        // 生成服务端存储的session
        const session = {
            expire: Date.now() + 1000 * 60 * 60 * 24, // 过期时间
            info: {
                // 保存的信息
                name: user
            }
        };
        sessions.set(sessionId, session);
        ctx.cookies.set("sessionId", sessionId);
        ctx.response.body = "登陆成功";
    } else {
        ctx.response.body = "登陆失败";
        ctx.response.status = 401;
    }
});

然后客户端在 cookies 中携带 sessionId,访问/userInfo接口,获得用户信息。服务端检查 sessionId 合法性,以及是否过期。代码如下:

router.get("/userInfo", async (ctx, next) => {
    const sessionId = ctx.cookies.get("sessionId");
    const session = sessions.get(sessionId);
    // 如果sessionId不存在
    if (!session) {
        ctx.response.body = "无法识别身份";
        ctx.response.status = 401;
        return;
    }
    // session过期
    if (session.expire <= Date.now()) {
        ctx.response.body = "session过期,请重新登陆";
        ctx.response.status = 401;
        return;
    }

    ctx.response.body = session.info;
});

打开 chrome dev tools,我们可以看到,http 请求自动携带了 cookie 中的 sessionId。并且通过后端检验,拿到了用户信息。

session 传输数据少,数据结构灵活:相较于 cookie 来说,session 存储在服务端,客户端仅保留换取 session 的用户凭证。因此传输数据量小,速度快。

session 更安全:检验、生成、验证都是在服务端按照指定规则完成,而 cookie 可能被客户端通过 js 代码篡改。

session 的不足:服务器是有状态的。多台后端服务器无法共享 session。解决方法是,专门准备一台 session 服务器,关于 session 的所有操作都交给它来调用。而服务器之间的调用,可以走内网 ip,走 RPC 调用(不走 http)。

Token

为什么需要 Token?

这也是我刚接触 token 时候的疑惑,因为 session 对比 cookie 来说,解决了很多问题,所以感觉上 session 已经很完美了。

但对于 session 来说,服务器是有状态的。这个事情就很麻烦,尤其是在分布式部署服务的时候,需要共享服务器之间的状态。总不能让用户不停重复登陆吧?虽然专门准备一个服务器用来处理状态是可行的,但是能不能让服务器变成无状态的,还不能像单纯 cookie 那么蹩脚?

token 就解决了这个问题。它将状态保存在客户端,并且借助加密算法进行验证保证安全性。

整体流程

如上图所示,整体流程总结如下:

  • 用户尝试登陆
  • 登陆成功后,后端依靠加密算法,将凭证生成 token,返回给客户端
  • 客户端保存 token,每次发送请求时,携带 token
  • 后端再接收到带有 token 的请求时,验证 token 的有效性

在整个流程中,比较重要的是:生成 token、验证 token 的过程。这里设计一种简单的技术实现

  • 生成:token 的组成为:${user}.${HS256(user, secret)}。其中,secret 是加密需要的密钥,保存在服务端,不能泄漏。HS256 是加密算法,使用 RS256、HS512 也可以。
  • 验证:将请求中携带的 token 按照.分开,得到payloadsig。用服务器密钥对payload进行加密,将加密结果和sig比较,如果相同,那么通过验证。

值得一提的是,这里无需对sig进行解密。

代码实现

借助crypto实现 HS256 算法加密:

/**
 * @param {string} content
 * @param {string} key
 * @return {string}
 */
function HS256(content, key = "jnajdnf9328u4") {
    const hmac = crypto.createHmac("sha256", key);
    hmac.update(content);
    return hmac.digest("hex");
}

用户登陆成功后,将用户名作为payload,生成对应的sig,拼接为 token,返回给客户端:

router.get("/login", async (ctx, next) => {
    const { user, pwd } = querystring.parse(ctx.request.search.slice(1));
    // mock数据,模拟一下登陆过程
    if (user === "test" && pwd === "123456") {
        ctx.response.body = {
            token: `${user}.${HS256(user)}`
        };
    } else {
        ctx.response.body = "登陆失败";
        ctx.response.status = 401;
    }
});

客户端再请求需要用户身份的 api 的时候,应该携带 token,服务端对 token 合法性进行检验:

router.get("/userInfo", async (ctx, next) => {
    const { token } = querystring.parse(ctx.request.search.slice(1));
    if (typeof token !== "string") {
        ctx.response.body = "请携带token";
        ctx.response.status = 401;
        return;
    }

    const [payload, sig] = token.split(".");
    if (!payload || !sig) {
        ctx.response.body = "token格式不合法";
        ctx.response.status = 401;
        return;
    }

    if (HS256(payload) !== sig) {
        ctx.response.body = "签名不合法";
        ctx.response.status = 401;
        return;
    }

    ctx.response.body = "用户信息是巴拉巴拉";
});

请求成功后,服务端返回数据,下图所示:

总结:token 真香

token 的优点多多:

  • 服务器变成无状态了,实现分布式 web 应用授权
  • 可以进行跨域授权,不再局限父子域名
  • token 设计绝对了它本身可以携带更多不敏感数据,例如最常用的 JWT
  • 安全性更高,密钥保存在服务器。若密钥被窃取,可以统一重新下发密钥。

当然,token 增加了服务器压力(毕竟要加密)。但还是真香

本文参与腾讯云自媒体分享计划,欢迎正在阅读的你也加入,一起分享。

我来说两句

0 条评论
登录 后参与评论

相关文章

  • 深入koa源码 - 手动实现玩具版koa

    设计思想和第三方库原理都在前 2 篇详细说明了。这篇主要目的是做一个验证检验,在语法使用 ES6/7 的语法。

    心谭博客
  • 剑指offer - 包含min函数的栈 - JavaScript

    题目描述:定义栈的数据结构,请在该类型中实现一个能够得到栈中所含最小元素的 min 函数(时间复杂度应为 O(1))。

    心谭博客
  • 设计模式 - 抽象工厂模式 - JavaScript

    按照之前的做法,这里我们实现几个实体类:Cat 和 Dog 一组、Male 和 Female 一组。

    心谭博客
  • app与后台交互之间的几种安全认证机制

    1、HTTP简单基本认证方式 这个是早期交互用得比较多的一种方式,主要是使用用户名和密码来交互,由于在每次的交互中,用户名和密码都会暴露给第三方,那么这...

    风间影月
  • 小程序·云开发的HTTP API调用丨实战

    通过应用生成器工具 express-generator 可以快速创建一个应用的骨架。

    腾讯云开发TCB
  • 十几行代码实现分布式session + 秒测登录接口

    最近喊同事吃饭的时候他在测接口,于是我就在他后面等了一会。他测的是一个需要登录的接口,步骤如下

    Java识堂
  • Maven使用详解

    什么是Maven? 如今我们构建一个项目需要用到很多第三方的类库,如写一个使用Spring的Web项目就需要引入大量的jar包。一个项目Jar包的数量之多往往...

    大闲人柴毛毛
  • 【thinkphp】app接口签名+验证签名

    【thinkphp】app接口签名+验证签名 app接口签名+验证签名 比较简单 求各位大牛指教 IndexController.class.php <?php...

    96php.cn
  • 淘宝sign加密算法

    淘宝对于h5的访问采用了和客户端不同的方式,由于在h5的js代码中保存appsercret具有较高的风险,mtop采用了随机分配令牌的方式,为每个访问端分配一个...

    小歪
  • 中国有微信和支付宝, 你为啥还费力不讨好去做区块链? | 人物志

    他是一位连续创业者,曾在当时互联网最大二级市场票务平台悠闲地“打工”,生活无忧无虑无所求。一次偶然的机会,他“勾搭”上了一位澳洲技术男,异地畅聊、跨国面基之后,...

    区块链大本营

扫码关注云+社区

领取腾讯云代金券