×
Community Blog In-Depth Exploring of the Spring MVC Best Practice for Cross-Origin Issues

In-Depth Exploring of the Spring MVC Best Practice for Cross-Origin Issues

This article analyzes Spring MVC's cross-origin issues and best practices, with a focus on JSONP interface upgrades.

By Gaodeng

1

This article provides an in-depth analysis of cross-origin issues in Spring MVC and the best practices for handling them. It specifically focuses on the challenges faced with cross-origin issues during the technical upgrade of JSONP interfaces. Through a specific case study, it illustrates the symptoms and causes of cross-origin issues with the original JSONP interface during the technical upgrade, and introduces the customization of Spring MVC Interceptors and MessageConverters as a solution to this problem.

1. Cross-Origin and JSONP

To block malicious websites from falsifying cross-site requests, browsers will restrict resource interaction between different sites. This behavior is referred to as the browser's Same Origin Policy. Simply put, for the request on the page of site A, the domain name in its URL generally cannot belong to other sites. Resource access between different sites is referred to as a cross-origin request.

When a cross-origin request is generated, even if the response result is returned from the server, the browser will intercept it and generate a CORS exception, prompting Access has been blocked by CORS policy.

However, it is very common to access data between multiple secure sites. For example, you may access the interface of other subsystems (such as data.alibaba.com) on the merchant console (with a domain name of i.alibaba.com). Therefore, it is necessary to provide a solution to the cross-origin issue.

There are two ways to solve the cross-origin problem: CORS and JSONP.

CORS (Cross-Origin Resouce Sharing)

CORS is a W3C specification for cross-origin issues. When accessing site B on the page of site A, if the service of site B adds the following headers to the response, the browser will not intercept the corresponding result, including:

Access-Control-Allow-Origin Permitted site (IP or domain name), which is also the original site A
Access-Control-Allow-Credentials Whether to allow the request to include cookies
Access-Control-Allow-Methods Permitted HTTP methods such as GET/POST
Access-Control-Allow-Headers Permitted headers

JSONP (JSON with Padding)

JSONP is not an official cross-origin solution. However, since it was used early, there are still many historical interfaces based on JSONP. The main idea is that the script content in the tag <script> is not restricted by the browser's Same Origin Policy, so the resource data can be "disguised" as a JS script.

For the server to handle cross-origin with JSONP, two things need to be performed:

(1) Fill JSON

1) First, when the frontend initiates a JSONP request, it will dynamically insert a <script> script to request data and provide a callback function. The function name is used as the query parameter, and the function body is the callback logic after the data is obtained, such as the jsonp_1718436528810_81650 in the following figure.

2

2) Second, the server fills JSON. The raw result of the server is in JSON format. After it is filled, a function call statement is obtained. The function name is the callback input parameter of the previous step, and the function argument is the original JSON result. This process of Padding is also the origin of the JSONP name.

3

3) Finally, the frontend executes the filled result, so the original JSON data can be obtained in the callback function jsonp_1718436528810_81650.

(2) Set the Content-Type of the Response

To bypass the Same Origin Policy, the browser needs to interpret the content returned by the request as a script, so the response's Content-Type should be set as application/javascript.

If the returned content is JSONP, but the Content-Type is application/json, the browser will not recognize it and generate ORB (Opaque Response Blocking) exception, prompting No data found for resource with given identifier.

4

Comparing the two cross-origin solutions, CORS is much clearer and simpler and gradually replacing JSONP. Many frameworks and third-party libraries such as Spring MVC and Fastjson are also gradually removing the implementation of JSONP. However, since JSONP is very widely used, based on practical situations, both methods coexist in many systems.

2. Background of the Problem

2.1 Manifestation of the Problem

In the process of upgrading a Web system, cross-origin problems occur in the JSONP interface, specifically:

(1) The HTTP header related to CORS is missing (that is, Access-Control-Allow-Credentials). Although it is not necessary in this scenario, these headers have been set but do not take effect.

(2) When the JSONP interface returns, the Content-Type value in the HTTP header is application/json instead of application/javascript, resulting in an ORB error.

