状态存储

252 阅读5分钟

状态存储

cookie

  • http是无状态的,服务器不知道这次请求的人是谁,所以就使用cookie来描述信息(发送请求的时候自动带上)
  • cookie前端与服务器都可以设置
  • 大约最大为4k,不精准
  • 默认不支持跨域
  • 因为它可以篡改,所以我们可以搞一个签名,每次设置的时候,给这个值也设置一个签名,获取的时候,拿签名比对,相同就是没篡改
  • 参数信息:juejin.cn/post/707976…
  • domain默认是当前域名
    • 可以自定义,比如直接设置为lbs.baidu.com,那么a.baidu.com就用不了。
    • 设置成.baidu.com,那么两个域名都可以用
    • 而且设置为.baidu.com和设置为lbs.baidu.com的两组数据并不会被合并,而是分别存储,取值的时候我们区分下就行了
  • path一般不设置,限制太死,默认是/
    • 作用:允许哪个pathname获取
    • /为所有,毕竟都已/开头,而且哪怕不写路径,请求打到服务端也是/
  • 过期时间一般用max-age相对时间,而不用expiress
    • 过期时间不设置的话,默认是session(会话存储),但关闭页卡不会马上丢失,在关闭浏览器的时候才会自动清除

示例

node设置cookie

    // 设置单个
    res.setHeader("Set-Cookie", "name=cui;");
    // 设置多个,以数组形式
    res.setHeader("Set-Cookie", ["name=cui;Domain=.baidu.com;Path=/login", "age=123;Httponly=true"]);

增加签名机制

  • 先将setCookie封装成一个方法,通过参数去控制,比较便捷
  • 用户本地能修改,所以安全性比较低,而我们又无法去控制用户行为,所以搞一个标识来证明他改没改,以此来验证标识的真实性
    • 签名一样也是放在cookie里
    • 既然是为了防止篡改,那么我们就需要进行验证,而且验证的这个东西还不能让用户看懂,那么使用加密算法
    • md5是摘要算法,而且相对普及较大。不太合适
    • sha256或sha1,也叫做加盐算法
      • 这个盐指的是我们加密解密都通过一个密钥,加密的时候把内容+密钥进行加密,解密的时候也把密钥补进去,单有内容解不出来的,安全性高一点
      • 我只需要保证两件事情:密钥的私密性、校验时比对解密内容与用户传递内容是否一致
      • 使用第三方模块:crypto
      • koa用的是sha1,express用的sha256
设置
const Koa = require("koa");

//第三方包
const Router = require("koa-router");
const crypto = require("crypto");

const app = new Koa();
const router = new Router();
app.keys = ["cc"];

// 加密
const sign = (value, secret) => {
  // crypto.createHmac("sha1", secret) 指定方式和密钥
  // update(value) 放入内容
  // digest("base64url") 输出格式
  if (Array.isArray(secret)) {
    // 如果密钥是array,那么进行合并
    secret = secret.join("");
  }
  let content = crypto
    .createHmac("sha1", secret)
    .update(value)
    .digest("base64url");
  return content;
};
app.use((ctx, next) => {
  let arr = [];
  ctx.res.setCookie = function (key, value, options = {}) {
    let optionsArr = [];
    const { domain, httponly, path, maxAge } = options;
    if (domain) {
      optionsArr.push(`domain=${domain}`);
    }
    if (httponly) {
      optionsArr.push(`httponly=${httponly}`);
    }
    if (path) {
      optionsArr.push(`path=${path}`);
    }
    if (maxAge != undefined) {
      optionsArr.push(`maxAge=${maxAge}`);
    }
    // 如果要加密的话
    if (options.signed) {
      // 算出签名,加个cookie,比如name就加一条name.sig
      const v = sign(`${key}=${value}`, app.keys);
      arr.push(`${key}.sig=${v}`);
    }
    // 将内容放到数组里
    arr.push(`${key}=${value};${optionsArr.join(";")}`);
    // 最后统一设置
    ctx.res.setHeader("Set-Cookie", arr);
  };
  return next();
});
app.use(router.routes());
router.get("/login", async (ctx, next) => {
  ctx.res.setCookie("age", "123", { httponly: true });
  ctx.cookies.set("name", "cui", { signed: true });
  ctx.body = "ok";
});

router.get("/read", (ctx, next) => {
  ctx.body = ctx.req.headers["cookie"] || "null";
});

app.listen(3000, () => {
  console.log("http://127.0.0.1:3000");
});

获取
  • 设置了加密,那么获取的时候我们肯定是希望进行校验的,否则没啥意义
ctx.req.getCookie = function (key, options = {}) {
const cookie = ctx.req.headers["cookie"];
const cookieObj = querystring.parse(cookie, "; ", "=");
if (options.signed) {
    // 传这个代表校验
    if (
    cookieObj[key + ".sig"] === sign(`${key}=${cookieObj[key]}`, app.keys)
    ) {
    // 如果获取的和加密结果一样,代表没问题
    return cookieObj[key];
    } else {
    // 不一样肯定是被篡改了
    return "被篡改了";
    }
}
return cookieObj[key] || "null";
}; 

// 调用
ctx.req.getCookie("name", { signed: true });

