长亭百川云 - 文章详情

深入SpringMVC聊跨域问题的最佳实践

阿里云开发者

72

2024-07-13

阿里妹导读

本文将深度剖析SpringMVC中的跨域问题及其最佳实践,尤其聚焦于Jsonp接口在技术升级中遇到的跨域难题。结合一个具体的案例,展示在技术升级过程中,原有JSONP接口出现跨域问题的表现及原因,以及如何通过自定义SpringMVC的拦截器和MessageConvertor来解决这个问题。

一、写在最前面,跨域和Jsonp

浏览器为了防止恶意网站进行跨站点的请求伪造,会限制不同站点之间的资源交互,这种行为被称为浏览器的同源策略(Same Origin Policy)。简单来说,在站点A的页面中的请求,请求url中的域名一般不能是其他站点。在不同站点之间进行资源访问,称为跨域(即Cross-Origin)。

当产生跨域请求时,即使响应结果从服务端返回了,浏览器也会将其拦截,产生CORS异常,提示:“Access has been blocked by CORS policy”。

然而,在多个安全的站点之间,互相访问数据是非常普遍的,比如在商家工作台页面(域名是i.alibaba.com)会访问其他子系统(比如数参data.alibaba.com)的接口,因此,需要为跨域问题提供解决方案。解决跨域问题的方法有两种:CORS和Jsonp。

CORS(Cross-Origin Resouce Sharing)

CORS是W3C官方提出的跨域解决方案。当在站点A的页面访问站点B时,若站点B的服务在响应中添加以下header,则浏览器不拦截对应的结果。header包括:

Access-Control-Allow-Origin

允许的站点(ip或域名),也就是原站点A

Access-Control-Allow-Credentials

是否可以携带Cookie

Access-Control-Allow-Methods

允许的HTTP方法,GET/POST等

Access-Control-Allow-Headers

允许的Header

JSONP(JSON with Padding)

Jsonp并不是官方提供的跨域解决方案,但是因为用的早,现在仍有非常多的历史接口还是基于jsonp。它主要的思路是:

对于服务端来说,用jsonp来处理跨域,需要做两件事:

1. 填充json

1.首先,当前端发起jsonp请求时,会动态插入一个



2.其次,服务端填充json。服务端的原始结果为json格式,将其填充后,会得到一条函数调用语句。函数名为上一步的callback入参,函数实参是原始的json结果。这个padding的过程也是jsonp名称的由来。



3.最后,前端执行填充后的结果,于是在回调函数jsonp_1718436528810_81650中可以获取原始json数据。

2. 设置响应的Content-Type

为了绕过同源策略,必须让浏览器认为这次请求返回的内容是一个script脚本,因此需要让响应的Content-Type是“application/javascript”。

如果返回的内容是jsonp,但Content-Type是“application/json”,浏览器会无法识别,并产生ORB(Opaque Response Blocking)异常,提示:“No data found for resource with given identifier”。



对比两种跨域的解决方案,CORS清晰简单,正在逐步替代Jsonp。很多框架和三方库(如spring mvc,fastjson等)也在逐步废弃jsonp的相关实现。但是,后者的使用非常广泛,基于现实情况,很多系统里是两种方式并存的。

二、问题背景

2.1 问题表现

在对某个Web系统做技术升级的过程中,有Jsonp接口出现跨域问题。现象是:

1.CORS相关的http header缺失(即“Access-Control-Allow-Credentials”等)。虽然在这个场景并不必要,但这些header被设置过,却未生效。

2.Jsonp接口返回时,http header中的Content-Type值为“application/json”,而不是“application/javascript”,导致出现ORB错误。

2.2 原实现方案

出问题的接口为jsonp接口,它通过自定义Spring MVC的拦截器(Interceptor)和MessageConvertor实现json padding并设置content-type,原实现方案如下。



1.声明自定义拦截器JsonpInterceptor,若请求URI以jsonp结尾,则需经过该拦截器

