前端跨域的一些常见问题

CORS 说了无数次,基本都是个别资源需要配置而已,在前面全站跨域的情况下还是出现了些小问题。

项目需要国内国外使用不同的api域来避免主域不能在国内备案的问题。所以在国内环境会启用新的备案域名做api接口请求域,不就是跨域嘛 简单…

原本以为只是需要配置下 Access-Control-Allow-xx 相关响应头,异步请求配置下 withCredentials 携带cookie,调整下相对路径,后面实际又陆续发现了新的问题,这个做下记录。

同源策略

首先还是说下同源策略,源文档和加载的资源需要 协议/主机/端口 完全一致,不完全相同的情况下会存在一定的限制,也就是跨域。

限制包含不限于如下:

  • LocalStorage、SessionStorage、Cookie等存储读写操作
  • 异步请求比如xhr、fetch 等
  • iframe,无法跨域读写操作等
  • script 无法捕获详细异常
  • canvas 无法导出图片等

但是也恰恰是因为了这些限制才保证了我们可以安全的浏览网页。常见的前端安全问题可以参见这里:常见前端安全问题

CORS

前面说了浏览器的同源策略和产生的一些限制,那对于解决异步请求限制最通用的方法就是使用CORS机制(Cross-Origin Resource Sharing),跨域资源共享。

常见CORS的场景

  • xhr fetch 异步请求
  • font-face跨域字体资源
  • canvas导出跨域绘制图片

常见的 CORS 头配置

使用 CORS 需要服务端在允许跨域的资源配置相关的相应头,浏览器会根据相关响应做放行或者拦截处理。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
# 允许跨域来访问的源站,* 表示所有,多个站点通过,分割
access-control-allow-origin: https://www.xxxx.com

# 允许的request自定义请求头
access-control-allow-headers: Origin, X-Requested-With, Content-Type, Accept

# 允许的request请求方法
access-control-allow-methods: GET, POST, OPTIONS

# 如果需要携带cookie凭证 (配置为true时 access-control-allow-origin 不能直接配置*)
access-control-allow-credentials: true

# 列出了哪些首部可以作为响应的一部分暴露给外部(异步下载文件需要注意)
access-control-expose-headers: Content-Disposition

另外根据请求的方法和请求头,会分为 简单请求非简单请求
非简单请求需要 预检,先发送 OPTIONS 请求通过后才能发送正式请求,可以通过 Access-Control-Max-Age 配置 OPTIONS 请求缓存,这里不细讲

遇到的问题

前面简单介绍了CORS的常用配置,下面主要记录下实际项目中遇到的一些常见而 不常见 问题。

异步请求下载文件失败

项目中有些导出文件采用 xhr 请求后端返回文件流的形式下载,同时文件名读取 content-disposition 响应头。

如下伪代码:

导出代码

1
2
3
4
5
6
7
8
async function exportXls() {
const res = await http(args);
const fileName = res.headers['content-disposition'].replace(/.*filename="(.*)?"/, '$1');

const blobUrl = window.URL.createObjectURL(res.body);
download(blobUrl, fileName);
window.URL.revokeObjectURL(blobUrl);
}

response头

1
2
3
...
content-disposition: attachment; filename="xxxRecord_2000_01_01.xls"
content-type: application/vnd.ms-excel;charset=utf-8

CORS后就出现了问题

content-disposition 头读取失败导致文件名获取失败。

查阅资料发现跨域时候默认 content-disposition 不会暴露给浏览器访问,需要配置 Access-Control-Expose-Headers 来指定。这个知识点真的有点偏…

默认情况下,只有六种 simple response headers (简单响应头)可以暴露给外部

  • Cache-Control
  • Content-Language
  • Content-Type
  • Expires
  • Last-Modified
  • Pragma

配置 Access-Control-Expose-Headers 格式如下:

1
Access-Control-Expose-Headers: <header-name>, <header-name>, ...

最终通过配置 access-control-expose-headers: Content-Disposition 解决了问题

SameSite 的兼容方案

SameSite 是Chrome为了加强浏览器安全性,防止 CSRF 攻击和用户追踪的cookie属性,在Chrome 70多的版本之后开始疯狂提示注意升级,并且在 80的版本将会在浏览器中默认设置为 Lax

比较烦人的是这个存在兼容性问题,如下这样一条 setCookie 语句,可能在ie或者其它不支持SameSite的浏览器下无法成功写入cookie。

1
set-cookie: cookieName=hzhzhz; expires=Thu, 27-Aug-2020 07:08:16 GMT; Max-Age=7200; path=/; SameSite=None; secure

最终通过写入两条同名cookie,一个旧的一个sameSite,关于 SameSite 属性这里单独介绍下 cookie-sameSite-兼容方案

Safari itp 协议导致三方cookie被限制

ITP协议是苹果为了加强用户隐私对站点存储的一些限制,最新的2.0的版本会限制三方cookie。目的是和SameSite类似,但比SameSite更加强硬。

不过目前暂时不影响 session cookie,所以到还好。

对于广告最终或用户统计类服务来说会有较大影响,更多关于Safari-itp协议应对策略这里单独介绍:Safari-itp协议

2020-09-01 网站在 Safari 下无法登录了,发现跨域 http response 的setCookie也无法写入和发送了。因为已经完全被阻止了三方cookie,即使是httponly,只能调整通过token来做用户认证

使用相对路径的资源请求

这一点是和实际业务相关的,改起来挺多的也最容易遗漏。

比如:

  • 异步请求所有都是以 / 开头,需要调整到新的域名
  • 一些三方组件可能也继承了数据请求,比如图片上传组件,那配置的url也需要处理
  • 通过a标签直接下载文件的场景
  • 等等..

解决:

  • 首先提供 getApiPath() 方法来获取到实际的接口地址
  • 公共 http service 会处理异步请求接口地址,但是三方插件还是有漏网然后通过重写 XMLHttpRequest.prototype.open 方法在过滤了一次 / 开头地址
  • a 标签这种只能统一排查修改

主要就是一不小心发现有漏改 …