因WEB安全原因,Ajax默认情况下是不能进行跨域请求的,遇到这种问题,自然难不倒可以改变世界的程序猿们,于是 JSONP(JSON with Padding) 被发明了,其就是对JSON的一种特殊,简单来说就是在原有的JSON数据上做了点手脚,从而达到可以让网页可以跨域请求。 在现在互联网技术对“前后分离”大规模应用的时期,JSONP可谓意义重大啊。
假设我们原来的JSON数据为 {“hello”:”你好”,”veryGood”:”很好”},那么对应的JSONP的格式就是 functionName({“hello”:”你好”,”veryGood”:”很好”}) ,其中“functionName”不是固定值,自己定义。
SpringMVC中实现支持JSONP
在SpringMVC中实现支持JSONP总结为如下几点:
- response 响应类型为 application/javascript
- 进行json请求的URL中需要携带参数 jsonp 或 callback,并指定值。
如 http://mydomain/index.jsonp?callback=myfun
或 http://mydomain/index.jsonp?jsonp=myfun
其中 myfun 就为最终包裹在原有JSON外的函数名 - 如果你在配置文件中配置过 MappingJacksonJsonView 那么请修改使用 MappingJackson2JsonView
- Controller 中的方法需要返回 ModelAndView 或者未使用 @ResponseBody 注解的返回 String 页面。也就是说最终怎么呈现结果,交由SpringMVC来给我们完成。
- 针对显式注解 @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的该如何处理呢?
原理
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步处理:
- 为 MappingJackson2HttpMessageConverter 添加 application/javascript 响应类型支持。
- 包装 ServletPathExtensionContentNegotiationStrategy ,重写 resolveMediaTypes 方法,根据JSONP特性 (callback参数),自动确定 application/javascript 请求类型。
下面是代码,其中 ResponseBodyWrapHandler 和 ContentNegotiationStrategyWrap 为包装类,ResponseBodyProcessor 为统一处理类。
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); } }
ResponseBodyWrapHandler
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); } }
ContentNegotiationStrategyWrap
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); // 吐槽一下云栖社区,使用 a l e r t 提交不了文章,请自行修改为英文半角(吐槽时间:2016-03-01 13:37)。 }, 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.name); }, 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"); }
实际应用中,根据自己的需要选择合适的方法。
注:本文内容来自互联网,旨在为开发者提供分享、交流的平台。如有涉及文章版权等事宜,请你联系站长进行处理。