SpringBoot实现jsonp跨域通信

Easter79
• 阅读 363

实现jsonp跨域通信

实现基于jsonp的跨域通信方案

原理

浏览器对非同源ajax请求有限制,不允许发送跨域请求
目前跨域解决方案有两种

  • cros配置
  • jsonp请求

cros为新规范,通过一个head请求询问服务器是否允许跨域,若不允许则被拦截
jsonp则为利用浏览器不限制js脚本的同源性,通过动态创建script请求,服务器传递回一个js函数调用语法,浏览器端按照js函数正常调用回调函数

实现思路

首先确定服务器端应该如何返回数据

一次正确的jsonp请求,服务器端应该返回如下格式数据

jQuery39948237({key:3})

其中,jQuery39948237为浏览器端要执行的函数名,该函数由ajax库动态创建,并将函数名作为一个请求参数和该次请求的其余参数一并发送,服务器端无需对此参数做过多处理

{key:3}为此次请求返回的数据,作为函数参数传递


其次,服务器端如何处理?

为了兼容jsonp和cros方案,服务器端应该在请求带有函数名参数时返回函数调用,否则正常返回json数据即可


最后,为了减少代码的侵入,不应该将上述流程放入一个Controller正常逻辑中,应该考虑使用aop实现

实现

前端

前端本次使用jquery库(本来想用axios库的,但是axios不支持jsonp)

代码如下

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

Jquery默认jsonp函数名参数name为callback

后端

本次采用aop实现

具体思路为: 给Controller添加后切点,判断request是否有函数名参数,如果有则修改返回的数据,没有则不做处理

而aop又有两种方案

  • 常规aop,自己定义切点
  • ResponseBodyAdvice,Spring提供的可直接用于数据返回的工具类

本次使用第二种方案


首先是Controller的接口实现

@RequestMapping("dto")
public Position dto() {
    return new Position(239, 43);
}

返回一个复杂类型,Spring会自动对其做json序列化操作


然后的ResponseBodyAdvice实现

该类全路径为:org.springframework.web.servlet.mvc.method.annotation.ResponseBodyAdvice

/**
 * 处理controller返回值,对于有callback值的使用jsonp格式,其余不处理
 */
@RestControllerAdvice(basePackageClasses = IndexController.class)
public class JsonpAdvice implements ResponseBodyAdvice {

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

    //jquery默认是callback,其余jsonp库可能不一样
    private final String callBackKey = "callback";

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