`@Configuration``public class MvcConfiguration implements WebMvcConfigurer {`    `    @Autowired`    `private JsonpInterceptor jsonpInterceptor;`    `    @Override`    `public void addInterceptors(InterceptorRegistry registry) {`        `registry.addInterceptor(jsonpInterceptor)`                `.addPathPatterns("/**/*.jsonp");`    `}``}`

2.声明类JsonpWrapper。

3.在controller中,对所有的返回值做统一处理:若是jsonp请求,则将结果对象包裹在JsonpWrapper中。

`public static Object getJsonOrJsonpObj(String callback, Object obj) {`    `if (StringUtils.isBlank(callback)) {`        `return obj;`    `} else {`        `JsonpWrapper jsonp = new JsonpWrapper();`        `// callback是一个入参,且会返回到浏览器,因此需要做处理,防止脚本注入`        `jsonp.setCallback(SecurityUtil.escapeHtml(callback));`        `jsonp.setValue(obj);`        `return jsonp;`    `}``}`

4.自定义JavascriptConvertor,并替换SpringMVC默认的json convertor。Message Convertor是Spring MVC用于处理请求和返回数据的类,更多的细节先按下不表。

5.当返回结果为JsonpWrapper时,按照jsonp的格式,在结果前后填充数据,输出最终结果。AbstractJackson2HttpMessageConverter是SprigMVC提供用于处理json类数据的Message Convertor的父类。

`public class JavaScriptMessageConverter extends AbstractJackson2HttpMessageConverter {`    `// 省略其他`    `    @Override`    `protected void writePrefix(JsonGenerator generator, Object object) throws IOException {`        `String callback = (object instanceof JsonpWrapper ? ((JsonpWrapper) object).getCallback() : null);`        `if (callback != null) {`            `generator.writeRaw("/**/");`            `generator.writeRaw(callback + "(");`        `}`    `}``   `    `@Override`    `protected void writeSuffix(JsonGenerator generator, Object object) throws IOException {`        `String callback = (object instanceof JsonpWrapper ? ((JsonpWrapper) object).getCallback() : null);`        `if (callback != null) {`            `generator.writeRaw(");");`        `}`    `}``}`

6.在拦截的后置处理中,向response中写入CORS请求,并修改ContentType为application/javascript

`public class JsonpInterceptor extends HandlerInterceptorAdapter {`    `    public void afterCompletion(HttpServletRequest request, HttpServletResponse response, Object handler,`      `@Nullable Exception ex) {`        `response.setHeader("Access-Control-Allow-Credentials", "true");`        `response.setHeader("Access-Control-Allow-Headers", "*");`        `response.setHeader("Access-Control-Allow-Methods", "POST, GET, PUT, DELETE, OPTIONS");`        `// 省略safeOrigin获取过程`        `response.setHeader("Access-Control-Allow-Origin", safeOrigin);`        `response.setHeader("Content-Type", "application/javascript");`    `}``}`

上面提到的JsonpInterceptor,JsonpWrapper,JavascriptConvertor均为自定义实现。这个方案在很长的时间里也能正常工作。但请注意,设置Http Header的位置是在拦截器的afterCompletion方法中。

2.3 问题定义

将问题表现和原实现方案结合看,流程中,最后一步的setHeader和问题直接相关。所以从这里开始,只需要解答一个问题,那就是:

为什么自定义的interceptor中,对response的header设置不生效了?

三、Spring MVC的工作流程

拦截器(Interceptor)是Spring MVC提供的工具。因此下面简要回顾Spring MVC的工作流程,且重点放在Response的Header和内容写入上。

3.1 一张古老的时序图



上图展示了Spring MVC的几个关键组件:

1.DispatcherServlet:Spring MVC的前端控制器,负责接受请求,并分发到合适的处理器(Controller)上

2.HandlerMapping:根据请求的URL找到对应的处理器和方法

