Ajax跨域问题解决(Ajax JSONP)

因WEB安全原因,Ajax默认情况下是不能进行跨域请求的,遇到这种问题,自然难不倒可以改变世界的程序猿们,于是JSONP(JSON with Padding)被发明了,其就是对JSON的一种特殊,简单来说就是在原有的JSON数据上做了点手脚,从而达到可以让网页可以跨域请求。在现在互联网技术对“前后分离”大规模应用的时期,JSONP可谓意义重大啊。

假设我们原来的JSON数据为 {“hello”:”你好”,”veryGood”:”很好”}

那么对应的JSONP的格式就是 functionName({“hello”:”你好”,”veryGood”:”很好”}) ,其中“functionName”不是固定值,自己定义。

在SpringMVC中实现支持JSONP总结为如下几点:

1. response 响应类型为 application/javascript

2. 进行json请求的URL中需要携带参数 jsonp 或 callback,并指定值。

http://mydomain/index.jsonp?callback=myfun

http://mydomain/index.jsonp?jsonp=myfun

其中 myfun 就为最终包裹在原有JSON外的函数名

3. 如果你在配置文件中配置过 MappingJacksonJsonView 那么请修改使用 MappingJackson2JsonView

4. Controller 中的方法需要返回 ModelAndView 或者未使用 @ResponseBody 注解的返回 String 页面。也就是说最终怎么呈现结果,交由SpringMVC来给我们完成。

5. 针对显式注解 @ResponseBody 的方法 (我们本来就是直接响应JSON的),我们需要做特殊处理,使用 MappingJacksonValue 进行封装处理。

说的有点抽象,下面看实际怎么做。

当然我们的原则就是“不对原有已经实现的代码进行任何修改”。

本文代码以SpringBoot为例。

使用 WebMvcConfigurerAdapter 配置 ContentNegotiatingViewResolver ,代码如下:

@Configuration
public class MyWebAppConfigurer
        extends WebMvcConfigurerAdapter {

    private static final Logger logger = LoggerFactory.getLogger(MyWebAppConfigurer.class);

    @Override
    public void configureContentNegotiation(ContentNegotiationConfigurer configurer) {
        configurer.defaultContentType(MediaType.TEXT_HTML)
        .ignoreAcceptHeader(true);
    }

    /*
     * Configure ContentNegotiatingViewResolver
     */
    @Bean
    public ViewResolver contentNegotiatingViewResolver(ContentNegotiationManager manager) {
        ContentNegotiatingViewResolver resolver = new ContentNegotiatingViewResolver();
        resolver.setContentNegotiationManager(manager);

        // Define all possible view resolvers
        List<ViewResolver> resolvers = new ArrayList<ViewResolver>();

        resolvers.add(new JsonViewResolver());

        resolver.setViewResolvers(resolvers);
        return resolver;
    }

}

JsonViewResolver.java

public class JsonViewResolver implements ViewResolver{

    private MappingJackson2JsonView view;

    public JsonViewResolver() {
        super();
        view = new MMappingJackson2JsonView();
        view.setPrettyPrint(true);
    }

    public View resolveViewName(String viewName, Locale locale) throws Exception {
        return view;
    }

}

MMappingJackson2JsonView.java

这个类并不是必须的,我写出来也是为了说明如果遇到和我一样的问题时怎么解决,注意看代码中的注释说明

public class MMappingJackson2JsonView extends MappingJackson2JsonView {

    /**
     * 排除JSON转换的时候 model 中自动加入的对象<br/>
     * 如果你在项目中使用了 @ControllerAdvice , 要特别注意了,我们在这里就是要排除掉因为@ControllerAdvice自动加入的值
     *
     */
    @Override
    protected Object filterModel(Map<String, Object> model) {
        Map<String, Object> result = new HashMap<String, Object>(model.size());
        if (model != null) {
            for (Map.Entry<String, Object> entry : model.entrySet()) {
                if (!"urls".equals(entry.getKey())) {// 对我在项目中使用 @ControllerAdvice 统一加的值,进行排除。
                    result.put(entry.getKey(), entry.getValue());
                }
            }
        }
        return super.filterModel(result);
    }

}

上面提到的 MappingJackson2JsonView 我们已经在代码中使用了。

