前往小程序,Get更优阅读体验!
立即前往
首页
学习
活动
专区
工具
TVP
发布
社区首页 >专栏 >浅学前端:跨域问题

浅学前端:跨域问题

原创
作者头像
传说之下的花儿
发布2023-11-10 18:27:21
3320
发布2023-11-10 18:27:21
举报

拓展:跨源资源共享

1. 实例

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

后端代码:

/server/main.go

代码语言:javascript
复制
 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)
 }

前端代码:

没有下载express的在/client目录执行: npm install express --save-dev

/client/main.js

代码语言:javascript
复制
 import express from 'express'
 ​
 // 返回了一个服务器对象
 const app = express()
 // express.static(): 指定静态资源所在目录
 app.use(express.static('./'))
 app.listen(8082)

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

/client/students.html

代码语言:javascript
复制
 <!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>

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

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

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

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

方法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,查看网络:

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

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

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

代码语言:javascript
复制
 w.Header().Set("Access-Control-Allow-Origin", "http://localhost:8082")

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

方法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

代码语言:javascript
复制
 // 修改请求地址,由8080改为8082
 const promiseResp = await fetch("http://localhost:8082/api/students")

/client/main.js

代码语言:javascript
复制
 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)

重启前端服务:node main.js

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

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

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

总结:

  1. 只要协议、主机、端口之一不同,就是不同源,比如: http://localhost:8080/ahttps://localhost:8080/b就不同源。
  2. 同源检查是浏览器的行为,而且只针对fetchXMLHttpRequest请求
    • 如果是其他客户端,例如golang net/http clientpostman,他们是不做同源检查的。
    • 通过表单提交,浏览器直接输入url地址这些方式发送的请求,也不会做同源检查。

2. 解决跨域问题

1.什么是跨域

由于浏览器的同源策略限制,进而产生跨域拦截问题。同源策略是浏览器最核心也最基本的安全功能;所谓同源(即指在同一个域)就是两个页面具有相同的协议(protocol),主机(host)和端口号(port)。

同源策略(Same origin policy)是一种约定,它是浏览器最核心也最基本的安全功能,如果缺少了同源策略,则浏览器的正常功能可能都会受到影响。可以说Web是构建在同源策略基础之上的,浏览器只是针对同源策略的一种实现。

同源策略在解决浏览器访问安全的同时,也带来了跨域问题,当一个请求url的协议域名端口三者之间任意一个与当前页面url不同即为跨域。

2.使用CORS解决跨域问题

CORS(Cross-origin resource sharing,跨域资源共享)是一个 W3C 标准,定义了在必须访问跨域资源时,浏览器与服务器应该如何沟通。CORS 背后的基本思想,就是使用自定义的 HTTP 头部让浏览器与服务器进行沟通,从而决定请求或响应是应该成功,还是应该失败。CORS 需要浏览器和服务器同时支持。 整个CORS通信过程,都是浏览器自动完成,不需要用户参与。对于开发者来说,CORS通信与同源的AJAX通信没有差别,代码完全一样。浏览器一旦发现AJAX请求跨源,就会自动添加一些附加的头信息,有时还会多出一次附加的请求,但用户不会有感觉。

浏览器将CORS请求分成两类:简单请求(simple request)和非简单请求(not-so-simple request)。

2.1 简单请求

