Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

JS 中常见的几种跨域 #20

Open
lurenacm opened this issue Jun 21, 2021 · 0 comments
Open

JS 中常见的几种跨域 #20

lurenacm opened this issue Jun 21, 2021 · 0 comments

Comments

@lurenacm
Copy link
Owner

lurenacm commented Jun 21, 2021

跨域

所谓的跨域是指:只要协议 http/https,域名,端口号有一个不一样就是跨域请求(非同源请求)。例如:http://example.com 和 http://api.example.com 之间的请求就是跨域请求。真实项目当中跨域是很常见的,真正的同源项目很少。

  • 跨域中http请求其实 浏览器依旧能够发送请求出去,服务器依旧能够响应请求,但是浏览器会将响应的数据给劫持了,所以前端拿不到这个响应。

跨域限制的是什么

  • 限制读取修改源域名的 DOM。
  • 限制读取携带源域名的 cookie、localStorage、indexDB。
  • 限制 XMLHttpRequest 的请求。

什么是同源策略

同源策略是浏览器的一个安全策略,如果两个 URL 协议、域名端口号一致就是同源,不一致就是非同源

产生跨域的原因

跨域是由于服务器的安全策略限制导致的,后端不存在跨域的问题。

  • 大型项目往往有多个服务器,比如专门的图片服务器,接口服务器,web 服务器等,这些服务器之间的域名肯定是不相同的,这样就存在跨域请求的问题。
  • 我们本地的服务器 http://localhost:3000 和获取线上服务器的数据也会造成跨域问题。
  • 调取第三方服务器接口也会造成跨域问题。

跨域有助于分离开发和部署。

浏览器是怎么实现将数据给劫持的?

浏览器中的进程

  • 浏览器是多进程的,比如 chrome 浏览器,就有5个进程。 渲染进程、浏览器主进程、GPU 进程、插件进程、网络进程
  • 渲染进程 又可以划分多个线程 GUI渲染线程、异步 http 请求线程、js引擎线程、定时器线程、事件处理线程。

劫持原理

  • 在调用 xhr.send() 时,浏览器为了防止黑客通过脚本获取到系统资源,浏览器会将渲染进程放入到 沙箱 中。在沙箱中欧你的渲染进程是没有办法进行网络请求的。但是只能通过网络进程发送请求,所以这里设计到进程间的通信方式。在操作系统中进程的通信方式有 消息队列、共享内存、管道、套接字 socket、信号量 等。
  • 浏览器中 chromium 内部使用的是 Unix socket 套接字,实现进程之间的相互通信,发起网络请求。服务端将响应发送回去,浏览器会去检查响应头中是否存在允许的跨域域名,没有就将响应体丢掉,达到数据劫持的目的。

所谓的沙箱是 操作系统限制进程对内存地址的一个访问权限。

常见的三种跨域方式

浏览器的安全策略会对 ajax 和 fetch 的跨域请求会有限制。提示一个跨域请求的错误信息。

JSONP

JSONP 实现跨域的原理:利用 script(/link/img也可以) 标签不存在跨域的限制,请求数据信息。

// 本地的地址是 http://localhost:3000
<script src="https://cdn.jsdelivr.net/npm/vue/dist/vue.js"></script>

本地的代码请求了一个域名和协议都不相同的地址,就会造成跨域,但是利用浏览器不对 script 标签请求限制实现跨域请求。

JSONP 具体实现跨域原因

一开始客户端向服务器发送了一个带有回调函数的url请求链接,比如

<script src="http://168.0.0.1/api/list?callback=func">

服务端接收到这个 callback 函数后执行,准备客服端需要的数据传递给客服端。浏览器接收到数据后执行服务器返回的回调函数。这个回调函数是一个全局下的函数。

模拟实现 JSONP 的跨域请求函数

  • 创建一个 script 标签,url 链接后面问号 ? 携带一个回调函数必须是一个全局函数。例如上面的http://168.0.0.1/api/list?callback=func
// 客户端 index.js
function jsonp(url, callback) {
    let script;
    url = url.includes('?') ? `${url}&${callback}` : `${url}?${callback}`
    script = document.createElement('script')
    script.src = url
    document.body.appendChild(script)
    // window['callback'] = callback
    // 这里加一层判断,callback 是否有放回值
    window['callback'] = (data) =>{
        callback && callback(data)
        delete window['callback']
    }
}

jsonp('http://127.0.0.1:3000', (data) => {
    console.log(data)
})

// server.js
let express = require('express')
let app = express()
app.get('/', function(req, res) {
  let {callback} = req.query
  console.log(callback)
  let data = {
    a: 12
  }
  res.send(`callback(${JSON.stringify(data)})`)
})
app.listen(3000, () => {
  console.log('ok')
})

特别注意,浏览器获取到 callback(${JSON.stringify(data)}) 后发现这段代码是字符串函数,会解析字符串函数,在全局环境下执行这段代码,所以我们需要在全局环境 window 中先挂载这个函数。

JSONP 缺陷

JSONP 的缺点也很明显,JSONP 利用的是 script 标签实现的跨域请求,但是 script 标签只能发送 GET 请求,不能发送 POST 等请求。

CORS 跨域

