Go实现简单CORS与非简单请求的预检请求案例

CORS

跨源资源共享(CORS)是一种基于 HTTP 头的机制,该机制通过允许服务器标示除了它自己以外的其他源(域、协议或端口),使得浏览器允许这些源访问加载自己的资源。

跨源资源共享还通过一种机制来检查服务器是否会允许要发送的真实请求,该机制通过浏览器发起一个到服务器托管的跨源资源的“预检”请求。在预检中,浏览器发送的头中标示有 HTTP 方法和真实请求中会用到的头。

构造简单前端

首先创建一个简单的前端页面,页面有一个用于显示消息的文本区域和一个带有两个按钮的简单表单。单击按钮时,它会调用 JavaScript 函数 onGet() ,向其传递版本号。其中 v1 将会因为 CORS 问题失败,v2 将解决这个问题

<html>
    <head>
        <meta charset="UTF-8" />
        <title>Fixing Common Issues with CORS</title>
    </head>
    <body>
        <h1>Fixing Common Issues with CORS</h1>
        <div>
            <textarea id="messages" name="messages" rows="10" cols="50">Messages</textarea><br/>
            <form id="form1">
                <input type="button" value="Get v1" onclick="onGet('v1')"/>
                <input type="button" value="Get v2" onclick="onGet('v2')"/>
            </form>
        </div>
    </body>
</html>

onGet() 函数将版本号插入到 URL 中,然后向 API 服务器发出提取请求。

function onGet(version) {
    const url = "http://localhost:8000/api/" + version + "/messages";
    var headers = {}
    
    fetch(url, {
        method : "GET",
        mode: 'cors',
        headers: headers
    })
    .then((response) => {
        if (!response.ok) {
            throw new Error(response.error)
        }
        return response.json();
    })
    .then(data => {
        document.getElementById('messages').value = data.messages;
    })
    .catch(function(error) {
        document.getElementById('messages').value = error;
    });
}

创建一个 go 文件,启动服务器 go run frontend.go 并输入 http://localhost:8080 查看静态内容。

package main

import (
	"github.com/gin-contrib/static"
	"github.com/gin-gonic/gin"
)

func main() {
	r := gin.Default()
	r.Use(static.Serve("/", static.LocalFile("./frontend", false)))
	r.Run(":8080")
}

构造简单的 REST API

每当向指定的 URL 发出 GET 请求时,都会调用该函数 GetMessages() 。它返回包含消息的 JSON 字符串。该 URL 包含一个路径参数,该参数将为 v1v2 。服务器侦听端口 8000。

package main

import (
	"fmt"
	"strconv"
	"net/http"

	"github.com/gin-gonic/gin"
)

var messages []string

func GetMessages(c *gin.Context) {
	version := c.Param("version")
	fmt.Println("Version", version)
	c.JSON(http.StatusOK, gin.H{"messages": messages})
}

func main() {
	messages = append(messages, "Hello CORS!")
	r := gin.Default()
	r.GET("/api/:version/messages", GetMessages)
	r.Run(":8000")
}

如何解决简单的 CORS 问题

解决方案

现在有两个服务,一个前端服务位于 http://localhost:8080,后端服务位于 http://localhost:8000,尽管他们有相同的主机名,但在 CORS 的角度上,他们监听不同的端口因此处于不同域。如果 JavaScript fetch 请求指定 mode 为 cors,则将添加一个请求标头,用于标识源:

Origin: http://localhost:8080

进入前端页面,点击 Get v1 ,会报错

Access to fetch at ‘http://localhost:8000/api/v1/messages’ from origin ‘http://localhost:8080’ has been blocked by CORS policy: No ‘Access-Control-Allow-Origin’ header is present on the requested resource. If an opaque response serves your needs, set the request’s mode to ‘no-cors’ to fetch the resource with CORS disabled.

该消息指出,由于 CORS 策略,浏览器已阻止该请求获取响应。它提出了两种解决方案。第二个建议是将 JavaScript 获取请求中的 mode from cors 更改为 no-cors 。这不是一个好的选择,因为浏览器在模式 no-cors 下总是会删除响应数据,以防止未经授权的客户端读取数据。
Request.mode - Web API 接口参考 | MDN (mozilla.org)

该问题的解决方案是让服务器设置一个响应标头,浏览器根据这个标头就知道能不能允许它发出跨域请求。

Access-Control-Allow-Origin: http://localhost:8080

这会告知 Web 浏览器,允许指定域的跨域请求。如果该响应标头中指定的域与 Origin 请求标头中指定的域匹配,则浏览器不会阻止 JavaScript 接收响应。
因此,当 URL 包含 v2GetMessages() 函数更改为即可解决简单的CORS问题。

func GetMessages(c *gin.Context) {
	version := c.Param("version")
	fmt.Println("Version", version)
	if version == "v2" {
		c.Header("Access-Control-Allow-Origin", "http://localhost:8080")
	}
	c.JSON(http.StatusOK, gin.H{"messages": messages})
}

预检请求

