什么是跨域(CORS)?

CORS,全称:跨域资源共享(Cross-origin resource sharing),是一种基于HTTP协议的机制,允许服务器跨网域访问资源。
这里的网域指的是HTTP网域,比如https://1.1.1.1 和http://1.1.1.1 ,这就是2个网域,怎么判断的呢?众所周知,一个基本的网域是由<协议>://<域名/IP地址>[:<端口号>]组成,只要这三项中,有任意一项不一致,则都不是同一个网域。

跨域会造成什么问题?

如果在默认情况下,通过主机A(假设为https://1.1.1.1 )去访问主机B(假设为https://1.1.1.2 )的资源的时候,根据我们的判断,这是两个域。
在这种情况下,会产生一个HTTP错误:Cross-Origin Request Blocked: The Same Origin Policy disallows...
这种错误并不是我们写的代码哪里是错误的;其实,这是一种安全机制,是允许主机访问同源的策略,但同时拒绝其他主机的请求,即使浏览器拿到了数据,但是由于违反了这个安全策略,浏览器也不会把数据交给你,如图:

CORS

所谓的同源,就是<协议>://<域名/IP地址>[:<端口号>]三个部分全部一致,这就叫做同源。

这种问题在刚刚入门的开发者中也很常见,比如Web的前后端分离的项目,不管在开发还是部署环境中,都会难免遇到跨域问题。

解决方案

通过代码解决

通过代码解决,我们需要知道,在什么情况下才不会出现这种问题。
我们可以得知,CORS有几个配套的HTTP标头,常见的例如:

  • Access-Control-Allow-Origin 允许指定源的访问
  • Access-Control-Allow-Methods 允许指定方法的访问
  • Access-Control-Allow-Headers 允许指定标头
  • Access-Control-Allow-Credentials 允许跨域携带凭据

不推荐的原因:很多人会因为方便,将其设置为允许所有源访问,其实这不是安全的操作,故此不建议在部署环境中使用此方法。

我们可以通过以上标头解决这类问题:

在Spring Boot中的解决方法:

  1. 我们在Spring Boot的配置包(一般情况下在启动类的同级目录中)
  2. 新建一个名为 CorsConfigure 的类,并填入如下代码:
// 包名略

import org.springframework.context.annotation.Configuration;
import org.springframework.web.servlet.config.annotation.CorsRegistry;
import org.springframework.web.servlet.config.annotation.EnableWebMvc;
import org.springframework.web.servlet.config.annotation.WebMvcConfigurer;

@EnableWebMvc
@Configuration
public class CorsConfigure implements WebMvcConfigurer {

    @Override
    public void addCorsMappings(CorsRegistry registry) {
        // 允许的路径
        registry.addMapping("/**")
        // 允许的源:*表示所有,也可以指定一个域,例如:http://localhost:8080
                .allowedOriginPatterns("*")
        // 允许的方法
                .allowedMethods("GET", "HEAD", "POST", "PUT", "DELETE", "OPTIONS")
        // 是否允许跨域携带凭据
                .allowCredentials(true)
        // 最大存活时间
                .maxAge(3600)
        // 允许的标头
                .allowedHeaders("*");
    }
}

在Golang的Gin中的解决方法:

创建一个中间件即可:

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

func CORSMiddleware() gin.HandlerFunc {
    return func(c *gin.Context) {
        origin := c.Request.Header.Get("Origin")
        // 这里原来的代码是在配置文件中获取允许的源的数组,为了方便写博客,所以改成了直接声明的数组
        allowOriginList := [4]string{"http://localhost:8080", "http://localhost:8081", "http://localhost:8082", "http://localhost:8083"}
        // 判断allowOriginList的长度是不是等于1,且第0个元素是否为"*"
        if len(allowOriginList) == 1 && allowOriginList[0] == "*" {
            // 写入标头,值为*,表示允许所有源
            c.Writer.Header().Set("Access-Control-Allow-Origin", "*")
        } else {
            // 遍历allowOriginList
            for _, allowOrigin := range allowOriginList {
                // 判断当前源是否为允许的源,如果不是,则直接跳过该步骤
                if origin == allowOrigin {
                    // 如果是允许的源,就将其写入标头
                    c.Writer.Header().Set("Access-Control-Allow-Origin", allowOrigin)
                    结束循环
                    break
                }
            }
        }
        // 是否允许跨域携带凭据
        c.Writer.Header().Set("Access-Control-Allow-Credentials", "true")
        // 允许的标头
        c.Writer.Header().Set("Access-Control-Allow-Headers", "Content-Type, Content-Length, Accept-Encoding, X-CSRF-Token, Authorization, accept, origin, Cache-Control, X-Requested-With")
        // 允许的请求方法
        c.Writer.Header().Set("Access-Control-Allow-Methods", "POST, OPTIONS, GET, PUT")
        // OPTIONS方法是一个特殊方法,用于请求预检,获取服务器是否允许该请求
        if c.Request.Method == "OPTIONS" {
            c.AbortWithStatus(http.StatusNoContent)
            return
        }
        // 继续下一个中间件
        c.Next()
    }
}

通过Nginx反向代理解决

适用于部署环境

用反向代理来解决,并不是设置标头,而是将2个不同的域代理成相同的域。

server {
        # 监听80端口(由于这是我服务器上面的配置,所以没有监听80)
        # listen 80;
        # 监听443端口,并装载SSL
        listen 443 ssl;
        # 主机名称(你想要通过什么访问该网站,可以是域名也可以是IP地址)
        server_name www.sgtu.org;
        # 前端根路径
        root /var/www/wordpress;
        # 前端的主页文件
        index index.html;

        # SSL证书(未启用SSL则不需要)
        ssl_certificate /etc/letsencrypt/live/shawngao.org/fullchain.pem;
        # SSL证书私钥(未启用SSL则不需要)
        ssl_certificate_key /etc/letsencrypt/live/shawngao.org/privkey.pem;

        # 监听根路径
        location / {
           try_files $uri $uri/ /index.php?$query_string;
        }

        # 监听请求的/api路径
        location /api {
          # 设置代理请求头:用于识别通过代理或负载均衡器方式连接到Web的客户端的原始IP地址和HTTP头字段
          proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;
          # 设置代理请求头:用于确定客户端发起的请求中使用的初始域名
          proxy_set_header X-Forwarded-Host $http_host:$server_port;
          # 设置代理请求头:用于确定客户端发起的请求中使用的代理服务器
          proxy_set_header X-Forwarded-Server $host;
          # 设置代理请求头:用于确定客户端真实IP
          proxy_set_header X-Real-IP $remote_addr;
          # 设置代理请求头:用于确定客户端发起的请求的协议
          proxy_set_header X-Forwarded-Proto $scheme;
          # 目标地址:http://【后端IP地址】:【后端端口】
          # 注意:后面不要加斜线“/”
          proxy_pass http://172.0.0.1:81;
          # 上传的最大文件尺寸
          client_max_body_size 20m;
        }
}

通过以上的Nginx的配置,前端(假设为:https://www.sgtu.org )直接请求后端(https://www.sgtu.org/api )即可解决跨域问题。


摸🐟从未停止,努力从未开始。