【Go Web开发】跨域请求

在接下来的文章中,我们将讨论一个全新的主题,并更新我们的应用程序,使其支持来自JavaScript的跨域请求(CORS: Cross-origin resource sharing))。

你将学习到:

  • 什么是跨域请求,为什么浏览器默认阻止跨域请求。
  • 普通请求和跨域请求之间的区别。
  • 如何使用Access-Control请求头来允许或拒绝特定的跨域请求。
  • 考虑到安全因素,你需要注意什么时候配置你的应用程序可以跨域请求。

CORS概述

在深入代码前,或者开始具体讨论跨域请求之前,让我们先来定义一下术语origin的含义。

本质上,如果两个url具有相同的schema、主机和端口(如果指定了),则称它们具备相同的域。为了说明这一点,让我们比较以下url:

URL A URL B 同域? 原因
https://foo.com/a http://foo.com/a NO schema不同(http vs https)
https://foo.com/a http://www.foo.com/a NO host不同(foo.com vs www.foo.com)
https://foo.com/a http://foo.com:443/a NO 端口不同(默认端口 vs 443)
https://foo.com/a http://foo.com/b NO url路径不同
https://foo.com/a http://foo.com/a?b=c NO 查询字符串不同
https://foo.com/a#b http://foo.com/a#c NO 路径不通
https://foo.com/a#b http://foo.com/a NO schema不同(http vs https)

理解什么是origin是很重要的,因为所有的web浏览器都实现了一种被称为同域策略的安全机制。在浏览器实现这个策略的方式上有一些非常小的区别,但广义上来说:

  • 一个域的网页可以在其HTML中嵌入来自另一个域的特定类型的资源——包括图像、CSS和JavaScript文件。例如,在你的网页中这样做是可以的:
 <img src="http://anotherorigin.com/example.png" alt="example image">
  • 一个域上的网页可以向另一个域发送数据。例如,网页中的HTML表单可以向不同的域提交数据。
  • 但是一个域上的网页不允许接收来自不同域的数据。

这里的关键是最后一个要点:同域策略不允许(潜在恶意)其他域网站从本域读取数据。

必须强调的是,同域策略不会阻止数据的跨域发送,尽管这很危险。事实上,这就是为什么CSRF攻击可能发生,以及为什么我们需要采取额外的步骤来防止它们——比如使用samsite cookie和CSRF令牌。

作为一名开发人员,您最可能遇到同域策略是在浏览器中运行JavaScript的跨域请求时。例如:假设你有一个http://foo.com网页包含一些前端JavaScript代码。如果这个JavaScript试图发起https://bar.com/data.json请求(不同的域),然后请求被发送到bar.com服务端处理,但用户的浏览器将阻止接收响应,以至于https://foo.com这边的JavaScript代码接收不到返回数据。

一般来说,同域策略是一种非常有用的安全保护措施。虽然它在一般情况下是好的,但在某些情况下,你可能想要将这种约束放开。例如,你有一个API服务域名为api.example.com和一个JavaScript前端应用运行在www.example.com,那么你可能想要允许www.example.com前端应用跨域访问API服务。

或者你有一个完全开放的公共API服务,你想允许来自任何地方的跨域请求,这样其他开发人员就可以很容易地与他们自己的网站集成。

幸运的是,大多数现代web浏览器允许你通过设置API响应的访问控制头来允许或禁止特定的跨域请求。在接下来的几节中,我们将详细解释如何做到这一点以及这些响应头是如何工作的。

演示同源(Same-Origin)策略

为了演示同源策略是如何工作的,以及如何在对API的请求中放开同源策略,我们需要模拟一个来自不同源的对API的请求。

可以快速地创建一个简单的Go应用程序来模拟这个跨域请求。从本质上说,我们希望第二个应用程序包含一些JavaScript的网页,然后向我们的 GET / v1 / healthcheck接口发起请求。

如果你跟随本系列文章的操作,创建文件cmd/example/cors/simple/main.go来写我们的第二个应用程序。

$ mkdir -p cmd/example/cors/simple
$ touch cmd/example/cors/simple/main.go

添加以下代码:

File:cmd/example/cors/simple/main.go


package main

import (
    "flag"
    "log"
    "net/http"
)