2.2 Original Implementation Scheme

The problematic interface is the JSONP interface which implements JSON Padding and sets Content-Type by customizing Spring MVC Interceptor and MessageConverter. The original implementation scheme is as follows.

5

(1) Declare a custom interceptor JsonpInterceptor. If the request URI ends with JSONP, the interceptor must be passed.

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

(2) Declare the class JsonpWrapper.

(3) In the controller, all return values are processed uniformly: if the request is jsonp, the result object is wrapped in the JsonpWrapper.

public static Object getJsonOrJsonpObj(String callback, Object obj) {
    if (StringUtils.isBlank(callback)) {
        return obj;
    } else {
        JsonpWrapper jsonp = new JsonpWrapper();
        // The callback parameter is an input parameter and will be returned to the browser. Therefore, the callback parameter should be processed to prevent script injection.
        jsonp.setCallback(SecurityUtil.escapeHtml(callback));
        jsonp.setValue(obj);
        return jsonp;
    }
}

(4) Customize the JavascriptConverter and replace the default json converter in Spring MVC. The Message Converter is a Spring MVC class used to process requests and return data. The detailed information will not be described here.

(5) If the returned result is JsonpWrapper, fill in the data before and after the result in the format of JSONP, and output the final result. AbstractJackson2HttpMessageConverter is the parent class of Message Converter provided by Spring MVC to process JSON class data.

public class JavaScriptMessageConverter extends AbstractJackson2HttpMessageConverter {
    // Other settings are omitted
    
    @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) In the post-processing of interception, write a CORS request to the response and change the ContentType to 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");
        // Omit the process to obtain the safeOrigin
        response.setHeader("Access-Control-Allow-Origin", safeOrigin);
        response.setHeader("Content-Type", "application/javascript");
    }
}

The JsonpInterceptor, JsonpWrapper, and JavascriptConverter mentioned above are all custom implementations. This scheme can work well for a long time. However, note that the HTTP Header is set in the interceptor's afterCompletion method.

2.3 Definition of the Problem

Combining the manifestation of the problem with the original implementation scheme, we can find that the setHeader in the last step is directly related to the problem. So from here on, only one question needs to be answered, that is:

Why does the header setting for response not take effect in the custom interceptor?

3. Workflow of Spring MVC

Interceptor is a tool provided by Spring MVC. The following is a brief review of the Spring MVC workflow, focusing on the Header and content writing of Response.

3.1 Spring MVC Workflow Diagram

6

The preceding diagram shows several key components of Spring MVC:

(1) DispatcherServlet: is the front controller of Spring MVC, and responsible for accepting and distributing requests to the appropriate Controller.

(2) HandlerMapping: finds the corresponding Controller and method based on the request URL.

(3) Controller: executes business logic and returns a view or response body.

(4) ViewResolver: is used to find the corresponding view.

(5) View: renders the page based on the model data returned by the Controller.

(6) HandlerAdapter: is a proxy for the call logic to handle different scenarios.

(7) Interceptor: intercepts before and after the call to the Controller. You can add processing logic for logging and authentication. The interceptor is optional.

The preceding diagram also describes a standard process in Spring MVC workflow, which can be briefly summarized as follows:

(1) HTTP requests first reach the core of Spring MVC -- DispatcherSerlvet.

(2) DispatcherSerlvet queries the HandlerMapping according to the request information (such as URL). The result returned in this step can be simply understood as the actual controller to be executed.

(3) Controller executes the business logic and returns ModelAndView.

(4) DispatcherSerlvet requests the ViewResolver to obtain the actual resolved View.

(5) View is called, and the page is resolved and returned to the browser.

3.2 More Specifications for Greater Responsibility

The diagram above is relatively simple, providing a comprehensive overview of the Spring MVC workflow. However, it does not reflect all the work scenarios in the framework nor exactly match the process discussed in this article. Although the request can return views or objects, neither JSON nor JSONP objects are views, so the logic related to View will not be run here.

