• 记一场与 cookie 的相遇


    简介: cookie 翻译过来为 “小甜点,一种酥性甜饼干,很美味的...”,咳咳,打住!我们这里说的是 “甜点” 文件,它是浏览器储存在用户电脑上的一小段纯文本格式的文件。

    由于 http 是一种无状态的协议(无状态是指对于客户端每次发送的请求都认为它是一个新的请求,上一次会话和下一次会话没有联系),服务器无法知道两个请求是否来自于同一个浏览器,因此 cookie 应运而生。cookie 可以记录用户的有关信息,最根本的是它可以帮助 Web 站点保存有关访问者的信息。

    我们通过一张图来说明下 cookie 的原理:

     

    客户端向服务端发送请求;接收到请求后,服务端在响应头中通过 set-cookie 携带 cookie 信息返回给客户端;客户端再次发送携带了 cookie 的请求;服务端根据 request header 里的 cookie 信息校验该请求,给出响应。

    接下来我们需要先简要了解下 cookie 的属性 [1][2]。

    cookie 属性

    1. Name

    cookie 的名字,相同域名只允许存在一个同名 cookie;一旦创建,该名称便不可更改。

    document.cookie = `jxi-m-sid=${cookie}`;
    

    jxi-m-sid 即为新建的 cookie 名字,该名字创建后不能修改,如果需要新的 cookie 只能再次创建。

    2. Value

    该 cookie 的值,如果值为 Unicode 字符,需要为字符编码;如果值为二进制数据,则需要使用 BASE64 编码。

    document.cookie = `jxi-m-sid=${cookie}`;

    ${cookie} 即为 cookie jxi-m-sid 的值,当需要修改值时,比如将 jxi-m-sid 值改为 2019,操作如下:

    document.cookie = `jxi-m-sid=2019`;

    3. Domain

    可以访问该 cookie 的域名。默认情况下,domain 会被设置为创建该 cookie 的页面所在的域名,所以当给相同域名发送请求时该 cookie 会被发送至服务器。

    有关 domain 的设置需要注意以下几点:

    (1)设置 domain 时,前面带点和不带点的区别是:

    带点:任何子域名都可以访问,包括父域名;

    document.cookie = `jxi-m-sid=${cookie};domain=.xx.com;path=/`;

    不带点:只有完全一样的域名才可以访问(IE除外,仍然支持子域名访问)

    document.cookie = `jxi-m-sid=${cookie};domain=xx.com;path=/`;

    (2)非顶级域名,如二级域名或者三级域名,设置的 cookie 的 domain 只能为顶级域名或者二级域名或者三级域名本身,不能设置其他二级域名的 cookie ,否则 cookie 无法生成;

    (3)二级域名能读取设置了 domain 为顶级域名或者自身的 cookie,不能读取其他二级域名 domain 的 cookie 。所以要想 cookie 在多个二级域名中共享,需要设置 domain 为顶级域名;

    (4)顶级域名只能获取到 domain 设置为顶级域名的 cookie 。

    4. Path

    可以访问该 cookie 的页面路径。比如 domain 是 abc.com ,path是 /test ,那么只有 /test 路径下的页面可以读取该 cookie。

    document.cookie = `jxi-m-sid=${cookie};domain=.abc.com;path=/test`;

    5. Expires/Max-Age

    该 cookie 的超时时间。若设置其值为一个时间,当到达此时间后,该 cookie 失效。默认有效期为 session ,即会话 cookie 。当浏览器关闭(不是浏览器标签页,而是整个浏览器) 后,该 cookie 失效。

    例如,我们希望该 cookie jxi-m-sid 过期,则可以将它的超时时间设置为客户端本地时间1分钟以前,代码如下:

    var exp = new Date(); //获取客户端本地当前系统时间
    exp.setTime(exp.getTime() - 60 * 1000); //将 exp 设置为客户端本地时间1分钟以前
    document.cookie = `jxi-m-sid=;expires=${exp.toUTCString()};domain=.xx.com;path=/`;

    注意:expires 必须是 GMT 格式的时间。

    6. Size

    该 cookie 的大小。cookie 的大小约 4K 左右,在所有浏览器中,任何 cookie 大小超过限制都会被忽略,且永远不会被设置。

    7. HTTP

    cookie 的 httponly 属性。默认情况下,httpOnly 选项为空,允许客户端通过 js 去访问(包括读取、修改、删除等)该 cookie ;若此属性为 true ,则只有在 http 请求头中会带有此 cookie 的信息,而不能通过 document.cookie 来访问此 cookie,意在提供一个安全措施来帮助阻止通过 JavaScript 发起的跨站脚本攻击 (XSS) 窃取 cookie 的行为。

    比如,服务端将 uid 设置为不允许客户端修改:

    response.addHeader("Set-Cookie", "uid=112; Path=/; HttpOnly");

    8. Secure

    用来设置 cookie 只在确保安全的请求中才会发送。当请求是 https 或者其他安全协议时,包含 secure 选项的 cookie 才能被发送至服务器。默认情况下,secure 选项为空,不管是 https 协议还是 http 协议的请求,cookie 都会被发送至服务端。若想在客户端通过 js 去设置 secure 类型的 cookie,必须保证网页是 https 协议的。比如:

    document.cookie = `jxi-m-sid=${cookie};secure`;

    cookie 的属性介绍完了,关于其使用需要注意:

    (1)每个 web 服务器(域名)保存的 cookie 数不能超过 50 个,每个 cookie 大小不能超过 4KB;

    (2)尽量让 cookie 的权限范围小一些,能子域可见 domain 绝不设为主域;

    (3)存储非敏感的用户信息且设置合理的过期时间,减少因此带来的网络流量(文档传输的负载)。

    cookie 的分类

    cookie 可以分为两类 [3]:会话 cookie 和持久 cookie。

    会话 cookie 是一种临时 cookie,若没有设置有效期,当用户关闭浏览器时,该 cookie 就会被删除。

    对于设置了有效期的 cookie 就被称为持久 cookie,它可以存储在硬盘上,当用户关闭浏览器或者机器重启时,该 cookie 依然存在,可以再次被读取使用。

    在常用的数据存储方法中,除了本文介绍的 cookie,还有 localStorage、sessionStorage、 session。下面简要介绍下这几种方法的区别:

    cookie 与 session

    session 机制是一种服务器端的机制,服务器使用一种类似于散列表的结构(也可能就是使用散列表)来保存信息。大多数的应用都是用 cookie 来实现 session 的跟踪,第一次创建 session 的时候,服务端会在 http 协议中告诉客户端,需要在 cookie 里面记录一个 sessionid ,以后每次请求把这个会话 id 发送到服务器。

    cookie 与 session 的区别 [4] 如下表所示:

    特性 cookie session
    存取方式 只能存储 ASCII 字符串 存取任何类型的数据
    隐私策略 存储在客户端,对用户可见且用户可对其处理 存储在服务器
    有效期 可以通过设置较大的过期时间实现长期有效 若设置的超时时间过长,容易导致内存溢出
    服务器压力 不占用服务器资源 并发用户过多时会耗费大量内存
    浏览器支持 需要浏览器支持,不支持时可以通过 session 和 URL 地址重写实现 只在当前窗口和其子窗口内有效
    跨域 支持 不支持

    关于 cookie 与 localStorage、sessionStorage 的区别也整理了一份图表如下所示:

    特性 cookie localStorage sessionStorage
    生命周期 一般由服务器生成,可设置失效时间。如果是浏览器端生成,默认关闭浏览器后失效 除非被清除,否则永久保存 仅在当前会话下有效,关闭页面或浏览器后被清除
    存放数据大小 4K 左右 一般为 5MB 同 localStorage
    与服务器端通信 始终携带在 http 头中,使用过多会带来性能问题 仅在客户端中保存,不和服务器的通信 同 localStorage
    易用性 需要程序员自己封装,源生的 cookie 接口不友好 源生接口可以接受,也可再次封装 同 localStorage

    需要注意的是,向 cookie、localStorage 和 sessionStorage 中存储数据时,都需要时刻注意是否有代码存在 XSS 注入的风险,避免存入一些敏感数据。

    重点来了:结合项目实践中的问题讨论下 cookie 跨域存储的问题。

    cookie 实践

    项目背景:在 a 小程序中通过 web-view 内嵌了 b 项目(H5,域名为 b.mm.com)和 c 项目(H5,域名为 c.mm.com),b 和 c 都调用了公共地址接口 interface(interface 所在的域名为 c.mm.com)。在 a 中登录成功之后,需要获取接口返回的 cookie 封装在每个接口的 request header 中,同时需要传递给 b、c;b 和 c 需要获取传递的 cookie 之后,写入到 interface 可访问的域名下,实现用户登录状态的验证。

    遇到的问题:小程序内访问线上项目 c 可以正常使用,访问 b 时会偶尔出现登录验证失败的情况。

    服务端反馈传入的 cookie 值有误,导致登录验证不通过。浏览器中查看 cookie 发现:存在两个同名但 domain 不同的 cookie。

    问题定位:项目 b、c 获取 cookie 的方法是一样的,区别就在于 cookie 值的存储与传递。

    项目 b 中 cookie 处理流程:

    (1)服务端:b 系统的 domain 为 mm.com;

    //application.yml 文件
    ...
    b:
        m:
            domain: mm.com
            scheme: http
    ...

    (2)前端:获取当前传入的 cookie,写入 mm.com 域名下。
    在项目 b 中,b.mm.com 域名下是无法向 c.mm.com 域名下写入 cookie,若要实现跨域写入,只能将 cookie 写入到其父级域名:mm.com。

    document.cookie = `jxi-m-sid=${cookie};domain=.mm.com;path=/`;

    注意:b 系统中的 domain 设置为 mm.com,主要是为了请求 c.mm.com 下的地址接口时 request header 中携带存入的 cookie 。

    项目 c 中 cookie 处理流程:

    (1)服务端:c 系统的 domain 为 c.mm.com;

    //application.yml 文件
    ...
    c:
        m:
            domain: c.mm.com
            scheme: http
    ...

    (2)前端:获取当前传入的 cookie,写入的域名是 c.mm.com。

    document.cookie = `jxi-m-sid=${cookie};domain=c.mm.com;path=/`;

    由系统 b 和 c 中 cookie 处理流程可以发现:写入 cookie 时设置的 domain 不同,导致出现了两个 jxi-m-sid 的 cookie,当 b 中请求地址接口时,request header 中携带的是错误的 cookie。

    解决方法:为了解决同时出现两个同 key 的 cookie 问题,将 c 中的 domain 统一修改为二级域名:mm.com。与此同时需要注意:服务器端需要将 c 系统的 domain 也改为 mm.com。

    ...
    c:
        m:
            domain: mm.com
            scheme: http
    ...
    document.cookie = `jxi-m-sid=${cookie};domain=.mm.com;path=/`;

    这样处理之后,两个同 key cookie 的问题解决了,为了保证写入的 cookie 是唯一的,在每次写入 cookie 之前做了清除同 key cookie 处理(利用 cookie 的超时时间属性)。

    removeCookie: (cookieName) => {
        var cookies = document.cookie.split(";");
    
        for (var i = 0; i < cookies.length; i++) {
    
            if (cookies[i].indexOf(" ") == 0) {
                cookies[i] = cookies[i].substring(1);
            }
    
            if (cookies[i].indexOf(cookieName) == 0) {
                var exp = new Date();
                exp.setTime(exp.getTime() - 60 * 1000);
    
                document.cookie = cookies[i] + "=;expires=" + exp.toUTCString() + ";domain=.xx.com;path=/"
    
                break;
            }
        }
    }
    
    removeCookie('jxi-m-sid');

    按照这种方法处理之后,很长一段时间内 cookie 的问题正常了,但是极少情况下还是会偶现服务端取到的 cookie 值不是最新的问题。

    终极方案:我们想出了另外一种方案:在 b 和 c 系统所有请求的 request header 中携带 cookie,服务端校验用户身份时,首先会从 request header 中获取,没有的话再从写入的 cookie 中取值,解决了上述前后端 cookie 值不一致问题。

    接下来我们将项目开发中整个 cookie 处理过程分为以下四步,简要说下每一步的处理过程:

    第一步:小程序中获取及传递 cookie;

    登录成功时,后端会将 cookie 种在 response header 的 set-cookie 中,我们获取到该 cookie 后先进行本地存储,然后封装到每次接口的 request header 中。

    //获取 cookie
    wx.setStorageSync('cookie', res.header["Set-Cookie"]);
    
    //统一封装 request 请求
    const request = parameter => {
        //url必填项
        if (!parameter || parameter == {} || !parameter.url) {
            console.log('Data request can not be executed without URL.');
            return false;
        } else {
            var murl = parameter.url;
            var headerCookie = wx.getStorageSync('cookie');
            //判断是否有独自cookie请求
            var selfCookie = parameter.selfCookie;
            selfCookie && (headerCookie += selfCookie);
            wx.request({
                url: murl,
                data: parameter.data || {},
                header: {
                    'Cookie': headerCookie
                },
                method: parameter.method || 'POST',
                success: function(res) {
                    parameter.success && parameter.success(res);
                },
                fail: function(e) {
                    parameter.fail && parameter.fail(e);
                },
                complete: function() {
                    parameter.complete && parameter.complete();
                }
            });
        }
    
    }

    这样在小程序内,每个接口的 request header 中都会携带该 cookie,服务端可以根据该 cookie 判断用户的登录状态。

    第二步:小程序向 web-view 内嵌的 H5 中传递 cookie;

    第三步:H5 中获取小程序传过来的 cookie ,通过 js 写入 [5];

    获取 cookie:

    getQueryString: (name) => {
        let reg = new RegExp("(^|&)" + name + "=([^&]*)(&|$)");
        let r = window.location.search.substr(1).match(reg);
        if (r != null) {
            return unescape(decodeURI(r[2]));
            return null;
        }
    }
    
    let cookie = getQueryString('cookie');

    通过 js 将 cookie 写入到父级域名:mm.com。写入方法如下:

    document.cookie = `jxi-m-sid=${cookie};domain=.mm.com;path=/`;

    第四步:接口 request header 中统一封装 key 为 jxi-m-sid 的 cookie,以 axios 库的 get 和 post 请求为例( headers 里定义):

    export default {
        post(url, data) {
            return axios({
                method: 'post',
                url,
                data: JSON.stringify(data),
                timeout: 30000,
                headers: {
                    'jxi-m-sid': cookie
                }
            })
        },
    
        get(url, params) {
            return axios({
                method: 'get',
                url,
                params,
                timeout: 30000,
                headers: {
                    'jxi-m-sid': cookie
                }
            })
        }
    }

    以上就是基于我们的项目背景,解决两个同 key cookie 且前后端获取值不一致问题的解决方案。涉及到 cookie 的 domain、path 问题请大家使用时高度重视,这将决定你所写入 cookie 的唯一性。

    cookie 一般是用来存储当前登录用户的会话信息且是存储在客户端,用户可以随意修改,所以存在一定的风险。针对这个问题,也有了比较成熟的解决方法,这里我们简要介绍下。

    cookie 防篡改机制

    敏感数据存储在服务器

    敏感数据避免存储在 cookie 里,可以根据 sessionid 将其存储在服务端。需要时根据 sessionid 获取即可。

    防篡改签名

    服务端为每个 cookie 生成签名,如果用户篡改了该 cookie 则签名是不一致的,服务端可以以此来判断该 cookie 是否被篡改。

    具体的实现步骤可以如下所示:
    (1)服务端提供签名生成算法 secret;
    (2)根据方法生成签名 secret(x);
    (3)将生成的签名放到 cookie 中,可以使用 | 将 cookie 内容与签名分隔开,如 name=x|yyyyy;
    (4)服务端校验收到的内容和签名,判断是否被篡改。

    以上的方法可以进一步确保 cookie 的数据安全 [6],在有需要的项目中大家可以尝试使用下。

    小结

    本文主要介绍了 cookie 的相关知识,包括:常用属性、修改、跨域存储、防篡改等。对于 cookie 的跨域传递,上述项目实践中的方法是我们的使用方法,大家在使用的时候可以根据自己项目的情况进行选择。

    敲黑板:cookie 中不要放敏感信息哦,再次友情提示~好了,关于 cookie 的问题就介绍到这里了,如有任何疑问,欢迎留言。

    扩展阅读

    [1] https://www.quirksmode.org/js/cookies.html

    [2] http://bubkoo.com/2014/04/21/http-cookies-explained/

    [3] http://www.allaboutcookies.org/cookies/cookies-the-same.html

    [4] https://www.jianshu.com/p/25802021be63

    [5] http://www.tutorialspoint.com/javascript/javascript_cookies.htm

    [6] https://juejin.im/post/5b02fe326fb9a07ab1117c82

  • 相关阅读:
    【转载】python基础-文件读写'r' 与 'rb' 和‘r+'与’rb+'区别
    python-IndexError: list index out of range
    NameError:name ‘xrange’ is not defined
    k8s 结合docker搭建私有仓库
    部署Kubernetes-dashboard
    通过Kubeadm搭建Kubernetes集群
    .net core +gogs + jenkins +docker自动化发布、部署
    .NET Core 使用ModelBinder去掉所有参数的空格
    mysql主从同步
    IdentityServer4同时使用多个GrantType进行授权和IdentityModel.Client部分源码解析
  • 原文地址:https://www.cnblogs.com/sunLemon/p/10563818.html
Copyright © 2020-2023  润新知