至于我还说到的 MappingJacksonValue 并不需要我们在哪里直接使用,其实 MappingJackson2JsonView 的源码中已经使用它做好了处理。我们只需要按上面说的在请求json的后面增加 jsonp 或 callback 参数即可。

那么如果我们对于使用 @ResponseBody 注解直接响应JSON的该如何处理呢?

Follow Me ……

原理:

ResponseBody 是通过 RequestResponseBodyMethodProcessor 来处理的,那我们就对这个类做一下包装处理。

RequestResponseBodyMethodProcessor 实现自接口 HandlerMethodReturnValueHandler,又因为Spring内部,同一个类型只能用一个的原则,我们实现自己的 HandlerMethodReturnValueHandler 实现类后,其中将原来的 RequestResponseBodyMethodProcessor 的原有对象包装进去,当我们完成自己的处理后,再讲处理权交给包装的 RequestResponseBodyMethodProcessor 对象。

对 ResponseBody 还需要处理响应类型 (application/javascript)

在Spring内部,先从 ContentNegotiationStrategy 的方法 resolveMediaTypes 中读取 requestMediaTypes ,然后再去匹配 MappingJackson2HttpMessageConverter 中所有支持的 MediaTypes ,从而确定最终响应的 contentType。代码层面的处理也就是 ContentNegotiationStrategy 的 resolveMediaTypes 与 MappingJackson2HttpMessageConverter 的 getSupportedMediaTypes 结果对比处理。

为了满足我们JSONP的要求,requestMediaTypes 和 getSupportedMediaTypes 中都要包含 application/javascript

所以我们还要做如下2步处理:

1、为 MappingJackson2HttpMessageConverter 添加 application/javascript 响应类型支持。

2、包装 ServletPathExtensionContentNegotiationStrategy ,重写 resolveMediaTypes 方法,根据JSONP特性 (callback参数),自动确定 application/javascript 请求类型。

下面是代码:

其中 ResponseBodyWrapHandler 和 ContentNegotiationStrategyWrap 为包装类,ResponseBodyProcessor 为统一处理类。

package org.springboot.sample.config.jsonp;

import java.util.ArrayList;
import java.util.List;

import org.springframework.beans.factory.InitializingBean;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.context.annotation.Configuration;
import org.springframework.http.MediaType;
import org.springframework.http.converter.HttpMessageConverter;
import org.springframework.http.converter.json.MappingJackson2HttpMessageConverter;
import org.springframework.web.accept.ContentNegotiationManager;
import org.springframework.web.accept.ContentNegotiationStrategy;
import org.springframework.web.accept.ServletPathExtensionContentNegotiationStrategy;
import org.springframework.web.method.support.HandlerMethodReturnValueHandler;
import org.springframework.web.servlet.config.annotation.WebMvcConfigurerAdapter;
import org.springframework.web.servlet.mvc.method.annotation.RequestMappingHandlerAdapter;
import org.springframework.web.servlet.mvc.method.annotation.RequestResponseBodyMethodProcessor;

/**
 * 处理Spring默认加载好的类,在原有类上使用自定义类进行包装处理。
 *
 * @author 单红宇(365384722)
 * @myblog http://blog.csdn.net/catoop/
 * @create 2016年2月29日
 */
@Configuration
public class ResponseBodyProcessor extends WebMvcConfigurerAdapter implements InitializingBean {

    @Autowired
    private RequestMappingHandlerAdapter adapter;

    @Autowired
    private ContentNegotiationManager manager;

    @Override
    public void afterPropertiesSet() throws Exception {
        List<HandlerMethodReturnValueHandler> returnValueHandlers = adapter.getReturnValueHandlers();
        List<HandlerMethodReturnValueHandler> handlers = new ArrayList<>(returnValueHandlers);
        decorateHandlers(handlers);
        adapter.setReturnValueHandlers(handlers);

        processContentNegotiationManager();
    }

    private void processContentNegotiationManager() {
        // 处理JSONP的响应ContentType
        List<ContentNegotiationStrategy> strategies = manager.getStrategies();
        for (int i = 0; i < manager.getStrategies().size(); i++) {
            if (manager.getStrategies().get(i) instanceof ServletPathExtensionContentNegotiationStrategy) {
                strategies.set(i, new ContentNegotiationStrategyWrap(manager.getStrategies().get(i)));
                manager = new ContentNegotiationManager(strategies);
                break;
            }
        }
    }

