后端开发问题详解

后端开发问题详解

前后端联调中的跨域问题

2024-03-31 20:42:51

为什么会出现跨域问题

首先我们要明白什么是跨域,对于浏览器而言跨域中的域指的是**域名+端口, 意思是如果当前站点 site.pandaer.com​ 中的JS文件发送了一个不同域的请求,这种情况就是跨域。记住这是浏览器的安全策略,主要是为了防止一些Cookie信息泄露,举个例子,你登陆了网易云音乐,浏览器在Cookie中存有登录的Token,这个时候你又打开了另外一个站点,然后这个站点的JS文件向网易云发送了一个请求,请求的内容是获取你喜欢的音乐名单,这个时候由于没有跨域的限制,发送请求时,浏览器会自动带上你的Cookie,你喜欢音乐的名单就在你不知情的情况下被泄露了。有了跨域之后,浏览器就会阻止这一行为,注意!!!是浏览器阻止。那为什么移动端和桌面端没有这个问题呢?因为桌面端和移动端的程序数据不共享,所以就拿不到对应的Cookie信息,所以就没有这个跨域限制呗。

如何解决跨域问题

有时候,我们就是想获取其他站点的资源,这个时候怎么办呢?HTTP的规范提出了一种解决办法 -- CORS(Cross-Origin Resource Sharing),另外还有很多解决办法,比如可以想法设法不跨域,比如反向代理。而且CORS只是一种规范,有许多具体的实现,这里主要聊聊SpringBoot的落地实现。

如果你遇到过跨域问题,对下面的代码一定很熟悉,但是它并不完全,或者说在某些时候会出问题,主要发生在我们有自己的拦截器,或者过滤器的时候**。

@Configuration
public class CorsConfig implements WebMvcConfigurer {
    @Override
    public void addCorsMappings(CorsRegistry registry) {
        registry.addMapping("/**")
                .allowCredentials(true)
                .allowedHeaders("*")
                .allowedOriginPatterns("*")
                .allowedMethods("*")
                .maxAge(3600);
    }
}

首先我们先简单来复习一下CORS的规范,对于请求分为两种,一种是简单请求,一种是为复杂请求(为了方便对应),浏览器在正式发起复杂请求时,会先发送一个预检请求(options),判断这个额外的站点是否支持资源共享,如果没有回应这个请求,或者这个请求的状态码不是2xx,那么浏览器就认为额外的站点不支持跨域,即不支持资源的共享。接下来我们通过debug的方式来了解一下SpringBoot是如何实现CORS的规范的。

实现CORS,其实主要就是处理好options这个预检请求,以及对跨域请求的响应头的设置。那我们的第一个断点应该打在哪里呢?我们知道无论怎么样,预检请求首先的是一个HTTP的请求,熟悉SpringMVC的伙伴应该知道SpringMVC有一个核心的类DisapatchServlet​,里面有个很重要的方法doDispatch​所有的请求都会先到这里,然后由这个方法去寻找可以处理这个请求的处理链,并交给这个处理链去处理。那么我们的第一个断点就下这个方法,等待我们的预检请求。如下图

image

这个时候我们抓住了预检请求,然后一步一步让他执行下去,看看是如何处理的

image

当执行到这里的时候,上面有一段注释很有意思:获取处理当前请求的handler,

image

我们看到了一个很有趣的单词,preflight​ 他的中文意思是预检

image

看看官方描述,所以我们有理由相信,这个AbstractHandlerMapping​中的PreFlightHandler​一个和预检请求有关,于是我们的第二个断点就找到了,

image

这次更加确定了,因为我们看见了CorsInterceptor​,我们可以大胆猜测一下,对于CORS的落地实现应该用的是Interceptor​的机制,于是我们的第三个断点找到了

image

看到这段代码,我们可以确定主要逻辑在最后一段代码中corsProcessor.processRequest(this.config, request, response);​,然后我们去看看

image

这是一个抽象方法,额,怎么办呢?退一步看看corsProcessor的具体实现,两种办法,直接利用idea中的eval能力,debug到这一步,然后eval就可以了,另外一种就是看看他在哪里被赋值或者初始化了。

image

