一个不大不小的问题

  1. 假设服务器有一个接口,通过请求这个接口,可以添加一个管理员,但是,不是任何人都有权力做这种操作的;

  2. 那么服务器如何知道请求接口的人是有权力的呢?答案是:只有登录过的管理员才能做这种操作;

  3. 可问题是,客户端和服务器的传输使用的是 http 协议,http 协议是无状态的,什么叫无状态,就是 服务器不知道这一次请求的人,跟之前登录请求成功的人是不是同一个人

  4. 由于 http 协议的无状态,服务器 忘记 了之前的所有请求,它无法确定这一次请求的客户端,就是之前登录成功的那个客户端;于是,服务器想了一个办法,它按照下面的流程来认证客户端的身份:

    • 客户端登录成功后,服务器会给客户端一个出入证;
    • 后续客户端的每次请求,都必须要附带这个出入证;
  5. 服务器发扬了认证不认人的优良传统,就可以很轻松的识别身份了;但是,用户不可能只在一个网站登录,于是客户端会收到来自各个网站的出入证,因此,就要求客户端要有一个类似于卡包的东西,能够具备下面的功能:

    • 能够存放多个出入证:这些出入证来自不同的网站,也可能是一个网站有多个出入证,分别用于出入不同的地方;
    • 能够自动出示出入证:客户端在访问不同的网站时,能够自动的把对应的出入证附带请求发送出去;
    • 正确的出示出入证:客户端不能将肯德基的出入证发送给麦当劳;
    • 管理出入证的有效期:客户端要能够自动的发现那些已经过期的出入证,并把它从卡包内移除;
  6. 能够满足上面所有要求的,就是 cookiecookie 类似于一个卡包,专门用于存放各种出入证,并有着一套机制来自动管理这些证件,卡包内的每一张卡片,称之为 一个 cookie

  1. cookie 是浏览器中特有的一个概念,它就像浏览器的专属卡包,管理着各个网站的身份信息;每个 cookie 就相当于是属于某个网站的一个卡片,它记录了下面的信息:

    属性名 描述
    key
    value
    domain 域,表示这个 cookie 是属于哪个网站的,比如 ricepudding.cn
    path 路径,表示这个 cookie 是属于该网站的哪个基路径的,比如 /news(后续详细解释)
    secure 是否使用安全传输(后续详细解释)
    expire 过期时间,表示该 cookie 在什么时候过期
  2. 当浏览器向服务器发送一个请求的时候,它会瞄一眼自己的卡包,看看哪些卡片适合附带捎给服务器,如果一个 cookie 同时满足以下条件,则这个 cookie 会被附带到请求中:

    • cookie 没有过期;
    • cookie 中的域和这次请求的域是匹配的,比如 cookie 中的域是 ricepudding.cn,则可以匹配的请求域是 ricepudding.cnwww.ricepudding.cnblogs.ricepudding.cn 等等;
    • cookie 中的 path 和这次请求的 path 是匹配的,比如 cookie 中的 path/news,则可以匹配的请求路径可以是 /news/news/detail/news/a/b/c 等等;
    • 验证 cookie 的安全传输,如果 cookiesecure 属性是 true ,则请求协议必须是 https ,否则不会发送该 cookie
  3. 如果一个 cookie 满足了上述的所有条件,则浏览器会把它自动加入到这次请求中:具体加入的方式是,浏览器会将符合条件的 cookie,自动放置到请求头中,例如,当我在浏览器中访问百度的时候,它在请求头中附带了下面的 cookie

  4. 看到打马赛克的地方了吗?这部分就是通过请求头 cookie 发送到服务器的,它的格式是 键=值; 键=值; 键=值; ...,每一个键值对就是一个符合条件的 cookie

  5. cookie 中包含了重要的身份信息,永远不要把你的 cookie 泄露给别人!!! 否则,他人就拿到了你的证件,有了证件,就具备了为所欲为的可能性;

  1. 由于 cookie 是保存在浏览器端的,同时很多证件又是服务器颁发的:

  2. 所以 cookie 的设置有两种模式:

    • 服务器设置:这种模式是非常普遍的,当服务器决定给客户端颁发一个证件时,它会在响应的消息中包含 cookie ,浏览器会自动的把 cookie 保存到卡包中;
    • 客户端设置:这种模式少见一些,不过也有可能会发生,比如用户关闭了某个广告,并选择了「以后不要再弹出」,此时就可以把这种小信息直接通过浏览器的 JS 代码保存到 cookie 中;后续请求服务器时,服务器会看到客户端不想要再次弹出广告的 cookie ,于是就不会再发送广告过来了;
  1. 服务器可以通过设置响应头,来告诉浏览器应该如何设置 cookie ,响应头按照下面的格式设置:

    set-cookie: cookie1
    set-cookie: cookie2
    set-cookie: cookie3
    ...
    
  2. 通过这种模式,就可以在一次响应中设置多个 cookie 了,具体设置多少个 cookie ,设置什么 cookie ,根据需要自行处理;其中,每个 cookie 的格式如下:每个 cookie 除了键值对是必须要设置的,其他的属性都是可选的,并且顺序不限;

    键=值; path=?; domain=?; expire=?; max-age=?; secure; httponly
    
  3. 当这样的响应头到达客户端后,浏览器会自动的将 cookie 保存到卡包中,如果卡包中已经存在一模一样的卡片(其他 path、domain 相同),则会自动的覆盖之前的设置

  4. 下面依次说明每个属性值:

    • path:设置 cookie 的路径;如果不设置,浏览器会将其自动设置为当前请求的路径;比如,浏览器请求的地址是 /login,服务器响应了一个set-cookie: a=1,浏览器会将该 cookiepath 设置为请求的路径 /login
    • domain:设置 cookie 的域;如果不设置,浏览器会自动将其设置为当前的请求域,比如,浏览器请求的地址是 http://www.ricepudding.cn,服务器响应了一个 set-cookie: a=1,浏览器会将该 cookiedomain 设置为请求的域 http://www.ricepudding.cn;
    • expire:设置 cookie 的过期时间;这里必须是一个有效的 GMT 时间,即格林威治标准时间字符串,比如 Fri, 17 Apr 2020 09:35:59 GMT,表示格林威治时间的 2020-04-17 09:35:59,即北京时间的 2020-04-17 17:35:59;当客户端的时间达到这个时间点后,会自动销毁该 cookie;
    • max-age:设置 cookie 的相对有效期;expiremax-age 通常仅设置一个即可;比如设置 max-age1000 ,浏览器在添加cookie 时,会自动设置它的 expire 为当前时间加上 1000 秒,作为过期时间;如果不设置 expire 又没有设置 max-age ,则表示会话结束后过期;对于大部分浏览器而言,关闭所有浏览器窗口意味着会话结束;
    • secure:设置 cookie 是否是安全连接;如果设置了该值,则表示该 cookie 后续只能随着 https 请求发送;如果不设置,则表示该 cookie 会随着所有请求发送;
    • httponly:设置 cookie 是否仅能用于传输;如果设置了该值,表示该 cookie 仅能用于传输,而不允许在客户端通过 JS 获取,这对防止跨站脚本攻击(XSS)会很有用;
  5. 下面来一个例子,客户端通过 post 请求服务器 http://ricepudding.cn/login,并在消息体中给予了账号和密码,服务器验证登录成功后,在响应头中加入了以下内容:set-cookie: token=123456; path=/; max-age=3600; httponly

    • 当该响应到达浏览器后,浏览器会创建下面的 cookie
    key: token
    value: 123456
    domain: ricepudding.cn
    path: /
    expire: 2020-04-17 18:55:00 # 假设当前时间是 2020-04-17 17:55:00
    secure: false  # 任何请求都可以附带这个cookie,只要满足其他要求
    httponly: true # 不允许 JS 获取该 cookie
    
    • 于是,随着浏览器后续对服务器的请求,只要满足要求,这个 cookie 就会被附带到请求头中传给服务器:
    cookie: token=123456; 其他 cookie...
    
  6. 删除浏览器的 cookie:只需要让服务器响应一个同样的域、同样的路径、同样的 key ,只是时间过期的 cookie 即可,所以删除 cookie 其实就是修改 cookie:浏览器按照要求修改了 cookie 后,会发现 cookie 已经过期,于是自然就会删除了;

    set-cookie: token=; domain=ricepudding.cn; path=/; max-age=-1
    

    无论是修改还是删除,都要注意 cookie 的域和路径,因为完全可能存在域或路径不同,但 key 相同的 cookie
    因此无法仅通过 key 确定是哪一个 cookie

  1. 既然 cookie 是存放在浏览器端的,所以浏览器向 JS 公开了接口,让其可以设置 cookie

    document.cookie = "键=值; path=?; domain=?; expire=?; max-age=?; secure";
    
  2. 可以看出,在客户端设置 cookie 和服务器设置 cookie 的格式一样,只是有下面的不同:

    • 没有 httponly ,因为 httponly 本来就是为了限制在客户端访问的,既然你是在客户端配置,自然失去了限制的意义;
    • path 的默认值,在服务器端设置 cookie 时,如果没有写 path 使用的是请求的 path;而在客户端设置 cookie 时,也许根本没有请求发生,因此 path 在客户端设置时的默认值是当前网页的 path
    • domain 的默认值,和 path 同理,客户端设置时的默认值是当前网页的 domain
    • 其他:一样;
    • 删除 cookie:和服务器也一样,修改 cookie 的过期时间即可;
  1. cookie 会作为 http 的请求报文进行传递,所以要注意 cookie 字段的大小,一般小于 4kb ,如果 cookie 存储的字段过大,浪费流量,服务端也会耗费性能解析 cookie

    解决方案:静态资源的请求,无必要可以不携带 cookie ,合理设置 cookie ,不必要字段不放 cookie 里面;

  2. cookie 保存在客户端,存在篡改或劫持的风险,故 cookie 不能存储重要信息,一般只保存凭证,服务端会根据 cookie 从数据库或 session 中查找具体信息(当给浏览器设置 cookie 时,可以增加签名,根据数据内容创建一个唯一的签名,读取 cookie 的时候验证签名,判断 cookie 是否被篡改);

    const http = require('http');
    const querystring = require('querystring');
    const crypto = require('crypto');
    const secret = 'adsadbAvhnrgreu657i76lu;oi3r';  // 自定义的秘钥
    
    // 签名
    const toSign = (value) => {
        // 当给浏览器设置 cookie 时,可利用 加盐算法 增加签名,根据数据内容创建一个唯一的签名
        // 相同的秘钥签名的结果是相同,并且不能反解
        let str = crypto.createHmac('sha256', secret).update(value).digest('base64');
        // /、=、+ 浏览器不能识别,直接去掉,只是做签名没必要改成其他字符(和后端统一规则即可)
        return str.replace(/\/|\=|\+/, '');
    }
    
    http.createServer((req, res) => {
        req.getCookie = function (key, options = {}) {
            let cookieObj = querystring.parse(req.headers.cookie, '; ', '=');
            if (options.signed) { // 是否验证签名
                let [value, signCode] = (cookieObj[key] || '').split('.');
                let newSign = toSign(value);
                if (newSign === signCode) {
                    return value; // 签名一致,说明这次的内容是没有被改过的
                } else {
                    return undefined; // 签名被篡改了,不能使用了
                }
            }
            return cookieObj[key];
        }
    
        res.setCookie = function (key, value, options = {}) {
            let opts = [];
            if (options.domain) opts.push(`domain=${options.domain}`);
            if (options.path) opts.push(`path=${options.path}`);
            if (options.maxAge) opts.push(`max-age=${options.maxAge}`);
            if (options.httpOnly) opts.push(`httpOnly=${options.httpOnly}`);
            if (options.signed) value = value + '.' + toSign(value);
            res.setHeader('Set-Cookie', [`${key}=${value}; ${opts.join('; ')}`]);
        }
    
        if (req.url == '/read') {
            // 读取 cookie
            res.end(req.getCookie('name', { signed: true }) || 'empty');
        } else if (req.url == '/write') {
            // 设置 cookie
            res.setCookie('name', 'zhangsan', { httpOnly: true, maxAge: 200, signed: true });
            res.setCookie('age', '20');
            res.end('write Ok');
        } else {
            res.end('Not Found');
        }
    }).listen(3000);
    

登录场景的使用

登录请求

  1. 浏览器发送请求到服务器,附带账号密码;
  2. 服务器验证账号密码是否正确,如果不正确,响应错误,如果正确,在响应头中设置 cookie 附带登录认证信息(至于登录认证信息是设么样的,如何设计,要考虑哪些问题,就是另一个话题了,可以百度 jwt
  3. 客户端收到 cookie 浏览器自动记录下来;

后续请求

  1. 浏览器发送请求到服务器,希望添加一个管理员,并将 cookie 自动附带到请求中;

  2. 服务器先获取 cookie ,验证 cookie 中的信息是否正确,如果不正确,不予以操作,如果正确,完成正常的业务流程;

打赏作者
您的打赏是我前进的动力
微信
支付宝
评论

中午好👏🏻,我是 ✍🏻   疯狂 codding 中...

粽子

这有关于前端开发的技术文档和你分享。

相信你可以在这里找到对你有用的知识和教程。

了解更多

目录

  1. 1. 一个不大不小的问题
  2. 2. cookie 的组成
  3. 3. 如何设置 cookie
    1. 3.1. 服务端设置 cookie
    2. 3.2. 客户端设置 cookie
  4. 4. cookie 存在的问题
  5. 5. 登录场景的使用