koa中使用

  • koa内部已经做了这一操作,封装在ctx.cookies上
  • 设置的话调用ctx.cookies.set的时候传一个参数即可
// 设置 
ctx.res.setCookie("name", "cui", { domain: ".baidu.com", signed: true });

// 获取,它匹配不到并不会给默认值
ctx.cookies.get("name", { signed: true }) || "null";

session

  • 存储在服务器,是用来存储敏感信息的
  • 因为cookie用户是可以篡改的,不太安全,所以我们可以给客户一个唯一标识,存在cookie里,然后请求过来根据cookie中的唯一标识拿到敏感信息
  • 缺陷:服务器重启数据丢失,需要存储session对象
  • 可以想成是一个{},只不过是存在服务器,里面要存啥你自己定义,但是记得用户来拿数据的时候,带个key(标识)就行了,保证标识的唯一性

示例

  • 将用户访问页面的次数记录下来
  • 需要保证令牌的唯一性,所以使用第三方库uuid
const uuid = require("uuid");

// cookie名
const sid = "connect.sid";
// 这个就是session
const session = {};

router.get("/visit", async (ctx) => {
  // 如果用户有这个cookie,代表访问过了,那肯定会记录的
  // 如果没有,代表是第一次访问,那么这次记录下
  let v = ctx.cookies.get(sid);
  if (!v) {
    let value = uuid.v4();
    // 在session记录下,第一次来
    session[value] = { visit: 1 };
    // 给用户发卡,要保证卡号唯一
    ctx.cookies.set(sid, value);
    ctx.body = `第一次访问`;
  } else {
    if (session[v]) {
      session[v].visit += 1;
      ctx.body = `第${session[v].visit}次访问`;
    } else {
      ctx.body = "您的卡号已失效";
    }
  }
});

sessionStorage

  • 存在浏览器上
  • 关闭浏览器就会丢失
  • 同一个页面,分成两个页卡,数据也不互通,所以称为:浏览器会话存储
  • 刷新页面不会丢失

localStorage

  • 存在浏览器上
  • 不手动清除,一直都在
  • 滥用会导致数据混乱,在a页面存了以后,在b页面再存,变量名相同话会覆盖a的。那么a刷新后拿的就是b存的结果
  • 跨域页面拿不到

jwt(json web token)

  • 不存标识了,但还是要给客户端发放一个唯一标识
  • 请求来的时候,通过固定的算法来反解析标识,来判断数据的真实性
  • 格式:
    • 以.分割为3部分
  • 好处:
    • 服务端不需要存储我给谁发了令牌,只要你拿标识来,且解析成功就行
    • 从哪里传(header、cookie、query....)都可以,只要协商好就行
      • 这里其实也顺带解决了cookie无法跨域的问题

示例

  • 这个标识前端存在哪里都可以
    • 比如说服务器直接写在setCookie里
    • 前端手动放在Storage里
  • 格式:
    • 以.分成三段
    • 第一段是固定内容base64后的结果
    • 第二段是payload处理后的结果
    • 第三段是密钥处理结果
    • 不能用摘要算法,我还要用密钥反解析内容
const crypto = require("crypto");

const jwt = {
  // 加密,返回的还是base64
  sign(value, secret) {
    return crypto
      .createHmac("sha256", secret)
      .update(value)
      .digest("base64url");
  },
  // 将内容处理成base64
  toBase64(value) {
    return Buffer.from(JSON.stringify(value)).toString("base64url");
  },
  // 生成令牌
  encode(payload, secret) {
    // 第一段是固定的,类型是jwt,使用算法是hs256
    const part1 = this.toBase64({ typ: "JWT", alg: "HS256" });
    const part2 = this.toBase64(payload);
    const part3 = this.sign(`${part1}.${part2}`, secret);
    return `${part1}.${part2}.${part3}`;
  },
  // 解密,解不出来抛出异常
  decode(token, secret) {
    const [part1, part2, part3] = token.split(".");
    const newSin = this.sign(`${part1}.${part2}`, secret);
    // 拿1和2 重新加密一下,判断新旧是否相等
    // 相等肯定是ok的
    if (newSin === part3) {
      // 解密
      return JSON.parse(Buffer.from(part2, "base64url").toString());
    } else {
      throw new Error("被篡改了,抛异常");
    }
  },
};


// 调用
router.get("/login", async (ctx, next) => {
  // 设置令牌
  const v = jwt.encode({ uid: 1 }, app.key);
  // eyJ0eXAiOiJKV1QiLCJhbGciOiJIUzI1NiJ9.eyJ1aWQiOjF9.Q-hWPSHotMHcTfMSfF-nh-ysYWU43Gzq5R0XoIVu_kU
  ctx.cookies.set("id", v); // 我们把它放在cookie上
  ctx.body = "ok";
});

router.get("/validate", async (ctx, next) => {
  try {
    let payload = jwt.decode(ctx.cookies.get("id"), app.key);
    ctx.body = payload;
  } catch (err) {
    ctx.body = "篡改了";
  }
});

第三方库

  • jwt-simple
  • 用法跟上边一样
const jwt = require("jwt-simple");
  • payload里面的内容自己定义,比如可以做:权限、uid、过期时间...
  • 需要注意的是内容越大,令牌越长