跨域请求无法携带 Cookie 的问题排查

问题描述

生产环境有一个场景是,a.com 会加载 b.com 下的一个静态资源 b.com/b.js,并且需要验证登陆才能拿到。通过如下代码获取

addScript('b.com/b.js')

function addScript(src, callback) {
  var script = document.createElement('script')
  script.src = src
  script.onload = script.onerror = function() {
    if (script.parentNode) {
      script.parentNode.removeChild(script)
    }
    if (callback) {
      callback()
      callback = null
    }
  }
  document.head.appendChild(script)
}

也就是我们常用的 jsonp 的形式

我们通常会手动创建 script 标签并设置 src 属性,来实现 jsonp 跨域请求

之前一直没问题,突然有一天有同事发现这个资源加载不到了,并且只是个别人会有这个问题。

查看之后发现,在出问题的浏览器中addScript('b.com/b.js')发出请求里没带 cookie,所以获取静态资源的请求直接 302 到了登陆页。经过一番查找,发现问题出在 cookie 的 SameSite 属性上。

在没有这个问题的浏览器中,通过手动设置 Chrome 的 chrome://flags/#same-site-by-default-cookies 为 Enabled 也能重现这个问题,至此问题确认。

image-20200804171245697.png

下面对 SameSite 做简单介绍。