The reason is that HTTP has many specifications so Spring MVC needs to support various types of protocols to handle the corresponding logic. Just as the return value can be a page View or a JSON object, it can even be a long connection corresponding to HttpEmitter in the SSE protocol. In the previous section, the component HandlerAdapter is deliberately ignored, but it is the core of Spring MVC in handling this work, with great responsibility.

Protocol-Oriented Abstraction: HandlerAdapter

HandlerAdapter plays the core role in processing business. A Controller, the processor of business logic, is executed by the instance proxy of this class. To handle different HTTP scenarios, HandlerAdapter (hereinafter referred to as HA) is responsible for parameter resolving and return value processing, in addition to calling the final controller. Therefore, parameter resolving, return value processing, and even ControllerAdvice aspects of all scenarios are maintained in HA, while differences across scenarios are selected and processed in HA.

RequestMappingHandlerAdapter is the default HandlerAdapter implementation.

7

The preceding figure focuses on the processing and resolving of return values. The process can be summarized as follows:

Step 1: HA uses ServletInvocableHandlerMethod (HM) to proxy subsequent steps (but you can ignore this proxy step here).

Step 2: HM calls the controller and obtains the return value by using invokeForRequest.

Step 3: HM calls the ReturnValueHandlers maintained in HA and processes the return value.

Step 4: ReturnValueHandlers selects the ValueHandler that handles this return value.

Step 5: This ValueHandler calls the handleReturnValue and handles the return value.

From Step 3 onwards, we delve into analyzing and handling the return value. In the following steps, we'll break down exactly what's happening between Steps 3 to 5.

Process Return Values through Composite

HandlerAdapter maintains the processing logic for all return values, and all these logics achieve HandlerMethodReturnValueHandler. To manage these Handlers, HA uses the design pattern of 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
}

That is, there is a Composite object that maintains a list of all the ReturnValueHandler, and the two most important methods are:

(1) selectHandler selects a ReturnValueHandler based on the controller's return value and method signature. Each handler needs to implement a method named supportsReturnType to explain whether it can resolve the return value of a certain controller.

(2) handleReturnValue processes the return value by selecting a ReturnValueHandler before calling the handler's resolve method.

Select Processors with Method Signatures

Different handlers that implement their own supportsReturnType as required can process the return values in different HTTP scenarios.

The problem scenario in this article: both the return values in JSON and JSONP formats are processed by RequestResponseBodyMethodProcessor. The cause lies in the implementation of its supportsReturnType method: if the return value of the controller is modified by the ResponseBody annotation, it can be handled by this class.

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);
}

Ultimately, the handleReturnValue method of RequestResponseBodyMethodProcessor is used to handle the return values of json/jsonp.

Obviously, how to distinguish and handle the return values of JSON/JSONP is the key to the problem (which will be described in detail in section 5.2). However, before diving into the details, let's take a look at another problem related to Spring MVC, that is, interceptors.

Chain of Responsibility for Execution: Interceptor and HandlerExecutionChain

In the workflow diagram mentioned before, in addition to HandleAdapter, there is another deliberately neglected role -- Interceptor.

When DispatcherServlet queries HandlerMapping, the returned object is not Controller but HandlerExecutionChain. This object is constructed based on the request URL and maintains the Spring MVC Interceptor that needs to be run for the request.

HandlerExecutionChain getHandlerExecutionChain(Object handler, HttpServletRequest request) {
    // ...
    String lookupPath = this.urlPathHelper.getLookupPathForRequest(request);
    for (HandlerInterceptor interceptor : this.adaptedInterceptors) {
        MappedInterceptor mappedInterceptor = (MappedInterceptor) interceptor;
        // Determine whether the interceptor should be added to the execution chain by matching the interceptor with the request URL
        if (mappedInterceptor.matches(lookupPath, this.pathMatcher)) {
            chain.addInterceptor(mappedInterceptor.getInterceptor());
        }
    }
    // ...
}

During the execution of DispatcherServlet, the chain is used to call the corresponding interceptor in sequence. The interceptor provided by Spring can be implemented in three methods, corresponding to the three stages of the execution process:

preHandle Before the Controller is called
postHandle After the Controller is called and before the View is rendered
afterCompletion After the View is rendered

