跨域问题的几种解决方案

跨域基本概念

  1. 触发环境:跨域一定是发生在浏览器端执行异步请求获取数据或资源的过程中的,后台程序或者客户端没有跨域问题
  2. 触发条件:只有浏览器页面的js代码中,请求资源的url的以下协议、域名、端口号,在与发送请求的网页的url不一致时,才会引发触发跨域问题
  3. 触发时机:浏览器响应内容跨域了,因此截获了响应内容

跨域?不跨域?

  1. 网页中以link,script,img,frame标签所请求的外部地址资源,因为标签的特殊性,所以不引发跨域问题。
  2. 前后端分离开发项目,但是war包和build包部署在了一个tomcat容器中,这种情况因为两者的协议、域名、端口都是一样的,只是在资源文件夹不一样,所以不引发跨域问题,例如存在一个tomcat中两个项目,各自具备不同的访问地址(如下所示),则在portal项目的任何地方发起ajax请求,访问admin的中接口,都不会引发跨域
http://localhost:8080/admin # 管理后台基于java编写
http://localhost:8080/portal # 展示前端基于react编写
  1. 有些后台服务器内置就是允许跨域,即在响应头中添加了参数response.setHeader("Access-Control-Allow-Origin", "*"),则虽然前端访问了外部资源(也确实跨域了),但没有引起跨域警告。
    2. 特别是:Arcgis-Server中的MapServer/FeatureServer,但不包括utils文件夹下的默认GP工具服务如几何服务,打印服务

跨域的常见解决办法

jsonp

原理关键

简单来说:就是前端动态创建script标签,并把后台请求资源的代码以回调函数的形式加载到前端中,然后在前端中进行执行。因为script没有跨域限制,因此可以解决跨域。

适用情况

  1. 基于JQuery或者原生JS进行开发
  2. 开发环境/测试环境/部署环境

不适用情况

  1. 只能处理get类型的请求,post/delete/Put等都不行

简单例子

$.ajax({
    url: "http://localhost:9090/student",
    type: "GET", //只能是GET!!!!!
    dataType: "jsonp", //指定服务器返回的数据类型
    success: function (data) {
            var result = JSON.stringify(data); //json对象转成字符串
            $("#text").val(result);
        }
});

项目的proxy改写

关键原理

一般来说:react或者vue的脚手架项目都有package.json包可以配置全局proxy属性地址,在开发测试中即可将要跨域的url进行唯一性的改写配置。即前端发送给些后的请求,被项目的proxy改写属性拦截,转发为外部url,浏览器只判定了一开始的请求,认为没有跨域。

适用情况

  1. react/vue脚手架项目
  2. 基于nodeJS的开发环境或运行环境且只有一个跨域规则

不适用情况

  1. 最新版本的react脚手架项目,只能配置一个转发规则,如果有多个目标地址需要准发,则该方式不适用
  2. 打包部署到nginx/tomcat等容器的部署环境或运行环境,proxy则转发无效

简单例子

package.json 最新版本只能配置字符串

"proxy": "http://172.16.136.249:8080"

package.json 老版本可以配置转发规则(但实际已经弃用了,会与其他包产生冲突)

"proxy": {
    "/api/**": {
      "target": "http://172.16.136.249:8080",
      "changeOrigin": true
    }
  }

nodeJS中间件

关键原理

  1. 简单来说就是通过在nodeJS环境中搭建一个临时的微型服务器,前端发送的请求先发送到这个服务器中,再被这个服务器以后台请求的方式进行转发,进而解决跨域问题。
  2. 类似于将前端项目和后台项目都部署在一个tomcat服务器上,协议,域名,端口都一致,只是资源文件夹不同,所以没有跨域的问题。
  3. 一般常用http-proxy-middleware组件

http-proxy-middleware注意

  1. 这虽然是个JS库,但其运行环境是NodeJS而不是浏览器
  2. 它创建了的是一个匿名的后台应用而不是一个纯粹的网页
  3. 它可以解决前端项目在开发调试过程中的跨域问题
  4. 若想它在实际运用工程中起作用,则开发的项目需要部署在NodeJS环境下

适用情况

  1. react/vue脚手架项目
  2. 基于nodeJS的开发环境或运行环境且有多个跨域规则

不适用情况

简单例子

  1. src包下创建setupProxy.js文件,并按照如下形式配置转发规则
const { createProxyMiddleware } = require('http-proxy-middleware');  

module.exports = function (app) {
    app.use(createProxyMiddleware('/geometry/arealength',
        {
            target: 'http://localhost:6080/arcgis/rest/services/Utilities/Geometry/GeometryServer/areasAndLengths',
            pathRewrite: {
                '^/geometry/arealength': '',
            }, //改写路径,将原来请求中的字符串/geometry/arealength替换为""
            changeOrigin: true, //允许跨域
            secure: false, // 是否验证证书
            ws: true, // 启用websocket
        }
    ));
    //其他转发规则
    app.use(.../....)
};

  1. 前端发起调用,注意url
