9. cookie/session/jwt

154 阅读5分钟

cookie/session/sessionStorage/localStorage/indexDB

  • cookie

    • tcp三次握手之后就可以不停的去发请求,每次请求都是基于http协议的。http协议的特性是“无状态”,因为这个特性,服务器并不知道用户的状态(是不是第一次访问)。可以通过浏览器添加cookie,服务端也可以设置cookie。设置完cookie之后 每次请求都会携带上cookie,所以可以用cookie来识别用户
    • cookie设置上之后 “每次请求”都会携带,会浪费流量(可能某个请求根本不需要校验信息,还是会把cookie携带上)
    • cookie是在http header中的,所以数据不宜过大,如果过大可能会造成页面白屏
    • cookie默认不能跨域 两个完全不同的域名 父子域(可以设置子域能拿到父域中的数据)
    • 可以用cookie来做用户识别 cookie存在前端(存在安全问题,重要信息不能放在cookie里)
  • session

    • 信息存在服务器里 默认浏览器是拿不到的
    • session存放数据 原则上没有上限而且安全
    • 是基于cookie的
    • session默认都是存在内存中,如果服务器down了,session就丢失了=>存到数据库 数据库数据也可能丢失
  • jwt

    • 服务器根据用户提供的信息生成一个令牌,这个令牌是独一无二的。每次带上令牌和用户信息,用用户信息再次生成令牌和用户信息进行对比
    • 不能存储隐私
    • token
  • sessionStorage 浏览器关闭就丢失 (不能跨域)

  • localStorage 浏览器关闭也不会丢失

  • indexDB 浏览器的数据库

  • 静态资源如何优化

    • 压缩 gzip
    • 缓存
    • localStorage存储js文件

cookie

  • cookie的签名只是为了防止用户篡改数据,如果用户篡改过了,就丢弃掉。并不是为了安全
const Koa = require("koa");
const Router = require("@koa/router");
const querystring = require("querystring");
// 不能用md5 是公开的算法 会知道
// sha256(express)  sha1(koa) 比md5多了个盐值 不知道密钥 基本撞库撞不到 更安全
const crypto = require("crypto"); 
const app = new Koa();