    /**
     * 在此处对返回值进行处理,需要特别注意如果是非String类型,会被Json序列化,从而添加了双引号,解决办法见
     *
     * @param body               返回值
     * @param methodParameter    方法参数
     * @param mediaType          当前contentType,非String类型为json
     * @param aClass             convert的class
     * @param serverHttpRequest  request,暂时支持是ServletServerHttpRequest类型,其余类型将会原样返回
     * @param serverHttpResponse response
     * @return 如果body是String类型,加上方法头后返回,如果是其他类型,序列化后返回
     * @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;
        // 如果返回String类型,media是plain,否则是json,将会经过json序列化,在下方返回纯字符串之后依然会被序列化,就会添上多余的双引号
        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
                if (body instanceof String) {
                    return callback + "(\"" + body + "\")";
                } else {
                    try {
                        String res = mapper.writeValueAsString(body);
                        logger.debug("转化后的返回值={},{}", res, callback + "(" + res + ")");

                        return callback + "(" + res + ")";
                    } catch (JsonProcessingException e) {
                        logger.warn("【jsonp支持】数据body序列化失败", e);
                        return body;
                    }
                }
            }
        } else {
            logger.warn("【jsonp支持】不支持的request class  ={}", serverHttpRequest.getClass());
        }
        return body;
    }
}

使用@RestControllerAdvice指明切点

bug

经过此步骤,理论上即可实现jsonp调用了。

然而实际测试发现,由于Spring json序列化策略的问题,如果返回jsonp字符串,json序列化之后,将会添上一对引号,如下

应该返回

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

实际返回

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

导致浏览器端无法正常运行函数


经多方查找资料后得知

由于在ResponseBodyAdvice中修改了实际的返回值类型为String,而字符串类型经过Jackson序列化后就会加上引号

解决办法为:修改默认的json序列化MessageConverter处理逻辑,对于实际是String的不做处理

代码如下

@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) {
            //绕开实际上返回的String类型,不序列化
            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<HttpMessageConverter<?>> converters) {
//        MappingJackson2HttpMessageConverter converter = mappingJackson2HttpMessageConverter();
        converter.setSupportedMediaTypes(new LinkedList<MediaType>() {{
            add(MediaType.TEXT_HTML);
            add(MediaType.APPLICATION_JSON_UTF8);
        }});
        converters.add(new StringHttpMessageConverter(StandardCharsets.UTF_8));
        converters.add(converter);
    }
}

todo

暂时不明白为什么需要两个类搭配使用

代码

具体实现可查阅github

点赞
收藏
评论区
推荐文章
刚刚好 刚刚好
4个月前
css问题
1、在IOS中图片不显示(给图片加了圆角或者img没有父级)<div<imgsrc""/</divdiv{width:20px;height:20px;borderradius:20px;overflow:h
blmius blmius
1年前
MySQL:[Err] 1292 - Incorrect datetime value: ‘0000-00-00 00:00:00‘ for column ‘CREATE_TIME‘ at row 1
文章目录问题用navicat导入数据时,报错:原因这是因为当前的MySQL不支持datetime为0的情况。解决修改sql\mode:sql\mode:SQLMode定义了MySQL应支持的SQL语法、数据校验等,这样可以更容易地在不同的环境中使用MySQL。全局s
晴空闲云 晴空闲云
4个月前
css中box-sizing解放盒子实际宽高计算
我们知道传统的盒子模型,如果增加内边距padding和边框border,那么会撑大整个盒子,造成盒子的宽度不好计算,在实务中特别不方便。boxsizing可以设置盒模型的方式,可以很好的设置固定宽高的盒模型。盒子宽高计算假如我们设置如下盒子:宽度和高度均为200px,那么这会这个盒子实际的宽高就都是200px。但是当我们设置这个盒子的边框和内间距的时候,那
艾木酱 艾木酱
3个月前
快速入门|使用MemFire Cloud构建React Native应用程序
MemFireCloud是一款提供云数据库,用户可以创建云数据库,并对数据库进行管理,还可以对数据库进行备份操作。它还提供后端即服务,用户可以在1分钟内新建一个应用,使用自动生成的API和SDK,访问云数据库、对象存储、用户认证与授权等功能,可专
Wesley13 Wesley13
1年前
JAVA服务端配置允许跨域请求
CORS是一个W3C标准,全称是”跨域资源共享”(Crossoriginresourcesharing)。它允许浏览器向跨源服务器,发出XMLHttpRequest请求,从而克服了AJAX只能同源使用的限制。1.加入CROS依赖的包<dependency<groupIdcom.thetransactionco
Stella981 Stella981
1年前
KVM调整cpu和内存
一.修改kvm虚拟机的配置1、virsheditcentos7找到“memory”和“vcpu”标签,将<namecentos7</name<uuid2220a6d1a36a4fbb8523e078b3dfe795</uuid
Wesley13 Wesley13
1年前
JavaEE从服务器端解决Ajax跨域问题
1、Ajax跨域简介  1、指的是浏览器不能执行其他网站的脚本。是浏览器施加的安全限制。js本身不跨域,使用form表单和iframe直接请求,是不会跨域的;  2、只要两个url的协议、域名、端口其中有一个不同,从其中一个url中使用ajax请求另一个url,则属于Ajax跨域;  3、ajax请求接口,只是不能进入回调函数,接口还是可以正常请
Stella981 Stella981
1年前
SpringBoot实现jsonp跨域通信
实现jsonp跨域通信实现基于jsonp的跨域通信方案原理浏览器对非同源ajax请求有限制,不允许发送跨域请求目前跨域解决方案有两种cros配置jsonp请求cros为新规范,通过一个head请求询问服务器是否允许跨域,若不允许则被拦截jso
Easter79 Easter79
1年前
springboot的跨域
https://www.cnblogs.com/520playboy/p/7306008.html1、对于前后端分离的项目来说,如果前端项目与后端项目部署在两个不同的域下,那么势必会引起跨域问题的出现。针对跨域问题,我们可能第一个想到的解决方案就是jsonp,并且以前处理跨域问题我基本也是这么处理。但是jsonp方式也同样有不足,不管是对
helloworld_28799839 helloworld_28799839
4个月前
常用知识整理
Javascript判断对象是否为空jsObject.keys(myObject).length0经常使用的三元运算我们经常遇到处理表格列状态字段如status的时候可以用到vue