简单请求是指满足下面两大条件的请求:

  1. 请求方法为 HEAD、GET、POST中的一种。
  2. HTTP头信息不超过一下几种:
    • Accept
    • Accept-Language
    • Content-Language
    • Last-Event-ID
    • Content-Type(只限于三个值application/x-www-form-urlencodedmultipart/form-datatext/plain

对于简单请求,浏览器回自动在请求的头部添加一个 Origin 字段来说明本次请求来自哪个源(协议 + 域名 + 端口),服务端则通过这个值判断是否接收本次请求。如果 Origin 在许可范围内,则服务器返回的响应会多出几个头信息:

代码语言:javascript
复制
 Access-Control-Allow-Credentials: true
 Access-Control-Allow-Headers: Content-Type, Content-Length
 Access-Control-Allow-Origin: *
 Content-Type: text/html; charset=utf-8

实际上后续我们就是通过配置这些参数来处理跨域请求的。

2.2 非简单请求

非简单请求是那种对服务器有特殊要求的请求,比如请求方法是 PUTDELETE ,或者 Content-Type 字段的类型是 application/json

非简单请求的CORS请求,会在正式通信之前,增加一次HTTP查询请求,称为"预检"请求(preflight),预检请求其实就是我们常说的 OPTIONS 请求,表示这个请求是用来询问的。头信息里面,关键字段 Origin ,表示请求来自哪个源,除 Origin 字段,"预检"请求的头信息包括两个特殊字段:

代码语言:javascript
复制
 // 该字段是必须的,用来列出浏览器的CORS请求会用到哪些HTTP方法
 Access-Control-Request-Method
 // 该字段是一个逗号分隔的字符串,指定浏览器CORS请求会额外发送的头信息字段.
 Access-Control-Request-Headers

浏览器先询问服务器,当前网页所在的域名是否在服务器的许可名单之中,以及可以使用哪些HTTP请求方法和头信息字段。只有得到肯定答复,浏览器才会发出正式的 XMLHttpRequest 请求,否则就报错。

2.3 配置CORS以解决跨域问题

上述介绍了两种跨域请求,其中出现了几种特殊的 Header 字段,CORS 就是通过配置这些字段来解决跨域问题的:

这都是后端配置的

  1. Access-Control-Allow-Origin: 该字段是必须的。它的值要么是请求时Origin字段的值,要么是一个*,表示接受任意域名的请求。
  2. Access-Control-Allow-Methods: 该字段必需,它的值是逗号分隔的一个字符串,表明服务器支持的所有跨域请求的方法。注意,返回的是所有支持的方法,而不单是浏览器请求的那个方法。这是为了避免多次"预检"请求。
  3. Access-Control-Allow-Headers: 如果浏览器请求包括Access-Control-Request-Headers字段,则Access-Control-Allow-Headers字段是必需的。它也是一个逗号分隔的字符串,表明服务器支持的所有头信息字段,不限于浏览器在"预检"中请求的字段。
  4. Access-Control-Expose-Headers: 该字段可选。CORS请求时,XMLHttpRequest对象的response只能拿到6个基本字段:Cache-ControlContent-LanguageContent-TypeExpiresLast-ModifiedPragma。如果想拿到其他字段,就必须在Access-Control-Expose-Headers里面指定。
  5. Access-Control-Allow-Credentials: 该字段可选。它的值是一个布尔值,表示是否允许发送Cookie。默认情况下,Cookie不包括在CORS请求之中。设为true,即表示服务器明确许可,Cookie可以包含在请求中,一起发给服务器。这个值也只能设为 true,如果服务器不要浏览器发送Cookie,删除该字段即可。
  6. Access-Control-Max-Age: 该字段可选,用来指定本次预检请求的有效期,单位为秒,在此期间,不用发出另一条预检请求。

3.Golang解决跨域拦截

Gin框架为例,配置处理跨域的中间件:

代码语言:javascript
复制
 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. [可选]是否允许后续请求携带认证信息Cookie,该值只能是true,不需要则不设置
     context.Header("Access-Control-Allow-Credentials", "true")
     // 6. 放行所有OPTIONS方法
     if method == "OPTIONS" {
         context.AbortWithStatus(http.StatusNoContent)
         return
     }
     context.Next()
 }

注意:上述context.Header("Access-Control-Allow-Origin", "*")如果将Access-Control-Allow-Origin设置为*时存在一个问题是不允许XMLHttpRequest携带Cookie,所以要实现通配的话可以采用动态获取Origin,即context.GetHeader("Origin")的方式。

net/http标准包:

代码语言:javascript
复制
 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 hello (w *http.ResponseWriter,r *http.Request){
     w.Write([]byte("hello world!"))
 }
 ​
 func main(){
         http.Handle("/", corsMiddleware(http.HandlerFunc(hello)))
 }

我正在参与2023腾讯技术创作特训营第三期有奖征文,组队打卡瓜分大奖!

原创声明:本文系作者授权腾讯云开发者社区发表,未经许可,不得转载。

如有侵权,请联系 cloudcommunity@tencent.com 删除。

原创声明:本文系作者授权腾讯云开发者社区发表,未经许可,不得转载。

如有侵权,请联系 cloudcommunity@tencent.com 删除。

评论
登录后参与评论
0 条评论
热度
最新
推荐阅读
目录
  • 拓展:跨源资源共享
    • 1. 实例
      • 后端代码:
      • 前端代码:
      • 方法1:
      • 方法2:
      • 总结:
    • 2. 解决跨域问题
      • 1.什么是跨域
      • 2.使用CORS解决跨域问题
      • 3.Golang解决跨域拦截
相关产品与服务
消息队列 TDMQ
消息队列 TDMQ (Tencent Distributed Message Queue)是腾讯基于 Apache Pulsar 自研的一个云原生消息中间件系列,其中包含兼容Pulsar、RabbitMQ、RocketMQ 等协议的消息队列子产品,得益于其底层计算与存储分离的架构,TDMQ 具备良好的弹性伸缩以及故障恢复能力。
领券
问题归档专栏文章快讯文章归档关键词归档开发者手册归档开发者手册 Section 归档