const secret = "mysecret"; // 密钥
const toBase64URL = (str) => {
    return str.replace(/\=/g, '').replace(/\+/g, '-').replace(/\//,'_')
}

// 自实现ctx.cookies.set同样的功能
app.use(async (ctx, next) => {
  const arr = [];
  // 拓展ctx.my  相当于原有的ctx.cookies
  ctx.my = {
    set(key, value, options = {}) {
      let optsArr = [];
      if(options.domain) {
        optsArr.push(`domain=${options.domain}`)
      }
      if(options.httpOnly) {
        optsArr.push(`httpOnly=${options.httpOnly}`)
      }
      if(options.maxAge) {
        optsArr.push(`max-age=${options.maxAge}`)
      }
      if(options.signed){ // 说明 为了安全 需要给数据签名(加盐)
        // base64在传输的时候会把 + / = 做特殊处理
        let sign = toBase64URL(crypto.createHmac('sha1',secret).update([key, value].join("=")).digest("base64"));
        arr.push(`agesign=${sign}`);
      }
      arr.push(`${key}=${value};${optsArr.join(";")}`);
      ctx.res.setHeader("Set-Cookie", arr);
    },
    get(key,options) {
      /*
      获取来使用的时候
      ctx.my.get('age', {signed: true}) 表示要校验
      */
      let cookieObj = querystring.parse(ctx.req.headers['cookie'], ';', '=');
      if(options.signed){
          // 上一次的签名
          const bool = toBase64URL(cookieObj[`${key}sign`] == crypto.createHmac('sha1',  mysecret).update(`${key}=${cookieObj[key]}`).digest('base64'))
          if(bool){
              return cookieObj[key];
          }else{ // 签名失效
              return 'error';
          }
      }
      
    }
  }
  return next();
})

const router = new Router();

router.get("/write", async (stc) => {
  // ctx.body = "ok";
  // 浏览器添加cookie: document.cookie
  // 服务器设置cookie
  // 如果是多对值 用数组
  // ctx.res.setHeader('Set-Cookie', 'name=zhuzhu');
  // ctx.res.setHeader('Set-Cookie', 'age=28'); // 这样写会覆盖
  ctx.res.setHeader('Set-Cookie', ['name=zhuzhu', 'age=28']);

  // 默认是给当前域名添加cookie 设置domain会给指定域名设置cookie
  ctx.res.setHeader('Set-Cookie', ['name=zhuzhu', 'age=28;domain=.zhuzhu.com;httpOnly=true']);

  // 在浏览器对cookie里对键值对勾选httpOnly  document.cookie就获取不到了
  
  /*
    cdn有一个好处: cdn是一个特殊域名 不会携带发送cookie
    key cookie的key
    value cookie的值
    domain 域名
    path 路径
    expires或max-age 存活时间
    httpOnly
      xsrf 诱导用户点击一个图片 发请求通过url把本地的cookie传递给他自己的服务器 (通过document.cookie)
  */

  // 上面设置的方式比较麻烦
  ctx.cookies.set("name", "zhuzhu", {
    domain: ".zhuzhu.com",
    httpOnly: true,
    maxAge: 10
  });
  ctx.cookies.set("age", "28");
  // 因为设置的cookie在浏览器是可以更改的 这时候服务器再去读就会不安全,所以要给cookie加盐
  ctx.cookies.set("age", "28",{signed : true});
  ctx.body = "write ok";
});
router.get('/read', async (ctx) => {
  // 获取浏览器发送过来的cookie
  ctx.body = ctx.req.headers['cookie']
  // 获取cookie的另一种方式 和ctx.cookies.set对应
  ctx.body = ctx.cookies.get("name")
})
// 后面是指如果请求不支持 会报这个方法不认 “method not allowed”
app.use(router.routes()).use(router.allowedMethods());

app.listen(3000, function() {
  console.log('server start 3000')
})
  • koa中提供了现成的做法
    • const app = new Koa(); const secret = 'mysecret'

    • app.keys = [secret] 提供cookie用于签名的密钥

    • ctx.cookies.set('name', 'zhuzhu', {domain:'baidu.com', httpOnly:true})

    • ctx.cookies.set('age', '28', {signed: true})

    • ctx.cookies.get('age', {signed: true})

session

  • session是基于cookie
  • ssr前后端同构时 用session实现用户鉴别是最方便的
  • koa-session
const Koa = require("koa");
const Router = require("@koa/router");
const uuid = require('uuid');
const app = new Koa();


const router = new Router();

const cardName = "zhuzhu"; //店铺名称
const session = {}; // session就是服务器的一个记账本 为了稍后能通过这个本找到具体信息
router.get('/wash', async (ctx) => {
  const hasVisit = ctx.cookies.get(cardName, {signed: true});
  if(hasVisit && session[hasVisit]) {
    session[hasVisit].mny -= 100;
    ctx.body="消费了100元"
  }else{
      const id = uuid.v4();
      session[id] = {mny: 500};
      ctx.cookies.set(cardName, id, {signed: true});
      ctx.body = "成为本店会员,有500元"
  }
})
// 后面是指如果请求不支持 会报这个方法不认 “method not allowed”
app.use(router.routes()).use(router.allowedMethods());

app.listen(3000, function() {
  console.log('server start 3000')
})

jwt

  • 为了鉴别身份 不考虑安全

  • token 可以在url/header/请求体

  • 主流的方案都是token jwt = json web token session不方便共享

  • jwt 通过令牌来识别身份

  • 非常像cookie的签名 服务器只存一个密钥 只是为了鉴别用户身份

  • 最终开发会使用jsonwebtoken这个包(功能强大,默认支持一些令牌的过期处理)

  • 生成令牌 const token = jwt.encode(user, secret);

  • 反解 let user = jwt.decode(cts.get('Authorization').split(" ")[1], secret);

const Koa = require("koa");
const Router = require("@koa/router");
const jwt = require('jwt-simple');
const app = new Koa();


const router = new Router();
const secret = "mysecret"; // 密钥
router.get('/login', async (ctx) => {
    // 这个数据在每次发过来请求的时候都会带上 所以数据不要太多
    // 一般情况下用用户的id就可以
    let user = {
        id: "111",
        name: "zhuzhu"
    };

    // 生成令牌
    const token = jwt.encode(user, secret);

    ctx.body = {
        err: 0,
        data: {
            token,
            user
        }
    }
})
router.get("/validate", async(ctx, next) => {
    // token 可以放请求体里 也可以放header中
    // ctx.get('Authorization') jwt的规范是 Authorization: Bearar token
    try{
        let user = jwt.decode(cts.get('Authorization').split(" ")[1], secret);
        ctx.body = {
            err: 0,
            data: {
                user
            }
        }
    }catch(e) {
        ctx.body = {
            err: 1
        }
    }
})
// 后面是指如果请求不支持 会报这个方法不认 “method not allowed”
app.use(router.routes()).use(router.allowedMethods());

app.listen(3000, function() {
  console.log('server start 3000')
})
// 生成的token由三段组成
// 第一段是固定的头 {typ: 'JWT', alg:'HS256'}
// 第二段是用户的内容
// 第三段是签名
const jwt = {
    sign(content, secret) {
        return this.toBase64URL(crypto.createHmac('sha256', secret).update(content).digest('base64'))
    },
    toBase64URL(str) {
        return str.replace(/\=/g, '').replace(/\+/g, '-').replace(/\//g, '_')
    },
    base64urlUnescape(str) {
        str += new Array(5 - str.length % 4).join('=');
        return str.replace(/-/g, '+').replace(/_/g, '/')
    },
    toBase64(content) { // 将内容进行base64
        return this.toBase64URL(Buffer.from(JSON.stringify(content)).toString('base64'))
    },
    encode(info, secret) {
        // 这里面只是转成了base64 所以是可以解码的
        const head = this.toBase64({typ:'JWT', alg:'HS256'});
        const content = this.toBase64(info);
        const sign = this.sign([head, '.', content].join(''), secret);
        return `${head}.${content}.${sign}`
    },
    decode(token, secret) {
        let [head, content, sign] = token.split(".");
        let newSign = this.sign([head, content].join("."), secret);
        if(newSign == sign) {
            const str = Buffer.from(this.base64urlUnescape(content), 'base64').toString();
            return JSON.parse(str);
        }else{
            throw new Error('用户更改了信息')
        }
    }
}