当前位置:首页 > 综合热点 > 正文

【工作篇】了解升级 Spring 版本导致的跨域问题

摘要: 【工作篇】了解升级Spring版本导致的跨域问题最佳答案53678位专家为你答疑解惑【工作篇】了解升级Spring版本导致的跨域...

【工作篇】了解升级 Spring 版本导致的跨域问题

最佳答案 53678位专家为你答疑解惑

【工作篇】了解升级 Spring 版本导致的跨域问题

一、背景

最近需要统一升级 Spring 的版本,避免 common 包和各个项目间的 Spring 版本冲突问题。这次升级主要是从 Spring 4.1.9.RELEASE 升级到 Spring 4.3.22RELEASE。

预备知识点OPTIONS 请求 https://developer.mozilla.org/zh-CN/docs/Web/HTTP/Methods/OPTIONSCORS 跨域请求https://developer.mozilla.org/zh-CN/docs/Web/HTTP/CORShttps://www.ruanyifeng.com/blog/2016/04/cors.html升级前相关环境

项目采用的方式是通过实现过滤器 Filter,在 Response 返回头文件添加跨域资源共享(CORS) 相关的参数。采用打 war 包部署到 Tomcat6.0.48,但是本地开发配置的 tomcat 版本是 Tomcat8.0.48(这里一般要与服务器环境一致,不然有不可预知问题出现)。

