Cookie/Session 与 Remix 中处理 Cookie/Session

1,670 阅读9分钟

屏幕截图 2023-04-19 174638.png

什么是 cookie ?

模块定义
理解Cookie 是一小块 数据(服务端发送给浏览器)。浏览器存储 Cookie 用于与服务端之间传输数据。
作用保持会话(登录状态)-个性化设置(主题)-浏览器行为追踪(分析用户行为)
种类会话(浏览器关闭就删除)、持久(登录状态等)、第一方(本网站)/第三方(其他域名)、安全(HTTPS、httpOnly、SmaeSite Cookie)

如何使用 Cookie

注意: Cookie 使用要安全,通常需要 加密

cookie 设置属性

名称类型描述
NameStringCookie 的名称,用于标识 Cookie。
ValueStringCookie 的值,包含该 Cookie 的具体信息。
Expires/Max-AgeDate/NumberCookie 的过期时间,可以通过设置 Expires 或者 Max-Age 属性来指定。如果不设置,那么该 Cookie 将在浏览器关闭时自动删除。
DomainStringCookie 所属的域名。如果不指定,那么该 Cookie 将仅适用于设置它的域名。
PathStringCookie 所在的路径。如果不指定,那么该 Cookie 将仅适用于设置它的路径。
SecureBoolean一个布尔值,表示是否仅在使用 HTTPS 协议时发送 Cookie。如果设置为 true,则该 Cookie 仅适用于 HTTPS 协议。
HttpOnlyBoolean一个布尔值,表示是否允许客户端通过 JavaScript 访问该 Cookie。如果设置为 true,则该 Cookie 仅适用于 HTTP 协议,并且无法通过 JavaScript 访问。

浏览器读取 Cookie 和解析 Cookie

览器提供了一个名为 document.cookie 的 API,可以用于读取和设置 Cookie。

const cookies = document.cookie.split("; ");
cookies.forEach((cookie) => {
  const [name, value] = cookie.split("=");
  console.log(`${name}=${value}`);
});

以 Node.js 为例子使用 Cookie

import http from 'node:http'