尽管简单修复了主要的 CORS 问题,但仍存在一些限制。其中一个限制是只允许使用 HTTP、GET 和 OPTIONS 方法。GET 和 OPTIONS 方法是只读的,并且被认为是安全的,因为它们不会修改现有内容。

POST、PUT 和 DELETE 方法 (非简单请求)可以添加或更改现有内容,这些被认为是不安全的,当用户想修改服务器数据的时候,浏览器会发出一个预检请求 (preflight),预检请求可以查看服务器是否支持当前跨域请求 (服务端正常会设置允许使用的方法),因此预检请求没有具体方法而是 OPTION

新建表单:

<form id="form2">
    <input type="text" id="puttext" name="puttext"/>
    <input type="button" value="Put v1" onclick="onPut('v1')"/>
    <input type="button" value="Put v2" onclick="onPut('v2')"/>
</form>

onPut 会生成一个 PUT 请求,在请求正文中发送表单参数。

function onPut(version) {
    const url = "http://localhost:8000/api/" + version + "/messages/0";
    var headers = {}

    fetch(url, {
        method : "PUT",
        mode: 'cors',
        headers: headers,
        body: new URLSearchParams(new FormData(document.getElementById("form2"))),
    })
    .then((response) => {
        if (!response.ok) {
            throw new Error(response.error)
        }
        return response.json();
    })
    .then(data => {
        document.getElementById('messages').value = data.messages;
    })
    .catch(function(error) {
        document.getElementById('messages').value = error;
    });
}

定义 PUT 处理程序和函数:

r.PUT("/api/:version/messages/:id", PutMessage)

//处理函数
func PutMessage(c *gin.Context) {
	version := c.Param("version")
	id, _ := strconv.Atoi(c.Param("id"))
	text := c.PostForm("puttext")
	messages[id] = text
	if version == "v2" {
	    c.Header("Access-Control-Allow-Origin", "http://localhost:8080")
	}
	c.JSON(http.StatusOK, gin.H{"messages": messages})
}

重新加载页面,点击 Put v1 按钮,会得到一个与之前不同的错误

Access to fetch at ‘http://localhost:8000/api/v1/messages/0’ from origin ‘http://localhost:8080’ has been blocked by CORS policy: Response to preflight request doesn’t pass access control check: No ‘Access-Control-Allow-Origin’ header is present on the requested resource. If an opaque response serves your needs, set the request’s mode to ‘no-cors’ to fetch the resource with CORS disabled.

错误显示进行了 preflight 检查,并且没有设置 Access-Control-Allow-Origin 标头。

浏览器发现,这是一个非简单请求,就自动发出一个"预检"请求,要求服务器确认可以这样请求。下面是这个"预检"请求的HTTP头信息。
在这里插入图片描述

查看 API 服务器控制台的输出:
[GIN] | 404 | 1.07µs | OPTIONS "/api/v1/messages/0"

与之前的示例不同,在 GET 示例中,浏览器发出了请求但阻止了响应。

而现在浏览器拒绝发出 PUT 请求,它向同一 URI 发送了 OPTIONS 请求。仅当 OPTIONS 请求返回正确的 CORS 标头时,它才会发送 PUT 请求。(由于服务器不知道 OPTIONS 预检请求是针对什么方法的,因此预检请求会在请求标头中指定该方法例如:Access-Control-Request-Method:PUT)
在这里插入图片描述

通过为 OPTIONS 请求添加一个处理程序来解决此问题

func OptionMessage(c *gin.Context) {
	c.Header("Access-Control-Allow-Origin", "http://localhost:8080")
}

func main() {
	messages = append(messages, "Hello CORS!")
	r := gin.Default()
	r.GET("/api/:version/messages", GetMessages)
	r.PUT("/api/:version/messages/:id", PutMessage)
	r.OPTIONS("/api/v2/messages/:id", OptionMessage)   //仅针对 `v2` 进行设置
	r.Run(":8000")
}

如果重启服务器点击 Put v2 会得到另一个错误:

Access to fetch at ‘http://localhost:8000/api/v2/messages/0’ from origin ‘http://localhost:8080’ has been blocked by CORS policy: Method PUT is not allowed by Access-Control-Allow-Methods in preflight response.

预检检查需要 Access-Control-Allow-Method 标头来阻止 PUT 请求被阻止,即需要设置允许使用的方法。

func OptionMessage(c *gin.Context) {
	c.Header("Access-Control-Allow-Origin", "http://localhost:8080")
	c.Header("Access-Control-Allow-Methods", "GET, OPTIONS, POST, PUT") //设置允许使用的方法
}

参考链接:
代码可以参考:解决 CORS 和 JavaScript 的常见问题 |Okta 开发人员 — Fixing Common Problems with CORS and JavaScript | Okta Developer
跨域资源共享 CORS 详解 - 阮一峰的网络日志 (ruanyifeng.com)
(55 封私信 / 80 条消息) Fetch API 的 mode 参数是用来干什么的? - 知乎 (zhihu.com)
跨源资源共享(CORS) - HTTP | MDN (mozilla.org)
跨域的解决方法有哪些?JSONP的原理?CORS怎么使用?Nginx如何设置?_哔哩哔哩_bilibili

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值