Implement JSONP cross – domain communication

Implement cross – domain communication scheme based on JSONP

The principle of

Browsers have restrictions on non-homologous Ajax requests and do not allow cross-domain requests to be sent. There are two cross-domain solutions

  • Cros configuration
  • The json request

Cros is a new specification, through a head request to ask the server whether to allow cross-domain, if not, will be intercepted jSONP to take advantage of the browser does not limit the homology of JS scripts, through the dynamic creation of script request, the server passes back a JS function call syntax, the browser side according to the JS function to call the callback function normally

Implementation approach

First determine how the server should return the data

For a correct JSONP request, the server should return data in the following format


jQuery39948237({key:3})

Copy the code

JQuery39948237 is the name of the function to be executed on the browser side, which is dynamically created by the Ajax library and sent as a request parameter along with the rest of the request parameters without much processing on the server side

{key:3} is the data returned by this request and is passed as a function parameter


Second, what about the server side?

To be compatible with the JSONP and CROS schemes, the server should return the function call if the request has a function name parameter, otherwise it should return JSON data normally


Finally, to reduce code intrusion, the above process should not be put into a Controller normal logic, and aop implementation should be considered

implementation

The front end

The front-end uses jquery library ~~(originally intended to use axios library, but axios does not support JSONP)~~

The following code


   $.ajax({
        url:'http://localhost:8999/boot/dto'.dataType:"jsonp".success:(response) = >{
            this.messages.push(response); }})Copy the code

Jquery default jSONp function name parameter name is callback

The back-end

The use of AOP implementation

Add a post-cut point to the Controller and check whether the request has a function name parameter. If so, modify the returned data. If not, no processing is performed

Aop, however, has two options

  • Regular AOP, define your own pointcuts
  • ResponseBodyAdvice, Spring provides utility classes that can be used directly for data return

The second option is used this time


The first is the implementation of the Controller interface

@RequestMapping("dto")
public Position dto(a) {
    return new Position(239.43);
}
Copy the code

Return a complex type to which Spring automatically serializes json


Then the ResponseBodyAdvice is implemented

The full path is: org. Springframework. Web. Servlet. MVC) method. The annotation. ResponseBodyAdvice


/** * handles controller return values, jSONP format for callback values, and */ is not handled for others
@RestControllerAdvice(basePackageClasses = IndexController.class)
public class JsonpAdvice implements ResponseBodyAdvice {

    private Logger logger = LoggerFactory.getLogger(getClass());
    @Autowired
    private ObjectMapper mapper;

    //jquery defaults to callback, other JSONp libraries may differ
    private final String callBackKey = "callback";

    @Override
    public boolean supports(MethodParameter methodParameter, Class aClass) {
        logger.debug("Returned class={}", aClass);
        return true;
    }

    /** * if the value is not a String, it will be serialized by Json, so that double quotes are added@paramBody returns the value *@paramMethodParameter methodParameter *@paramMediaType Current contentType, non-string json *@paramAClass convert class *@paramServerHttpRequest Request, ServletServerHttpRequest is currently supported. The rest types will return * as is@param serverHttpResponse response
     * @returnIf body is a String, it is returned after the method header. If body is of any other type, it is returned * after serialization@see com.inkbox.boot.demo.converter.Jackson2HttpMessageConverter
     */
    @Override
    public Object beforeBodyWrite(Object body, MethodParameter methodParameter, MediaType mediaType, Class aClass, ServerHttpRequest serverHttpRequest, ServerHttpResponse serverHttpResponse) {

        if (body == null)
            return null;
        // If media is plain, it will be serialized by JSON. If a plain String is returned, it will still be serialized by JSON, with extra double quotes
        logger.debug("body={},request={},response={},media={}", body, serverHttpRequest, serverHttpResponse, mediaType.getSubtype());


        if (serverHttpRequest instanceof ServletServerHttpRequest) {
            HttpServletRequest request = ((ServletServerHttpRequest) serverHttpRequest).getServletRequest();

            String callback = request.getParameter(callBackKey);

            if(! StringUtils.isEmpty(callback)) {// Jsonp is used
                if (body instanceof String) {
                    return callback + "(\" " + body + "\")";
                } else {
                    try {
                        String res = mapper.writeValueAsString(body);
                        logger.debug("Converted return value ={},{}", res, callback + "(" + res + ")");

                        return callback + "(" + res + ")";
                    } catch (JsonProcessingException e) {
                        logger.warn([JSONP support] failed to serialize data body, e);
                        returnbody; }}}}else {
            logger.warn("[jSONP support] request class ={}", serverHttpRequest.getClass());
        }
        returnbody; }}Copy the code

Specify the pointcut using @restControllerAdvice

bug

After this step, the JSONP call is theoretically ready to be implemented.

However, the actual test found that due to Spring’s JSON serialization strategy, if a JSONP string is returned, the JSON serialization will be enclosed with a pair of quotes, as follows

Should be returned

Jquery332({"x":239."y":43})
Copy the code

The actual return

"Jquery332({\"x\":239,\"y\":43})"

Copy the code

The browser cannot run functions properly


After searching for information in many ways

Because the actual return type is changed to String in ResponseBodyAdvice, the String type is quoted after Jackson serializes it

The solution is to modify the default JSON serialization MessageConverter processing logic to disregard the actual String

The following code

@Component
public class Jackson2HttpMessageConverter extends MappingJackson2HttpMessageConverter {

    private Logger logger = LoggerFactory.getLogger(getClass());

    @Override
    protected void writeInternal(Object object, Type type, HttpOutputMessage outputMessage) throws IOException, HttpMessageNotWritableException {
        if (object instanceof String) {
            // Bypass the actual returned String type, not serialized
            Charset charset = this.getDefaultCharset();
            StreamUtils.copy((String) object, charset, outputMessage.getBody());
        } else {
            super.writeInternal(object, type, outputMessage); }}}@Configuration
public class MvcConfig implements WebMvcConfigurer {

    private Logger logger = LoggerFactory.getLogger(getClass());

    @Autowired
    private MappingJackson2HttpMessageConverter converter;

    @Override
    public void configureMessageConverters(List
       
        > converters)
       > {
// MappingJackson2HttpMessageConverter converter = mappingJackson2HttpMessageConverter();
        converter.setSupportedMediaTypes(new LinkedList<MediaType>() {{
            add(MediaType.TEXT_HTML);
            add(MediaType.APPLICATION_JSON_UTF8);
        }});
        converters.add(newStringHttpMessageConverter(StandardCharsets.UTF_8)); converters.add(converter); }}Copy the code

todo

Why do you need two classes to work together

code

See Github for details