    private void decorateHandlers(List<HandlerMethodReturnValueHandler> handlers) {
        for (HandlerMethodReturnValueHandler handler : handlers) {
            if (handler instanceof RequestResponseBodyMethodProcessor) {
                // 用自己的ResponseBody包装类替换掉框架的,达到返回Result的效果
                ResponseBodyWrapHandler decorator = new ResponseBodyWrapHandler(handler);
                int index = handlers.indexOf(handler);
                handlers.set(index, decorator);
                break;
            }
        }
    }

    @Override
    public void extendMessageConverters(List<HttpMessageConverter<?>> converters) {
        for (HttpMessageConverter<?> httpMessageConverter : converters) {
            // 为 MappingJackson2HttpMessageConverter 添加 "application/javascript"
            // 支持,用于响应JSONP的Content-Type
            if (httpMessageConverter instanceof MappingJackson2HttpMessageConverter) {
                MappingJackson2HttpMessageConverter convert = (MappingJackson2HttpMessageConverter) httpMessageConverter;
                List<MediaType> medisTypeList = new ArrayList<>(convert.getSupportedMediaTypes());
                medisTypeList.add(MediaType.valueOf("application/javascript;charset=UTF-8"));
                convert.setSupportedMediaTypes(medisTypeList);
                break;
            }
        }
        super.extendMessageConverters(converters);
    }
}
package org.springboot.sample.config.jsonp;

import java.util.Arrays;
import java.util.LinkedHashSet;
import java.util.Set;
import java.util.regex.Pattern;

import org.apache.commons.logging.Log;
import org.apache.commons.logging.LogFactory;
import org.springframework.core.MethodParameter;
import org.springframework.http.converter.json.MappingJacksonValue;
import org.springframework.util.StringUtils;
import org.springframework.web.context.request.NativeWebRequest;
import org.springframework.web.method.support.HandlerMethodReturnValueHandler;
import org.springframework.web.method.support.ModelAndViewContainer;

/**
 * ResponseBody 处理类
 *
 * @author   单红宇(365384722)
 * @myblog  http://blog.csdn.net/catoop/
 * @create    2016年2月29日
 */
public class ResponseBodyWrapHandler implements HandlerMethodReturnValueHandler{  

    protected final Log logger = LogFactory.getLog(getClass());

    private final HandlerMethodReturnValueHandler delegate;  

    private Set<String> jsonpParameterNames = new LinkedHashSet<String>(Arrays.asList("jsonp", "callback"));

    /**
     * Pattern for validating jsonp callback parameter values.
     */
    private static final Pattern CALLBACK_PARAM_PATTERN = Pattern.compile("[0-9A-Za-z_\\.]*");

    private String getJsonpParameterValue(NativeWebRequest request) {
        if (this.jsonpParameterNames != null) {
            for (String name : this.jsonpParameterNames) {
                String value = request.getParameter(name);
                if (StringUtils.isEmpty(value)) {
                    continue;
                }
                if (!isValidJsonpQueryParam(value)) {
                    if (logger.isDebugEnabled()) {
                        logger.debug("Ignoring invalid jsonp parameter value: " + value);
                    }
                    continue;
                }
                return value;
            }
        }
        return null;
    }

    protected boolean isValidJsonpQueryParam(String value) {
        return CALLBACK_PARAM_PATTERN.matcher(value).matches();
    }

    public ResponseBodyWrapHandler(HandlerMethodReturnValueHandler delegate){
      this.delegate=delegate;
    }  

    @Override
    public boolean supportsReturnType(MethodParameter returnType) {
        return delegate.supportsReturnType(returnType);
    }  

    @Override
    public void handleReturnValue(Object returnValue, MethodParameter returnType, ModelAndViewContainer mavContainer, NativeWebRequest webRequest) throws Exception {  

        String jsonpParameterValue = getJsonpParameterValue(webRequest);
        if (jsonpParameterValue != null) {
            if (!(returnValue instanceof MappingJacksonValue)) {
                MappingJacksonValue container = new MappingJacksonValue(returnValue);
                container.setJsonpFunction(jsonpParameterValue);
                returnValue = container;
            }
        }

        delegate.handleReturnValue(returnValue,returnType,mavContainer,webRequest);
    }
}  
package org.springboot.sample.config.jsonp;