3.Controller:执行业务逻辑,返回视图或者响应体。

4.ViewResolver:通过该组件,找到对应视图

5.View:根据Controller返回的Model数据,渲染页面。

6.HandlerAdapter:对调用逻辑的一个代理,用于处理不同的场景。

7.Interceptor:在调用Controller前后拦截,可以添加处理逻辑,用于日志,鉴权等。拦截器是可选的。

上图同时也描述了Spring MVC工作中的一个标准流程,可以简要概括为:

1.HTTP请求首先到达Spring MVC的核心——DispatcherSerlvet;

2.DispatcherSerlvet根据请求信息(比如URL)查询HandlerMapping,这一步返回的结果,我们可以先简单理解为要执行的实际controller;

3.Controller执行完业务逻辑,返回视图ModelAndView;

4.DispatcherSerlvet请求ViewResolver获取实际解析的View;

5.View被调用,页面被解析并返回给浏览器;

3.2 规范越多,责任越大

上图的时序图比较简洁,从中能鸟瞰Spring MVC的工作流程。但是它不能反映框架里所有的工作场景,且跟本篇讨论的流程也不完全匹配。因为请求可以返回视图页面,也可以返回对象,但无论json还是jsonp对象都不是视图页面,所以在这里View相关的逻辑并不会被运行。

究其原因,是HTTP的规范内容众多,为此,Spring MVC需要支持各种类型的协议,以处理对应的逻辑。正如返回值可以是页面View,也可以是json对象,甚至在SSE协议中,还可以是HttpEmitter对应的长连接。而上一节中,被刻意忽略的组件HandlerAdapter,正是Spring MVC处理这一工作的核心角色,因此责任重大。

面向协议的抽象,HandlerAdapter

HandlerAdapter是处理业务的核心角色,业务逻辑的处理器——controller是被该类的实例代理执行,为了处理不同的HTTP场景,HandlerAdapter(以下简称HA)除了调用最终的controller外,还负责参数解析,返回值处理。因此,在HA中维护了所有场景的参数解析,返回值处理,甚至ControllerAdvice切面,而不同场景的差异,则在HA中被选择并处理。

RequestMappingHandlerAdapter是默认的HandlerAdapter实现。



上图将重点放在返回值的处理和解析上,流程可概括为:

1.HA用ServletInvocableHandlerMethod(简称HM),来代理后续的步骤(但请先忽略这个代理步骤)

2.HM通过invokeForRequest调用了controller,得到了返回值。

3.HM调用HA中维护的ReturnValueHandlers,处理返回值

4.ReturnValueHandlers选择处理这个返回值的ValueHandler。

5.该ValueHandler调用handleReturnValue,处理这个返回值。

从第3步开始,就进入到返回值的解析和处理环节。以下将详细说明3-5步具体干了什么。

处理返回值的组合模式

在HandlerAdapter中维护了所有返回值的处理逻辑,这些逻辑都实现了HandlerMethodReturnValueHandler,而HA要统一管理这些Handlers,使用了一种称为Composite的设计模式(组合模式)。

`public class HandlerMethodReturnValueHandlerComposite implements HandlerMethodReturnValueHandler {`    `    private final List<HandlerMethodReturnValueHandler> returnValueHandlers = new ArrayList<>();``   `    `HandlerMethodReturnValueHandler selectHandler(Object value, MethodParameter returnType) {`    `// ...`    `for (HandlerMethodReturnValueHandler handler : this.returnValueHandlers) {`      `// ...`      `if (handler.supportsReturnType(returnType)) {`        `return handler;`      `}`    `}`        `// ...`  `}``   `    `void handleReturnValue``}`

简单来说,存在一个Composite对象,维护了所有ReturnValueHandler的列表,其中最重要的方法有两个:

1.selectHandler根据controller的返回值和方法签名,选择一个ReturnValueHandler。每个handler需要自己实现一个名为supportsReturnType的方法,来说明自己能否解析某种controller的返回值。