axios.post("/geometry/arealength", qs.stringify(data)).then(value => {
    //....
}).catch(error => {
    //....
}).finally(() => {
    //....
})
  1. 补充说明
  1. setupProxy.js文件配置的转发规则没有热更新性,每次修改后需要手工停止并重新start项目
  2. setupProxy.js具体的配置规则,可参考https://www.cnblogs.com/zhaoweikai/p/9969282.html

nginx配置

关键原理

  1. 本质上和NodeJS中间件差不多,都是构建一个与前端网页应用同源的后台服务器,截获前端的外部请求url,并在nginx内部进行以后台请求的方式进行转发
  2. 只是说nginx是一个独立的代理服务器,不随每一次的项目的调试部署而创建销毁

适用情况

  1. 以nginx自身作为项目运行的部署环境

不适用情况

  1. 原生脚手架的react/vue开发测试环境
  2. 暂时没有找到比较成熟的基于Nginx环境进行调试的打包或者测试插件

补充说明

  1. nginx上配置的转发规则独立于任何项目,只要请求满足nginx的代理规则都会被转发,nginx上如果部署了多个一定要注意规则是否冲突
  2. nginx的代理规则没有自动热更新性,一旦修改了代理规则一定要nignx -s reload或者重启
  3. 纯前端项目(没有后台服务器或者后台服务器不可修改)可以采取nodeJS中间件+nginx代理搭配的方式,即:
    1. 开发时使用http-proxy-middleware,方便调试与修改
    2. 部署时将转发规则重新配置到nginx上,方便部署与运行

简单示例

nginx的conf文件配置如下

server {
	server_name  localhost;
	listen 80;
		
	location / {
        root   html/demo;
        index  index.html index.htm;
    }
		
	location /geometry/arealength
	{
		proxy_pass 'http://localhost:6080/arcgis/rest/services/Utilities/Geometry/GeometryServer/areasAndLengths';
		proxy_set_header host $host;
		proxy_set_header X-Real-IP      $remote_addr;
		proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;
	}
}

网页前端发送请求(前端项目build后放在nginx服务器可代理访问的路径中)

axios.post("/geometry/arealength", qs.stringify(data)).then(value => {
    //....
}).catch(error => {
    //....
}).finally(() => {
    //....
})

proxy.jsp代理转发

  1. 该种方式实际是前后端一体化综合应用模式,适用于前端项目打包后发布到Tomcat容器而不是Nginx
    1. 前端项目在开发过程中还是使用http-proxy-middleware配置代理规则
    2. 前端项目在**部署时*该变请求头,以同源目录下的proxy.jsp转发并接收前端请求
  2. 该方式在开发过程和部署过程的代理地址是不一样的,因此需要通过process.env.NODE_ENV变量进行判断并合成对应的请求头
  3. 该种方式也可以通过引入.env文件的形式,实现多环境切换更好的满足开发/发布不同环境的业务需求

有关react项目中使用.env实现环境变量切换的内容,可参考这篇博客

核心思路

  1. 调试环境:请求地址带有一个XXXX的标识符前缀,让http-proxy-middleware对其拦截并转发,解决跨域
  2. 部署环境:请求地址带有一个proxy.jsp?的标识前缀,让tomcat后台调用jsp文件,对请求进行后台转发,解决跨域

实现步骤

在这里插入图片描述

创建env文件
  1. 在项目根目录下创建一个名为.env的文件,用于配置项目打包时的请求目标地址
REACT_APP_HelloMsg = "打包版本"
REACT_APP_ServerBaseUrl = http://192.168.1.94:6080/
  1. 在项目更目录下创建一个名为.env.development的文件,用于配置项目开发时的请求目标地址
REACT_APP_HelloMsg = "开发版本"
REACT_APP_ServerBaseUrl = http://localhost:6080/
编写请求帮助类

在项目src包下任意一个位置,创建APIs.js帮助类,编写该帮助类的目的如下:

  1. 利用process.env.NODE_ENV区分实际环境,合成不同的请求地址
  2. 封装一些请求过程中的固定参数处理过程

写法不唯一只要能实现上述两个功能要求即可

/*
* 开发模式走http-proxy-middleware代理
* 发布模式走proxy.jsp代理
*/
function decorateBaseUrl(url, prefix) {
    console.log(process.env.NODE_ENV)
    if (process.env.NODE_ENV === "development")
        return "/" + prefix + "/" + url;
    else
        return "./proxy.jsp?" + url
}
//合成前端实际发送的请求的主机地址
const processServerHost = decorateBaseUrl(process.env.REACT_APP_ServerBaseUrl, "geometryAPI")

