什么是跨域问题
说到跨域,首先需要解释下为什么会出现这样的跨域问题。这其实都源于浏览器的同源策略。
同源策略是浏览器中的一个重要的安全策略,是Netscape公司在1995年引入。同源策略的作用就是为了限制不同源之间的交互,从而能够有效避免XSS、CSFR等浏览器层面的攻击。
同源指的是两个请求接口URL的协议(protocol)、域名(host)和端口(port)一致。
同源策略
比如以下例子:
同源与非同源接口
说到浏览器的攻击手段,XSS指的是恶意攻击者往Web页面里插入恶意HTML代码,利用的是用户对指定网站的信任。
而CSFR指的是跨站请求伪造,是攻击者通过一些技术手段欺骗用户的浏览器去访问一个自己曾经认证过的网站并执行一些操作(如发邮件,发消息,甚至财产操作如转账和购买商品)。
由于浏览器曾经认证过,所以被访问的网站会认为是真正的用户操作而去执行。这利用了Web中用户身份验证的一个漏洞:简单的身份验证只能保证请求是发自某个用户的浏览器,却不能保证请求本身是用户自愿发出的。这实际上利用的是网站对用户网页浏览器的信任。
所以根据浏览器的是否同源判定,可以有选择的限制网站的一些行为。比如非同源的站点会被限制访问cookie、localStorage以及IndexDB,同时也无法获取网页DOM以及JavaScript对象,甚至AJAX的请求也会被拦截。
这样一来,就能够有效限制利用浏览器以及历史访问站点来进行攻击的目的。
跨域问题
本质上浏览器不允许跨域请求好像是件好事,因为这样对于前端来说更安全。人家辛辛苦苦设计的同源策略,为啥会成为咱们的一个问题呢。
其实这也是没办法的事,毕竟前后端分离的项目,迫不得已就是需要进行跨域请求。比如在本地开发的环境中,很有可能端口号不一样。
本地环境中的跨域问题
在线上环境中,也同样会出现这样的情况。
线上环境中的跨域问题
前后端分离中的跨域问题
前后端分离项目在开发的时候,前端通过 nodejs 来运行,需要一个单独的端口,后端通过 Tomcat 或者 Jetty 来运行,也需要端口,两个不同的端口,就造成了跨域。如果你直接在本地后端项目中引入 Vue.min.js进行前端开发,那没什么问题,你应该也不会有跨域的疑问。但是如果你通过vscode进行前端开发,做单页面应用(SPA),那么必然会有这样的疑问,跨域问题怎么搞!这时前端端口和后端端口不一致,直接访问会造成跨域问题。
因为在单页面应用中,前端项目可以单独通过 node 启动,它单独占用一个端口,后端项目启动后也是另外一个端口,此时从前端发送请求到后端,由于两者处于不同的端口之上,因此必然存在一个跨域问题。
这个跨域有可能只是在开发环境下存在,生产环境下有可能不存在。因为当项目开发完成之后,我们对前端项目进行打包,打包后部署在 Nginx 上或者直接拷贝到后端项目中运行都可以(一般使用前者):
-
如果是前者,后端接口也通过 Nginx 进行映射,这个时候就不会存在跨域问题了
-
如果是后者,那就更简单了,部署的时候前后端代码放在一起,更不会有跨域问题了
在项目正式部署上线后,跨域问题就会得到解决。
Nginx解决生产环境前后端分离项目跨域问题
结合 Nginx 来部署前后端分离项目算是目前的主流方案。一来部署方便,二来通过动静分离也可以有效提高项目的运行效率。
大家知道我们项目中的资源包含动态资源和静态资源两种,其中:
-
动态资源就是那些需要经过容器处理的资源,例如 jsp、freemarker、各种接口等。
-
静态资源则是那些不需要经过容器处理,收到客户端请求就可以直接返回的资源,像 js、css、html 以及各种格式的图片,都属于静态资源。
将动静资源分开部署,可以有效提高静态资源的加载速度以及整个系统的运行效率。
在前后端分离项目部署中,我们用 Nginx 来做一个反向代理服务器,它既可以代理动态请求,也可以直接提供静态资源访问。我们来一起看下。建议大家先阅读松哥以前关于 Nginx 的一篇旧文,可以有效帮助大家理解后面的配置:
后端部署
后端接口的部署,主要看项目的形式,如果就是普通的 SSM 项目,那就提前准备好 Tomcat ,在 Tomcat 中部署项目,如果是 Spring Boot 项目,可以通过命令直接启动 jar,如果是微服务项目,存在多个 jar 的话,可以结合 Docker 来部署(参考一键部署 Spring Boot 到远程 Docker 容器),无论是那种形式,对于我们 Java 工程师来说,这都不是问题,我相信这一步大家都能搞定。
后端项目可以在一个非 80 端口上部署,部署成功之后,因为这个后端项目只是提供接口,所以我们并不会直接去访问他。而是通过 Nginx 请求转发来访问这个后端接口。
松哥这里以我去年为一个律所的小程序为例,后端是一个 Spring Boot 工程,那么我可以通过 Docker 部署,也可以直接通过命令来启动,这里简单点,直接通过命令来启动 jar ,如下:
nohup java -jar jinlu.jar > vhr.log &
后端启动成功之后,我并不急着直接去访问后端,而是安装并且去配置一个 Nginx,通过 Nginx 来转发请求,Nginx 的基本介绍与安装,大家可以参考(Nginx 极简入门教程!),我这里就直接来说相关的配置了。
这里我们在 nginx.conf 中做出如下配置:
首先配置上游服务器:
upstream zqq.com{
server 127.0.0.1:9999 weight=2;
}
在这里主要是配置服务端的地址,如果服务端是集群化部署,那么这里就会有多个服务端地址,然后可以通过权重或者 ip hash 等方式进行请求分发。
然后我们在 server 中配置转发规则:
location /jinlu/ {
proxy_pass http://zqq.com;
tcp_nodelay on;
proxy_set_header Host $host;
proxy_set_header X-Real-IP $remote_addr;
proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;
}
这样配置完成后,假设我目前的域名是 javaboy.org,那么用户通过 http://www.javaboy.org/jinlu/**
格式的地址就可以访问到我服务端的接口。
前端部署
以 Vue 为例,如果是 SPA 应用,项目打包之后,就是一个 index.html 还有几个 js、css、images 以及 fonts ,这些都是静态文件,我们将静态文件首先上传到服务器,然后在 nginx.conf 中配置静态资源访问,具体配置如下:
location ~ .*\.(js|css|ico|png|jpg|eot|svg|ttf|woff|html|txt|pdf|) {
root /usr/local/nginx/html/;#所有静态文件直接读取硬盘
expires 30d; #缓存30天
}
当然我这里是按照资源类型来拦截的,即后缀为 js、css、ico 等的文件,统统都不进行请求分发,直接从本地的 /usr/local/nginx/html/ 目录下读取并返回到前端(我们需要将静态资源文件上传到 /usr/local/nginx/html/
目录下)。
如果我们的服务器上部署了多个项目,这种写法就不太合适,因为多个项目的前端静态文件肯定要分门别类,各自放好的,这个时候我们一样可以通过路径来拦截,配置如下:
location /jinlu-admin/ {
root /usr/local/nginx/html/jinlu-admin/;#所有静态文件直接读取硬盘
expires 30d; #缓存30天
}
这样,请求路径是 /jinlu-admin/ 格式的请求,则不会进行请求分发,而是直接从本机的 /usr/local/nginx/html/jinlu-admin/
目录下返回相关资源。采用这方方式配置静态资源,我们就可以部署多个项目了,多个项目的部署方式和上面的一样。
这样部署完成之后,假设我的域名是 javaboy.org ,那么用户通过 http://www.javaboy.org/jinlu-admin/**
格式的请求就可以访问到前端资源了。此时大家发现,前端的静态资源和后端的接口现在处于同一个域之中了,这样就不存在跨域问题,所以我一开始基说不必用 JSONP 或者 CORS 去解决跨域。特殊情况可能需要在 nginx 中配置跨域。
开发环境中跨域问题的解决
在前端 nodejs 中配置请求转发
vue-cli2 方案
如果我们使用的 vue-cli2 来创建的 SPA 应用,创建成功之后,在项目的 config 目录下有一个 index.js 文件,在这个文件中,我们可以进行请求转发配置,如下图:
配置内容如下:
module.exports = {
dev: {
// Paths
assetsSubDirectory: 'static',
assetsPublicPath: '/',
proxyTable: {
'/': {
target: 'http://localhost:8082',
changeOrigin: true,
pathRewrite: {
'^/': ''
}
},
'/ws/*': {
target: 'ws://127.0.0.1:8082',
ws: true
}
},
...
}
proxyTable 就是我们配置的转发路由表。这个里边我们一共配置了两个规则:
-
第一个是拦截所有 HTTP 请求,将之转发到后端服务器上(前端默认端口是 8080),后端的端口是 8082。至于拦截规则
/
,大家可以自定义,根据实际情况来写,例如所有的 HTTP 请求都有一个统一的前缀 api,那么这里就可以写/api
。 -
第二个是拦截所有的 websocket 请求进行转发,我这里给所有的 websocket 请求取了一个统一的前缀
/ws
如果你有更多的拦截规则,继续在这里配置就可以了,这些配置只会在开发环境下生效,当项目编译打包时,这些配置是不会打包进去的,也就是说,项目发布的时候,这些配置是失效的,这个时候我们通过 Nginx 或者将前端代码拷贝到后端,就可以解决生产环境下的跨域问题了(相当于开发时候的跨域在生产环境下不存在)。
相对来说,vue-cli2 在这里的配置还比较容易。
vue-cli3 方案
vue-cli3 去年出来后,当时就尝了一把鲜,但是可能 vue-cli2 用久了,一时半会还不愿意接受 vue-cli3 ,于是尝鲜完了之后就放下了,没怎么用了。直到前两天,新项目尝试了一下 vue-cli3,结果在请求转发这块就掉坑里了。
一开始没多想,还是 vue-cli2 里边的老办法,只不过是在 vue-cli3 创建的项目的 vue.config.js 文件中进行配置,文件位置如下图:
注意,使用 vue-cli3 创建的 SPA 应用,没有 config 目录了,因此请求转发的配置我们要在 vue.config.js 这个配置文件中来配置。
一开始我直接把 vue-cli2 中的请求转发配置拷贝过来,这样发送 HTTP 请求倒是没问题,但是 websocket 请求一直有问题,后来经过仔细分析,发现这两者在请求转发配置上有一点点差异,我们来看看 vue-cli3 中的请求转发配置(这也是我这里 vue.config.js 文件的完整内容);
let proxyObj = {};
proxyObj['/ws'] = {
ws: true,
target: "ws://localhost:8081"
};
proxyObj['/'] = {
ws: false,
target: "http://localhost:8081",
changeOrigin: true,
pathRewrite: {
'^/': ''
}
};
module.exports = {
devServer: {
host: 'localhost',
port: 8080,
proxy: proxyObj
}
}
首先我们创建一个 proxyObj 用来放各种代理对象,至于代理的内容这里的则和 vue-cli2 中的没有太多差异。要注意的是,HTTP 请求代理中,多了一个属性 ws: false
,用过 vue-cli3 同学可能发现了,如果不加这个属性,浏览器控制台会一直报连不上 socket 的错,加上就没事了。
最后在 devServer 中指定项目的 host 和 port ,然后再配置一下 proxy 对象就可以啦。
这就是我们在 vue-cli3 中请求的配置。
不过这里的配置老实说没有什么难度,做过一次就会啦,要是没做过,头一次可能得折腾半天。