2.handleReturnValue处理返回值。先选择一个ReturnValueHandler,再调用该handler的解析方法。

用方法签名选择处理器

不同的handler,根据要求实现各自的supportsReturnType,就可以解决不同HTTP场景下的返回值处理。

本文的问题场景:JSON和JSONP格式的返回值,均被RequestResponseBodyMethodProcessor所处理。原因就在于他的supportsReturnType方法的实现:若controller的返回值被ResponseBody注解所修饰,则可被该类处理。

`public boolean supportsReturnType(MethodParameter returnType) {`    `return (AnnotatedElementUtils.hasAnnotation(returnType.getContainingClass(), ResponseBody.class) ||`            `returnType.hasMethodAnnotation(ResponseBody.class));``}``   ``public void handleReturnValue(@Nullable Object returnValue, MethodParameter returnType,`      `ModelAndViewContainer mavContainer, NativeWebRequest webRequest)`      `throws IOException, HttpMediaTypeNotAcceptableException, HttpMessageNotWritableException {`    `// ...`    `ServletServerHttpRequest inputMessage = createInputMessage(webRequest);`    `ServletServerHttpResponse outputMessage = createOutputMessage(webRequest);`    `writeWithMessageConverters(returnValue, returnType, inputMessage, outputMessage);``}`

最终,RequestResponseBodyMethodProcessor的方法handleReturnValue被用来处理json/jsonp的返回值。

显然,“如何区分并处理json/jsonp的返回值”是问题的关键所在(将在5.2节详细描述),但在陷入细节之前,我们跳出来看一下跟Spring MVC相关的另外一个问题,那就是拦截器。

执行的责任链,Interceptor和HandlerExecutionChain

在那张古老的时序图里,除了HandleAdapter外,还有一个被刻意忽视的角色——Interceptor。

当DispatcherServlet查询HandlerMapping时,返回的对象并不是Controller,而是HandlerExecutionChain。该对象根据请求的url构造,维护了该请求需要运行的Spring MVC拦截器。

`HandlerExecutionChain getHandlerExecutionChain(Object handler, HttpServletRequest request) {`    `// ...`    `String lookupPath = this.urlPathHelper.getLookupPathForRequest(request);`    `for (HandlerInterceptor interceptor : this.adaptedInterceptors) {`        `MappedInterceptor mappedInterceptor = (MappedInterceptor) interceptor;`        `// 通过匹配该interceptor和请求的url,来判断是否应该加入到执行链中`        `if (mappedInterceptor.matches(lookupPath, this.pathMatcher)) {`            `chain.addInterceptor(mappedInterceptor.getInterceptor());`        `}`    `}`    `// ...``}`

在DispatcherServlet的执行过程中,会依次使用chain去调用对应的interceptor。Spring提供的拦截器有三个可实现的方法,分别对应执行过程的三个阶段:

preHandle

controller调用前

postHandle

controller调用后,视图渲染前

afterCompletion

视图完成渲染后

因此,3.1中对“Controller执行完业务逻辑,返回视图ModelAndView”这段描述应该被细化成:

1.HandlerExecutionChain调用对应的interceptor中的preHandle;

2.HandlerAdapter调用invokeAndHandle,执行业务逻辑,并返回结果;

3.HandlerExecutionChain调用对应的interceptor中的postHandle;

4.视图返回后(也可能根本不存在视图),调用interceptor中的afterCompletion;

由此可见:HandlerAdapter和HandlerExecutionChain(或者说Interceptor)是两个平级的角色,它们各司其职,需要被独立看待。

postHandle和afterCompletion的区别

从时序图来看,拦截器里的postHandle和afterCompletion方法之间有诸多差异,通常认为:

postHandle:在控制器方法(Controller的处理方法)执行完毕并且视图对象已经确定(但还未进行视图渲染)之后被调用。这意味着你在这个阶段仍然有机会修改模型数据(Model)或者视图(View)。

afterCompletion:在完整的请求处理完毕之后被调用,包括视图渲染完成和响应数据已经发送给客户端之后。这意味着所有响应处理都已经完成,包括视图渲染和流的关闭。

而原方案中,设置CORS和ContentType header的位置均在afterCompletion方法中,那么问题的答案好像呼之欲出:响应的处理已经完成,再设置任何Http Header都无法生效了。

四、被误解的拦截器

再次回忆那张古老的时序图,里面有个根本不会在本文场景里出现的重要角色——视图(View)。但是,每当提到拦截器中两个方法(postHandle和afterCompletion)的区别时,均是在视图的场景下进行探讨。颇有“用前朝的剑斩本朝的官”的意思。

那么,afterCompletion方法被调用时,响应是否已经完成,不可以再设置header了呢?根据实际的观察,不一定。也就是说,有可能已完成,也有可能未完成。这涉及到一个概念叫做 response的committed。

4.1 Response的Committed

调用response的write方法,只会将内容写到缓冲区。如果一个响应被提交(committed),那响应的内容才从缓冲区发送到客户端(比如浏览器)上。

如果响应被committed,那此时再设置响应的header是无效的。

以下是tomcat-embed-core-9.0.31版本的Response实现(org.apache.catalina.connector.Response),当isCommitted为true, setHeader方法会立即返回:

`public void setHeader(String name, String value) {`    `//...`    `if (isCommitted()) {`        `return;`    `}`    `//...`    `char cc=name.charAt(0);`    `if (cc=='C' || cc=='c') {`        `if (checkSpecialHeader(name, value)) {`            `return;`        `}`    `}``   `    `getCoyoteResponse().setHeader(name, value);``}`

本文所提及的部分jsonp接口中,由于在HandleAdapter的处理范围里,并没有组件主动commit响应,因此无论在拦截器里的postHandle还是afterCompletion方法里,响应都未committed,此时setHeader都可以生效。在整个请求流程的末尾(超出了Spring MVC的作用范围),才由tomcat处理缓冲区,将响应发送到浏览器。



而这就是为什么原实现方案的setHeader写在afterCompletion里,但接口却一直能正常工作的原因。

4.2 preHandle才是setHeader的合理位置

但既然发生了问题,说明某些jsonp接口并不能如上所述地正常setHeader。

通常Request和Response都会被框架层层包装,下面的逻辑导致Response的commit操作是无法被预期的。

`public abstract class OnCommittedResponseWrapper extends HttpServletResponseWrapper {`    `    @Override`    `public void write(char[] buf, int off, int len) {`        `checkContentLength(len);`        `this.delegate.write(buf, off, len);`    `}`    `    private void checkContentLength(long contentLengthToWrite) {`    `this.contentWritten += contentLengthToWrite;`    `boolean isBodyFullyWritten = this.contentLength > 0`        `&& this.contentWritten >= this.contentLength;`    `int bufferSize = getBufferSize();`    `boolean requiresFlush = bufferSize > 0 && this.contentWritten >= bufferSize;`    `if (isBodyFullyWritten || requiresFlush) {`      `doOnResponseCommitted();`    `}`  `}``}`

上述代码,反映了HandlerAdapter将controller的返回值写入response的逻辑片段:

1.在HandlerAdapter的实现里,当处理返回值的过程中,会通过write方法,不断往response里写数据。

2.Response被spring-security提供的OnCommittedResponseWrapper所包装(org.springframework.security.web.util.OnCommittedResponseWrapper)。

3.该类在写数据前,会checkContentLength,一旦超过缓冲区,则触发Response的commit(第16行)。

因此引入了该Response的实现,或者缓冲区被调整,就会导致某些接口在HandlerAdapter的处理过程中被commit。

