web

记一次前端跨域的问题

自力更生, 丰衣足食

Posted by Lorry on May 13, 2019

文章字数:4511, 阅读全文大约需要:12 分钟

跨域资源共享 CORS(Cross Origin Resource Sharing)

浏览器实现对不同源的网址的资源获取的行为.详见维基百科.这篇文章的起因来源于需要向别的部门进行资源请求, 不在同一个域, 资源请求不到, 等了两天还没解决后便通过node的express来代理请求, 解决了这个问题.

定义(WHAT)

  1. 什么叫不同源(满足下列条件任意一个)
  • 不同的域名(domain)
    • baidu.comgoogle.com 这样名称不相同的就叫不同 domain
  • 不同的端口
    • localhost:8000localhost:3000
    • 如果不写端口号, 根据协议有默认值
      • http: 80
      • https: 443
      • ftp: 21
  • 不同的协议
    • http https ftp ws wss等两两都属于不同地方协议.
  1. 什么叫跨域请求?

利用ajax, image, script等方式, 向不同域请求资源的行为称为跨域请求, 浏览器会与服务端进行沟通, 去判断是否可以进行跨域资源获取, 从而决定请求时成功还是失败.

假设当前域为(可在请求头中查看) Origin: http://www.foo.com

服务器端设置: Access-Control-Allow-Origin: http://www.foo.com

如果时公共资源:设置为 *, 即让任意域的请求都可以响应.

  1. 跨域驳回

如果服务器的设置与当前请求的Origin不匹配, 那么浏览器就会驳回这个请求, 无法请求到任何资源, 并会在console中报CORS的错误.所以请注意, 跨域请求并不是没有发请求, 只是浏览器发现跨域后拦截了响应.

为什么(WHY)

小网站可能把所有的资源都放在同一个域下, 但是大型网站内容太多了, 一个服务器或者一个域是放不下了, 而且跨部门的接口调用也很常见, 所以需要使用CORS. 那么又会扯出一个新的问题, 为什么要有同源策略?一句话: 你会同意www.porn.com这个网站访问到你爱存不存的银行账户吗?

这里需要区别一下CSRF(Cross-Site-Request-Forgery)

  • CSRF不需要响应, 只需要你的身份凭证向第三方发送请求. 大致流程:
    • 登陆支付宝账户
    • 访问a.com
    • a.com向支付宝发送转账到hacker的请求
    • 浏览器发现之前支付宝的cookie信息还未过期, 带着cookie一起发送过去
    • 支付宝发现请求中带有之前已验证的cookie, 认为是本人操作, 同意转账 可以看到CSRF更多的是在服务端的防御性. 而CORS是着眼客户端的资源获取.他们都属于同源策略下的产物, 但有很大的区别

怎么做(HOW)

实现跨域的具体方式:

最常用的: XMLHTTPRequest

const xhr = new XMLHTTPRequest()
xhr.onreadystatechange = function() {
    if (xhr.readyState == 4) {
        // 判断响应有效性
        if ((xhr.status >= 200 && xhr.status < 300) || xhr.status == 304) {
            console.log(xhr.responseText)
        } else {
            console.error('something error: ', xhr.status)
        }
    }
}
xhr.open('get', 'www.foo.com', true);
xhr.send(null)

open方法参数 - 两个必选参数: - method: ‘post’, ‘get’ - url: ‘www.foo.com’ - 三个可选参数 - async: true/false 是否异步 - username:string - password: string

跨域请求的限制 - 无法接收和发送cookies, 那么如果有需要进行用户验证的话可选参数中的username和password, 请避免使用明文, 可加密后传输 - 无法通过setRequestHeader()自定义头部, 要不然服务器要啥origin我传啥origin, 就不会有任何作用了. - 调用getAllResponseHeaders()获取详细的响应头也只会返回一个空字符串

PreFlighted Request 预发请求

其中有一个细节需要注意的就是浏览器在发现是跨域请求的时候, 是不会先发送这个请求, 而实先发一个 OPTIONS 请求, 这个就是预发请求.将会发送以下头部:

Origin: www.bar.com
Access-Origin-Request-Method: GET
Access-Origin-Request-Header: ABC(可选)

发送请求后, 服务器决定是否接收请求并响应

Access-Control-Allow-Origin: www.bar.com
Access-Control-Allow-Method: GET, POST
Access-Control-Allow-Header: ABC
Access-Control-Max-Age: 3600

这里需要说一下的就是这个Access-Control-Max-Age, 这里规定了多少时间内可以不用再发Option去检测, 而是直接发送请求.所以preflight request的额外请求消耗只在第一次的时候发送.

还有一个特殊的响应头:Access-Control-Allow-Credentials: true/false, 默认情况下跨域请求时部提供cookie, HTTP认证等凭据的. 这里不会影响发送的请求, 只会影响响应, 如果发送的请求携带了验证信息, 但是服务端的响应头里的该字段为false的话浏览器不会将响应交给JavaScript, 即xhr.responseText为空字符串.

注意, 如果将该字段设置为true之后, Access-Controle-Origin-Allow: *是不允许的, 必须指定一个特定的域. 这还是为了安全着想.避免之前说的CSRF

你看, 跨域很简单, 但是需要服务端配合.如果使用express的话可以这样去配置:

// node
const express = require('express');
const cors = require('cors')
const app = express()
const CORSConfig = cors({
    origin: 'http://localhost:8000'
})
app.get('/res', CORSConfig, (req, res) => {
    console.log(req)
    res.send('done')
})
app.listen(3000, () => {
    console.log('listening 3000')
})

<!--index.html -->
<script>
    const xhr = new XMLHttpRequest()
    xhr.onreadystatechange = () => {
        if (xhr.readyState == 4) {
            // 判断响应有效性
            if ((xhr.status >= 200 && xhr.status < 300) || xhr.status == 304) {
                console.log(xhr.responseText) // 'done'
            } else {
                console.error('something error: ', xhr.status)
            }
        }
    }

    xhr.open('get', 'http://localhost:3000/res', true)
    xhr.send(null)  
</script>

其他跨域技术

图像ping

<img>标签可以指定src, src可以传入一个网址, 但无法处理响应, 所以, 一般只是为了不需要任何返回的统计数据用.

const img = new Image()
img.src = 'www.someAnalisticSite.com'
img.onload = function() }{
    console.log('done')
}

其余类似的还有 <link>, <video> <audio> <embed> <object>等可引用外部文件的标签

JSONP(JSON with padding 填充式JSON或参数式JSON)

JSONP的实现方式其实就是利用 <script>. 在script的src中传入一个callback

function callAfter(response) {
    console.log(`name: ${response.name}, age: ${response.age}`)
}
const script = document.createElement('script')
script.src = 'www.someBakeEnd.com/json/?callback=callAfter';
// 该接口返回的json格式为: {name: 'lorry', age: '26'}
document.body.insertBefore(script, document.body.firstChild)

JSONP相比于Img的优势在于可以处理响应的数据, 但是缺点还是安全性, 一定保证请求的接口可信任, 要不然请求后的json数据是直接加载到当前js环境中搞破坏.第二个缺点就是不知道JSONP的响应状态, 不想XHR请求有readystatechange事件可以监听.不过H5标准对script元素加了onerror事件.可以通过onload和onerror来进行状态监听.