SameSite 属性(引用自阮一峰博客

Chrome 51 开始,浏览器的 Cookie 新增加了一个SameSite属性,用来防止 CSRF 攻击和用户追踪。

Cookie 的SameSite属性用来限制第三方 Cookie,从而减少安全风险。

它可以设置三个值。

  • Strict
  • Lax
  • None

2.1 Strict

Strict最为严格,完全禁止第三方 Cookie,跨站点时,任何情况下都不会发送 Cookie。换言之,只有当前网页的 URL 与请求目标一致,才会带上 Cookie。

Set-Cookie: CookieName=CookieValue; SameSite=Strict;

这个规则过于严格,可能造成非常不好的用户体验。比如,当前网页有一个 GitHub 链接,用户点击跳转就不会带有 GitHub 的 Cookie,跳转过去总是未登陆状态。

2.2 Lax

Lax规则稍稍放宽,大多数情况也是不发送第三方 Cookie,但是导航到目标网址的 Get 请求除外。

Set-Cookie: CookieName=CookieValue; SameSite=Lax;

导航到目标网址的 GET 请求,只包括三种情况:链接,预加载请求,GET 表单。详见下表。

请求类型示例正常情况Lax
链接<a href="..."></a>发送 Cookie发送 Cookie
预加载<link rel="prerender" href="..."/>发送 Cookie发送 Cookie
GET 表单<form method="GET" action="...">发送 Cookie发送 Cookie
POST 表单<form method="POST" action="...">发送 Cookie不发送
iframe<iframe src="..."></iframe>发送 Cookie不发送
AJAX$.get("...")发送 Cookie不发送
Image<img src="...">发送 Cookie不发送

设置了StrictLax以后,基本就杜绝了 CSRF 攻击。当然,前提是用户浏览器支持 SameSite 属性。

2.3 None

Chrome 计划将Lax变为默认设置。这时,网站可以选择显式关闭SameSite属性,将其设为None。不过,前提是必须同时设置Secure属性(Cookie 只能通过 HTTPS 协议发送),否则无效。

下面的设置无效。

Set-Cookie: widget_session=abc123; SameSite=None

下面的设置有效。

Set-Cookie: widget_session=abc123; SameSite=None; Secure

解决方案

解决方案就是显式设置 cookie 的 SameSite 属性为 None。一定是显式设置,response 中的 Set-Cookie 头里一定要包含 SameSite: None 字样。

参考文献

备注:文中提到 jsonp 以及 scrip 标签的 src 属性,主要是为了能让搜这几个关键字的人有可能搜索到这篇文章

一次 groovy 导致的 oom 排查

关键词:groovyshell evaluate 内存泄露

grovvy 版本 2.4.10

1. 起因

代码在线上运行一段时间后,容器会自动重启,查看容器监控发现是内存溢出导致了自动重启。

查看 jvm 堆情况。堆内存的使用实际不大,但是 docker 实例的内存却又占满了,又因为 docker 实例并没有安装 agent 进程,说明很大可能是 JVM 堆外内存导致。

使用 jstat -gc pid 查看,发现 Metaspace 占用很大

回想该应用出现状况前的改动,主要是新增了 groovy 脚本执行任务。查看代码发现每次执行都会创建对象

public Object evaluate(String name, String script, Binding binding) {
    binding.setVariable("logger", LoggerFactory.getLogger("script-" + name));
    binding.setVariable("builder", new Builder());
    binding.setVariable("selector", this.selector);
    GroovyShell shell = new GroovyShell(binding);
    return shell.evaluate(script);
}

看监控类加载总量很大,猜测是因为生成很多临时 class 对象,无法 gc

2. 排查之后的具体原因

new GroovyShell() 未指定 patentClassloader

所以 GroovyShell 初始化的时候会自动 new 一个 classLoader 并设置 GroovyShell.class.getClassLoader() 为父 classloader

所以最终加载脚本文件的时候是交给 GroovyShell.class.getClassLoader() 处理的

执行 evaluate 方法,只传入了脚本字符串,直接 evaluate 字符串不会缓存脚本,所以每次执行都会重新加载一次,生成一个全新的 class 对象

但是 JVM 中的 Class 只有满足以下三个条件,才能被回收(也就是该 Class 被卸载 unload)

  • 类所有的实例都已经被 GC,也就是 JVM 中不存在该 Class 的任何实例。
  • 加载该类的 ClassLoader 已经被 GC。
  • 该类的 java.lang.Class 对象没有在任何地方被引用

由于实际负责加载类的 classloader 不会回收,导致 class 对象一直存在于 Metaspace 并且随着时间推移越来越多,并且启动时没有设置 Metaspace 最大值,导致 Metaspace 空间无限增长,最终容器自身 oom,自动重启

对于64位JVM来说,元空间的默认初始大小是20.75MB,默认的元空间的最大值是无限

mysql 随便记

多个单独索引是可能被同时使用到的

MySQL5.0之前,单表一次查询只能使用一个索引,无法同时使用多个索引分别进行条件扫描。但是从5.1开始,引入了 index merge 优化技术,对同一个表可以使用多个索引分别进行条件扫描。相关文档:http://dev.mysql.com/doc/refman/5.6/en/index-merge-optimization.html

index(a) index(b)

select * from table where a=1 and b=1 可能会用到索引 a 和 b(index_merge),也可能只用到 a 或者 b

mysql 会选择它觉得快的索引策略,但是 index_merge 不一定能比单索引查询快,这个要根据表里的数据具体分析。

如果没有单独按照 b 查询的场景,这里可以设置为联合索引 index(a, b),查询条件为 a=1 and b=2 时会比上述索引快一些

事务隔离级别

  • 读未提交(Read Uncommitted)
  • 读提交(Read Committed)
  • 可重复读(Repeated Read)
  • 串行化(Serializable)

默认是:可重复读(Repeated Read)

在事务隔离级别为「可重复读」的情况下,会出现「幻读」,这是网上以及各种面试经常看到的问题。

但是「幻读」如何定义,哪些现象被称为「幻读」,却很少有明确的说法。

网上能看到最多的关于幻读的描述是:一个事务相同的 sql 语句读取 2 次,得到的记录条数不一致(由于其他事务在第二次读取之前插入了符合查询条件的数据)。但是 mysql 中隔离级别设置为「可重复读」时,并不存在这个现象,实验环境为10.1.22-MariaDB,大家可以自己实验下。

关于「幻读」的定义还有另一个说法,我们模拟一个业务中比较常见的场景来说明。例如:

给用户给帖子点赞,用 uid 表示用户 id,tid 表示帖子 id,uid + tid 设置为唯一索引。
假如说这时候用户 1 要为帖子 2 点赞,代码做的事情就是:查询用户 1 对帖子 2 有没有点赞记录,如果有就什么都不做,没有则新增一条记录。假如这个操作并发了,A 事务查询点赞记录时没有查到,同时 B 事务查询点赞记录也没有查到,之后 A 事务插入了点赞数据,并且提交事务,然后 B 事务再插入这条数据的时候就会报唯一键冲突,引发异常。这个场景下 B 事务遇到的情况就属于「幻读」

因为前一个说法在 mysql 默认的隔离级别下不存在,据说是“由于 InnoDB 引擎的「可重复读」级别还使用了 MVCC,避免了这个问题”,所以我个人理解后一种才算是「幻读」

关于 mvcc 这里涉及数据库的「并发控制」,可以参考浅谈数据库并发控制 - 锁和 MVCC

这里我还想喷一下部分公司的面试官,上来就问如果出现了「幻读」怎么解决,而不提具体场景,事实上可能大家理解的「幻读」就不是一回事,并且「幻读」是一个抽象概念,不如直接说场景,不知道幻读的概念,不见得不能解决问题

网上关于幻读的解释

  1. 第一个事务对一个表中的数据进行了修改,这种修改涉及到表中的全部数据行。同时,第二个事务也修改这个表中的数据,这种修改是向表中插入一行新数据。那么,以后就会发生操作第一个事务的用户发现表中还有没有修改的数据行,就好象发生了幻觉一样。来源

mysql autocommit

表示除非显式地开始一个事务,否则每个查询都被当做一个单独的事务自动执行

关于行锁

InnoDB 通过索引来实现行锁,而不是通过锁住记录。因此,当操作的两条不同记录拥有相同的索引时,也可能会因为行锁而发生等待。

参考文献:

由“高数”想到的

任何复杂的东西,都是在简单东西的基础上构建出来的,关键是思路

任何东西,刚开始都是简陋的,随着时间的推移变得更好,关键是迭代

所以不要怕东西做的不好看,或者功能不全,先出一个能用的版本再说,否则想得再好都是一场空

遇到一个复杂问题不要怂,要能够拆解开来变成简单问题,等解决的复杂问题足够多了,之前的“复杂问题”就转变成了“简单问题”,之后就可以解决更复杂的问题。数学、物理等学科就很好的印证了这一点

所以我们需要的能力有两个:

  1. 解决简单问题的能力
  2. 化复杂为简单的能力

这两个能力组合在一起,就可以升级打怪,完成越来越复杂的任务

其实我们从小到大的学习过程,就是在培养这样一种能力

我相信这两个能力,智力正常的人都能够掌握。差别在于掌握这个能力的快慢,人与人的差异也就体现在这里

如果设定一个能力值,再给足够长的时间,所有人都能达到这个能力水平。可惜人的生命有限,不是所有人都能达到

那么我们让自己更强,就取决于花在提升自我能力上的时间,但是提升自我需要跳出舒适区,是很费力的事情,这个就需要人的自控力,做到坚持不懈才行

综上,人的核心竞争力在于 自控力思路,思路我认为是是天生的东西,后天难以培养,所以对于大众来说,提升自控力是一个成功的不二法门,但是与人先天的惰性做斗争也是一件很难的事情,所以成功不易,加倍努力

思路这个概念有点宽泛,指天才的想法

设置免密码 sudo 用户

这里要设置一个添加一个非 sudo 用户组的

例如要添加一个名为deploy的用户

  • 执行adduser deploy,按照提示添加用户
  • 执行visudo编辑 sudo 列表,不能直接vim /etc/sudoers,在# User privilege specification下面,加入以下内容

deploy ALL=(ALL) NOPASSWD:NOPASSWD:ALL

  • Ctrl + x,选择 Y,退出

然后就可以了

要改用户组的话,执行usermod -aG www-data deploy

www-data 是用户组