4种常见跨域方式总结

前言

跨域是前端开发中经常遇到的一个问题。本文梳理了 4 种常见的跨域解决方案,并附以示例来体现方案的解决思路。

前置知识

cookie 是如何工作的?

cookie 是 key-value 形式,存放在浏览器端的存储数据,常用于登录状态的保持。

交互过程
  1. 用户首次从网页登录站点,输入账号密码并发送请求到服务器端。
  2. 服务器验证登录请求的账号密码,验证成功后会生成一个 cookie。并将 cookie 放在 http 响应报文中,一起返回给浏览器。
  3. 浏览器接收到响应报文后,会将其中的 cookie 存储在本地。
  4. 当用户下一次访问网站时,请求的报文会携带有本地已存储过的所有 cookie。服务器端从请求报文中的 cookie 中,验证用户是否已经登录过。

同源过程

同源策略是浏览器一种重要的安全策略。它可以帮助减少恶意文档攻击的风险,如 XSS、CSRF 等攻击。 同源是指协议、域名和端口三者需要完全匹配才算同源。

限制内容
  • Cookie、LocalStorage、Indexed DB 等存储性内容(这里是指不同域直接不能相互访问对应域的存储内容)
  • Ajax、XMLHttpRequest 请求
  • DOM 结点的访问

跨域

不同域之间相互请求资源,就算作“跨域”。

允许跨域的标签
  • img
  • link
  • script
过程

跨域请求实际上已经正常发出去,服务器端收到请求并正常返回结果,只是结果被浏览器拦截了。

解决方案

前置操作

  1. npm init: 创建一个仓库
  2. npm install express --save,命令执行
  3. 使用 express 本地托管网页并访问,基础代码:
let express = require('express');
let app = express();
app.use(express.static(__dirname));
app.listen(3000);

__dirname 表示的是当前路径(存在 index.html),在浏览器端可通过 localhost:3000/index.html 访问托管的静态页面。

jsonp

原理:jsonp 方式通过 script 标签没有跨域限制的漏洞,可以得到其他域动态产生的 JSON 数据,但是需要服务端的支持。

优缺点:只支持 Get 请求,不支持 Post 等其他方式的请求,需要传递 JSON 数据时比较麻烦。

过程
  1. 浏览器端创建一个 script 标签,并设置该标签的 src 属性。src 属性是类似于 get 请求的 url 地址。并在请求中向服务器端传递一个 callback 参数。

  2. 服务器端接收到该请求,并将返回数据包裹在 callback 函数参数中,统一作为一个新的返回数据返回给前端。

  3. 返回成功后,浏览器端会根据调用 callback,此时 callback 中的参数就是服务器端返回的数据。callback 中可以添加对返回数据的处理

整个过程类似于服务器端调用前端传递的 callback 函数,并将服务器端需要返回的数据放在 callback 的参数中。

实现
  • frontend/index.html
<!DOCTYPE html>
<html lang="en">
  <head>
    <meta charset="UTF-8">
    <meta name="viewport" content="width=device-width, initial-scale=1.0">
    <title>Document</title>
  </head>
  <body>
    <script>
      function jsonp ({ url, params, callback }) {
        return new Promise((resolve, reject) => {
          // 创建 script 标签
          const script = document.createElement('script')
          // 将 callback 函数挂载在 window 对象上
          window[callback] = function (data) {
            resolve(data)
            document.body.removeChild(script)
          }
          params = { ...params, callback }
          // 生成参数 url 部分,形式类似于:wd=b&c=show
          let arrs = []
          for (let key in params) {
            arrs.push(`${key}=${params[key]}`)
          }
          // 设置 script 的 src 标签
          script.src = `${url}?${arrs.join('&')}`
          document.body.appendChild(script)
        })
      }

      jsonp({
        url: 'http://localhost:3000/msg',
        params: { msg: 'i_am_msg_from_client' },
        callback: 'show'
      }).then(data => {
        console.log('服务器端返回的数据:', data)
      })
    </script>
  </body>
</html>
  • backend/server.js
let express = require('express')
let app = express()
app.get('/msg', (req, res) => {
  console.log('req.query = ', req.query)
    const { wd, callback } = req.query
    console.log(wd)
    console.log(callback)
    res.end(`${callback}('i am msg from server')`)
})

app.listen(3000)
console.log('服务器正在监听 3000 端口...')

cors

原理:后端设置 Access-Control-Allow-Origin 就可以开启 CORS。该字段表示哪些域是可以访问资源的。而浏览器端在发送请求时,会自动在请求里加上一个 Origin 字段,代表当前域。当返回报文中的 Access-Control-Allow-Origin 与当前域的 Origin 字段值一致时。就代表浏览器可以访问访问跨域的资源。

  • frontend/index.html
