大部分前后端开发可能知道发生跨域在代码上怎么解决,但是不知道为什么会跨域,以及解决跨域的技术是什么。
本文将从以下方面讲解什么是跨域:
- 什么是同源策略
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
:
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;
}
}
后端与代理,只需要其中一方设置即可。
评论区