Therefore, the description of Controller executes the business logic and returns ModelAndView in section 3.1 should be refined to:

(1) HandlerExecutionChain calls preHandle in the corresponding interceptor.

(2) HandlerAdapter calls invokeAndHandle, executes the business logic, and returns the result.

(3) HandlerExecutionChain calls postHandle in the corresponding interceptor.

(4) After the View returns (or there may be no View at all), call afterCompletion in the interceptor.

This shows that HandlerAdapter and HandlerExecutionChain (or Interceptor) are two equal roles that perform their own functions and need to be regarded independently.

Difference between postHandle and afterCompletion

From the diagram, there are many differences between the postHandle and the afterCompletion method in the interceptor. Generally, they are considered as:

postHandle: is called after the controller method (the Controller's processing method) has been executed and the View object has been determined (but the View has not yet been rendered). This means that you still have the opportunity to modify the Model data or View at this stage.

afterCompletion: is called after the complete request has been processed, including after the View has been rendered and the response data has been sent to the client. This means that all response processing has been completed, including View rendering and the closing of the stream.

However, in the original scheme, the CORS and ContentType header are both set in the afterCompletion method, so the answer is likely that: the response processing has been completed, and any setting for the HTTP Header cannot take effect.

4. Misunderstood Interceptors

Recall the diagram again, and there is an important role that will not appear in the scenario of this article -- View. However, whenever the difference between the two methods (postHandle and afterCompletion) in the interceptor is mentioned, it is discussed in the context of View.

Then, when the afterCompletion method is called, does it mean that the response has been completed and the header cannot be set? Not necessarily. In other words, it may be completed, or not. This involves a concept that Response is committed.

4.1 Committed Response

The write method that calls the response will only write the contents to the buffer. If a response is committed, the contents of the response are sent from the buffer to the client (such as a browser).

Therefore, if a response is committed, it will be invalid to set the header for the response at this time.

The following is the implementation of a response (org.apache.catalina.connector.Response) in tomcat-embed-core-9.0.31. When isCommitted is true, the setHeader method returns immediately.

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);
}

In some of the JSONP interfaces mentioned in this article, no component actively commits a response within the processing scope of HandleAdapter, so no matter in the postHandle or afterCompletion method in the interceptor, the response is not committed and setHeader can take effect at this time. At the end of the entire request flow (beyond the scope of Spring MVC), Tomcat processes the buffer and sends the response to the browser.

8

This is why the setHeader of the original implementation is written in the afterCompletion, but the interface always works normally.

4.2 Reasonable Position for setHeader: preHandle

Since there is a problem, it means that setHeader cannot work normally as described above in some JSONP interfaces.

Generally, requests and responses are wrapped in layers by the framework. The following logic will cause the commit operation of a response to be unexpected.

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();
    }
  }
}

The preceding code reflects the logic segment used by HandlerAdapter to write the return value of the Controller to the Response:

(1) In the implementation of HandlerAdapter, when the return value is processed, the write method is used to continuously write data to the response.

(2) Response is wrapped by OnCommittedResponseWrapper provided by Spring Security (org.springframework.security.web.util.OnCommittedResponseWrapper).

(3) This class will checkContentLength before writing data. Once it exceeds the buffer, it will trigger the commit of Response (line 16).

Therefore, when the implementation of this Response is introduced, or the buffer is adjusted, it will cause some interfaces to be committed during the processing of HandlerAdapter.

However, HandlerAdapter processes return values before postHandle and afterCompletion are called. Therefore, setHeader does not take effect in both methods.

To sum up, setHeader may not take effect in both postHandle and afterCompletion. Therefore, the setHeader should be in the preHandle method of the interceptor.

After the preceding adjustment, the CORS-related Header is successfully set, but the Content-Type is still application/json, which is not as expected.

5. Special Response Type

5.1 Disconnect of Content-Type

First, the fact is stated that HandlerMethodReturnValueHandler in the HandleAdapter will write the header to the Response.

Secondly, we propose a hypothesis that since the Interceptor's preHandle is before this stage and the previous step sets the content type to application/javascript, this setting should be adhered to in subsequent processing.