export default class APIs {
    //按照功能合成相应的请求url
    distanceAndareas_test(geometry="", wkid=4490) {
        let url = processServerHost + "arcgis/rest/services/Utilities/Geometry/GeometryServer/areasAndLengths"
        console.log(url)
        //模拟测试,这里之直接使用静态的mock数据
        let data = {
            polygons: JSON.stringify([{"rings":[[[0,0],[0,100000],[100000,100000],[100000,0],[0,0]]]}]),
            sr: 102009,
            lengthUnit: 9001,
            areaUnit: " { \"areaUnit\": \"esriSquareMiles\" } ",
            calculationType: "preserveShape",
            f: "pjson"
        }
        return axios.post(url, QueryString.stringify(data))
    }
}

配置http-proxy-middleware路由规则
const { createProxyMiddleware } = require('http-proxy-middleware');

module.exports = function (app) {
    app.use(createProxyMiddleware('/geometryAPI/',
        {
            target: process.env.REACT_APP_ServerBaseUrl,
            changeOrigin: true,
            pathRewrite: {
                '^/geometryAPI/': '',
            },
            secure: false, // 是否验证证书
            ws: true, // 启用websocket
        }
    ));
};

配置完成后即可在开发环境正常的进行开发测试,调用过程如下:

function TestContainer(props) {
    
    function Clicked(){
        new APIs().distanceAndareas_test().then(response=>{
            console.log(response.data)
        }).catch(error=>{
            console.error(error)
        })
    }


    return (
        <div>
            this is test container
            <div>
                {process.env.REACT_APP_HelloMsg}
                <Button onClick = {Clicked}>走代理正确</Button>
            </div>
        </div>
    );
}

export default TestContainer;

注意事项

  1. 这里实际就是对APIs.js中的访问地址机按照/geometryAPI/进行拦截,并原来请求的地址中的/geometryAPI/字串移除
  2. target参数是由process.env提供的全局参数,建议只配到主机+域名即可,原因有2
    1. 直接使用.env的配置的主机地址,不用二次编写
    2. 方便pathRewrite实现路径改写,较少代理规则的配置数目,使用该种规则配置后满足/geometryAPI/xxxxx/xxx格式的地址,都会被重写成process.env.REACT_APP_ServerBaseUrl/xxxxx/xxxx
tomcat发布
  1. 在public文件根目录下创建一个proxy.jsp的文件,其内容为
<%@page session="false"%>
<%@page import="java.net.*, java.io.*"%>
<%
	try {
		String reqUrl = request.getQueryString();
		URL url = new URL(reqUrl);
		HttpURLConnection con = (HttpURLConnection)url.openConnection();
		con.setDoOutput(true);
		con.setReadTimeout(600000);
		con.setRequestMethod(request.getMethod());
		if (request.getContentType() != null) {
			con.setRequestProperty("Content-Type", request.getContentType());
		}
		con.setRequestProperty("Referer", request.getHeader("Referer"));
		int clength = request.getContentLength();
		if (clength > 0) {
			con.setDoInput(true);
			InputStream istream = request.getInputStream();
			OutputStream os = con.getOutputStream();
			final int length = 5000;
			byte[] bytes = new byte[length];
			int bytesRead = 0;
			while ((bytesRead = istream.read(bytes, 0, length)) > 0) {
				os.write(bytes, 0, bytesRead);
			}
		} else {
			con.setRequestMethod("GET");
		}
		out.clear();
		out = pageContext.pushBody();
		OutputStream ostream = response.getOutputStream();
		response.setContentType(con.getContentType());
		InputStream in = con.getInputStream();
		final int length = 5000;
		byte[] bytes = new byte[length];
		int bytesRead = 0;
		while ((bytesRead = in.read(bytes, 0, length)) > 0) {
			ostream.write(bytes, 0, bytesRead);
		}
	} catch (Exception e) {
		e.printStackTrace();
		System.out.println(e.getMessage());
		response.setStatus(500);
	}
%>
  1. 使用npm run build 对项目进行打包,并发布到tomcat容器中,关于React项目发布到Tomcat一些注意事项,参考这篇博客

summary

  1. 跨域问题,是只发生在浏览器端访问通过代码访问非本网页同源(协议、域名、端口号)内容时,引起的安全策略问题(后台不存在跨域)
  2. 跨域的表现是浏览器不能收到非同源策略的资源响应,而不是不能发起非同源策略的请求
  3. 解决跨域问题的本质上就三种思路
    1. 前端特殊标签处理:jsonp
    2. 后台转发:nodeJS的http-proxy-middleware,或者Nginx,或者Tomcat+Proxy.jsp
    3. 后台允许:重新设置响应头
  4. 没有万能的方法,一定要看具体前端项目和后端接口的实际使用情况
  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值