import java.util.ArrayList;
import java.util.Arrays;
import java.util.LinkedHashSet;
import java.util.List;
import java.util.Set;
import java.util.regex.Pattern;

import org.apache.commons.logging.Log;
import org.apache.commons.logging.LogFactory;
import org.springframework.http.MediaType;
import org.springframework.util.StringUtils;
import org.springframework.web.HttpMediaTypeNotAcceptableException;
import org.springframework.web.accept.ContentNegotiationStrategy;
import org.springframework.web.context.request.NativeWebRequest;

/**
 * 对 ServletPathExtensionContentNegotiationStrategy 进行包装
 *
 * @author   单红宇(365384722)
 * @myblog  http://blog.csdn.net/catoop/
 * @create    2016年2月29日
 */
public class ContentNegotiationStrategyWrap implements ContentNegotiationStrategy {

    protected final Log logger = LogFactory.getLog(getClass());

    private final ContentNegotiationStrategy strategy;

    private Set<String> jsonpParameterNames = new LinkedHashSet<String>(Arrays.asList("jsonp", "callback"));

    /**
     * Pattern for validating jsonp callback parameter values.
     */
    private static final Pattern CALLBACK_PARAM_PATTERN = Pattern.compile("[0-9A-Za-z_\\.]*");

    private String getJsonpParameterValue(NativeWebRequest request) {
        if (this.jsonpParameterNames != null) {
            for (String name : this.jsonpParameterNames) {
                String value = request.getParameter(name);
                if (StringUtils.isEmpty(value)) {
                    continue;
                }
                if (!isValidJsonpQueryParam(value)) {
                    if (logger.isDebugEnabled()) {
                        logger.debug("Ignoring invalid jsonp parameter value: " + value);
                    }
                    continue;
                }
                return value;
            }
        }
        return null;
    }

    protected boolean isValidJsonpQueryParam(String value) {
        return CALLBACK_PARAM_PATTERN.matcher(value).matches();
    }

    public ContentNegotiationStrategyWrap(ContentNegotiationStrategy strategy) {
        super();
        this.strategy = strategy;
    }

    @Override
    public List<MediaType> resolveMediaTypes(NativeWebRequest request) throws HttpMediaTypeNotAcceptableException {

        // JSONP 响应类型处理 ---- BEGIN
        String jsonpParameterValue = getJsonpParameterValue(request);
        if (jsonpParameterValue != null) {
            List<MediaType> mediaTypes = new ArrayList<>(1);
            mediaTypes.add(MediaType.valueOf("application/javascript"));
            return mediaTypes;
        }
        // JSONP 响应类型处理 ---- END

        return this.strategy.resolveMediaTypes(request);
    }

}


然后新建一个PageController来测试下效果:

@Controller
public class PageController {

    private static final Logger log = LoggerFactory.getLogger(PageController.class);

    /**
     * 默认页<br/>
     * @RequestMapping("/") 和 @RequestMapping 是有区别的
     * 如果不写参数,则为全局默认页,加入输入404页面,也会自动访问到这个页面。
     * 如果加了参数“/”,则只认为是根页面。
     *
     * @return
     * @author SHANHY
     * @create  2016年1月5日
     */
    @RequestMapping(value = {"/","/index"})
    public String index(Map<String, Object> model){
        model.put("time", new Date());
        model.put("message", "小单,你好!");

        return "index";
    }

    /**
     * 响应到JSP页面page1
     *
     * @return
     * @author SHANHY
     * @create  2016年1月5日
     */
    @RequestMapping("/page1")
    public ModelAndView page1(){
        log.info(">>>>>>>> PageController.page1");
        // 页面位置 /WEB-INF/jsp/page/page.jsp
        ModelAndView mav = new ModelAndView("page/page1");
        mav.addObject("content", hello);
        return mav;
    }

    @RequestMapping("/testJson")
    @ResponseBody
    public Map<String, String> getInfo(@RequestParam(required=false) String name,
            @RequestParam(required=false) String name1) {
        Map<String, String> map = new HashMap<>();
        map.put("name", name);
        map.put("name1", name1);
        return map;
    }   

}

测试结果截图如下:

