进阶教程 11. 同源策略/跨域

532 阅读4分钟

这是我参与8月更文挑战的第22天,活动详情查看:8月更文挑战

一、同源策略

1.1 什么是同源策略?

同源策略(Same-origin Policy):为了保证浏览器的信息安全,浏览器采用同源策略,保证当前源中的资源只能在当前的源中使用;其他源如果需要使用当前源资源,需要特殊技术,这种A源访问B源的资源的通信称为跨域;

1.2 示例:

let xhr = new XMLHttpRequest();
xhr.open('GET', 'https://www.baidu.com/', true);
xhr.onreadystatechange = function () {
   if (xhr.readyState === 4 && xhr.status === 200) {
      console.log('xxx')
   }
};
xhr.send();

  • 以上请求会报错:
Access to XMLHttpRequest at 'https://www.baidu.com/' from origin 'http://localhost:63342' has been blocked by CORS policy: No 'Access-Control-Allow-Origin' header is present on the requested resource.

当出现以上错误时说明你正在进行一个跨域的操作;

1.3 同源策略的要求:

同源策略要求通信的两个源的协议、域名、端口号要相同,如果三者中任意一个不同就是不满足同源策略;不满足同源策略的通信就是跨域;

1.4 常用的跨域解决方案:

  1. JSONP
  2. 服务端转发,因为同源策略只在客户端存在,在服务端是不存在的;所以可以由服务端转发请求;
  3. nginx转发,nginx是服务器应用程序,它可以接受客户端的请求,然后根据规则可以配置自动转发;
  4. CORS: Cross-Origin-Resource-Sharing: 需要目标域设置 Access-Control-Allow-Origin 头信息;

二、JSONP

JSONP是一种常用的解决跨域的方式;

2.1 原理:

利用script的src属性是不受同源策略约束的,可以访问不同服务器或者端口号下的数据

  1. 提前声明一个叫做fn的函数,给fn设置一个形参;
  2. 在页面给script的src的指向的路径拼接一个callback属性,callback=fn;当浏览器解析到这个script标签时,会向src指向的路径发起http请求;
  3. 服务器收到这个请求后,会返回一个fn(这里面是服务器返回的数据)
  4. fn({xxx}) 这个是让fn执行,小括号里面就是服务器发送给我们的数据

2.2 示例:

JS代码:

function fn(data) {
   console.log(data);
}

HTML代码

<script src="http://matchweb.sports.qq.com/kbs/calendar?columnId=100000&callback=fn"></script>

三、CORS

3.1 定义

CORS: CORS是一个W3C标准,全称是"跨域资源共享"(Cross-origin resource sharing)。 它允许浏览器向跨源服务器,发出XMLHttpRequest请求,从而克服了AJAX只能同源使用。

使用 CORS 来解决跨域问题时,通过约定 HTTP 响应头信息告知客户端目前的跨域允许情况,常见有以下这些头信息:

  1. Access-Control-Allow-Headers 允许携带的请求头,* 表示任意;
  2. Access-Control-Allow-Methods 跨域访问时使用的方法,常见的方法 GET,PUT,POST,DELETE,OPTIONS
  3. Access-Control-Allow-Max-Age 设置一个跨域配置信息的缓存期限,比如 10s, 标识 10s 内不要再发 OPTIONS 请求询问跨域配置情况;
  4. Access-Control-Allow-Credentials, 设置是否允许携带 cookietrue 是否允许携带 cookie 来,如果不设置这个值,目标源也无法设置客户端的 cookie

3.2 一个示例

以下是一个 Nodejs 开发的简易 server 代码:

const http = require('http');
const fs = require('fs');
const path = require('path')
const url = require('url');


http.createServer((req, res) => {
  let { pathname, query } = url.parse(req.url, true)
  let method = req.method.toUpperCase()
  res.setHeader('Access-Control-Allow-Headers', 'token'); 
  res.setHeader('Access-Control-Allow-Methods', 'GET,PUT,POST,DELETE,OPTIONS'); 
  res.setHeader('Access-Control-Allow-Max-Age', '10'); 
  res.setHeader('Access-Control-Allow-Credentials', true); 

  // res.setHeader()
  if (method === 'OPTIONS') {
    return res.end();
  }
  if (pathname === '/user') {
    switch (method) {
      case 'GET':
        res.end(JSON.stringify({ name: 'ok' }))
        break
      case 'POST':
        let arr = []
        req.on('data', chunk => arr.push(chunk))
        req.on('end', () => {
          let r = Buffer.concat(arr).toString()
          res.setHeader('Set-Cookie', 'a=333333')
          res.end(JSON.stringify({b: 'post'}))
        })
    }
    return
  }

  let filepath = path.join(__dirname, pathname)

  fs.stat(filepath, (err, statObj) => {
    if (err) {
      res.statusCode = 404;
      return res.end('not found')
    }
    if (statObj.isFile()) {
      return fs.createReadStream(filepath).pipe(res)
    }
    res.statusCode = 404;
    res.end('not found')
  })
}).listen(8081, () => {
  console.log('ok 8081')
})

3.3 注意事项

3.3.1 form 表单

form 表单的提交是不存在跨域问题的,form 的提交可以提交到任意的源;

<form action="http://www.unkown.com/some/path/app.php" method="get">
  <p>First name: <input type="text" name="f_name" /></p>
  <p>Last name: <input type="text" name="l_name" /></p>
  <input type="submit" value="Submit" />
</form>

3.3.2 复杂请求和 OPTIONS

当浏览器识别到这是一个跨域行为时,如果该请求是个复杂请求则会发起一次 OPTIONS 请求,向服务器问询是否允许跨域等相关配置;

GET/POST 都是简单请求,如果请求中包含了自定义的请求头将变为复杂请求,这个时候浏览器自动发送 OPTIONS 请求,再得到服务端正确的 CORS 响应后才会发起真正的请求。如果服务器 OPTIONS 请求没有返回,或者返回不正确,则不回复发送真正请求并抛出同源策略限制的错误;