很好,找到了具体的一个实现了,我们的第四个断点就找到了,去看看

	public boolean processRequest(@Nullable CorsConfiguration config, HttpServletRequest request,
			HttpServletResponse response) throws IOException {

		Collection<String> varyHeaders = response.getHeaders(HttpHeaders.VARY);
		if (!varyHeaders.contains(HttpHeaders.ORIGIN)) {
			response.addHeader(HttpHeaders.VARY, HttpHeaders.ORIGIN);
		}
		if (!varyHeaders.contains(HttpHeaders.ACCESS_CONTROL_REQUEST_METHOD)) {
			response.addHeader(HttpHeaders.VARY, HttpHeaders.ACCESS_CONTROL_REQUEST_METHOD);
		}
		if (!varyHeaders.contains(HttpHeaders.ACCESS_CONTROL_REQUEST_HEADERS)) {
			response.addHeader(HttpHeaders.VARY, HttpHeaders.ACCESS_CONTROL_REQUEST_HEADERS);
		}

		if (!CorsUtils.isCorsRequest(request)) {
			return true;
		}

		if (response.getHeader(HttpHeaders.ACCESS_CONTROL_ALLOW_ORIGIN) != null) {
			logger.trace("Skip: response already contains \"Access-Control-Allow-Origin\"");
			return true;
		}

		boolean preFlightRequest = CorsUtils.isPreFlightRequest(request);
		if (config == null) {
			if (preFlightRequest) {
				rejectRequest(new ServletServerHttpResponse(response));
				return false;
			}
			else {
				return true;
			}
		}

		return handleInternal(new ServletServerHttpRequest(request), new ServletServerHttpResponse(response), config, preFlightRequest);
	}

截图不好看,直接把代码拿过来,前面一堆的判断都在往一个响应头Vary​,加入一些东西,这个对于我们来说不是很重要,可以暂时跳过,我们先建立一个todo。

  • Vary的作用

boolean preFlightRequest = CorsUtils.isPreFlightRequest(request);​看看这段代码,这里就是在判断这个请求是不是预检请求。

image

handleInternal(new ServletServerHttpRequest(request), new ServletServerHttpResponse(response), config, preFlightRequest);​这段代码也是一个核心

走,我们去看看

	protected boolean handleInternal(ServerHttpRequest request, ServerHttpResponse response,
			CorsConfiguration config, boolean preFlightRequest) throws IOException {

		//取出请求头中的origin
		String requestOrigin = request.getHeaders().getOrigin();
		//请求中origin是否在配置中
		String allowOrigin = checkOrigin(config, requestOrigin);
		//获取响应头
		HttpHeaders responseHeaders = response.getHeaders();

		//请求中的origin不在配置中,直接拒绝请求
		if (allowOrigin == null) {
			logger.debug("Reject: '" + requestOrigin + "' origin is not allowed");
			rejectRequest(response);
			return false;
		}

		//获取到真实请求方法
		HttpMethod requestMethod = getMethodToUse(request, preFlightRequest);
		List<HttpMethod> allowMethods = checkMethods(config, requestMethod);
		//判断真实请求的方法是否在配置中 不在就拒绝
		if (allowMethods == null) {
			logger.debug("Reject: HTTP '" + requestMethod + "' is not allowed");
			rejectRequest(response);
			return false;
		}

		//同理,获取真实请求的请求头
		List<String> requestHeaders = getHeadersToUse(request, preFlightRequest);
		List<String> allowHeaders = checkHeaders(config, requestHeaders);
		//如果请求头不在,拒绝
		if (preFlightRequest && allowHeaders == null) {
			logger.debug("Reject: headers '" + requestHeaders + "' are not allowed");
			rejectRequest(response);
			return false;
		}

		//将请求中的origin设置到AccessControlAllowOrigin请求头中
		responseHeaders.setAccessControlAllowOrigin(allowOrigin);

		//预检请求就设置AccessControlAllowMethod的请求头
		if (preFlightRequest) {
			responseHeaders.setAccessControlAllowMethods(allowMethods);
		}
		//同理
		if (preFlightRequest && !allowHeaders.isEmpty()) {
			responseHeaders.setAccessControlAllowHeaders(allowHeaders);
		}

		//同理
		if (!CollectionUtils.isEmpty(config.getExposedHeaders())) {
			responseHeaders.setAccessControlExposeHeaders(config.getExposedHeaders());
		}

		//同理
		if (Boolean.TRUE.equals(config.getAllowCredentials())) {
			responseHeaders.setAccessControlAllowCredentials(true);
		}

		//同理
		if (Boolean.TRUE.equals(config.getAllowPrivateNetwork()) &&
				Boolean.parseBoolean(request.getHeaders().getFirst(ACCESS_CONTROL_REQUEST_PRIVATE_NETWORK))) {
			responseHeaders.set(ACCESS_CONTROL_ALLOW_PRIVATE_NETWORK, Boolean.toString(true));
		}

		//同理
		if (preFlightRequest && config.getMaxAge() != null) {
			responseHeaders.setAccessControlMaxAge(config.getMaxAge());
		}

		response.flush();
		//放行
		return true;
	}