<!DOCTYPE html>
<html lang="en">
  <head>
      <meta charset="UTF-8">
      <meta name="viewport" content="width=device-width, initial-scale=1.0">
      <title>cors 跨域方式</title>
  </head>
  <body>
      <script>
          // 简单请求 GET
          let gXhr = new XMLHttpRequest();
          gXhr.open('GET', 'http://localhost:4000/getData', true);
          gXhr.onreadystatechange = function() {
              if(gXhr.readyState === 4 && gXhr.status === 200) {
                  console.log('服务端响应 GET 请求成功:', gXhr.response);
              }
          }
          gXhr.send();
      </script>
      <script>
          // 复杂请求 PUT 
          let xhr = new XMLHttpRequest();
          // 设置 cookie, cookie 不能跨域
          document.cookie = 'name=shenzhen';
          // 前端设置请求带 cookie
          xhr.withCredentials = true;
          xhr.open('PUT', 'http://localhost:4000/getData', true);
          xhr.setRequestHeader('name', 'shenzhen')
          xhr.onreadystatechange = function () {
              if (xhr.readyState === 4) {
                  if ((xhr.status >= 200 && xhr.status < 300) || xhr.status === 304) {
                      console.log(xhr.response)
                      // 得到响应头,后端通过 Access-Control-Expose-Headers 设置 reponse 的响应头
                      console.log(xhr.getResponseHeader('name'))
                  }
              }
          }
          xhr.send()
      </script>
  </body>
</html>
  • backend/server.js
let express = require('express');
let app = express();
// 设置白名单,也就是设置哪些域可以访问当前域的资源
let whiteList = ['http://localhost:3000'];
app.use((req, res, next) => {
    // 前端发送 http 请求时,浏览器会在 header 里面加上 origin 字段,字段值是当前域
    let origin = req.headers.origin;
    if(whiteList.includes(origin)) {
        // 设置哪个源可以访问我
        res.setHeader('Access-Control-Allow-Origin', origin);
        // 允许携带哪个头访问我
        res.setHeader('Access-Control-Allow-Headers', 'name');
        // 允许哪个方法访问我
        res.setHeader('Access-Control-Allow-Methods', 'PUT');
        // 允许携带 cookie
        res.setHeader('Access-Control-Allow-Credentials', true);
        // 预检的存活时间
        res.setHeader('Access-Control-Max-Age', 6);
        // 允许返回的头
        res.setHeader('Access-Control-Expose-Headers', 'name');
        
        if (req.method === 'OPTIONS') {
            res.end()   // OPTIONS 请求不做任何处理
        }
    }
    next()
})

// 响应 PUT 请求
app.put('/getData', (req, res) => {
    console.log(`server: ${req.headers}`)
    res.setHeader('name', 'jw');
    res.end('I am msg form server. Response for PUT request.')
})

// 响应 GET 请求
app.get('/getData', (req, res) => {
    console.log(`server:${req.headers}`);
    res.end('I am msg from server. Response for GET request.')
})

app.use(express.static(__dirname));
app.listen(4000);
console.log('服务器端正在监听 4000 端口...')
  • 结果

可以看到,按照 index.html 中的执行顺序。首先会先执行 get 请求,它属于简单请求,在服务端设置 Access-Control-Allow-Origin 后就可以正常获取数据。而 put 请求属于复杂请求,需要发送一次预检 options 请求。通过对比 options 请求中的 origin ,判断当前是否能正常跨域。校验通过后,再发送真正的 put 请求,就可以获取返回数据了。

Node 服务器代理

node 服务器代理通过在浏览器与目标服务器间架设一个代理服务器负责转发实现的。因为服务器之间进行 http 请求时,是不会产生跨域问题的。但是,浏览器与代理服务器需要解决跨域的问题。

  • frontend/index.html
<script src="https://cdn.bootcss.com/jquery/3.3.1/jquery.min.js"></script>
<script>
    $.ajax({
        url: 'http://localhost:3000',
        type: 'post',
        data: { name: 'shenzhen', password: '123456' },
        contentType: 'application/json;charset=utf-8',
        success: function (result) {
            console.log('receive msg from server: ', result)
        },
        error: function (msg) {
            console.log(msg)
        }
    })
</script>

浏览器端向代理服务器发起请求。

  • mid_proxy_server/mid_server.js
const http = require('http')

// 接收浏览器端请求,需要先使用 cors 方式解决浏览器端与中间服务器的跨域问题
const server = http.createServer((request, response) => {
    // 代理服务器,直接和浏览器交互,需要设置 CORS 的头部字段
    response.writeHead(200, {
        'Access-Control-Allow-Origin': '*',
        'Access-Control-Allow-Methods': '*',
        'Access-Control-Allow-Headers': 'Content-Type'
    })

    // 将请求转发至目标服务器
    const proxyRequest = http.request({
        host: '127.0.0.1',
        port: 4000,
        url: '/',
        method: request.method,
        headers: request.headers
    }, serverResponse => {
        var body = '';
        serverResponse.on('data', chunk => {
            body += chunk
        });

        serverResponse.on('end', () => {
            console.log('The data is ' + body)
            // 将响应结果转发给浏览器
            response.end(body)
        });
    }).end()
})

server.listen(3000, () => {
    console.log('中间代理服务器运行在 http://localhost:3000')
})

