使用 Jsonp 解决 Ajax 跨域问题

跨域

同源策略

同源策略是一种约定,它是浏览器最核心也最基本的安全功能,如果缺少了同源策略,浏览器很容易受到 XSS、CSFR 等攻击。同源策略会阻止一个域的 javascript 脚本和另外一个域的内容进行交互。所谓同源是指"协议+域名+端口"三者相同。

什么是跨域

当一个请求 url 的协议、域名、端口三者之间任意一个与当前页面 url 不同即为跨域。

跨域有以下限制:

  • 无法读取非同源网页的 Cookie、LocalStorage 和 IndexedDB 等存储型内容
  • 无法接触非同源网页的 DOM节点
  • 无法向非同源地址发送 AJAX 请求

解决策略

解决跨域的方法很多,大致有以下下几种:

  • Jsonp(只支持 get 请求)
  • CORS
  • Iframe
  • Proxy

本文先介绍使用 Jsonp 解决 Ajax 跨域问题,后续还会有 CORS、Proxy 解决跨域的文章。

客户端

$.ajax({
    type: 'get',
    url: url,
    dataType: 'jsonp',
    jsonp: 'callback',
    jsonpCallback: 'callback' 
}).done(function (data) {
    alert(data);
}).error(function (XMLHttpRequest,textStatus,errorThrown) {
    alert('fail');
})

type: jsonp 只支持 get 请求
dataType:设定为 jsonp
jsonp: 若无显示指定,默认为 callback,传递给后台,用以获取 jsonp 的回调函数名
jsonpCallback:jsonp 的回调函数名,若无显示指定,则自动生成类似 jQuery11100431856629965818_1585317491198 的随机函数名

该请求会自动在 url 后追加 ?callback=callbackMethodName&_=1585317491199,其中 callback 即为 jsonp 的属性值,callbackMethodName 即为 jsonpCallback 的属性值。主要作用是告诉服务器我的本地回调函数叫做 callbackMethodName,请要把查询结果传入这个函数中,即 callbackMethodName({"name":"xiaoming","age":18}) 这种形式。

服务端

方法一

@GetMapping("/test")
@ResponseBody
public String test() {
    CommonResult commonResult = new CommonResult();
    String responseStr = JSONObject.toJSONString(commonResult);
    return "callback" + "(" + responseStr + ")";
}

“callback” 需要跟 Ajax 中 jsonpCallback 的属性值一致

方法二

使用 FastJson 提供的 com.fasterxml.jackson.databind.util.JSONPObject

@GetMapping("/test")
@ResponseBody
public JSONPObject test() {
    CommonResult commonResult = new CommonResult();
    JSONPObject jo = new JSONPObject("callback", commonResult);
    return jo;
}

“callback” 需要跟 Ajax 中 jsonpCallback 的属性值一致

方法三

FastJson 对 Jsonp 提供了支持

配置

本实验环境为 SpringBoot 2.1.5.RELEASE,FastJson 1.2.58,采用 Java Config 形式实现。其他版本以及其他形式配置请参考在 Spring 中集成 Fastjson

@Configuration
public class FastJsonConfigurer implements WebMvcConfigurer {
    
    /**
     * 设置 FastJson 作为默认的 java 对象与 json 互相转换的工具
     */
    @Override
    public void configureMessageConverters(List<HttpMessageConverter<?>> converters) {
        FastJsonHttpMessageConverter converter = new FastJsonHttpMessageConverter();
        // 创建配置类
        FastJsonConfig config = new FastJsonConfig();
        // 自定义格式化输出
        config.setSerializerFeatures(
                SerializerFeature.DisableCircularReferenceDetect,
                SerializerFeature.WriteMapNullValue,
                SerializerFeature.WriteNullListAsEmpty,
                SerializerFeature.WriteNullStringAsEmpty,
                SerializerFeature.WriteNullBooleanAsFalse);
        // 处理中文乱码问题
        List<MediaType> fastMediaTypes = new ArrayList<>();
        fastMediaTypes.add(MediaType.APPLICATION_JSON_UTF8);
        converter.setSupportedMediaTypes(fastMediaTypes);
        converter.setFastJsonConfig(config);
        converters.add(0, converter);
    }

