侧边栏壁纸
博主头像
术业有道之编程博主等级

亦是三月纷飞雨,亦是人间惊鸿客。亦是秋霜去叶多,亦是风华正当时。

  • 累计撰写 99 篇文章
  • 累计创建 50 个标签
  • 累计收到 0 条评论

目 录CONTENT

文章目录

跨域

Administrator
2022-08-05 / 0 评论 / 0 点赞 / 215 阅读 / 8914 字

大部分前后端开发可能知道发生跨域在代码上怎么解决,但是不知道为什么会跨域,以及解决跨域的技术是什么。

本文将从以下方面讲解什么是跨域:

  • 什么是同源策略
  • jsonp原理
  • cors系统

一、什么是跨域

当一个html访问非同源的服务器时,受到了目标服务器的拒绝,即为跨域。

二、什么是同源策略

同源策略是web安全的一个规范,它规定了当一个origin文档(html)加载自己的文件脚本时,如何与另一个文件交互。它的存在直接阻隔恶意资源,减少了可能被攻击的媒介。

假设没有同源策略,就像你邀请一个朋友去你家玩,你刚打开门,可能就有一堆乱七八糟的人涌进你家,你不认识他们,但是他们可以在你家吃喝玩乐,最后垃圾还得你自己收拾。

这么重要的同源策略,到底规范了什么呢?

  • 通讯协议必须相同
  • 通讯域名或ip必须相同
  • 通讯端口必须相同

而这三者就是通讯必须具备的协议/主机/端口元组

前面讲过同源策略是web安全的一个规范,所以它只对http、https有效。

有些人会说,我用websocket也会发生跨域啊,不是只在http上才会发生吗?当你搞清楚websocket的本质,也就明白这个问题了(为什么socket前面要加web呢?它代表了什么?)。

看到这,要是还不理解,我只能举例子了:

  • 这是同源(协议,主机,端口都相同,与端口后的路由无关):
http://localhost:8080
http://localhost:8080/a
http://localhost:8080/b
  • 这是不同源(协议,主机,端口任意一个不同)
http://localhost:8080
http://localhost:8081
http://127.0.0.1:8080

三、jsonp原理

一句总结jsonp原理:<script>标签加callback函数。

有些html的标签是不受同源策略约束的比如<script>,它允许加载并运行非同源的脚本,比如加载一个在线直播的插件,地图jsdk等等。

下面是实现一个jsonp的例子:

# 一个script标签
<script src=""></script>

# jsop实现
function jsonp(req){
    var script = document.createElement('script');
    var url = req.url + '?callback=' + req.callback.name;
    script.src = url;
    # 触发 script 标签行为
    document.getElementsByTagName('head')[0].appendChild(script);
}
# jsonp请求到的数据处理函数-回调函数
function hello(res){
    alert('hello ' + res.data);
}
# 开始发起jsonp请求
jsonp({
    url : 'http://www.baidu.com', # 指定一个跨域的url地址
    callback : hello # 指定请求回来的数据用什么函数处理
});

# 这段代码最后的结果就是将前面定义的 script 改成这样
<script src="http://www.baidu.com?callback=hello"></script>

解释一下:

  • <script>加载脚本实际上是一个get请求(这也就是为什么jsonp只能发送get请求的原因)
  • callback指定的函数名在当前的html中被定义了
  • <script>会认为自己拿回来的是一个可执行脚本,按照脚本执行机制会优先执行callback函数
  • hello函数的入参是缺省的,默认就是当前返回的内容当成了入参。于是hello函数就拿到了最终的返回数据。

四、cors系统

cors(跨域资源共享),它由一系列传输的http头组成,这些http头决定浏览器是否阻止前端js代码获取跨域请求的响应。

在同源策略的影响下,默认阻止了跨域获取资源,而cors给了web服务器一些权限,让服务器可以选择,允许跨域请求访问到它们的资源。

可以这样理解:

小区禁止外人进入,这个时候来个亲戚探访很不方便,于是小区制定了一个规则,外人进入小区必须登记个人信息。于是一个外人按照小区提供的规则做了必要的信息登记,就可以出入小区了。但是没登记的人还是进不来的。

cors就是制定并验证了这套规则。

