1. 跨域

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

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

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

1.1.1. jsonp跨域

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

这里我们拿懒人教程示例

  1. <!DOCTYPE html>
  2. <html>
  3. <head>
  4. <meta charset="utf-8">
  5. <title>JSONP 实例</title>
  6. <script src="https://cdn.static.runoob.com/libs/jquery/1.8.3/jquery.js"></script>
  7. </head>
  8. <body>
  9. <div id="divCustomers"></div>
  10. <script>
  11. $.getJSON("https://www.runoob.com/try/ajax/jsonp.php?jsoncallback=?", function(data) {
  12. var html = '<ul>';
  13. for(var i = 0; i < data.length; i++)
  14. {
  15. html += '<li>' + data[i] + '</li>';
  16. }
  17. html += '</ul>';
  18. $('#divCustomers').html(html);
  19. });
  20. </script>
  21. </body>
  22. </html>

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

1.1.2. nginx代理跨域

nginx配置解决iconfont跨域

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

  1. location / {
  2. add_header Access-Control-Allow-Origin *;
  3. }

nginx 反向代理

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

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

示例

  1. server {
  2. listen 80;
  3. server_name http://domain1;
  4. location / {
  5. proxy_pass http://domain2:8081/;
  6. proxy_set_header Host $host;
  7. proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;
  8. proxy_set_header X-Forwarded-Proto $scheme;
  9. proxy_set_header X-Forwarded-Port $server_port;
  10. }
  11. }

1.1.3. nodejs 代理

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

修改app.js

  1. var express = require('express');
  2. const proxy = require('http-proxy-middleware');
  3. const app = express();
  4. app.set('port', '809');
  5. app.all('*', function (req, res, next) { // 解决跨域问题
  6. res.header("Access-Control-Allow-Origin", "*");
  7. res.header("Access-Control-Allow-Headers", "Content-Type,Content-Length, Authorization, Accept,X-Requested-With");
  8. res.header("Access-Control-Allow-Methods", "PUT,POST,GET,DELETE,OPTIONS");
  9. if (req.method == "OPTIONS") {
  10. res.send(200);
  11. } else {
  12. next();
  13. }
  14. });
  15. var options = {
  16. target: 'http://localhost:8090',
  17. changeOrigin: true,
  18. };
  19. var exampleProxy = proxy(options);
  20. app.use('/', exampleProxy);
  21. app.listen(app.get('port'), () => {
  22. console.log(`server running @${app.get('port')}`);
  23. });

如是vue+nodejs环境

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

  1. devServer: {
  2. host: '0.0.0.0',
  3. port: 8080,
  4. disableHostCheck: true,
  5. proxy: {
  6. '/*': {
  7. target: 'https://www.runoob.com',
  8. secure: false,
  9. changeOrigin: true
  10. }
  11. }
  12. }

1.1.4. cors

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

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

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

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

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

示例1

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

示例 2

gin 框架跨域中间件

  1. package main
  2. import (
  3. "github.com/gin-gonic/gin""net/http"
  4. )
  5. funcmain() {
  6. r := gin.Default()
  7. r.Use(Cors())//默认跨域
  8. r.GET("/", func(c *gin.Context) {
  9. c.JSON(200, gin.H{
  10. "message": "pong",
  11. })
  12. })
  13. r.Run(":8090")
  14. }
  15. funcCors() gin.HandlerFunc {
  16. returnfunc(c *gin.Context) {
  17. method := c.Request.Method
  18. origin := c.Request.Header.Get("Origin")
  19. if origin != "" {
  20. c.Header("Access-Control-Allow-Origin", "*")
  21. c.Header("Access-Control-Allow-Methods", "POST, GET, OPTIONS, PUT, DELETE")
  22. c.Header("Access-Control-Allow-Headers", "Content-Type,AccessToken,X-CSRF-Token, Authorization")
  23. c.Header("Access-Control-Allow-Credentials", "true")
  24. c.Set("content-type", "application/json")
  25. }
  26. //放行所有OPTIONS方法if method == "OPTIONS" {
  27. c.AbortWithStatus(http.StatusNoContent)
  28. }
  29. c.Next()
  30. }
  31. }

gin有个官方的跨域中间件

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

注意

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

Content-Type 的值仅限于下列三者之一:

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

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

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

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

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

1.1.5. golang语言

net/http

  1. package main
  2. import (
  3. "encoding/json"
  4. "fmt"
  5. "io/ioutil"
  6. "net/http"
  7. )
  8. func cors(f http.HandlerFunc) http.HandlerFunc {
  9. return func(w http.ResponseWriter, r *http.Request) {
  10. w.Header().Set("Access-Control-Allow-Origin", "*") // 允许访问所有域,可以换成具体url,注意仅具体url才能带cookie信息
  11. w.Header().Add("Access-Control-Allow-Headers", "Content-Type,AccessToken,X-CSRF-Token, Authorization, Token") //header的类型
  12. w.Header().Add("Access-Control-Allow-Credentials", "true") //设置为true,允许ajax异步请求带cookie信息
  13. w.Header().Add("Access-Control-Allow-Methods", "POST, GET, OPTIONS, PUT, DELETE") //允许请求方法
  14. w.Header().Set("content-type", "application/json;charset=UTF-8") //返回数据格式是json
  15. if r.Method == "OPTIONS" {
  16. w.WriteHeader(http.StatusNoContent)
  17. return
  18. }
  19. f(w, r)
  20. }
  21. }
  22. type User struct {
  23. Username string `json:"username"`
  24. Password string `json:"password"`
  25. }
  26. func index(w http.ResponseWriter, r *http.Request) {
  27. body, _ := ioutil.ReadAll(r.Body)
  28. fmt.Println(string(body))
  29. var user User
  30. if err := json.Unmarshal(body, &user); err == nil {
  31. fmt.Println(user)
  32. } else {
  33. fmt.Println(err)
  34. }
  35. w.Write([]byte("Hello Golang"))
  36. }
  37. func main() {
  38. http.HandleFunc("/", cors(index))
  39. http.ListenAndServe(":8000", nil)
  40. }

gin 框架

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

示例

  1. type User struct {
  2. Username string `form:"username" json:"username" binding:"required"`
  3. Password string `form:"password" json:"password" binding:"required"`
  4. }
  5. func Login(c *gin.Context) {
  6. var u User
  7. err :=c.BindJSON(&u)
  8. fmt.Println(err)
  9. fmt.Println(u)
  10. }

先建一个结构体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请求的方式比较便利。
  • 跨域需要带cookie信息,则必须满足服务端 设置"Access-Control-Allow-Origin"为固定url,且Access-Control-Allow-Credentials: true,前端js 也要设置withCredentials: true
  • 前端Content-Type设置为 application/json时,服务端在接收参数数据方式不同。