你真的了解跨域吗?如何解决?为Go程序员准备的跨域教程

1,802 阅读7分钟
原文链接: mp.weixin.qq.com

点击上方蓝色“ Go语言中文网”关注我们, 领全套Go资料,每天学习 Go 语言

本文作者:guyan0319

原文链接:https://segmentfault.com/a/1190000020869645

跨域指的是浏览器不能执行其他网站或域名下的脚本。之所以形成跨域,是因为浏览器的同源策略[1]造成的,是浏览器对 javascript 程序做的安全限制,现在所有支持 JavaScript[2]的浏览器都会使用这个策略。

在实际应用中会遇到需要跨域的场景,比如前后端分离,前后端不在同域(这里的同域指的是同一协议,同一域名,同一端口),那么,它们之间相互通信如何解决呢?

跨域解决有以下几种方法:

jsonp 解决跨域

这里jsonp[3]跨域其实是利用 iframe、img、srcipt,link 标签的 src 或 href 属性来实现的,这些标签都可以发送一个 get 请求资源,src 和 href 并没有受同源策略的限制。

这里我们拿懒人教程示例

<!DOCTYPE html><html><head><metacharset="utf-8"><title>JSONP 实例</title><scriptsrc="https://cdn.static.runoob.com/libs/jquery/1.8.3/jquery.js"></script></head><body><divid="divCustomers"></div><script>$.getJSON("https://www.runoob.com/try/ajax/jsonp.php?jsoncallback=?", function(data) {    var html = '<ul>';    for(var i = 0; i < data.length; i++)    {        html += '<li>' + data[i] + '</li>';    }    html += '</ul>';    $('#divCustomers').html(html);});</script></body></html>

jsonp 主要站在前端的角度去解决问题,这种方式有一定的局限性,就是仅适用 get 请求。

nginx 代理解决跨域

1、nginx 配置解决 iconfont 跨域

众所周知 js、css、img 等常用资源不受浏览器同源策略限制,但一些特殊资源如 iconfont 字体文件(eot|otf|ttf|woff|svg)除外,这里通过修改 nginx 配置就可以解决。

location / {  add_header Access-Control-Allow-Origin *;}

2、nginx 反向代理

同源策略是浏览器的安全策略,不属于 http 协议一部分,限制的是 js 脚本。而服务器端调用的 http 接口,不受同源策略限制,也不存在跨域问题。

实现思路:nginx 服务器作为中间代理(或跳转机),实现从域名 A 访问域名 B,像访问同域一样。

示例

