ajax json springboot,Spring Boot 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 resolvers = new ArrayList();

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 中自动加入的对象

* 如果你在项目中使用了@ControllerAdvice , 要特别注意了,我们在这里就是要排除掉因为@ControllerAdvice自动加入的值

*

*/

@Override

protected Object filterModel(Map model) {

Map result = new HashMap(model.size());

if (model != null) {

for (Map.Entry 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 returnValueHandlers = adapter.getReturnValueHandlers();

List handlers = new ArrayList<>(returnValueHandlers);

decorateHandlers(handlers);

adapter.setReturnValueHandlers(handlers);

processContentNegotiationManager();

}

private void processContentNegotiationManager() {

// 处理JSONP的响应ContentType

List 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 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> converters) {

for (HttpMessageConverter> httpMessageConverter : converters) {

// 为 MappingJackson2HttpMessageConverter 添加 "application/javascript"

// 支持,用于响应JSONP的Content-Type

if (httpMessageConverter instanceof MappingJackson2HttpMessageConverter) {

MappingJackson2HttpMessageConverter convert = (MappingJackson2HttpMessageConverter) httpMessageConverter;

List 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 mons.logging.Log;

import mons.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 jsonpParameterNames = new LinkedHashSet(Arrays.asList("jsonp", "callback"));

/**

* Pattern for validating jsonp callback parameter values.

*/

private static final Pattern CALLBACK_PARAM_PATTERN = pile("[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 mons.logging.Log;

import mons.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 jsonpParameterNames = new LinkedHashSet(Arrays.asList("jsonp", "callback"));

/**

* Pattern for validating jsonp callback parameter values.

*/

private static final Pattern CALLBACK_PARAM_PATTERN = pile("[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 resolveMediaTypes(NativeWebRequest request) throws HttpMediaTypeNotAcceptableException {

// JSONP 响应类型处理 ---- BEGIN

String jsonpParameterValue = getJsonpParameterValue(request);

if (jsonpParameterValue != null) {

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

/**

* 默认页

*@RequestMapping("/") 和@RequestMapping 是有区别的

* 如果不写参数,则为全局默认页,加入输入404页面,也会自动访问到这个页面。

* 如果加了参数“/”,则只认为是根页面。

*

*@return

*@author SHANHY

*@create 2016年1月5日

*/

@RequestMapping(value = {"/","/index"})

public String index(Map 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 getInfo(@RequestParam(required=false) String name,

@RequestParam(required=false) String name1) {

Map map = new HashMap<>();

map.put("name", name);

map.put("name1", name1);

return map;

}

}

测试结果截图如下:

请求JSONP数据

正常请求JSON数据

直接请求显示页面

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

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

$("#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');

}

});

});

});

jQuery AJAX 的跨域请求

JSONP请求 (预期结果为成功)

JSON请求 (预期结果为失败)

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

不过还有另一种很简单的方法来支持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 domainList = new ArrayList<>();

domainList.add("");

domainList.add("");

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

}

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

  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值