public class CrossFilter extends OncePerRequestFilter {    @Override    protected void doFilterInternal(HttpServletRequest request, HttpServletResponse response, FilterChain chain)            throws ServletException, IOException {        response.setHeader("Access-Control-Allow-Methods", "POST, GET, OPTIONS, DELETE");        response.setHeader("Access-Control-Max-Age", "3600");        response.addHeader("Access-Control-Allow-Headers", "Origin, X-Requested-With, Content-Type, Accept, CMALL_TOKEN"); //这里自定义的请求头不规范,应该使用"-",CMALL-TOKEN,不然需要配置nignx识别        response.setHeader("Access-Control-Allow-Credentials", "true"); // cookie        String origin=request.getHeader("Origin");        response.setHeader("Access-Control-Allow-Origin", origin);  //注意:这里以前并没有限制合法的 域名        //注意:这里如果是预检请求,还是会执行下一个Filter,最好是直接返回响应前端        chain.doFilter(request, response);    }}
二、排查问题

在本地开发环境升级了 Spring 版本为后 Spring 4.3.22RELEASE 后,没有修改 CorsFilter 相关的参数,运行测试没有跨域问题,其它功能正常。 然后部署到测试环境,发现了跨域问题。

通过排查,发现本地的 Tomcat 版本是 Tomcat8.0.48,而测试环境的版本是 Tomcat6.0.48,大意了,平常开发环境也没有注意规范,要与线上,测试等环境保持一致。本地重新配置 Tomcat6.0.48 后重现了跨域问题。

2.1、初步分析

开始排查具体的失败问题,发现

1、Spring4.3.22RELEASE tomcat 6.048 会出现跨域问题

2、Spring 4.1.9RELEASE (Tomcat6.0.48、Tomcat 8.0.48 ) 不会出现跨域问题

3、Spring4.3.22RELEASE (Tomcat8.048) 不会出现跨域问题

从而得出以下疑问?

1、Spring 4.1.9RELEASE 到 Spring4.3.22RELEASE 版本,针对 CORS,有什么新特性发布?

2、Tomcat6.0.48、Tomcat 8.0.48 有什么区别?

2.1.1、首先查看 Spring 版本的差异

通过查看 SpringMVC 官方文档,从 4.2.0 版本开始,SpringMVC 开始支持 CORS 跨域解决方案,主要表现是通过简单的配置,就可以支持 CORS

https://docs.spring.io/spring-framework/docs/4.2.0.RELEASE/spring-framework-reference/html/cors.htmlhttps://github.com/spring-projects/spring-framework/issues/13916

主要可以通过以下方式配置跨域支持

1、通过注解 @CrossOrigin 为单独的请求配置跨域
@RestController@RequestMapping("/account")public class AccountController {@CrossOrigin@RequestMapping("/{id}")public Account retrieve(@PathVariable Long id) {    // ...}@RequestMapping(method=RequestMethod.DELETE, path="/{id}")public void remove(@PathVariable Long id) {    // ...}}

2、全局配置方式

Java Config  配置方式
@Configuration@EnableWebMvcpublic class WebConfig extends WebMvcConfigurerAdapter {    @Override    public void addCorsMappings(CorsRegistry registry) {        registry.addMapping("/api/**")            .allowedOrigins("http://domain2.com")            .allowedMethods("PUT", "DELETE")            .allowedHeaders("header1", "header2", "header3")            .exposedHeaders("header1", "header2")            .allowCredentials(false).maxAge(3600);    }}
Xml 配置方式
<mvc:cors>    <mvc:mapping path="/api/**"        allowed-origins="http://domain1.com, http://domain2.com"        allowed-methods="GET, PUT"        allowed-headers="header1, header2, header3"        exposed-headers="header1, header2" allow-credentials="false"        max-age="123" />    <mvc:mapping path="/resources/**"        allowed-origins="http://domain1.com" /></mvc:cors>
2.1.2、Tomcat 版本的关键区别

查看 Tomcat 版本的发布信息:

https://archive.apache.org/dist/tomcat/tomcat-6/v6.0.48/RELEASE-NOTEShttps://archive.apache.org/dist/tomcat/tomcat-8/v8.0.48/RELEASE-NOTES

得出对于这次跨域问题,可能有影响的区别是:

Tomcat 6.0 支持的 Servlet 版本为 2.5Tomcat 8.0 支持的 Servlet 版本为 3.12.2、得出解决方案

对于上面的查找资料的过程,其实已经可以得出解决方案了(升级到 Spring4.3.22RELEASE):

因为我们使用的是自实现 Filter 过滤器的方式来处理跨域问题的,是不涉及框架问题才对,这里主要是我们没有对预检请求进行拦截并响应告知前端通过跨域请求。

方法一、为了不怎么改动代码,我们还是采用在原来的过滤器中处理预检请求
public class CorsFilter extends OncePerRequestFilter {@Overrideprotected void doFilterInternal(HttpServletRequest request, HttpServletResponse response, FilterChain filterChain) throws ServletException, IOException {    response.setHeader("Access-Control-Allow-Methods", "POST, GET, OPTIONS, DELETE");    response.setHeader("Access-Control-Max-Age", "3600");    response.addHeader("Access-Control-Allow-Headers", "Origin, X-Requested-With, Content-Type, Accept, CMALL-TOKEN");    response.setHeader("Access-Control-Allow-Credentials", "true"); // cookie    response.setHeader("Access-Control-Allow-Origin", "http://localhost:63342");    String origin=request.getHeader("Origin");//响应预检请求//不让过滤器执行下去,Spring默认配置的cors跨域处理器就没法处理处理OPTIONS请求    if (origin !=null &&            HttpMethod.OPTIONS.matches(request.getMethod()) &&            request.getHeader(HttpHeaders.ACCESS_CONTROL_REQUEST_METHOD) !=null) {        response.setStatus(HttpServletResponse.SC_OK);        return;    }    filterChain.doFilter(request, response);}}
方法二、抛弃原先写的过滤器,使用 Spring 提供的方案
@Configuration@EnableWebMvcpublic class CorsConfig extends WebMvcConfigurerAdapter {    @Override    public void addCorsMappings(CorsRegistry registry) {        registry.addMapping("/**")                .allowedOrigins("http://localhost:63342")                .allowedMethods("POST", "GET", "OPTIONS", "DELETE", "PUT")                .allowedHeaders("Origin", "X-Requested-With", "Content-Type", "Accept")                .exposedHeaders("CMALL-TOKEN")                .allowCredentials(true)                .maxAge(3600);    }}
2.3、深入源码分析

虽然解决了这个跨域问题,但是还是要看看没有修改代码前为什么升级到 Spring4.3.22RELEASE,部署到 Tomcat 6.0.48 会出现跨域问题,而部署到 Tomcat 8.048 则不会。

2.3.1、回顾一下 SpringMVC 的执行过程image用户发送请求经过 Filter 过滤器,Spring 拦截器,到达前端处理器 DispatchServletDispatcherServlet 收到请求调用 HandlerMapping(处理器映射器)HandlerMapping 找到具体的处理器(Controller) 和 处理器拦截器(HandlerInterceptor)组成处理器执行链对象DispatcherServlet 通过处理器(Controller)找到对应的处理器适配器(HandlerAdapter)处理器适配器(HandlerAdapter)执行具体的处理器(Controller)Controller 执行完成返回 ModelAndView 对象。DispatcherServlet 将 ModelAndView 传给 ViewReslover(视图解析器)。ViewReslover 解析后返回具体 View(视图)。DispatcherServlet 根据 View 进行渲染视图(即将模型数据填充至视图中)。DispatcherServlet 响应用户。2.3.2、Spring 是如何提供 CORS 支持的?

SpringMVC 的入口文件 DispatcherServlet,默认情况下 DispatcherServlet 继承自 FrameworkServlet,FrameworkServlet 处理了所有的 http 请求,调用 processRequest() 方法。

SpringMVC 处理 Option 请求源码

@Overrideprotected void doOptions(HttpServletRequest request, HttpServletResponse response)        throws ServletException, IOException {    //dispatchOptionsRequest 是否开启对options请求的处理,默认值false    //CorsUtils.isPreFlightRequest(request) 判断是否是预检请求    if (this.dispatchOptionsRequest || CorsUtils.isPreFlightRequest(request)) {        //处理 OPTIONS 请求        processRequest(request, response);        //包含 Allow响应头部,则请求已被正常处理,直接返回        if (response.containsHeader("Allow")) {            // Proper OPTIONS response coming from a handler - we're done.            return;        }    }    //调用父类的doOptions()方法,用于设置 Allow 响应头部    // Use response wrapper for Servlet 2.5 compatibility where    // the getHeader() method does not exist    super.doOptions(request, new HttpServletResponseWrapper(response) {        @Override        public void setHeader(String name, String value) {            if ("Allow".equals(name)) {                value=(StringUtils.hasLength(value) ? value + ", " : "") + HttpMethod.PATCH.name();            }            super.setHeader(name, value);        }    });}

在执行 processRequest 方法时的执行链是: FrameworkServlet.processRequest()->DispatcherServlet.doService()->DispatcherServlet.doDispatch()。

        ...try {ModelAndView mv=null;Exception dispatchException=null;try {    processedRequest=checkMultipart(request);    multipartRequestParsed=(processedRequest !=request);    // Determine handler for the current request.    // 获取HandlerMapping(处理器映射器)    mappedHandler=getHandler(processedRequest);    if (mappedHandler==null || mappedHandler.getHandler()==null) {        noHandlerFound(processedRequest, response);        return;    }    // Determine handler adapter for the current request.    //处理器适配器(HandlerAdapter)    HandlerAdapter ha=getHandlerAdapter(mappedHandler.getHandler());    // Process last-modified header, if supported by the handler.    String method=request.getMethod();    boolean isGet="GET".equals(method);    if (isGet || "HEAD".equals(method)) {        long lastModified=ha.getLastModified(request, mappedHandler.getHandler());        if (logger.isDebugEnabled()) {            logger.debug("Last-Modified value for [" + getRequestUri(request) + "] is: " + lastModified);        }        if (new ServletWebRequest(request, response).checkNotModified(lastModified) && isGet) {            return;        }    }    //执行拦截器的前置方法    if (!mappedHandler.applyPreHandle(processedRequest, response)) {        return;    }    // Actually invoke the handler.    //执行具体的控制器(Controller)    mv=ha.handle(processedRequest, response, mappedHandler.getHandler());    if (asyncManager.isConcurrentHandlingStarted()) {        return;    }    applyDefaultViewName(processedRequest, mv);    mappedHandler.applyPostHandle(processedRequest, response, mv);}catch (Exception ex) {    dispatchException=ex;}catch (Throwable err) {    // As of 4.3, we're processing Errors thrown from handler methods as well,    // making them available for @ExceptionHandler methods and other scenarios.    dispatchException=new NestedServletException("Handler dispatch failed", err);}            ...

继续查看 CORS 的实现原理,getHandler 方法源码

protected HandlerExecutionChain getHandler(HttpServletRequest request) throws Exception {        for (HandlerMapping hm : this.handlerMappings) {            if (logger.isTraceEnabled()) {                logger.trace(                        "Testing handler map [" + hm + "] in DispatcherServlet with name '" + getServletName() + "'");            }            HandlerExecutionChain handler=hm.getHandler(request);            if (handler !=null) {                return handler;            }        }        return null;    }

针对请求 request,在 handlerMappings 这个 Map 中相应的处理器,在 SpringMVC 执行 init 方法时,已经预加载处理器 Map。处理器映射器实现了 HandlerMapping 接口的 getHandler 方法。看到默认 AbstractHandlerMapping 抽象类实现了该方法。

@Overridepublic final HandlerExecutionChain getHandler(HttpServletRequest request) throws Exception {    Object handler=getHandlerInternal(request);    if (handler==null) {        handler=getDefaultHandler();    }    if (handler==null) {        return null;    }    // Bean name or resolved handler?    if (handler instanceof String) {        String handlerName=(String) handler;        handler=getApplicationContext().getBean(handlerName);    }    //获取处理器执行链    HandlerExecutionChain executionChain=getHandlerExecutionChain(handler, request);    //判断是否是跨域请求    if (CorsUtils.isCorsRequest(request)) {        //获取 cors 配置        CorsConfiguration globalConfig=this.globalCorsConfigSource.getCorsConfiguration(request);        CorsConfiguration handlerConfig=getCorsConfiguration(handler, request);        CorsConfiguration config=(globalConfig !=null ? globalConfig.combine(handlerConfig) : handlerConfig);        executionChain=getCorsHandlerExecutionChain(request, executionChain, config);    }    return executionChain;}

如果是预检请求,则使用在 AbstractHandlerMapping 定义的内部类 PreFlightHandler 处理器处理预检请求

protected HandlerExecutionChain getCorsHandlerExecutionChain(HttpServletRequest request,        HandlerExecutionChain chain, CorsConfiguration config) {    if (CorsUtils.isPreFlightRequest(request)) {        HandlerInterceptor[] interceptors=chain.getInterceptors();        chain=new HandlerExecutionChain(new PreFlightHandler(config), interceptors);    }    else {        chain.addInterceptor(new CorsInterceptor(config));    }    return chain;}

而 PreFlightHandler 又委托给 CorsProcessor 处理

private CorsProcessor corsProcessor=new DefaultCorsProcessor();private class PreFlightHandler implements HttpRequestHandler, CorsConfigurationSource {    private final CorsConfiguration config;    public PreFlightHandler(CorsConfiguration config) {        this.config=config;    }    @Override    public void handleRequest(HttpServletRequest request, HttpServletResponse response) throws IOException {        corsProcessor.processRequest(this.config, request, response);    }    @Override    public CorsConfiguration getCorsConfiguration(HttpServletRequest request) {        return this.config;    }}

CorsProcessor 的 processRequest 方法是 SpringMVC 支持 Cors 的具体实现,到此已经了解了 Spring 对 Cors 支持的源码实现。但是为什么升级到 Spring4.3.22RELEASE,部署到 Tomcat 6.0.48 会出现跨域问题,而部署到 Tomcat 8.048 则不会这个问题,我们继续看 ServletServerHttpResponse 类

@Override@SuppressWarnings("resource")public boolean processRequest(CorsConfiguration config, HttpServletRequest request, HttpServletResponse response)        throws IOException {    if (!CorsUtils.isCorsRequest(request)) {        return true;    }    ServletServerHttpResponse serverResponse=new ServletServerHttpResponse(response);    //如果设置了 Access-Control-Allow-Origin 响应头,则直接返回    if (responseHasCors(serverResponse)) {        logger.debug("Skip CORS processing: response already contains \"Access-Control-Allow-Origin\" header");        return true;    }    ServletServerHttpRequest serverRequest=new ServletServerHttpRequest(request);    if (WebUtils.isSameOrigin(serverRequest)) {        logger.debug("Skip CORS processing: request is from same origin");        return true;    }    boolean preFlightRequest=CorsUtils.isPreFlightRequest(request);    if (config==null) {        if (preFlightRequest) {            rejectRequest(serverResponse);            return false;        }        else {            return true;        }    }    return handleInternal(serverRequest, serverResponse, config, preFlightRequest);}

前面项目中有自定义 Filter 来处理跨域问题,而设置了对应的跨域响应头。在 ServletServerHttpResponse 类 的构造方法里,会根据 servlet 版本实例化不同的 headers。

public ServletServerHttpResponse(HttpServletResponse servletResponse) {        Assert.notNull(servletResponse, "HttpServletResponse must not be null");        this.servletResponse=servletResponse;        this.headers=(servlet3Present ? new ServletResponseHttpHeaders() : new HttpHeaders());    }

ServletResponseHttpHeaders 与 HttpHeaders 的区别是?

ServletResponseHttpHeaders 是 HttpHeaders 的子类ServletResponseHttpHeaders 在获取响应头时,会先从当前响应中获取,也会从由外部传入的 header Map 中获取在实例化 ServletServerHttpResponse 类时,并没有传入 header ,所以在 servlet3 以下版本下,获取不到 Access-Control-Allow-Origin 响应头,没有跳过 Cors 请求处理
//ServletResponseHttpHeaders.get方法@Overridepublic List<String> get(Object key) {    Assert.isInstanceOf(String.class, key, "Key must be a String-based header name");    //从当前响应中获取响应头    Collection<String> values1=servletResponse.getHeaders((String) key);    boolean isEmpty1=CollectionUtils.isEmpty(values1);    //再调用父类HttpHeaders.get方法获取响应头    List<String> values2=super.get(key);    boolean isEmpty2=CollectionUtils.isEmpty(values2);    if (isEmpty1 && isEmpty2) {        return null;    }    List<String> values=new ArrayList<String>();    if (!isEmpty1) {        values.addAll(values1);    }    if (!isEmpty2) {        values.addAll(values2);    }    return values;}
三、总结在设置 Access-Control-Allow-Origin 时,要注意验证请求域名合法问题平常要注意与正式环境配置一置,在小公司很多问题都没有意识到虽然这次的问题很简单,但是要多问为什么? 多研究一下,才能提升自己

相关实践代码

https://github.com/h-dj/Spring-Learning/tree/master/spring-cors参考跨域资源共享 CORS 详解 阮一峰https://developer.mozilla.org/zh-TW/docs/Web/HTTP/CORShttp://nginx.org/en/docs/http/ngx_http_core_module.html#underscores_in_headershttps://www.ruanyifeng.com/blog/2019/09/cookie-samesite.htmlhttps://developer.mozilla.org/zh-CN/docs/Web/HTTP/Headers/Set-Cookie/SameSitehttps://github.com/spring-projects/spring-framework/issues/13916https://www.cnblogs.com/wxw16/p/10674539.htmlSpring4.2 开始支持 CORS 跨域https://docs.spring.io/spring-framework/docs/4.2.0.RELEASE/spring-framework-reference/html/new-in-4.2.html#_web_improvements_2https://docs.spring.io/spring-framework/docs/4.2.0.RELEASE/spring-framework-reference/html/cors.htmlTomcathttps://archive.apache.org/dist/tomcat/tomcat-6/v6.0.48/RELEASE-NOTEShttps://archive.apache.org/dist/tomcat/tomcat-8/v8.0.48/RELEASE-NOTES

SpringBoot 跨域问题

一、同源策略

同源策略是由Netscape提出的一个著名的安全策略,它是浏览器最核心也最基本的安全功能,现在所有支持JavaScript的浏览器都会使用这个策略。所谓同源是指协议、域名以及端口要相同。同源策略是基于安全方面的考虑提出来的,这个策略本身没问题,但是我们在实际开发中,由于各种原因又经常有跨域的需求,传统的跨域方案是JSONP,JSONP虽然能解决跨域但是有一个很大的局限性,那就是只支持GET请求,不支持其他类型的请求,而今天我们说的CORS(跨域源资源共享)(CORS,Cross-origin resource sharing)是一个W3C标准,它是一份浏览器技术的规范,提供了Web服务从不同网域传来沙盒脚本的方法,以避开浏览器的同源策略,这是JSONP模式的现代版。

在Spring框架中,对于CORS也提供了相应的解决方案。

非同源限制

无法读取非同源网页的 Cookie、LocalStorage 和 IndexedDB无法接触非同源网页的 DOM无法向非同源地址发送 AJAX 请求

二、java后端实现CORS跨域请求的方式

返回新的CorsFilter重写 WebMvcConfigurer使用注解 @CrossOrigin手动设置响应头 (HttpServletResponse)自定web filter 实现跨域

注意:

CorFilter / WebMvConfigurer / @CrossOrigin 需要 SpringMVC 4.2以上版本才支持,对应springBoot 1.3版本以上上面前两种方式属于全局 CORS 配置,后两种属于局部 CORS配置。如果使用了局部跨域是会覆盖全局跨域的规则,所以可以通过 @CrossOrigin 注解来进行细粒度更高的跨域资源控制。其实无论哪种方案,最终目的都是修改响应头,向响应头中添加浏览器所要求的数据,进而实现跨域

1.返回新的 CorsFilter(全局跨域)

在任意配置类,返回一个 新的 CorsFIlter Bean ,并添加映射路径和具体的CORS配置路径。

 @Configuration public class GlobalCorsConfig {     @Bean     public CorsFilter corsFilter() {         //1. 添加 CORS配置信息         CorsConfiguration config=new CorsConfiguration();         //放行哪些原始域         config.addAllowedOrigin("*");         //是否发送 Cookie         config.setAllowCredentials(true);         //放行哪些请求方式         config.addAllowedMethod("*");         //放行哪些原始请求头部信息         config.addAllowedHeader("*");         //暴露哪些头部信息         config.addExposedHeader("*");         //2. 添加映射路径         UrlBasedCorsConfigurationSource corsConfigurationSource=new UrlBasedCorsConfigurationSource();         corsConfigurationSource.registerCorsConfiguration("/**",config);         //3. 返回新的CorsFilter         return new CorsFilter(corsConfigurationSource);     } }

2. 重写 WebMvcConfigurer(全局跨域)

 @Configuration public class CorsConfig implements WebMvcConfigurer {     @Override     public void addCorsMappings(CorsRegistry registry) {         registry.addMapping("/**")               //是否发送Cookie               .allowCredentials(true)               //放行哪些原始域               .allowedOrigins("*")               .allowedMethods(new String[]{"GET", "POST", "PUT", "DELETE"})               .allowedHeaders("*")               .exposedHeaders("*");     } }

3. 使用注解 (局部跨域)

在控制器(类上)上使用注解 @CrossOrigin:,表示该类的所有方法允许跨域。

 @RestController @CrossOrigin(origins="*") public class HelloController {     @RequestMapping("/hello")     public String hello() {         return "hello world";     } }

在方法上使用注解 @CrossOrigin:

 @RequestMapping("/hello") @CrossOrigin(origins="*") //@CrossOrigin(value="http://localhost:8081") //指定具体ip允许跨域 public String hello() {     return "hello world"; }

4. 手动设置响应头(局部跨域)

用HttpServletResponse添加响应头Access-Control-Allow-Origin授权原始域,这里 Origin的值也可以设置为 “*”,表示全部放行。

 @RequestMapping("/index") public String index(HttpServletResponse response) {     response.addHeader("Access-Allow-Control-Origin","*");     return "index"; }

5. 使用自定义filter实现跨域

首先编写一个过滤器,可以起名字为MyCorsFilter.java

 @Component public class MyCorsFilter implements Filter {     public void doFilter(ServletRequest req, ServletResponse res, FilterChain chain) throws IOException, ServletException {         HttpServletResponse response=(HttpServletResponse) res;         response.setHeader("Access-Control-Allow-Origin", "*");         response.setHeader("Access-Control-Allow-Methods", "POST, GET, OPTIONS, DELETE");         response.setHeader("Access-Control-Max-Age", "3600");         response.setHeader("Access-Control-Allow-Headers", "x-requested-with,content-type");         chain.doFilter(req, res);     }     public void init(FilterConfig filterConfig) {}     public void destroy() {} }

在web.xml中配置这个过滤器,使其生效

 <!-- 跨域访问 START--> <filter>   <filter-name>CorsFilter</filter-name>   <filter-class>com.mesnac.aop.MyCorsFilter</filter-class> </filter> <filter-mapping>   <filter-name>CorsFilter</filter-name>   <url-pattern>/*</url-pattern> </filter-mapping> <!-- 跨域访问 END  -->

三、例子

首先创建两个普通的SpringBoot项目,第一个命名为provider提供服务,第二个命名为consumer消费服务,第一个配置端口为8080,第二个配置配置为8081,然后在provider上提供两个hello接口,一个get,一个post,如下:

 @RestController public class Provider {     @GetMapping("/hello")     public String hello() {         return "hello";     }      @PostMapping("/hello")     public String hello2() {         return "post hello";     } }

在consumer的resources/templates目录下创建一个html文件,发送一个简单的ajax请求,如下:

<!DOCTYPE html><html lang="en">  <head>    <meta charset="UTF-8">    <title>CORS</title>  </head>  <body>    <div id="app"></div>    <input type="button" onclick="btnClick()" value="get_button">    <input type="button" onclick="btnClick2()" value="post_button">    <script>        function btnClick() {            $.get('http://localhost:8080/hello', function (msg) {                $("#app").html(msg);            });        }        function btnClick2() {            $.post('http://localhost:8080/hello', function (msg) {                $("#app").html(msg);            });        }    </script>  </body></html>

然后分别启动两个项目,发送请求按钮,观察浏览器控制台如下:

Access to XMLHttpRequest at 'http://localhost:8080/hello' from origin 'http://localhost:8081' has been blocked by CORS policy: No 'Access-Control-Allow-Origin' header is present on the requested resource

可以看到,由于同源策略的限制,请求无法发送成功。

使用CORS可以在前端代码不做任何修改的情况下,实现跨域,那么接下来看看在provider中如何配置。首先可以通过@CrossOrigin注解配置某一个方法接受某一个域的请求,如下:

@RestControllerpublic class Provider {    @CrossOrigin(value="http://localhost:8081")    @GetMapping("/hello")    public String hello() {        return "hello";    }    @CrossOrigin(value="http://localhost:8081")    @PostMapping("/hello")    public String hello2() {        return "post hello";    }}

这个注解表示这两个接口接受来自http://localhost:8081地址的请求,配置完成后,重启provider,再次发送请求,浏览器控制台就不会报错了,consumer也能拿到数据了。

provider上,每一个方法上都去加注解未免太麻烦了,在Spring Boot中,还可以通过全局配置一次性解决这个问题,全局配置只需要在配置类中重写addCorsMappings方法即可,如下:

@Configurationpublic class WebMvcConfig implements WebMvcConfigurer {    @Override    public void addCorsMappings(CorsRegistry registry) {        registry.addMapping("/**")                .allowedOrigins("http://localhost:8081")                .allowedMethods("*")                .allowedHeaders("*");    }}

/**表示本应用的所有方法都会去处理跨域请求,allowedMethods表示允许通过的请求数,allowedHeaders则表示允许的请求头。经过这样的配置之后,就不必在每个方法上单独配置跨域了。

四、存在的问题

了解了整个CORS的工作过程之后,我们通过Ajax发送跨域请求,虽然用户体验提高了,但是也有潜在的威胁存在,常见的就是CSRF(Cross-site request forgery)跨站请求伪造。跨站请求伪造也被称为one-click attack 或者 session riding,通常缩写为CSRF或者XSRF,是一种挟制用户在当前已登录的Web应用程序上执行非本意的操作的攻击方法,举个例子:

假如一家银行用于运行转账操作的URL地址如下:http://icbc.com/aa?bb=cc,那么,一个恶意攻击者可以在另一个网站上放置如下代码:<img src="http://icbc.com/aa?bb=cc">,如果用户访问了恶意站点,而她之前刚访问过银行不久,登录信息尚未过期,那么她就会遭受损失。

基于此,浏览器在实际操作中,会对请求进行分类,分为简单请求,预先请求,带凭证的请求等,预先请求会首先发送一个options探测请求,和浏览器进行协商是否接受请求。默认情况下跨域请求是不需要凭证的,但是服务端可以配置要求客户端提供凭证,这样就可以有效避免csrf攻击。

发表评论