看完这段代码,你就知道了SpringBoot是如何落地实现CORS的了。

逻辑很简单就是根据CORS的规范设置几个响应头就好了,而且那几个响应头还都是在预检请求中呢。

读完这段代码不知道你发现一个问题没有,SpringBoot的实现中,并没有将Options请求立即打回,仅仅是flush了一下,为什么要这样设计了?为什么不立即打回?这两个问题值得思考,但是我还没有想法,我认为立即打回更好,这样就不会导致后面自定义了拦截器和过滤器产生的跨域请求失败的问题了。那自定义了拦截器和过滤器为什么就会导致跨域请求失败呢?原因就在于浏览器不仅仅要看option的响应头是否带了规范中提到了响应头,整体这个Options响应必须是OK的,即2xx的响应码。

image

看吧,响应头中该有的东西都有,只是没有返回正确的响应码还是会导致跨域请求失败。然后我们在实现拦截器和过滤器的时候,我们一般只考虑业务吧,没有单独考虑到这个预检请求的问题吧。

所以我的建议是如何你有自定义拦截器和过滤器,那么我建议专门定义一个解决预检请求的拦截器,来处理预检请求就像下面这样

        if (HttpMethod.OPTIONS.matches(request.getMethod())) {
            response.setStatus(HttpStatus.NO_CONTENT.value());
            return false;
        }

所以这就是我有点不理解的地方,SpringBoot为什么不直接在设置完响应头后就打回预检请求呢?

到这里,对于SpringBoot是如何实现CORS规范的就解释完了,本次实验用的SpringBoot的版本是v3.2.4

SpringBoot其实也提供了注解来解决跨域问题,这个注解可以更加细粒度的控制需要共享的资源,今天就不具体展开讲了,如果感兴趣可以看看官方文档:https://spring.io/guides/gs/rest-service-cor

总结

跨域问题是HTTP为了解决用户的隐私问题而产生的,如果我们就是想要跨站点共享资源,可以试试官方的CORS规范,现在主流的框架都有对其的具体实现,不需要我们手动实现。

