状态存储
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) => {
if (Array.isArray(secret)) {
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) {
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");
const sid = "connect.sid";
const session = {};
router.get("/visit", async (ctx) => {
let v = ctx.cookies.get(sid);
if (!v) {
let value = uuid.v4();
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)
- 不存标识了,但还是要给客户端发放一个唯一标识
- 请求来的时候,通过固定的算法来反解析标识,来判断数据的真实性
- 格式:
- 好处:
- 服务端不需要存储我给谁发了令牌,只要你拿标识来,且解析成功就行
- 从哪里传(header、cookie、query....)都可以,只要协商好就行
示例
- 这个标识前端存在哪里都可以
- 比如说服务器直接写在setCookie里
- 前端手动放在Storage里
- 格式:
- 以.分成三段
- 第一段是固定内容base64后的结果
- 第二段是payload处理后的结果
- 第三段是密钥处理结果
- 不能用摘要算法,我还要用密钥反解析内容
const crypto = require("crypto");
const jwt = {
sign(value, secret) {
return crypto
.createHmac("sha256", secret)
.update(value)
.digest("base64url");
},
toBase64(value) {
return Buffer.from(JSON.stringify(value)).toString("base64url");
},
encode(payload, secret) {
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);
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);
ctx.cookies.set("id", v);
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 = "篡改了";
}
});
第三方库
const jwt = require("jwt-simple");
- payload里面的内容自己定义,比如可以做:权限、uid、过期时间...
- 需要注意的是内容越大,令牌越长