代理服务器首先是要解决与浏览器端的跨域问题,通过后端设置 Access-Control-Allow-Origin 即可。在收到请求后,将请求转发至目标服务器,并将目标服务器返回结果再返回给浏览器。

nginx 代理

nginx 的整体思路实际上是和 node 中间件代理是类似的,代理和转发交由 nginx 进行处理。

  • nginx.conf 配置文件
 # nginx 反向代理
server {
    listen       80;
    server_name  127.0.0.1;  # 需要中转的请求
    location / {
       proxy_pass http://127.0.0.1:4000; # 反向代理
       proxy_cookie_domain http://127.0.0.1:4000 http://127.0.0.1; # 修改cookie里域名
       index index.html index.htm;
       # 当用 webpack-dev-server 等中间件代理接口访问nignx时,此时无浏览器参与,故没有同源限制,下面的跨域配置可不启用
       add_header Access-Control-Allow-Origin http://localhost:3000; # 当前端只跨域不带 cookie 时,可为 *
       add_header Access-Control-Allow-Credentials true;
    }
}
  • backend/server.js
var http = require('http')
var server = http.createServer()
var qs = require('querystring')
server.on('request', function (req, res) {
  console.log('服务器端接收到转发的请求...', req.url)
  var params = qs.parse(req.url.substring(2))
  var data = 'Login success!'
  // 向前台写cookie
  res.writeHead(200, {
    'Set-Cookie': 'l=a123456;Path=/;Domain=127.0.0.1:4000;HttpOnly', // HttpOnly:脚本无法读取
  })
  res.write(JSON.stringify(data))
  res.end()
})
server.listen('4000')
console.log('Server is running at port 4000...')

源码地址

4 种跨域方式示例实现

参考

原文链接:juejin.im

上一篇:Flutter——实现网易云音乐的滑动冲突处理效果
下一篇:[译]分享十五条 JavaScript 编程技巧

相关推荐

  • 高效的jQuery代码编写技巧总结

    最近写了很多的js,虽然效果都实现了,但是总感觉自己写的js在性能上还能有很大的提升。本文我计划总结一些网上找的和我本人的一些建议,来提升你的jQuery和javascript代码。

    4 年前
  • 高效地调试npm包总结

    【前言】 现在有这么一个场景,比如一个项目A中引用了一个npm包a, 如何高效地进行联调,当然如果直接调试npm包就能搞定,就不用联调,但很多情况并不如此。 现在提供一种思路: 在a中通过监听文件变化...

    1 个月前
  • 项目总结 - 构建vue cli3.0+express项目

    简介:本篇是记录搭建流程,不过多叙述,搭建的细节,主要以前端为主,项目是主要是为了重构前端代码,后端代码完全复用,还会有篇主要讲node项目的搭建 项目背景: 一个客服项目,原来是react+expr...

    2 年前
  • 项目开发中常常会遇到详情返回列表,列表默认在点击详情的高度的问题,特此总结一下,希望可以帮到你们

    刚刚解了一个详情返回列表时候,列表高度保持在点击进去的详情的问题,特此做个分享,希望大家碰到类似的问题可以直接一遍过,不用卡壳(因为这个在实际项目开发中经常会用到所以分享了出来) Vue api 是这...

    2 年前
  • 项目中遇到的vue知识点总结

    1.v-bind的常见用法: v-bind用来动态绑定属性值(使其能响应式更新)语法糖为:,与其对应还有v-on,用来监听事件,语法糖为@。 绑定属性值 &lt;div id="app"&...

    22 天前
  • 面试总结

    前端 前端 更新于 4月14日 约 45 分钟 1. px、rem 和 em的区别? em是相对于父元素的font-size的大小,一单位的em的值根据父元素font-size的改变而改变 rem是...

    7 个月前
  • 面试必备! CSS知识点总结

    一、 元素水平垂直居中的方法 水平居中 行内元素:text-align:center 已知元素的宽度 设置margin:0 auto 元素的宽度不确定 flex 布局 justify-...

    5 个月前
  • 面试常见问题之 JavaScript 内存机制 总结

    一、我们前端为什么要关注内存 1.防止内存泄露时网页占用的内存过大、引起客户端卡顿、无响应等给用户造成不良体验。 2.Node.js使用的是V8引擎,内存对于后端服务的性能至关重要,因为后端服务的持久...

    2 个月前
  • 面试官:说说Vue组件间通信有哪些方式(总结了6种)

    前言 当组件间通信的逻辑较为简单时,使用 Prop 和自定义事件足以应对; 但是当出现全局共享的状态、兄弟组件间通信等场景时,使用 Prop 和自定义事件可能会让逻辑变得非常复杂。

    2 个月前
  • 面试之深度与广度,前端几年总结

    游泳健身了解一下:github 和小伙伴一起搞的日常总结 深度与广度 有一定的深度,且广度也需要全面,可以不会,但要听说过(没吃过猪肉,也要见过猪跑),不然哪来方案 以点破面 用公司角度去考虑,如...

    1 个月前

官方社区

扫码加入 JavaScript 社区