• Golang——通过实例了解并解决CORS跨域问题


    跨源资源共享

    实例

    ​ 运行在http://localhost:8082端口的前端服务器express和运行在http://localhost:8080端口的后端服务器golang net/http。前端的javaScript代码使用fetch()函数发起一个到http://localhost:8080/api/students的请求。

    后端代码:

    /server/main.go

    import (
    	"encoding/json"
    	"fmt"
    	"net/http"
    
    	_ "github.com/go-sql-driver/mysql"
    	"github.com/jmoiron/sqlx"
    )
    
    var db *sqlx.DB
    
    type Student struct {
    	ID   int64
    	Name string
    	Sex  string
    	Age  int64
    }
    
    // 连接数据库
    func init(){
        dns := "user:pwd@tcp(localhost:3306)/db_name?charset=utf8&parseTime=True&loc=Local"
    	db, err = sqlx.Open("mysql", dns)
    	if err != nil {
            fmt.Println(err)
            return
    	}
    	db.SetMaxOpenConns(2000)
    	db.SetMaxIdleConns(1000)
    }
    
    // 获取所有学生信息(数据自己事先插入)
    func GetAllStudent() []Student {
    	sqlStr := "SELECT * FROM student"
    	students := make([]Student, 0)
    	rows, _ := db.Query(sqlStr)
    	student := Student{}
    	for rows.Next() {
    		rows.Scan(&student.ID, &student.Name, &student.Sex, &student.Age)
    		students = append(students, student)
    	}
    	defer rows.Close()
    	return students
    }
    
    func GetAllStudentInfo(w http.ResponseWriter, r *http.Request) {
    	students := GetAllStudent()
    	resp := make(map[string]interface{})
    	resp["msg"] = "成功"
    	resp["code"] = "200"
    	resp["data"] = students
    	jsonResp, err := json.Marshal(resp)
    	if err != nil {
    		fmt.Println(err)
    		return
    	}
    	w.Write(jsonResp)
    }
    
    func main() {
    	http.HandleFunc("/api/students", GetAllStudentInfo)
    	http.ListenAndServe(":8080", nil)
    }
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11
    • 12
    • 13
    • 14
    • 15
    • 16
    • 17
    • 18
    • 19
    • 20
    • 21
    • 22
    • 23
    • 24
    • 25
    • 26
    • 27
    • 28
    • 29
    • 30
    • 31
    • 32
    • 33
    • 34
    • 35
    • 36
    • 37
    • 38
    • 39
    • 40
    • 41
    • 42
    • 43
    • 44
    • 45
    • 46
    • 47
    • 48
    • 49
    • 50
    • 51
    • 52
    • 53
    • 54
    • 55
    • 56
    • 57
    • 58
    • 59
    • 60
    • 61
    • 62

    前端代码:

    没有下载express的在/client目录执行:

    npm install express --save-dev

    /client/main.js

    import express from 'express'
    
    // 返回了一个服务器对象
    const app = express()
    // express.static(): 指定静态资源所在目录
    app.use(express.static('./'))
    app.listen(8082)
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7

    启动前端服务器:node main.js

    /client/students.html

    DOCTYPE html>
    <html lang="zh">
    <head>
        <meta charset="UTF-8">
        <title>学生信息title>
    head>
    <body>
        <script>
            // 以同步方式获取响应
            async function getStudents() {
                const promiseResp = await fetch("http://localhost:8080/api/students")
                const resp = await promiseResp.json()
                console.log(resp)
            }
            getStudents()
        script>
    body>
    html>
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11
    • 12
    • 13
    • 14
    • 15
    • 16
    • 17
    • 18

    访问:http://localhost:8082/students.html

    [外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-J8Wzk6Cp-1663642784944)(images/image-20220919222211766.png)]

    可以看到控制台里打印的并不是我们预期的后端给的数据,这是为什么呢?

    首先,我们要知道照成这个错误的原因是什么,我们先看整个请求相应的流程是什么样的:

    [外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-IsstQQj6-1663642784945)(images/image-20220919224956766.png)]

    问题清楚了,那么如何解决呢?

    解决方法1:

    交给后端来做

    其实我们发送fetch请求的时候,如果你的发送者和你要访问的资源不同源的情况下,就会在请求中包含一个特殊的头Origin,这个头代表着发送者的源是谁,比如说我们这个例子里,发送者是students.html,它的源是localhost:8082,所以当students.html发一个请求给后端服务器的时候,就会携带Origin:http://localhost:8082,告诉后端服务器发送者来自于哪里(通俗来说就是,我是8082端口的人,我来要你8080端口的资源,你给不给吧),那么对于后端服务器这边来讲就要对这个请求做出选择了,如果允许8082访问自己的资源,就需要在响应里包含一个Access-Control-Allow-Origin头,如果不允许8082访问自己的资源,不加这个头即可。如果这个头的内容是Access-Control-Allow-Origin:http://localhost:8082,意思就是后端服务器这个响应只能给http://localhost:8082端口使用,别人不让用,如果这个头的内容是Access-Control-Allow-Origin:*,意思就是这个响应谁都可以用。

    我们打开F12,查看网络:

    [外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-mb5HEs6p-1663642784946)(images/image-20220920100058884.png)]

    [外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-ww68r5Wg-1663642784946)(images/image-20220920100141932.png)]

    可以看到请求头里是有一个上面说的Origin头,上面说了,只要他fetch发生了跨域,就会有一个Origin头。

    我们来看服务器的响应,可以看到并没有做处理,服务器响应这边并没有Access-Control-Allow-Origin头,所以浏览器拿到这个响应之后报错了,发现后端服务器那边没有允许。

    [外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-yytlUITi-1663642784947)(images/image-20220920100354945.png)]

    说到这里,想必也知道如何处理了,在后端服务器的响应里加入这个头,允许http://localhost:8082使用这个响应即可:

    w.Header().Set("Access-Control-Allow-Origin", "http://localhost:8082")
    
    • 1

    重新启动后端服务器,刷新页面可以看到浏览器将响应给了students.html页面,此时在查看响应表头,就会发现有了Access-Control-Allow-Origin头:

    [外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-zMNOjtaC-1663642784948)(images/image-20220920100901698.png)]

    解决方法2:

    交给前端来做

    除了上面说的解决方法1,还可以通过代理解决:

    在这里插入图片描述

    ​ 这次我们在前端服务器里加入了一个代理的插件,此时前端服务器就和浏览器有一个约定,原本浏览器有一部分请求发送给8082,有一部分发送给8080,这个新的约定就是说:

    以后浏览器的所有请求都发给前端服务器8082,所以发请求就应该是向http://localhost:8082/api/students发了,可是8082并有这个数据呀,8080才有, 所以这个请求就要发给前端服务器的代理,然后由代理间接的再找8080请求数据,然后8080会把数据响应给8082,再由8082间接的返回给浏览器里的students.html

    这时候我们来看,对于浏览器来说,有没有发生跨域问题?

    • 并没有,因为它是向同源的8082发的请求,是没有Origin头的。

    至于代理发的请求,它是通过JavaScript的API发请求,接响应的,是没有什么同源策略、跨域问题。

    跨域和同源都是浏览器的特殊行为。

    如何区分我这个请求到底是走8082还是走8080呢?

    ​ 一般是通过请求的前缀路径来区分的,比如说需要找后端要的数据,咱们都给他加一个特殊的前缀/api/,这样只要你的请求是以/api/开头的,这些请求都是走代理,然后经过代理间接找后端请求的,如果你的请求没有加/api/这个前缀,这些请求就访问8082自己,找到这些网页资源。

    看下面代码就明白了:

    如果没有下载http-proxy-middleware,在/client目录执行:

    npm install http-proxy-middleware --save-dev

    /client/students.html

    // 修改请求地址,由8080改为8082
    const promiseResp = await fetch("http://localhost:8082/api/students")
    
    • 1
    • 2

    /client/main.js

    import express from 'express'
    import { createProxyMiddleware } from 'http-proxy-middleware'
    
    // 返回了一个服务器对象
    const app = express()
    // express.static(): 指定静态资源所在目录
    app.use(express.static('./'))
    // 添加代理,凡是以/api为前缀的,都代理到 http://localhost:8080
    app.use('/api', createProxyMiddleware({
        target: "http://localhost:8080",
        changeOrigin: true
    }
    ));
    
    app.listen(8082)
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11
    • 12
    • 13
    • 14
    • 15

    重启前端服务:node main.js

    再次访问http://localhost:8082/student.html

    可以看到响应被获取到了:

    [外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-Ab20C6zF-1663642784950)(images/image-20220920105601280.png)]

    查看网络,请求头里是没有Origin头的:

    [外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-FEjIzFYs-1663642784951)(images/image-20220920105729208.png)]

    总结:

    1. 只要协议、主机、端口之一不同,就是不同源,比如:

      http://localhost:8080/ahttps://localhost:8080/b就不同源。

    2. 同源检查是浏览器的行为,而且只针对fetchXMLHttpRequest请求

      • 如果是其他客户端,例如golang net/http clientpostman,他们是不做同源检查的。

      • 通过表单提交,浏览器直接输入url地址这些方式发送的请求,也不会做同源检查。

        [外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-th5FpjnA-1663642784949)(images/image-20220920104637137.png)]

    3. 更多相关知识请参考:

      MDN:https://developer.mozilla.org/zh-CN/docs/Web/HTTP/CORS

    补充:

    Golang解决跨域的完整代码:

    上面给的解决方法1,只是针对例子的简陋的版本,真正go通过CORS解决跨域问题的完整代码:

    gin中间件:

    func Cors(context *gin.Context) {
    	method := context.Request.Method
    	 // 1. [必须]接受指定域的请求,可以使用*不加以限制,但不安全
    	//context.Header("Access-Control-Allow-Origin", "*")
    	context.Header("Access-Control-Allow-Origin", context.GetHeader("Origin"))
    	fmt.Println(context.GetHeader("Origin"))
    	// 2. [必须]设置服务器支持的所有跨域请求的方法
    	context.Header("Access-Control-Allow-Methods", "POST, GET, PUT, DELETE, OPTIONS")
    	 // 3. [可选]服务器支持的所有头信息字段,不限于浏览器在"预检"中请求的字段
    	context.Header("Access-Control-Allow-Headers", "Content-Type, Content-Length, Token")
    	 // 4. [可选]设置XMLHttpRequest的响应对象能拿到的额外字段
    	context.Header("Access-Control-Expose-Headers", "Access-Control-Allow-Headers, Token")
    	// 5. [可选]是否允许后续请求携带认证信息Cookir,该值只能是true,不需要则不设置
    	context.Header("Access-Control-Allow-Credentials", "true")
    	// 6. 放行所有OPTIONS方法
    	if method == "OPTIONS" {
    		context.AbortWithStatus(http.StatusNoContent)
    		return
    	}
    	context.Next()
    }
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11
    • 12
    • 13
    • 14
    • 15
    • 16
    • 17
    • 18
    • 19
    • 20
    • 21

    原生HTTP中间件:

    func corsMiddleware(next http.Handler) http.Handler {
    	return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
    		// 1. [必须]接受指定域的请求,可以使用*不加以限制,但不安全
    		// w.Header().Set("Access-Control-Allow-Origin", "*")
    		w.Header().Set("Access-Control-Allow-Origin", r.Header.Get("Origin"))
    		// 2. [必须]设置服务器支持的所有跨域请求的方法
    		w.Header().Set("Access-Control-Allow-Methods", "POST,GET,PUT,DELETE,OPTIONS")
    		// 3. [可选]服务器支持的所有头信息字段,不限于浏览器在"预检"中请求的字段
    		w.Header().Set("Access-Control-Allow-Headers", "Content-Type,Content-Length,Token")
    		// 4. [可选]设置XMLHttpRequest的响应对象能拿到的额外字段
    		w.Header().Set("Access-Control-Expose-Headers", "Access-Control-Allow-Headers,Token")
    		// 5. [可选]是否允许后续请求携带认证信息Cookir,该值只能是true,不需要则不设置
    		w.Header().Set("Access-Control-Allow-Credentials", "true")
    		next.ServeHTTP(w, r)
    	})
    }
    
    func main() {
    	http.Handle("/api/test", corsMiddleware(http.HandlerFunc(test)))
    	http.ListenAndServe(":8080", nil)
    }
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11
    • 12
    • 13
    • 14
    • 15
    • 16
    • 17
    • 18
    • 19
    • 20
    • 21
  • 相关阅读:
    java-php-python-ssm校园失物招领系统计算机毕业设计
    SCA Nacos 服务注册和配置中心(一)
    linux系统离线安装docker(分步法&一键法)
    win10系统下Yolov5目标检测环境搭建(Anaconda3+Pytorch+Yolov5,CPU,无GPU)
    LeetCode13——罗马数字转整数
    Webpack5 Asset Module 使用小结
    没有几年经验你真学不会这份SpringCloud实战演练文档
    MyBatis #{} 和 ${} 的区别
    【PAT甲级 - C++题解】1101 Quick Sort
    三种常见的移动底盘运动学模型分析
  • 原文地址:https://blog.csdn.net/weixin_52000204/article/details/126949152