CORS 需要浏览器和服务器端的配合,主要实现是在服务端设置允许的请求的第三方源。让浏览器跨域的安全策略可以允许第三方源的请求。

  • CORS 跨域的请求分为 简单请求和复杂请求两种

CORS 跨域的简单请求

GET、POST 或者 HEAD 发起的请求

  • 所谓的简单请求就是 浏览器在请求头中添加了一个字段 origin:域名(要向服务器请求的源),服务器拿到请求的源后,会响应回一个字段 Access-Control-Allow-Origin:域名,如果 origin 不在这个字段的范围内,浏览器就会抛出跨域的错误。成功的话才会发送真正的请求。

CORS 跨域的非简单请求

PUT、delete、connection等

  • CORS 跨域非简答请求中,浏览器会向服务器发送一个试探请求,看是否能和服务器发送跨域请求,服务器放回 200 表示可以,浏览器才继续发送真实的请求。
  • 非简单请求会先发送一个 预检请求,预检请求的请求方法是 option,同时也会携带上 origin 字段。
OPTIONS / HTTP/1.1
Origin: 当前地址
Host: xxx.com
Access-Control-Request-Method: PUT
Access-Control-Request-Headers: X-Custom-Header
  • 预检请求的响应回来后如果满足 Access-Control-Allow-Origin,那么才会发送真正的请求体。
HTTP/1.1 200 OK
Access-Control-Allow-Origin: *
Access-Control-Allow-Methods: GET, POST, PUT
Access-Control-Allow-Headers: X-Custom-Header
Access-Control-Allow-Credentials: true
Access-Control-Max-Age: 1728000
Content-Type: text/html; charset=utf-8
Content-Encoding: gzip
Content-Length: 0

CORS 跨域具体实现

// 设置可以跨域的请求源,
Access-Control-Allow-Origin: "http://loacalhost:3000"

// 设置是否可以携带资源凭证 cookie
Access-Control-Allow-Credentials: true

Access-Control-Allow-Origin 可以设置所有源 *,也可以携带单一源。注意所有源不能携带资源凭证 cookie,单一源可以携带。

Proxy

webpack 的 Proxy

Proxy 跨域代理是在 webpack 中 devServer 配置需要跨域的域名

const path = require('path')
module.exports = {
  entry:'./index.js',
  output:path.resolve(__dirname, 'dist'),
  devServer:{
    port: '3000',
    hot: true,
    proxy:{
      '/api': {
        target: 'http://168.0.1.1:1001',
        changeOrigin: true,
      },
    }
  }
}

proxy 表示访问 /api 的接口都会被代理到 http://localhost:3000 上面。

webpack 的 proxy 跨域原理

上面我们提到过跨域是浏览器的安全策略限制导致的,但是后台之间的通信不存在跨域的问题。webpack 中的 devServer 会在本地开始一个node服务端口号是 3000,虽然外部服务器的端口号是 1001 也是可以通信的。node 在上面中提供了中间层的代理而已,负责转发和接收的作用。

缺点:这种方式只适合在本地开发,生产环境不适合。那么如何在生产环境下也适合呢?这可以使用 nginx 代理

nginx 代理

nginx 代理也是起到一个中间处理的作用,在 nginx 配置中,配置需要代理的域名即可。

  • nginx 中的 server 配置项中有一个 location 对象,location 对象中的 proxy_pass 可以配置方向代理的域名
// proxy服务器
server {
    listen       81;
    server_name  www.example.com;
    location / {
        proxy_pass   http://www.example2.com:8080;  #反向代理
        root html;
    }
}

nginx 中配置 cors 实现跨域

  • 为什么需要将 cors 跨域移动到 nginx 中实现,如果请求都是经过 nginx 才进入的可以移动到 nginx 中配置 cors。也可以直接在服务端中配置 cors 跨域。
  • 在 nginx 中通过 server 中的 location 配置项中 写入 if 判断请求的域名信息,如果命中就通过add_header 写入允许跨域的请求源 Allow-access-control-origin 域名,凭证,请求方法等。
location ^~ /api/v1 {
    add_header 'Access-Control-Allow-Origin' "$http_origin"; 
    add_header 'Access-Control-Allow-Methods' 'GET, POST, PUT, DELETE, OPTIONS'; 
    add_header 'Access-Control-Allow-Credentials' 'true'; 
    if ($request_method = 'OPTIONS') { 
        add_header 'Access-Control-Allow-Origin' "$http_origin"; 
        add_header 'Content-Type' 'text/html charset=UTF-8'; 
        add_header 'Content-Length' 0; 
        return 200; 
    } 
    # 这下面是要被代理的后端服务器,它们就不需要修改代码来支持跨域了
    proxy_pass http://127.0.0.1:8085; 
    proxy_set_header Host $host; 
    proxy_redirect off; 
    proxy_set_header X-Real-IP $remote_addr; 
    proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for; 
    proxy_connect_timeout 60; 
    proxy_read_timeout 60; 
    proxy_send_timeout 60; 
}

总结

  • 最简单的使用方式就是 proxy 代理方式,开发环境下可以直接使用 webpack 的 devServer 中的 proxy 配置。线上环境可以使用 nginx 代理服务器的方式。
  • CORS 代理需要服务器的单独配置。
  • JSONP 使用的是 script 标签不受浏览器的跨域策略的限制
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Projects
None yet
Development

No branches or pull requests

1 participant