cors定义的请求头规则:

  • Access-Control-Allow-Origin 指示请求的资源能共享给哪些域
  • Access-Control-Allow-Credentials 指示当请求的凭证标记为 true 时,是否响应该请求
  • Access-Control-Allow-Headers 用在对预请求的响应中,指示实际的请求中可以使用哪些 HTTP
  • Access-Control-Allow-Methods 指定对预请求的响应中,哪些 HTTP 方法允许访问请求的资源
  • Access-Control-Expose-Headers 指示哪些 HTTP 头的名称能在响应中列出
  • Access-Control-Max-Age 指示预请求的结果能被缓存多久
  • Access-Control-Request-Headers 用于发起一个预请求,告知服务器正式请求会使用那些 HTTP
  • Access-Control-Request-Method 用于发起一个预请求,告知服务器正式请求会使用哪一种 HTTP 请求方法
  • Origin 指示获取资源的请求是从什么域发起的

当发生跨域时,服务器通常只需要按cors定义的请求头增加相应的请求头即可。

绝大多数时候我们是让服务器放开所有的访问源,也就是任何人都可以跨域访问,这是不安全的。下面是这种情况使用nginx配置跨域访问的例子:

 add_header Access-Control-Allow-Origin '*';
 add_header Access-Control-Allow-Methods 'GET, POST, OPTIONS';
 add_header Access-Control-Allow-Headers 'DNT,X-Mx-ReqToken,Keep-Alive,User-Agent,X-Requested-With,If-Modified-Since,Cache-Control,Content-Type,Authorization';  

但你需要发送cookie给服务器的时候,cors就要求服务器必须显式指定其访问源,而不能使用*通配所有源。下面是这种情况使用nginx配置跨域访问的例子:

 add_header Access-Control-Allow-Origin 'www.xxx.com';
 add_header Access-Control-Allow-Methods 'GET, POST, OPTIONS';
 add_header Access-Control-Allow-Headers 'DNT,X-Mx-ReqToken,Keep-Alive,User-Agent,X-Requested-With,If-Modified-Since,Cache-Control,Content-Type,Authorization';  

所以,大部分前端开发者都只会把cookie当本地存储,甚至抛弃。转而把一些信息用自定义请求头来发送给服务器。例如下图中的x-token

自定义请求头发送给服务器.png

cors使用建议:

对于安全级别高的web要求,应当遵守cors规则,服务器显式指定其访问源。避免其他站点发起的恶意访问。

五、代码上解决跨域

  • 前端VUE => vue.config.js
devServer: {
    port: port,
    open: true, 
    host: "localhost",
    https: false, # https的方式
    proxy: {
      # 匹配跨域的路由,可以设置多个
      [process.env.VUE_APP_BASE_API]: {
        # 实际要请求的地址,同源策略的三元组
        target: "http://localhost:8089",
        # 开启跨域
        changeOrigin: true,
        # 按需设置(非必须)
        ws: false,
        # 按需设置(非必须)
        secure: false,
        # 重写url请求,按需设置(非必须)
        pathRewrite: {
          ["^" + process.env.VUE_APP_BASE_API]: ""
        }
      }
    },
    // before: require('./mock/mock-server.js')
  }
  • 后端 spring boot

import org.springframework.beans.factory.annotation.Value
import org.springframework.context.annotation.Configuration
import org.springframework.web.servlet.config.annotation.CorsRegistry
import org.springframework.web.servlet.config.annotation.WebMvcConfigurer

@Configuration
class CorsConfig : WebMvcConfigurer {
    @Value("\${server.port}")
    private val serverPort: String? = null

    @Value("\${info.domain}")
    private val domain: String? = null
    // 用指定同源策略的三元组方式 打开 allowedOrigins 、allowCredentials 注解
    val ORIGINS = arrayOf("http://${domain}:${serverPort}", "http://${domain}:8080")

    override fun addCorsMappings(registry: CorsRegistry) {
        registry.addMapping("/**")
            .allowedOrigins("*")
//            .allowedOrigins(*ORIGINS)
//            .allowCredentials(true)
            .allowedHeaders("*")
            .allowedMethods("*")
            .maxAge(3600)

    }
}
  • 代理nginx
location / {
                proxy_pass http://khl-image-server;
                // 用指定同源策略的三元组方式
                // add_header Access-Control-Allow-Origin 'www.xxx.com';
                add_header Access-Control-Allow-Origin '*';
                add_header Access-Control-Allow-Methods 'GET, POST, OPTIONS';
                add_header Access-Control-Allow-Headers 'DNT,X-Mx-ReqToken,Keep-Alive,User-Agent,X-Requested-With,If-Modified-Since,Cache-Control,Content-Type,Authorization';
                proxy_set_header X-Real-IP $remote_addr;
                proxy_set_header Host $host;
                proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;
                proxy_set_header Accept-Encoding '';
                proxy_cache_valid any 1m;
                if ($request_method = 'OPTIONS') {
                    return 204;
                }
            }

后端与代理,只需要其中一方设置即可。

个人公众号

0

评论区