//定义HTML网页字符串常量。
const html = `
<!DOCTYPE html> 
<html lang="en"> 
<head>
    <meta charset="UTF-8"> 
</head>
<body>
    <h1>Simple CORS</h1> 
    <div id="output"></div> 
    <script>
        document.addEventListener('DOMContentLoaded', function() { 
            fetch("http://localhost:4000/v1/healthcheck").then(
                function (response) { 
                    response.text().then(function (text) {
                        document.getElementById("output").innerHTML = text; 
                    });
                }, 
                function(err) {
                    document.getElementById("output").innerHTML = err; 
                }
            ); 
        });
    </script> 
</body>
</html>`

func main()  {
    //允许服务地址在运行时可根据命令行参数配置
    addr := flag.String("addr", ":9000", "Server address")
    flag.Parse()

    log.Printf("starting server on %s", *addr)
    //启动HTTP服务,并监听给定的地址。并对上面的HTML中所有请求进行应答。
    err := http.ListenAndServe(*addr, http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
        w.Write([]byte(html))
    }))
    log.Fatal(err)
}

这里Go代码我们应该很熟悉了,下面一起来看看<script>标签中的JavaScript代码:

<script>
        document.addEventListener('DOMContentLoaded', function() { 
            fetch("http://localhost:4000/v1/healthcheck").then(
                function (response) { 
                    response.text().then(function (text) {
                        document.getElementById("output").innerHTML = text; 
                    });
                }, 
                function(err) {
                    document.getElementById("output").innerHTML = err; 
                }
            ); 
        });
    </script>