Since the final setting is not the result assumed above, it is necessary to confirm how the header reading and writing processes occur.

(1) In the reading phase, how does HandlerMethodReturnValueHandler obtain the content-type?

As can be seen from the parent class AbstractMessageConverterMethodProcessor that processes the return value, HandleAdapter obtains the ContentType from the header of the passed-in Response, as shown in line 4 below.

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 is only a proxy for the response, so we can confirm that the contentType is indeed obtained from the header of the passed-in Response.

public MediaType getContentType() {
    // getFirst is the first header used to obtain the name from the Header
    String value = getFirst(CONTENT_TYPE);
    return (StringUtils.hasLength(value) ? MediaType.parseMediaType(value) : null);
}

However, there is an incomprehensible phenomenon: the Content-Type set in the Interceptor cannot be obtained here.

(2) In the writing phase, how is the Content-Type set?

Obviously, the Content-Type is set in the preHandle method of the Interceptor by calling the setHeader of the Response. However, in the specific implementation, from the position where the Content-Type is set in the tomcat response, we can find that if the header is Content-Type, it cannot be set through setHeader (lines 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;
}

That is to say, there is a disconnect between the implementation of setting the content-type header in tomcat and the implementation of obtaining the content type in Spring MVC.

(1) In the tomcat's method checkSpecialHeader, if it is Content-Type, the header will not be set.

(2) The downstream Spring MVC obtains Content-Type from the header.

This disconnect means that no matter how Content-Type is set in the preHandle method of the interceptor, the manually set result cannot be obtained when HA processes the result.

This raises two questions:

(1) Is it possible to set it in the postHandle of the interceptor?

As mentioned in the previous section, it is impossible to predict whether the header can be set in the postHandle. Therefore, the setting may be successful in some interfaces, but if a large number of results are returned (the buffer limit is reached), the setting will fail.

(2) Given that the Content-Type of the Response cannot be set actively, does this mean that all HTTP requests are problematic?

The answer is quite obvious, that is, there is no problem at all.

This is because the Content-Type of the Response should not be manually determined, but is independently decided by Spring MVC.

5.2 Independent Selection of Spring MVC: MessageConverter

Section 3.2 leaves a crucial question unanswered: how to differentiate and manage the return values of JSON/JSONP. In Section 5.1, we also come to the conclusion that the Content-Type of the response should be independently chosen by Spring MVC. The Message Converter plays a vital role in this choice.

Message Converter to be Selected

With the return value being modified by ResponseBody in the method signature of the controller, the HandlerAdapter (specifically, the RequestMappingHandlerAdapter) delegates the processing logic for results to the RequestResponseBodyMethodProcessor.

As a result, it is responsible for distinguishing and handling the return values of json/jsonp. The specific processing logic is shown in the following figure:

9

First, several roles are involved:

(1) RequestResponseBodyMethodProcessor: is the handler that processes the results of the ResponseBody class. It is responsible for interacting with HandlerAdapter. (It is interesting to note that while other handlers are called ReturnValueHandlers, this one ends with Processor in its name, indicating that it can do more than just process results. However, this is not related to the issue in this article, so we won't delve into it.)

(2) AbstractMessageConverterMethodProcessor: is the parent class of Processor. This class implements the selection of the ContentType and controls the writing of the converter.

(3) MessageConverter: is the handling class for output results. The logic of filling JSON is also implemented in the converter.

(4) MediaType: indicates the type of the content. This parameter is directly related to the result of the Content-Type parameter.

(5) UTF8JsonGenerator: is the class used for output. The converter will use it to interact with the Response and write the results.

Second, the process of resolving the object returned by the Controller and writing the Response is as follows:

(1) Obtain the acceptable Content-Type. Resolve the Accept field in the request to view the types the browser can receive. A value of */* indicates that all types can be received.

(2) Obtain the producible Content-Type. Traverse all MessageConverters in the system to view all types that can be generated for the response. Specifically, call the canWrite method of Converter in sequence to determine whether the Converter can process the response. If yes, all types supported by Converter are returned.

// clazz is the type of the returned object
boolean canWrite(Class<?> clazz, @Nullable MediaType mediaType)

(3) Determine whether the producible type can be accepted and then only retain acceptable types.

(4) Sort the types that can be processed.

(5) Traverse the MessageConverters and output in sequence.

To sum up, the corresponding content-type of the object returned by the Controller and the output method depend on which Message Converter can process the object.

Ideally, for each type, only one MessageConverter is registered in Spring MVC to handle it. However, in the original implementation described in this article, the situation becomes complicated. In particular, from Steps 4-5 above, the so-called sorting becomes unclear.

The Order of MediaType is Important

In the last section, we first give the conclusion that the order of MediaType is wrong and has not been set correctly.

In the original scheme, the MediaType supported by the custom Converter is JSON and JavaScript, resulting in the Content-Type output by HA is always JSON, but since the interceptor will reset the Content-Type to JavaScript in the afterCompletion method, JSONP requests can always be returned normally.

Now, the position of setting Content-Type is changed to preHandle, and the wrong order of MediaType is exposed.

The sorting method is MediaType.sortBySpecificityAndQuality, and the logic is:

(1) The wild-card MediaType parameter (that is, the MediaType parameter contains a wildcard character (*)) is ranked last.

(2) The MediaType with a higher q-value has a higher priority. q-value is a type parameter. For example, the accept Header of HTTP can be: application/json:q=1, and the default q-value is 1, that is, the maximum, which is completely dependent on the request parameters.

(3) MediaTypes with different types do not affect each other. The "type" here refers to the first half of the / in the parameter. For example, the types of "text/plain" and "application/json" are "text" and "application" respectively. Their order is defined according to the system.

(4) MediaTypes with different subTypes do not affect each other. The "type" here refers to the second half of the / in the parameter. For example, the subTypes of "application/json" and "application/javascript" are "json" and "application" respectively. Their order is also defined according to the system.

According to the above principle, the MediaType supported by the custom Converter is JSON and JavaScript, their subType is different, and the q-value is also 1, so the order is the order defined in the code.

The output Content-Type of the response processed by the custom converter is always JSON, which is the first one.

6. Solve the Problem

Based on the above analysis, in order to solve the problem in the section 2.1, the final solution is:

(1) For the setHeader of CORS, the position is placed in the preHandle method of the interceptor.

(2) The custom Message Converter only supports the application/javascript type of MediaType. You can modify the canWrite method to filter requests that are not sent in JSON.

boolean canWrite(Class<?> clazz, @Nullable MediaType mediaType) {
    // When the return value is of the JsonpWrapperObject type, it is a JSON request
    // Other requests are handled by the default JSON-based MessageConverter in Spring MVC
return clazz == JsonpWrapperObject.class;
    return clazz == JsonpWrapperObject.class;
}

(3) Adjust the registration order of Message Converter in Spring MVC, and customize the Converter that processes JSON to rank first.

7. Best Practice for Cross-Origin Issues

In summary, implementing JSONP required several tricky operations, such as using an interceptor and a custom converter. This doesn't seem to be the best practice.

Yes, when compared to cross-origin implementation, CORS is much simpler than JSONP.

Additionally, for JSONP, some open-source code provides better support. For example, the JSONPResponseBodyAdvice provided by Fastjson implements a global controller aspect. In Section 5.2's diagram, we can also see that in the AbstractMessageConverterMethodProcessor, there is a step that uses advice to perform some processing before the response body is written.

The implementation of the JSONPResponseBodyAdvice is as follows:

@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);
        // ...
    }
    // ...
}

However, this requires that the return value of the Controller be modified by the ResponseJSONP annotation of Fastjson.

Due to development and regression costs, this approach is not used in this article. Instead, the original Interceptor and Message Converter continue to be used.

It is expected that a simpler implementation scheme can be used in the future when dealing with JSONP. Oh, wait, next time, JSONP will not be used again to solve the cross-origin issue.


Disclaimer: The views expressed herein are for reference only and don't necessarily represent the official views of Alibaba Cloud.

0 1 0
Share on

Alibaba Cloud Community

999 posts | 242 followers

You may also like

Comments