而由于HandlerAdapter处理返回值的过程在postHandle和afterCompletion被调用之前,因此,此时在这两个方法中setHeader均不会生效。

出问题的jsonp接口的作用是获取全量美杜莎文案,返回数据量较大,恰好对应了缓冲区满的可能。而到底是因为技术升级后,新引入了OnCommittedResponseWrapper还是因为缓冲区被隐性调整,已经无从考证了。

综上,因为无论在postHandle和afterCompletion中setHeader都可能不生效,所以setHeader的合理位置是interceptor的preHandle方法内。

作出上述调整后,CORS相关的Header被成功设置,但是Content-Type则始终还是application/json,依旧不符合预期。

五、特立独行的响应类型

5.1 Content-Type,不可思议的脱节

首先,陈述一个事实,HandleAdapter中的HandlerMethodReturnValueHandler会将header写入Response。

其次,提出一个假设,因为Interceptor的preHandle在该环节之前,那么前一个步骤设置了content type为“application/javascript”,在后续的处理中应该以设置的为准。

由于最终设置的,并不是上述假设的结果,所以需要确认header的读取和写入两个步骤是如何发生的。

1.读取阶段,HandlerMethodReturnValueHandler是如何获取content-type的

从处理返回值的父类AbstractMessageConverterMethodProcessor可见,HandleAdapter获取ContentType,是从传入的Response的header中拿到的,见下方第4行。

`void writeWithMessageConverters(@Nullable T value, MethodParameter returnType,`      `ServletServerHttpRequest inputMessage, ServletServerHttpResponse outputMessage)`    `//...`    `MediaType contentType = outputMessage.getHeaders().getContentType();`    `//...`    `List<MediaType> acceptableTypes = getAcceptableMediaTypes(request);`  `List<MediaType> producibleTypes = getProducibleMediaTypes(request, valueType, targetType);`    `if (genericConverter != null ?`            `((GenericHttpMessageConverter) converter).canWrite(targetType, valueType, selectedMediaType) :`            `converter.canWrite(valueType, selectedMediaType)) {`        `body = getAdvice().beforeBodyWrite(body, returnType, selectedMediaType,`                `(Class<? extends HttpMessageConverter<?>>) converter.getClass(),`                `inputMessage, outputMessage);`        `// ...`        `genericConverter.write(body, targetType, selectedMediaType, outputMessage);`        `//...``        return;`    `}``}`

getHeaders仅仅是对Response的代理,由此可知,contentType确实从传入的Response的header里获取。

`public MediaType getContentType() {`    `// getFirst是从Header中获取该name的第一个Header`    `String value = getFirst(CONTENT_TYPE);`    `return (StringUtils.hasLength(value) ? MediaType.parseMediaType(value) : null);``}`

但是这里却出现了一个难以理解的现象:在Interceptor中被设置过的Content-Type无法在此处被获取到。

2.写入阶段,content-type是如何被设置的

显然,content-type是在拦截器的preHandle方法中,通过调用Response的setHeader设置的。但是,深入到具体实现,也就是tomcat response中设置content-type的位置能发现:若header是content-type,是无法通过setHeader被设置的(第13-15行)。

`public void setHeader(String name, String value) {`    `//...`    `char cc=name.charAt(0);`    `if (cc=='C' || cc=='c') {`        `if (checkSpecialHeader(name, value)) {`            `return;`        `}`    `}`    `//...``}``   ``private boolean checkSpecialHeader(String name, String value) {`    `if (name.equalsIgnoreCase("Content-Type")) {`        `setContentType(value);`        `return true;`    `}`    `return false;``}`

也就是说,:tomcat设置content-type header的实现和spring mvc获取content type的实现产生了脱节。

1.在tomcat的checkSpecialHeader方法中,若是Content-Type,则不会设置header。

2.下游的spring mvc则又是从header中获取Content-Type。