在这个代码中:

  • 我们使用fetch()函数向API服务的健康检查接口发起请求。默认发送的是GET请求,但是也可以配置不同的HTTP方法,并添加自定义请求头。稍后我们再介绍如何实现。
  • fetch()方法是异步工作的,并返回响应。然后使用then()方法在响应中设置回调函数:如果接口调用成功就执行第一个回调函数,否则就执行第二个回调。
  • 在“成功”回调中使用response。text()读取响应body,然后使用document.getElementById("output").innerHTML将响应body替换<div id="output"><div>元素。
  • 在“失败”回调中将返回的错误消息替换<div id="output"><div>元素。
  • 整个逻辑放在document.addEventListener(''DOMContentLoaded', function(){...}),表示fetch()函数只有在用户浏览器把HTML内容加载完之后才会执行。

提示:这里不是关于JavaScript的,不用担心其中的细节。你需要知道的就是JavaScript向API服务的健康检查接口发起请求,然后将响应内容填入到<div id="output"><div>元素中。

演示

下面开始测试下,请启动第二个应用程序:

$ go run ./cmd/example/cors/simple 
2022/01/09 09:57:20 starting server on :9000

然后打开一个新的终端,把我们的API应用服务启动:

$ go run ./cmd/api 
{"level":"INFO","time":"2022-01-09T01:58:42Z","message":"database connection pool established"}
{"level":"INFO","time":"2022-01-09T01:58:42Z","message":"starting server","properties":{"addr":":4000","env":"development"}}

此时你的API服务启动的域为http://localhost:4000,JavaScript所在网页运行在域为:http://localhost:9000。因为端口不一样,它们处在不同域。因此,浏览器访问http://localhost:9000时,fetch()向http://localhost:4000/v1/healthcheck发起请求时会被同域策略阻止。具体来说,API服务会接收和处理请求的,但是浏览器会阻塞以至于JavaScript无法读取到响应。

让我们来测试下。打开浏览器然后访问http://localhost:9000,你会看到CORS报头后面跟着类似这样的错误消息:

提示:这里的错误消息是浏览器定义的,我用的是chrome浏览器,因此如果你使用其他浏览器可能错误不太一样。

这里你可以打开浏览器开发者工具,并刷新页面,然后看看控制台日志。你应该能看到一条消息表示GET /v1/healthcheck接口的响应因同域策略被阻止。如下所示:

Access to fetch at 'http://localhost:4000/v1/healthcheck' from origin 'http://localhost:9000' has been blocked by CORS policy

您可能还希望打开开发者工具中的网络活动选项,并检查与被阻止的请求相关的HTTP头。


这里有些重要的信息需要指出。首先请求的url说明是把请求发送到我们的API服务的,并且API服务处理完请求后将200 OK返回给浏览器。请求本身并没有被同域策略阻止,而是浏览器没有将响应内容传给JavaScript。

第二个需要指出的是:web浏览器自动设置请求的Origin头,以显示请求的来源,如下所示:

Origin: http://localhost:9000

我们将在下一节中使用这个头信息来帮助我们有选择地放开同域策略,这取决于我们是否信任请求的来源。最后需要强调的是同域策略只在浏览器中使用。在浏览器之外,任何应用都可以向API服务发起请求,使用curl、wget等工具都可以读取到返回内容。这完全不受同源策略的影响。

©著作权归作者所有,转载或内容合作请联系作者
  • 序言:七十年代末,一起剥皮案震惊了整个滨河市,随后出现的几起案子,更是在滨河造成了极大的恐慌,老刑警刘岩,带你破解...
    沈念sama阅读 217,406评论 6 503
  • 序言:滨河连续发生了三起死亡事件,死亡现场离奇诡异,居然都是意外死亡,警方通过查阅死者的电脑和手机,发现死者居然都...
    沈念sama阅读 92,732评论 3 393
  • 文/潘晓璐 我一进店门,熙熙楼的掌柜王于贵愁眉苦脸地迎上来,“玉大人,你说我怎么就摊上这事。” “怎么了?”我有些...
    开封第一讲书人阅读 163,711评论 0 353
  • 文/不坏的土叔 我叫张陵,是天一观的道长。 经常有香客问我,道长,这世上最难降的妖魔是什么? 我笑而不...
    开封第一讲书人阅读 58,380评论 1 293
  • 正文 为了忘掉前任,我火速办了婚礼,结果婚礼上,老公的妹妹穿的比我还像新娘。我一直安慰自己,他们只是感情好,可当我...
    茶点故事阅读 67,432评论 6 392
  • 文/花漫 我一把揭开白布。 她就那样静静地躺着,像睡着了一般。 火红的嫁衣衬着肌肤如雪。 梳的纹丝不乱的头发上,一...
    开封第一讲书人阅读 51,301评论 1 301
  • 那天,我揣着相机与录音,去河边找鬼。 笑死,一个胖子当着我的面吹牛,可吹牛的内容都是我干的。 我是一名探鬼主播,决...
    沈念sama阅读 40,145评论 3 418
  • 文/苍兰香墨 我猛地睁开眼,长吁一口气:“原来是场噩梦啊……” “哼!你这毒妇竟也来了?” 一声冷哼从身侧响起,我...
    开封第一讲书人阅读 39,008评论 0 276
  • 序言:老挝万荣一对情侣失踪,失踪者是张志新(化名)和其女友刘颖,没想到半个月后,有当地人在树林里发现了一具尸体,经...
    沈念sama阅读 45,443评论 1 314
  • 正文 独居荒郊野岭守林人离奇死亡,尸身上长有42处带血的脓包…… 初始之章·张勋 以下内容为张勋视角 年9月15日...
    茶点故事阅读 37,649评论 3 334
  • 正文 我和宋清朗相恋三年,在试婚纱的时候发现自己被绿了。 大学时的朋友给我发了我未婚夫和他白月光在一起吃饭的照片。...
    茶点故事阅读 39,795评论 1 347
  • 序言:一个原本活蹦乱跳的男人离奇死亡,死状恐怖,灵堂内的尸体忽然破棺而出,到底是诈尸还是另有隐情,我是刑警宁泽,带...
    沈念sama阅读 35,501评论 5 345
  • 正文 年R本政府宣布,位于F岛的核电站,受9级特大地震影响,放射性物质发生泄漏。R本人自食恶果不足惜,却给世界环境...
    茶点故事阅读 41,119评论 3 328
  • 文/蒙蒙 一、第九天 我趴在偏房一处隐蔽的房顶上张望。 院中可真热闹,春花似锦、人声如沸。这庄子的主人今日做“春日...
    开封第一讲书人阅读 31,731评论 0 22
  • 文/苍兰香墨 我抬头看了看天上的太阳。三九已至,却和暖如春,着一层夹袄步出监牢的瞬间,已是汗流浃背。 一阵脚步声响...
    开封第一讲书人阅读 32,865评论 1 269
  • 我被黑心中介骗来泰国打工, 没想到刚下飞机就差点儿被人妖公主榨干…… 1. 我叫王不留,地道东北人。 一个月前我还...
    沈念sama阅读 47,899评论 2 370
  • 正文 我出身青楼,却偏偏与公主长得像,于是被迫代替她去往敌国和亲。 传闻我的和亲对象是个残疾皇子,可洞房花烛夜当晚...
    茶点故事阅读 44,724评论 2 354

推荐阅读更多精彩内容