请求JSONP数据



正常请求JSON数据



直接请求显示页面



至此,我们的服务端代码改造完毕,我们在 “不对原有业务代码进行任何修改的前提下” 完成了处理,接下来是在HTML页面中使用jQuery来请求JSONP实现跨域访问。

将下面的代码存储为一个普通的HTML页面,然后用浏览器打开就可以测试了,当然别忘了启动你的web服务:

<!DOCTYPE html>
<html>
<head>
<meta charset="utf-8">
<script src="http://code.jquery.com/jquery-2.1.3.min.js"></script>
<script type="text/javascript">
$(document).ready(function(){ 

    $("#b1").click(function(){
         $.ajax({
            url:‘http://localhost:8080/myspringboot/testJson.json?name=Shanhy&name1=Lily‘,
            type: "get",
            async: false,
            dataType: "jsonp",
            jsonp: "callback", //服务端用于接收callback调用的function名的参数(请使用callback或jsonp)
            jsonpCallback: "fun_jsonpCallback", //callback的function名称
            success: function(json) {
                alert(json.name);
            },
            error: function(){
                alert(‘Request Error‘);
            }
        });
    });        

    $("#b2").click(function(){
         $.ajax({
            url:‘http://localhost:8080/myspringboot/testJson.json?name=Shanhy&name1=Lily‘,
            type: "get",
            async: false,
            //dataType: "jsonp",
            //jsonp: "callback", //服务端用于接收callback调用的function名的参数(请使用callback或jsonp)
            //jsonpCallback: "fun_jsonpCallback", //callback的function名称
            success: function(json) {
                alert(json.name1);
            },
            error: function(){
                alert(‘Request Error‘);
            }
        });
    });   

});
</script>
</head>
<body> 

    <div id="div1"><h2>jQuery AJAX 的跨域请求</h2></div>
    <button id="b1">JSONP请求 (预期结果为成功)</button> <br/>
    <button id="b2">JSON请求 (预期结果为失败)</button> 

</body>
</html>

至此,相信已经满足应用的需求,对部署容器不需要做任何修改。

不过还有另一种很简单的方法来支持Ajax的跨域请求,那就是在响应头中添加支持,如下:

// 指定允许其他域名访问(必须)
response.addHeader("Access-Control-Allow-Origin", "*");
// 响应类型(非必须)
response.addHeader("Access-Control-Allow-Methods", "POST");
// 响应头设置(非必须)
response.addHeader("Access-Control-Allow-Headers", "x-requested-with,content-type");

如果你前端使用到 ApacheServer、Nginx 那么也可以在他们的配置文件中直接配置,具体查一下资料即可。

这里有一点要注意:Access-Control-Allow-Origin 的 * 是允许所有,如果要针对域名设置,直接指定域名即可,但是请注意这里你不可以用逗号分割的方式同时配置多个域名。

如果你真的需要,可以参考如下代码:

        List<String> domainList = new ArrayList<>();
        domainList.add("http://www.domain1.com");
        domainList.add("http://www.domain2.com");
        domainList.add("http://localhost:8088");
        String requestDomain = request.getHeader("origin");
        log.info("requestDomain = " + requestDomain);
        if(domainList.contains(requestDomain)){
            response.addHeader("Access-Control-Allow-Origin", requestDomain);
            response.addHeader("Access-Control-Allow-Methods", "GET");
            response.addHeader("Access-Control-Allow-Headers", "x-requested-with,content-type");
        }

实际应用中,根据自己的需要选择合适的方法。

时间: 2024-08-18 09:11:20

Ajax跨域问题解决(Ajax JSONP)的相关文章

ajax 跨域获取数据jsonp使用