然后解释了SpringBoot处理跨域请求时会遇到的一些问题即自定义拦截器和过滤器的时候。

  • 6
    点赞
  • 5
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
基于STM32节点和阿里云IoT平台 的物联网应用开发 系列课程 第四章 服务端的应用开发 课程内容下载、观看 • 视频观看:AI电堂、阿里云大学IoT课堂 • 课件胶片下载:STMCU中文官网、阿里云大学IoT课堂 • 课件项目下载:STMCU中文官网、阿里云大学IoT课堂 2 STM32公众号 阿里云大学 电堂公众号STM 中文官网 第四章内容简介 • 第一节:综合软件架构介绍 • 软件架构,知识结构梳理 • 第二节:后端服务开发后端框架,后端项目:和阿里云IoT平台对接,操作数据库,和前端应用对接 • 第三节:前端服务开发体验 • 系统应用前端开发详解 3 STM32-阿里云IoT 联合课件开发 第四章 . 第二节 后端服务开发 第四章.第二节 内容简介 • 认识后端框架 • Mysql数据库介绍 • MyBatis框架 • SpringBoot框架 • Maven介绍 • 初始化并运行第一个后端项目 • 第一个springboot项目详解 • 从无到有搭建第一个springboot项目 5 • 应用系统开发 • 存储数据库设计 • 配置服务端订阅 • 应用系统与iot平台连接 • 调用阿里云物联网接口 • 接口编写 • 跨域请求 • 应用调试与部署 • 项目打包 • 项目部署 了解后端基础概念 • 后端的基本概念 • 在软体架构和程序设计领域,前端是软件系统中直接和用户交互的部分,而后端控制着软 件的输出 • 前端通过ajax等技术向后端进行网络请求;后端收到请求后对数据库进行操作,返回给前 端JSON数据;前端把相应数据展示在页面上 • 将软件分为前端和后端是一种将软件不同功能的部分相互分离的抽象
提供的源码资源涵盖了安卓应用、小程序、Python应用和Java应用等多个领域,每个领域都包含了丰富的实例和项目。这些源码都是基于各自平台的最新技术和标准编写,确保了在对应环境下能够无缝运行。同时,源码中配备了详细的注释和文档,帮助用户快速理解代码结构和实现逻辑。 适用人群: 这些源码资源特别适合大学生群体。无论你是计算机相关专业的学生,还是对其他领域编程感兴趣的学生,这些资源都能为你提供宝贵的学习和实践机会。通过学习和运行这些源码,你可以掌握各平台开发的基础知识,提升编程能力和项目实战经验。 使用场景及目标: 在学习阶段,你可以利用这些源码资源进行课程实践、课外项目或毕业设计。通过分析和运行源码,你将深入了解各平台开发的技术细节和最佳实践,逐步培养起自己的项目开发问题解决能力。此外,在求职或创业过程中,具备跨平台开发能力的大学生将更具竞争力。 其他说明: 为了确保源码资源的可运行性和易用性,特别注意了以下几点:首先,每份源码都提供了详细的运行环境和依赖说明,确保用户能够轻松搭建起开发环境;其次,源码中的注释和文档都非常完善,方便用户快速上手和理解代码;最后,我会定期更新这些源码资源,以适应各平台技术的最新发展和市场需求。
提供的源码资源涵盖了安卓应用、小程序、Python应用和Java应用等多个领域,每个领域都包含了丰富的实例和项目。这些源码都是基于各自平台的最新技术和标准编写,确保了在对应环境下能够无缝运行。同时,源码中配备了详细的注释和文档,帮助用户快速理解代码结构和实现逻辑。 适用人群: 这些源码资源特别适合大学生群体。无论你是计算机相关专业的学生,还是对其他领域编程感兴趣的学生,这些资源都能为你提供宝贵的学习和实践机会。通过学习和运行这些源码,你可以掌握各平台开发的基础知识,提升编程能力和项目实战经验。 使用场景及目标: 在学习阶段,你可以利用这些源码资源进行课程实践、课外项目或毕业设计。通过分析和运行源码,你将深入了解各平台开发的技术细节和最佳实践,逐步培养起自己的项目开发问题解决能力。此外,在求职或创业过程中,具备跨平台开发能力的大学生将更具竞争力。 其他说明: 为了确保源码资源的可运行性和易用性,特别注意了以下几点:首先,每份源码都提供了详细的运行环境和依赖说明,确保用户能够轻松搭建起开发环境;其次,源码中的注释和文档都非常完善,方便用户快速上手和理解代码;最后,我会定期更新这些源码资源,以适应各平台技术的最新发展和市场需求。
提供的源码资源涵盖了安卓应用、小程序、Python应用和Java应用等多个领域,每个领域都包含了丰富的实例和项目。这些源码都是基于各自平台的最新技术和标准编写,确保了在对应环境下能够无缝运行。同时,源码中配备了详细的注释和文档,帮助用户快速理解代码结构和实现逻辑。 适用人群: 这些源码资源特别适合大学生群体。无论你是计算机相关专业的学生,还是对其他领域编程感兴趣的学生,这些资源都能为你提供宝贵的学习和实践机会。通过学习和运行这些源码,你可以掌握各平台开发的基础知识,提升编程能力和项目实战经验。 使用场景及目标: 在学习阶段,你可以利用这些源码资源进行课程实践、课外项目或毕业设计。通过分析和运行源码,你将深入了解各平台开发的技术细节和最佳实践,逐步培养起自己的项目开发问题解决能力。此外,在求职或创业过程中,具备跨平台开发能力的大学生将更具竞争力。 其他说明: 为了确保源码资源的可运行性和易用性,特别注意了以下几点:首先,每份源码都提供了详细的运行环境和依赖说明,确保用户能够轻松搭建起开发环境;其次,源码中的注释和文档都非常完善,方便用户快速上手和理解代码;最后,我会定期更新这些源码资源,以适应各平台技术的最新发展和市场需求。

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值