const server = http.createServer((req, res) => { {
    // 在响应头中设置 Cookie
    const server = http.createServer((req, res) => { // 服务器响应请求的逻辑代码 });
    // 从请求头中读取 Cookie:
    const cookie = req.headers.cookie;
    // ...
});
server.listen(3000, () => { console.log('Server is running on port 3000'); });

当然如果你使用 express 等框架会提供更加简单的操作 cookie 方式:cookie-parser

cookie 的限制

限制/属性描述格式/可选值
大小限制每个 Cookie 的大小4KB 以内
数量限制每个域名下 Cookie 的数量50 个以内
安全限制Securetrue/false
安全限制HttpOnlytrue/false
有效期限制Expires格式:Wdy, DD-Mon-YYYY HH:MM:SS GMT
有效期限制Max-Age单位:秒
路径限制PathCookie 的路径

cookie 应用场景

应用场景描述
身份验证通过在用户登录时创建 Cookie 来标识用户身份,使用户可以在会话期间保持登录状态。
记住密码创建一个包含用户名和密码的 Cookie,使得用户可以在下一次登录时免输入账号密码。
记录用户偏好在 Cookie 中存储用户的偏好设置,例如语言、主题、字体大小等,以便用户下次访问时恢复相应的设置。
购物车在 Cookie 中存储用户加入购物车的商品信息,以便用户可以在下次访问时恢复购物车状态。
个性化推荐根据用户的历史浏览记录和偏好设置,在 Cookie 中存储相关数据,以便进行个性化推荐。
统计分析在 Cookie 中存储用户行为数据,例如页面访问次数、停留时间等,以便进行统计分析和用户行为分析。

基于 JS 封装 Cookie 类

class MyCookie {
  static set(name, value, options = {}) {
    const { expires, path, domain, secure } = options;
    document.cookie =
      `${name}=${encodeURIComponent(value)}` +
      (expires ? `; expires=${expires.toUTCString()}` : "") +
      (path ? `; path=${path}` : "") +
      (domain ? `; domain=${domain}` : "") +
      (secure ? "; secure" : "");
  }

  static get(name) {
    const cookieArr = document.cookie.split("; ");
    for (let i = 0; i < cookieArr.length; i++) {
      const [cookieName, cookieValue] = cookieArr[i].split("=");
      if (cookieName === name) {
        return decodeURIComponent(cookieValue);
      }
    }
    return null;
  }

  static delete(name, options = {}) {
    const { path, domain } = options;
    document.cookie =
      `${name}=; expires=Thu, 01 Jan 1970 00:00:00 UTC` +
      (path ? `; path=${path}` : "") +
      (domain ? `; domain=${domain}` : "");
  }
}

// 测试用例示例
describe("Cookie", () => {
  // 测试设置 Cookie
  it("应该使用给定的名称、值和选项设置一个 Cookie", () => {
    Cookie.set("test", "123", {
      expires: new Date(Date.now() + 86400000),
      path: "/",
      domain: "example.com",
      secure: true,
    });

    const cookie = document.cookie;
    expect(cookie).toMatch("test=123");
    expect(cookie).toMatch("expires");
    expect(cookie).toMatch("path=/");
    expect(cookie).toMatch("domain=example.com");
    expect(cookie).toMatch("secure");
  });

  // 测试获取 Cookie
  it("应该返回给定名称的 Cookie 的值", () => {
    document.cookie = "test=123";

    const value = Cookie.get("test");
    expect(value).toBe("123");
  });

  // 测试删除 Cookie
  it("应该删除具有给定名称和选项的 Cookie", () => {
    Cookie.delete("test", {
      path: "/",
      domain: "example.com",
    });

    const cookie = document.cookie;
    expect(cookie).not.toMatch("test=123");
  });
});

Remix 中如何处理 Cookie

Remix 中对 Cookie 进行了封装, Remix 提供了 Cookie API

api 和属性

api 导入

import { isCookie, createCookie } from "@remix-run/node";

属性

const cookie = createCookie();
cookie.name; // 设置 Cookie 的名字
cookie.isSinged; // 是否使用了 secrets
cookie.expires; // 过期时间

创建 createCookie

import { createCookie } from "@remix-run/node"; // or cloudflare/deno

export const userPrefs = createCookie("user-prefs", {
  maxAge: 604_800, // one week
});

// 加密
const cookie = createCookie("user-prefs", {
  secrets: ["s3cret1"],
});

userPrefs 具有: parse 解析/serialize 序列化, 配合 Remix loader/action 来使用

import { userPrefs } from "~/cookies";

// loader/action
const cookieHeader = request.headers.get("Cookie");
const cookie = (await userPrefs.parse(cookieHeader)) || {};

// 序列化
redirect("/", {
    headers: {
      "Set-Cookie": await userPrefs.serialize(cookie),
    })

判断是否 isCookie

import { isCookie } from "@remix-run/node";
const cookie = createCookie("user-prefs");
console.log(isCookie(cookie)); // true

Remix 官方使用示例

示例:gdpr-cookie-consent

  • 设置 cookie
import { createCookie } from "@remix-run/node";

export const gdprConsent = createCookie("gdpr-consent", {
  maxAge: 31536000, // One Year
});
  • 处理 cookie
import type { ActionArgs } from "@remix-run/node";
import { json } from "@remix-run/node";

import { gdprConsent } from "~/cookies";

export const action = async ({ request }: ActionArgs) => {
  const formData = await request.formData();
  const cookieHeader = request.headers.get("Cookie");
  const cookie = (await gdprConsent.parse(cookieHeader)) || {};

  if (formData.get("accept-gdpr") === "true") {
    cookie.gdprConsent = true;
  }

  return json(
    { success: true },
    {
      headers: {
        "Set-Cookie": await gdprConsent.serialize(cookie),
      },
    }
  );
};

如果仅仅使用 cookie 完成登录

优点:

  • 不需要频繁输入账号密码,直接通过 cookie 传输

缺点:

  • 不安全,容易被篡改
  • 如果多页面,需要频繁读取 cookie 造成各种风险

基于 Cookie 的缺点,这里我们就需要引出 Session 绘画来弥补 Cookie 在登录等业务上的缺点。

Session

session.png

Session 是一个在 Web 应用中跟踪用户会话状态的机制。当用户第一次访问 Web 应用时,服务器会创建一个唯一的 Session ID,并将其存储在 Cookie 中返回给客户端。

Cookie-Session 作为一种 最简单 的鉴权方式, 广泛被使用。

Session 的主要作用

在服务器端存储用户的状态信息,这些信息可以是任何类型的数据,例如登录信息、购物车内容、用户配置等。

Session 的生命周期

阶段描述
创建阶段当用户第一次访问网站时,服务器会创建一个唯一的 Session ID,并将该 ID 存储在 Cookie 中返回给客户端。
活动阶段在用户会话期间,客户端的每个请求都会带上 Cookie 中存储的 Session ID。服务器使用 Session ID 来获取对应的 Session 数据,以此来保持用户状态和跟踪用户活动。
过期阶段一般情况下,Session 有一个过期时间,如果用户在一段时间内没有活动,则服务器会删除该 Session 数据,以释放资源。

Session 的存储方式

Session 的存储方式说明
Cookie-based将 Session ID 存储在客户端浏览器的 Cookie 中,每次请求时携带 Cookie 进行验证
Server-side将 Session ID 及其对应的数据存储在服务器端,客户端请求时携带 Session ID 进行验证

Session 缺点

  • 有被劫持 Session ID 的可能,对 Session ID 的强壮性加强,也可以通过定期更换和有效的加密方式的等方式处理
  • Session 与设备之间存在一对多,和多对一的关系,所以共享和同步的问题需要正确的处理。

Session 应用场景

Session 在 Web 应用中的应用描述
用户认证和授权Session 可以存储用户登录状态和权限信息,实现用户认证和授权
购物车和在线支付Session 可以存储用户的购物车信息和支付状态,实现购物车和在线支付等功能
分布式应用的集群部署Session 需要实现共享和同步,以保证多个应用实例之间的数据一致性
Session 劫持和防范Session 可能被黑客攻击和劫持,需要采取相应的安全措施
Session 共享和同步分布式应用部署时,需要实现 Session 共享和同步,以保证多个应用实例之间的数据一致性
Session 数据加密和保护为了提高数据的安全性,需要对 Session 数据进行加密和保护

Session 优化

Session 的优化和性能描述
存储和读取优化使用内存数据库如 Redis、Memcached 存储 Session 数据,以减少数据库的读写次数,并对 Session 数据进行序列化和反序列化
过期时间和清理策略根据业务需求和系统性能进行调整,例如根据用户活跃度、Session 数据量和服务器负载等进行设置
缓存和 Session 结合应用使用分布式缓存如 Redis、Memcached 存储 Session 数据和缓存页面数据,以提高系统的响应速度和并发访问量

Remix 中 session

功能方法名描述
创建 session 对象createSession创建一个 session 对象
创建 session 存储库createSessionStorage可以方便地将 session 数据存储到外部数据库,而不是本机内存中
创建内存 session 存储库createMemorySessionStorage将所有的 session 数据保存在服务器内存中,速度较快但不够安全
创建基于文件的 session 存储库createFileSessionStorage可以将 session 数据保存到文件中,相对于内存存储更加稳定和安全,但速度较慢

目前 Remix 也支持了 Cloudflare Workder 和 Amazon Session 存储。

Remix 中 Session 设计与 Cookie 类似,使用方法也类似:

import { isSession, createSession } from "@remix-run/node";

// 定义 session 数据,创建 session 对象
const sessionData = { foo: "bar" };
const session = createSession(sessionData, "remix-session");
console.log(isSession(session));

session.has;
session.set;
session.flash; // `session.flash` 是一种在 Web 应用中实现消息提示的机制。
session.get;
session.unset;

在 Loader 和 Action 中使用 Session

import { commitSession, getSession } from "../sessions";

export async function action({ params, request }: ActionArgs) {
  // 从 getSession 和 Cookie 中获取 session
  const session = await getSession(request.headers.get("Cookie"));
  const deletedProject = await archiveProject(params.projectId);

  // 设置首次读取时将取消设置的会话值
  session.flash(
    "globalMessage",
    `Project ${deletedProject.name} successfully archived`
  );

  return redirect("/dashboard", {
    headers: {
      "Set-Cookie": await commitSession(session), // 提交 session
    },
  });
}

session 示例

小结

本文主要回顾了 cookie/session 的基础,然后使用了 Remix 中 cookie/session 的用法优缺点,是使用 createCookie 函数创建 cookie, 以及属性和方法, 使用各种不的 session 创建方法并创建不同的 session 和其优缺点以及 Remix 官方创建的示例。

微信搜索并关注公踪号 进二开物,更多技术 JS/TS/CSS/Rust 文章...