同源策略与安全机制

在Web安全中,同源策略(Same-Origin Policy)是浏览器实施的一项重要安全机制,用于保护用户信息安全,防止恶意网站访问其他网站的数据。 什么是同源? 两个URL在以下三个方面完全一致时,才被视为同源:

  1. 协议(Protocol):如http://或https://
  2. 域名(Domain):如www.example.com
  3. 端口号(Port):如80或443(默认端口可省略)

示例分析:

URL 1 URL 2 是否同源 原因
https://www.example.com https://www.example.com 协议、域名、端口完全相同
http://www.example.com https://www.example.com 协议不同(http vs https)
https://www.example.com https://api.example.com 子域名不同(www vs api)
https://www.example.com:80 https://www.example.com:8080 端口号不同(80 vs 8080)

同源策略的作用:

  • 防止恶意网站窃取用户敏感信息
  • 限制跨站脚本攻击(XSS)
  • 阻止未经授权的数据访问

跨域是什么?

跨域,全称叫做跨域资源共享。是浏览器的一种保护机制,浏览器最基本的安全功能。如果缺少了同源策略,浏览器的正常功能都会受到影响。同源策略下只允许网页请求同一域名下的服务 ,即协议,域名和端口都要保持一致。更详细的同源政策可以参考「MDN文档 同源策略

在同源策略下,会有以下限制:

  • 无法获取非同源的 Cookie、LocalStorage、SessionStorage 等
  • 无法获取非同源的 dom
  • 无法向非同源的服务器发送 ajax 请求

在日常开发中,前后端分离,经常需要在不同源的情况下,需要 ajax 请求数据,携带cookie等,那我们就要规避这种限制。

如何解决跨域问题?

前端本地调试,解决跨域问题

跨域是浏览器的保护机制,所以我们解决跨域问题就可以*在前端开发时配置代理,中转请求。我们使用一个中转服务器来发送请求和接收响应。大部分的前端脚手架都支持代理。 例如在一个用vite脚手架创建的项目中,在vue.config.js中去配置。

export default defineConfig({
  server: {
    proxy: {
      '/api': {
        target: 'http://localhost:3000',
        changeOrigin: true,
        rewrite: (path) => path.replace(/^\/api/, ''),
      },
    },
  },
});

如果是next.js项目,在next.config.js中配置。

module.exports = {
  async rewrites() {
    return [
      { source: '/api/:path*', destination: 'http://localhost:3000/:path*' },
    ];
  },
};

后端配置,解决跨域问题

使用nginx、node中间件转发,将跨域转成同源

可以使用nginx转发,将跨域转成同源。跨域限制的时候浏览器不能跨域访问服务器,node中间件和nginx反向代理,都是让请求发给代理服务器,静态页面面和代理服务器是同源的,然后代理服务器再向后端服务器发请求,服务器和服务器之间不存在同源限制。也是一种解决跨域问题的方法。

使用CORS,解决跨域问题

使用CORS,是后端服务器设置HTTP头,允许跨域请求。也是日常开发中最常用的解决跨域问题的方法。CORS 跨域的原理实际上是浏览器与服务器通过一些 HTTP 协议头来做一些约定和限制。可以查看 HTTP-访问控制CORS

下面是使用CORS解决跨域问题的Golang示例, 其它语言代码可以通过chatgpt生成类似处理:

//cmd/serve.go

func newServer(cfg *config.Config) *gin.Engine {
    //... update cfg
    gin.DisableConsoleColor()
    r := gin.New()
    l := utils.DefaultLogger()
    r.Use(utils.MiddlewareCORS)
    r.Use(utils.GinLogger(l), utils.GinRecovery(l, true))

    r.GET("/api/v1/health", func(c *gin.Context) {
        c.String(http.StatusOK, "ok")
    })
    // ...
    return r
}

// utils/middleware.go
func MiddlewareCORS(c *gin.Context) {
    origin := c.Request.Header.Get("Origin")
    c.Writer.Header().Set("Access-Control-Allow-Origin", origin)
    c.Writer.Header().Set("Access-Control-Allow-Credentials", "true")
    c.Writer.Header().Set("Access-Control-Allow-Headers", "Access-Control-Allow-Headers, Origin, Accept, X-Requested-With, Content-Type, Access-Control-Request-Method, Access-Control-Request-Headers, Cache-Control, Cookie, Content-Encoding")
    c.Writer.Header().Set("Access-Control-Allow-Methods", "GET, POST, OPTIONS, PUT, DELETE")

    if c.Request.Method == "OPTIONS" {
        c.AbortWithStatus(204)
        return
    }

    c.Next()
}