    /**
     * 对 JSONP 支持
     */
    @Bean
    public JSONPResponseBodyAdvice jsonpResponseBodyAdvice() {
        return new JSONPResponseBodyAdvice();
    }
}

如果使用的 FastJson 版本小于 1.2.36 的话(强烈建议使用最新版本),在与 Spring MVC 4.X 版本集成时需使用 FastJsonHttpMessageConverter4。

SpringBoot 2.0.1 版本中加载 WebMvcConfigurer 的顺序发生了变动,故需使用 converters.add(0, converter); 指定 FastJsonHttpMessageConverter 在 converters 内的顺序,否则在 SpringBoot 2.0.1 及之后的版本中将优先使用 Jackson 处理。

使用

Fastjson 提供了 @ResponseJSONP 注解,该注解组合了 @ResponseBody,也就意味着使用了 @ResponseJSONP 就没必要再添加 @ResponseBody 了。另外该注解有个 callback 成员变量(默认值为 callback ),即为 Ajax 中 jsonp 属性值。

@Documented
@Target({ElementType.TYPE, ElementType.METHOD})
@Retention(RetentionPolicy.RUNTIME)
@ResponseBody
public @interface ResponseJSONP {
    String callback() default "callback";
}

@ResponseJSONP 可以修饰类或方法,若修饰在类上则该类下所有方法都生效,方法上优先级大于类。

@ResponseJSONP // 类级别
@Controller
@RequestMapping("/jsonp")
public class JsonpController {

    @ResponseJSONP(callback = "callback") // 方法级别
    @GetMapping("/test")
    public Object test() {
       ...
    }
}
源码
@Order(-2147483648)
@ControllerAdvice
public class JSONPResponseBodyAdvice implements ResponseBodyAdvice<Object> {
    public final Log logger = LogFactory.getLog(this.getClass());

    public JSONPResponseBodyAdvice() {
    }
    // 当前消息转换器为 FastJsonHttpMessageConverter 同时类或方法上有 @ResponseJSONP 注解,才会进行返回体封装
    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 = (ResponseJSONP)returnType.getMethodAnnotation(ResponseJSONP.class);
        if (responseJsonp == null) {
            // 若方法上不存在则获取类上的注解
            responseJsonp = (ResponseJSONP)returnType.getContainingClass().getAnnotation(ResponseJSONP.class);
        }

        HttpServletRequest servletRequest = ((ServletServerHttpRequest)request).getServletRequest();

        // 根据设置的 callback 获取请求中的 callbackMethodName 回调函数名
        String callbackMethodName = servletRequest.getParameter(responseJsonp.callback());
        if (!IOUtils.isValidJsonpQueryParam(callbackMethodName)) {
            if (this.logger.isDebugEnabled()) {
                this.logger.debug("Invalid jsonp parameter value:" + callbackMethodName);
            }

            callbackMethodName = null;
        }
        // 包装成类似 callbackMethodName({"name":"xiaoming","age":18})
        JSONPObject jsonpObject = new JSONPObject(callbackMethodName);
        jsonpObject.addParameter(body);
        this.beforeBodyWriteInternal(jsonpObject, selectedContentType, returnType, request, response);
        return jsonpObject;
    }

    public void beforeBodyWriteInternal(JSONPObject jsonpObject, MediaType contentType, MethodParameter returnType, ServerHttpRequest request, ServerHttpResponse response) {
    }

    protected MediaType getContentType(MediaType contentType, ServerHttpRequest request, ServerHttpResponse response) {
        return FastJsonHttpMessageConverter.APPLICATION_JAVASCRIPT;
    }
}

方法四

从上面源码可以看出返回体封装必须要求当前消息转换器为 FastJsonHttpMessageConverter,也就是必须要将 FastJsonHttpMessageConverter 作为默认的 json 与 java 对象的转换器。但是有些公司内部禁止使用 FastJson,这个时候我们可以仿照上面的源码自定义 JSONPResponseBodyAdvice 实现 ResponseBodyAdvice,重写 supports 和 beforeBodyWrite 方法。

参考

  • 2
    点赞
  • 4
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值