这种不可思议的脱节,导致在拦截器的preHandle方法中,无论如何设置Content-Type,在HA处理结果时,都无法获取人为设置的结果。

这里引申出两个问题:

1.在拦截器的postHandle中设置是否可以呢?

在上一节已经说过,是否可以在postHandle中设置header是无法预期的。所以可能有些接口能设置成功,有些结果返回内容较多(达到缓冲区上限),则无法设置成功。

2.既然无法主动设置response的Content-Type,那所有的Http请求岂不是都有问题?

答案也是显然的,那就是“没有问题”。

因为response的content-type不应该由人为决定,而是Spring MVC的自主选择。

5.2 MessageConvertor,Spring MVC的自主选择

在3.2节中遗留了一个关键问题:“如何区分并处理json/jsonp的返回值”。同时5.1节也得到结论,response的Content-Type应该由Spring MVC自主选择。而Message Convertor则是实现这一选择的关键角色。

待候选的Message Convertor

由于在controller的方法签名中,返回值被ResponseBody所修饰,所以HandlerAdapter(更具体的,就是RequestMappingHandlerAdapter)将结果的处理逻辑交给了RequestResponseBodyMethodProcessor。

所以“区分并处理json/jsonp的返回值”的控制权就交给了它。从下图看具体的处理逻辑:



首先,涉及的几个角色有:

1.RequestResponseBodyMethodProcessor:处理ResponseBody类结果的Handler,负责和HandlerAdapter交互。(比较有意思的是,其他的Handler都是叫ReturnValueHandler,它却叫Processor,说明它不只可以做结果的处理,不过这跟本文问题无关,就不深入了)

2.AbstractMessageConverterMethodProcessor:Processor的父类。在该类中实现对ContentType的选择,以及控制convertor写入。

3.MessageConverter:输出结果的处理类。将json做填充的逻辑也是在convertor中实现

4.MediaType:内容的类型,该对象跟content-type的结果直接相关

5.UTF8JsonGenerator:使用到的输出工具类,convertor会使用该工具类和Response交互,将结果写入。

其次,解析controller返回对象并写入Response的流程为:

1.获取Acceptable的内容类型。解析request中的Accept字段,查看浏览器可以接收的类型。如果是“*/*”表示所有类型都可以接收。

2.获取Producible的内容类型。遍历系统中的所有Message Convertor,看针对该响应能产生的所有类型。具体是:依次调用Convertor的canWrite方法,判断该Convertor是否能处理该响应。若能处理则返回改Convertor能支持的所有类型。

`// clazz为返回对象的类型``boolean canWrite(Class<?> clazz, @Nullable MediaType mediaType)`

3.判断Producible的类型是否可被accept,仅保留可接收的类型

4.对可处理的类型进行排序。

5.遍历Message Convertor,按照顺序进行输出。

总结一下就是:controller返回的对象所对应的content-type,以及输出的方式,取决于哪一个Message Convertor能够处理该对象。

比较理想的情况是,对于每一种类型,Spring MVC中仅注册了一个对其处理的Message Convertor。但是在本文描述的原实现方案中,情况变得复杂。特别是上面的第4-5步,这所谓的排序,开始变得不清不楚了。

MediaType顺序很重要

在最后的一节中,我们先给出结论:MediaType的顺序是有错误的,未被正确设置。

在原方案中,自定义的Convertor所支持的MediaType为:json和javascript,导致HA输出的content-type始终为json,但是由于拦截器在afterCompletion方法中会再重新设置content-type为javascript,所以jsonp的请求一直可以正常返回。

如今设置content-type的位置被改到preHandle中,MediaType的错误顺序问题被暴露了出来。

排序的方法为MediaType.sortBySpecificityAndQuality中,逻辑是:

1.通配的MediaType(也就是MediaType中是否包含通配符*),排在最后

2.q-value较大Media Type,优先级较高。q-value是类型的参数,比如HTTP的accept Header可以是:"application/json:q=1",默认的q-value是1,即最大,这完全依赖请求的参数。

3.type不同的MediaType,互不影响。type为参数中/的前半部分,比如text/plain和application/json的type分别是text和application。它们的顺序根据系统定义。

4.subType不同的MediaType,互不影响。type为参数中/的后半部分,比如application/json和application/javascript的subType分别是json和application。它们的顺序也根据系统定义。

根据上述原则,因为自定义的Convertor所支持的MediaType为:json和javascript,它们的subType不同,q-value也都是1,因此顺序就是代码里定义的顺序。

经过自定义的convetor处理的响应,最终输出的content-type始终是json,也就是第一个。

六、问题解决

基于上述的分析,为了解决2.1中的问题,最终的方案为:

1.对CORS的setHeader,位置被放置在拦截器的preHandle方法中

2.自定义的Message Convertor只支持MediaType为application/javascript的类型,通过改写canWrite方法,过滤非jsonp的请求。

`boolean canWrite(Class<?> clazz, @Nullable MediaType mediaType) {`    `// 当返回值为JsonpWrapperObject类型时,才是jsonp的请求`    `// 其他的请求,交由spring mvc默认对json的Message Convertor处理`    `return clazz == JsonpWrapperObject.class;``}`

3.调整Message Convertor在Spring MVC中的注册顺序,自定义对jsonp处理的Convertor排第一个。

七、跨域问题的最佳实践

综上会发现,为了实现Jsonp,这里做了非常多tricky的操作(拦截器+自定义的convertor)。看起来,这并不是一个最佳的实践方案。

是的,对比跨域的实现方式来说,使用CORS相比jsonp要简单太多。

同时就算是jsonp,一些开源代码也提供了更好的支持。比如:fastjson提供的JSONPResponseBodyAdvice,实现全局的controller切面。在5.2节的时序图中,也可以看到AbstractMessageConverterMethodProcessor中有一个步骤是通过Advice来在响应体写入前做一些处理。

JSONPResponseBodyAdvice的实现如下:

`@ControllerAdvice``public class JSONPResponseBodyAdvice implements ResponseBodyAdvice<Object> {`    `//...`    `public boolean supports(MethodParameter returnType, Class<? extends HttpMessageConverter<?>> converterType) {`        `return FastJsonHttpMessageConverter.class.isAssignableFrom(converterType)`                `&&`                `(returnType.getContainingClass().isAnnotationPresent(ResponseJSONP.class) || returnType.hasMethodAnnotation(ResponseJSONP.class));`    `}`    `    public Object beforeBodyWrite(Object body, MethodParameter returnType, MediaType selectedContentType,`                                  `Class<? extends HttpMessageConverter<?>> selectedConverterType, ServerHttpRequest request,`                                  `ServerHttpResponse response) {`        `ResponseJSONP responseJsonp = returnType.getMethodAnnotation(ResponseJSONP.class);`        `// ...`    `}`    `// ...``}`

但是这就要求controller的返回值,需要被fastjson的ResponseJSONP注解所修饰。

出于开发和回归成本的考虑,本文并没有使用这种方式,然是继续沿用原有的拦截器和Message Convertor。

希望以后再用到jsonp的时候,能够使用更简单的实现方案。哦,不对,下次解决跨域问题时,应该不会再用jsonp了。

高可用及共享存储 Web 服务

随着业务规模的增长,数据请求和并发访问量增大、静态文件高频变更,企业需要搭建一个高可用和共享存储的网站架构,以确保网站服务能够7*24小时运行的同时,可保障数据一致性和共享性,并降低数据重复存储的成本。快点击阅读原文体验吧~

相关推荐
关注或联系我们
添加百川云公众号,移动管理云安全产品
咨询热线:
4000-327-707
百川公众号
百川公众号
百川云客服
百川云客服

Copyright ©2024 北京长亭科技有限公司
icon
京ICP备 2024055124号-2