server {     listen80;     server_name http://domain1;     location / {         proxy_pass http://domain2:8081/;         proxy_set_header Host $host;         proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;         proxy_set_header X-Forwarded-Proto $scheme;         proxy_set_header X-Forwarded-Port $server_port;      }}

nodejs 代理解决跨越

nodejs 实现原理和 nginx 基本类似。

修改 app.js

var express = require('express');const proxy = require('http-proxy-middleware');const app = express();app.set('port', '809');app.all('*', function (req, res, next) {    // 解决跨域问题    res.header("Access-Control-Allow-Origin", "*");    res.header("Access-Control-Allow-Headers", "Content-Type,Content-Length, Authorization, Accept,X-Requested-With");    res.header("Access-Control-Allow-Methods", "PUT,POST,GET,DELETE,OPTIONS");    if (req.method == "OPTIONS") {        res.send(200);    } else {        next();    }});var options = {        target: 'http://localhost:8090',        changeOrigin: true,    };var exampleProxy = proxy(options);app.use('/', exampleProxy);app.listen(app.get('port'), () => { console.log(`server running @${app.get('port')}`);});

如是 vue+nodejs 环境

通过只修改 vue.config.js,不用修改 nodejs 也可以实现代理跨域。

devServer: {host: '0.0.0.0',port: 8080,disableHostCheck: true,proxy: {'/*': {target: 'https://www.runoob.com',secure: false,changeOrigin: true}}}

cors

跨域资源共享(CORS[4]) 是一种机制,它使用额外的HTTP [5]头来告诉浏览器 让运行在一个 origin (domain) 上的 Web 应用被准许访问来自不同源服务器上的指定的资源。当一个资源从与该资源本身所在的服务器不同的域、协议或端口请求一个资源时,资源会发起一个跨域 HTTP 请求

比如,站点http://domain-a.com[6]的某 HTML 页面通过的 src [7]请求http://domain-b.com/image.jpg[8]。网络上的许多页面都会加载来自不同域的 CSS 样式表,图像和脚本等资源。

出于安全原因,浏览器限制从脚本内发起的跨源 HTTP 请求。例如,XMLHttpRequest 和 Fetch API 遵循同源策略。这意味着使用这些 API 的 Web 应用程序只能从加载应用程序的同一个域请求 HTTP 资源,除非响应报文包含了正确 CORS 响应头。

前面扯了很多方法,其实归根结底是围绕 cors 机制来实现(除了 nginx 反向代理)的,具体就是服务端发送`Access-Control-Allow-Origin`[9]以及相关响应头,来通知浏览器有权访问资源。

前面讲了 nodejs 或 nginx 服务器端通过设置`Access-Control-Allow-Origin`[10],可以实现跨域,这里讲一下 golang 实现方式,当然 php、java 等也可以实现、原理相同。

示例 1

package mainimport (    "net/http")funccors(f http.HandlerFunc)http.HandlerFunc {    returnfunc(w http.ResponseWriter, r *http.Request) {        w.Header().Set("Access-Control-Allow-Origin", "*")  // 允许访问所有域,可以换成具体url,注意仅具体url才能带cookie信息        w.Header().Add("Access-Control-Allow-Headers", "Content-Type,AccessToken,X-CSRF-Token, Authorization, Token") //header的类型        w.Header().Add("Access-Control-Allow-Credentials", "true") //设置为true,允许ajax异步请求带cookie信息        w.Header().Add("Access-Control-Allow-Methods", "POST, GET, OPTIONS, PUT, DELETE") //允许请求方法        w.Header().Set("content-type", "application/json;charset=UTF-8")             //返回数据格式是jsonif r.Method == "OPTIONS" {            w.WriteHeader(http.StatusNoContent)            return        }        f(w, r)    }}funcindex(w http.ResponseWriter, r *http.Request) {    w.Write([]byte("Hello Golang"))}funcmain() {    http.HandleFunc("/", cors(index))    http.ListenAndServe(":8000", nil)}

示例 2

gin 框架跨域中间件

package mainimport (    "github.com/gin-gonic/gin""net/http")funcmain() {    r := gin.Default()    r.Use(Cors())//默认跨域    r.GET("/", func(c *gin.Context) {        c.JSON(200, gin.H{            "message": "pong",        })    })    r.Run(":8090")}funcCors() gin.HandlerFunc {    returnfunc(c *gin.Context) {        method := c.Request.Method        origin := c.Request.Header.Get("Origin")        if origin != "" {            c.Header("Access-Control-Allow-Origin", "*")            c.Header("Access-Control-Allow-Methods", "POST, GET, OPTIONS, PUT, DELETE")            c.Header("Access-Control-Allow-Headers", "Content-Type,AccessToken,X-CSRF-Token, Authorization")            c.Header("Access-Control-Allow-Credentials", "true")            c.Set("content-type", "application/json")        }        //放行所有OPTIONS方法if method == "OPTIONS" {            c.AbortWithStatus(http.StatusNoContent)        }        c.Next()    }}

gin 有个官方的跨域中间件

https://github.com/gin-contri...[11]

注意 :

某些简单请求不会触发CORS 预检请求[12]

`Content-Type`[13]的值仅限于下列三者之一:

  • text/plain
  • multipart/form-data
  • application/x-www-form-urlencoded 默认

现在应用中越来越多前端和服务端都采用 json 通讯,如 vue 等。

要求前端`Content-Type`[14]设置为 application/json,且是 post 请求,这属于复杂请求,将触发 CORS 预检请求[15]。即浏览器会先发送一次 options 请求,同意后才继续发送 post 请求。

当发送这种请求时,在浏览器的 network 会发现两条请求。同时在服务端接收前端参数时需要注意,以前通过 get 、post 方法会失效。

具体接收参数方法,php 语言为 file_get_contents('php://input') 。

golang 语言

net/http

package mainimport (    "encoding/json""fmt""io/ioutil""net/http")funccors(f http.HandlerFunc)http.HandlerFunc {    returnfunc(w http.ResponseWriter, r *http.Request) {        w.Header().Set("Access-Control-Allow-Origin", "*")  // 允许访问所有域,可以换成具体url,注意仅具体url才能带cookie信息        w.Header().Add("Access-Control-Allow-Headers", "Content-Type,AccessToken,X-CSRF-Token, Authorization, Token") //header的类型        w.Header().Add("Access-Control-Allow-Credentials", "true") //设置为true,允许ajax异步请求带cookie信息        w.Header().Add("Access-Control-Allow-Methods", "POST, GET, OPTIONS, PUT, DELETE") //允许请求方法        w.Header().Set("content-type", "application/json;charset=UTF-8")             //返回数据格式是jsonif r.Method == "OPTIONS" {            w.WriteHeader(http.StatusNoContent)            return        }        f(w, r)    }}type User struct {    Username string`json:"username"`    Password  string`json:"password"`}funcindex(w http.ResponseWriter, r *http.Request) {    body, _ := ioutil.ReadAll(r.Body)    fmt.Println(string(body))    var user User    if err := json.Unmarshal(body, &user); err == nil {        fmt.Println(user)    } else {        fmt.Println(err)    }    w.Write([]byte("Hello Golang"))}funcmain() {    http.HandleFunc("/", cors(index))    http.ListenAndServe(":8000", nil)}

gin 框架

对于 gin 框架我们就需要 bind 来解决这个问题

示例

type Userstruct{    Username string `form:"username" json:"username" binding:"required"`    Password   string `form:"password" json:"password" binding:"required"`}funcLogin(c *gin.Context) {    var u User    err :=c.BindJSON(&u)    fmt.Println(err)    fmt.Println(u)}

先建一个结构体 user,再使用 BindJSON 绑定,将 request 中的 Body 中的数据按照 JSON 格式解析到 User 结构体中。

需要注意:

  • binding:"required" 字段对应的参数未必传没有会抛出错误,非 banding 的字段,对于客户端没有传,User 结构会用零值填充。对于 User 结构没有的参数,会自动被忽略。
  • 结构体字段类型和所传参数类型要一致。

Bind 的实现都在gin/binding里面. 这些内置的 Bind 都实现了Binding接口, 主要是Bind()函数.

  • context.BindJSON() 支持 MIME 为 application/json 的解析
  • context.BindXML() 支持 MIME 为 application/xml 的解析
  • context.BindYAML() 支持 MIME 为 application/x-yaml 的解析
  • context.BindQuery() 只支持 QueryString 的解析, 和 Query()函数一样
  • context.BindUri() 只支持路由变量的解析
  • Context.Bind() 支持所有的类型的解析, 这个函数尽量还是少用(当 QueryString, PostForm, 路由变量在一块同时使用时会产生意想不到的效果), 目前测试 Bind 不支持路由变量的解析, Bind()函数的解析比较复杂, 这部分代码后面再看

总结

  • 通常在解决跨域问题时,通过在服务端设置 head 请求的方式比较便利。
  • 设置"Access-Control-Allow-Origin"为"*"匹配所有域名,但无法带 cookie,影响登录。设置具体域名则不受影响。
  • 前端`Content-Type`[16]设置为 application/json 时,服务端在接收参数数据方式不同。

参考

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

https://blog.csdn.net/qq_37960007/article/details/91359579

http://www.okyes.me/2016/05/03/go-gin.html

https://www.cnblogs.com/CyLee/p/7644380.html

更多golang 开发资料[17]

喜欢本文的朋友,欢迎关注“Go语言中文网 ”:

推荐阅读

文中链接 [1]

同源策略: https://baike.baidu.com/item/%E5%90%8C%E6%BA%90%E7%AD%96%E7%95%A5/3927875?fr=aladdin

[2]

JavaScript: https://baike.baidu.com/item/JavaScript/321142

[3]

jsonp: https://baike.baidu.com/item/jsonp/493658?fr=aladdin

[4]

CORS: https://developer.mozilla.org/en-US/docs/Glossary/CORS

[5]

HTTP: https://developer.mozilla.org/en-US/docs/Glossary/HTTP

[6]

http://domain-a.com: http://domain-a.com

[7]

的 src : https://developer.mozilla.org/zh-CN/docs/Web/HTML/Element/Img#Attributes

[8]

http://domain-b.com/image.jpg: http://domain-b.com/image.jpg

[9]

Access-Control-Allow-Origin: https://developer.mozilla.org/zh-CN/docs/Web/HTTP/Headers/Access-Control-Allow-Origin

[10]

Access-Control-Allow-Origin: https://developer.mozilla.org/zh-CN/docs/Web/HTTP/Headers/Access-Control-Allow-Origin

[11]

https://github.com/gin-contri...: https://github.com/gin-contrib/cors

[12]

CORS 预检请求: https://developer.mozilla.org/zh-CN/docs/Web/HTTP/Access_control_CORS#Preflighted_requests

[13]

Content-Type: https://developer.mozilla.org/zh-CN/docs/Web/HTTP/Headers/Content-Type

[14]

Content-Type: https://developer.mozilla.org/zh-CN/docs/Web/HTTP/Headers/Content-Type

[15]

CORS 预检请求: https://developer.mozilla.org/zh-CN/docs/Web/HTTP/Access_control_CORS#Preflighted_requests

[16]

Content-Type: https://developer.mozilla.org/zh-CN/docs/Web/HTTP/Headers/Content-Type

[17]

golang开发资料: https://github.com/guyan0319/golang_development_notes/blob/master/zh/preface.md