昨天帮同事从其他服务器传过来的json数据进行处理,遇到该问题.开始我的思路是用ajax直接请求把数据弄出来就OK了,然而出错了.原因是我使用的ajax 返回类型为json,默认ajax阻止跨服获取数据的.结合其他博文,ajax的dataType使用jsonp来解决此问题.开始觉得jsonp与json的使用类似,一步步的写着代码,如下: $.ajax({ type:'get', async:false, url:'http://112.11.131.238/nanhunongjing/GetCo

JavaScript之Ajax-7 Ajax跨域请求(Ajax跨域概述、Ajax跨域实现)

一.Ajax跨域概述 同源策略 - 同源策略(Same origin policy)是一种约定,它是浏览器的核心也最最基本的核心.如果少了同源策略,则浏览器的正常功能可能都会收到影响.可以说Web是构建在同源策略基础上的,浏览器只是针对同源策略的一种实现 - 它是由 Netscape 提出的一个著名的安全策略 - 现在所有支持 JavaScript 的浏览器都会使用这个策略 - 所谓同源策略是指,域名.协议.端口相同 域名概述 - 域名(Domain Name) 是由一串用点分隔的名字组成的In

Ajax跨域请求解决方案——jsonp

转自:http://www.cnblogs.com/dowinning/archive/2012/04/19/json-jsonp-jquery.html 1.一个众所周知的问题,Ajax直接请求普通文件存在跨域无权限访问的问题,甭管你是静态页面.动态网页.web服务.WCF,只要是跨域请求,一律不准: 2.不过我们又发现,Web页面上调用js文件时则不受是否跨域的影响(不仅如此,我们还发现凡是拥有"src"这个属性的标签都拥有跨域的能力,比如<script>.<im

ajax 跨域请求之jsonp

需求 遇到的问题 解决办法 需求 今天项目需要访问一个外部链接获取数据,是跨域的.使用ajax 请求一直提示: 遇到的问题 1. 如何使用ajax 跨域请求数据 2. 能不能post请求 解决办法 经过网上查找资料,能使用jsonp请求跨域数据. jsonp请求数据只能get,不支持post跨域请求 使用方法,见代码: $.ajax({ type: "post",//这里写post也没用,也是get请求 url: "url", dataType: "jso

ajax跨域问题解决思路

ajax跨域问题的解决思路主要分为3种: 1.浏览器限制解决思路:不让浏览器做出限制解决方法:通过指定参数,让浏览器不做跨域校验评价:价值不大,需要每个人都做改动,而且改动是客户端的改动 2.XHR请求解决思路:不使用XHR解决方法:JSONP缺点:无法满足现有的开发要求 3.跨域(重要)解决思路:(1)被调用方修改代码,使其支持跨域(2)调用方隐藏跨域解决思路:(1)被调用方通过修改返回的信息,加入一些字段,允许调用方调用,此时只要通过浏览器跨域校验则允许跨域(2)使用代理,通过指定的url转

IE9版本号下面ajax 跨域问题解决

ajax跨域请求数据在谷歌火狐我本地IE11都是没问题的. 让測试就发现问题了,IE8下请求不到数据.然后我查看一下自己写的js看有没有不兼容问题.但是都没有啊.为什么就请求不到呢. 我把ajax的error打印出来提示no transport.网上找了资料在js中第一行加这个就能够了jQuery.support.cors = true; 好了这个问题没有了,但是又有还有一个error没有权限.这个问题百度了好多都没有我想要的,最后看了一篇文章让我豁然开朗这是IE浏览器的安全性设置问题, 解决方

IE9版本以下ajax 跨域问题解决

ajax跨域请求数据在谷歌火狐我本地IE11都是没问题的. 让测试就发现问题了,IE8下请求不到数据,然后我查看一下自己写的js看有没有不兼容问题,可是都没有啊,为什么就请求不到呢. 我把ajax的error打印出来提示no transport,网上找了资料在js中第一行加这个就可以了jQuery.support.cors = true; 好了这个问题没有了,可是又有另一个error没有权限.这个问题百度了好多都没有我想要的,最后看了一篇文章让我豁然开朗这是IE浏览器的安全性设置问题, 解决方法

Ajax跨域 取值 Jsonp的定义注意事项

今天要做一个去之前的项目上取数据,打算建一个接口,WebServer.中间遇到了一些问题.就是跨域取值的问题 客户端页面 1.首先Ajax请求的DataTy:'jsonp'这种格式,还要加一个 jsonp: "callback",最为主要的是有一个 回调函数Callback(),可把我折腾坏了.直接上客户端代码: <script type="text/javascript"> $(function () { $.ajax({ type: "GE

ajax跨域请求获取jsonp数据

<!DOCTYPE html><html><head><meta charset="UTF-8"><title>Insert title here</title><script src="jquery.js"></script><script type="text/javascript">function getIntface(){ //?