1.为什么会出现跨域?

当访问资源与目标资源的协议、域名或端口三者其一出现不一致时就会出现跨域问题。而源(origin)由协议、域名、端口3者组成,url由协议、域名、端口和路径组成,如果访问资源url与目标资源url的协议、域名、端口全部相同时,则表示同源,否则就会出现跨域问题。同源策略(Same origin policy)是一种约定,它是浏览器最核心也最基本的安全功能,如果缺少了同源策略,则浏览器的正常功能可能都会受到影响。可以说Web是构建在同源策略基础之上的,浏览器只是针对同源策略的一种实现。简单来说因为浏览器的安全机制,对于访问非同源的资源时就会出现跨域问题。同源策略分为以下两种:

  • DOM同源策略:禁止对不同源页面DOM 进行操作。这里主要场景是iframe跨域的情况,不同域名的iframe是限制互相访问的。
  • XMLHttpRequest同源策略:禁止使用XHR对象向不同源的服务器地址发起HTTP请求。

假设目标url是https://www.baidu.com/index.html与下面的访问url进行跨域对比:

访问URL 是否跨域 原因
https://www.baidu.com/more/index.html 协议、域名、端口都相同
https://map.baidu.com/ 域名跟目标域名不同
http://www.baidu.com/index.html 协议不同,这里访问URL协议是http,目标URL协议是https
https://www.baidu.com:81/index.html 端口号不同

那么如何解决跨域呢?解决跨域的方式有很多种,前端可以在开发环境中开启本地代理解决跨域,但解决跨域大多数是后端的事情,后端解决跨域最常见的方式是通过nginx进行请求代理转发,下面介绍SpringBoot解决跨域的三种方式。

2.CORS介绍

上面说了因为浏览器的同源策略,访问非同源的资源会造成跨域,而CORS就是解决跨域最常见的一种方式。
CORS是一个W3C标准,全称是”跨域资源共享”(Cross-origin resource sharing)。它允许浏览器向跨源服务器,发出XMLHttpRequest请求,从而克服了AJAX只能同源使用的限制。

CORS需要浏览器和服务器同时支持。目前,所有浏览器都支持该功能,IE浏览器不能低于IE10。
整个CORS通信过程,都是浏览器自动完成,不需要用户参与。对于开发者来说,CORS通信与同源的AJAX通信没有差别,代码完全一样。浏览器一旦发现AJAX请求跨源,就会自动添加一些附加的头信息,有时还会多出一次附加的请求,但用户不会有感觉。
因此,实现CORS通信的关键是服务器。只要服务器实现了CORS接口,就可以跨源通信。

浏览器将CORS请求分成两类:简单请求(simple request)和非简单请求(not-so-simple request)。
只要同时满足以下两大条件,就属于简单请求。
(1).请求方法是以下三种方法之一:
HEAD
GET
POST
(2).HTTP的头信息不超出以下几种字段:
Accept
Accept-Language
Content-Language
Last-Event-ID
Content-Type:只限于三个值application/x-www-form-urlencoded、multipart/form-data、text/plain

对于简单请求,浏览器直接发出CORS请求。具体来说,就是在头信息之中,增加一个Origin字段。

非简单请求是那种对服务器有特殊要求的请求,比如请求方法是PUT或DELETE,或者Content-Type字段的类型是application/json。
非简单请求的CORS请求,会在正式通信之前,增加一次HTTP查询请求,称为”预检”请求(preflight)。
浏览器先询问服务器,当前网页所在的域名是否在服务器的许可名单之中,以及可以使用哪些HTTP动词和头信息字段。只有得到肯定答复,浏览器才会发出正式的XMLHttpRequest请求,否则就报错。

详解响应头:

  • Access-Control-Allow-Origin 该字段必填。它的值要么是请求时Origin字段的具体值,要么是一个*,表示接受任意域名的请求。
  • Access-Control-Allow-Methods 该字段必填。它的值是逗号分隔的一个具体的字符串或者*,表明服务器支持的所有跨域请求的方法。注意,返回的是所有支持的方法,而不单是浏览器请求的那个方法。这是为了避免多次”预检”请求。
  • Access-Control-Expose-Headers 该字段可选。CORS请求时,XMLHttpRequest对象的getResponseHeader()方法只能拿到6个基本字段:Cache-Control、Content-Language、Content-Type、Expires、Last-Modified、Pragma。如果想拿到其他字段,就必须在Access-Control-Expose-Headers里面指定。
  • Access-Control-Allow-Credentials 该字段可选。它的值是一个布尔值,表示是否允许发送Cookie.默认情况下,不发生Cookie,即:false。对服务器有特殊要求的请求,比如请求方法是PUT或DELETE,或者Content-Type字段的类型是application/json,这个值只能设为true。如果服务器不要浏览器发送Cookie,删除该字段即可。
  • Access-Control-Max-Age 该字段可选,用来指定本次预检请求的有效期,单位为秒。在有效期间,不用发出另一条预检请求。

    3.SpringBoot解决跨域

    3.1 实现WebMvcConfigurer接口重写addCorsMappings方法

    此种方式是全局配置的,除了实现WebMvcConfigurer接口外,还有一种基于Spring旧版本的方式,比如实现WebMvcConfigurerAdapter接口重写addCorsMappings方法,但WebMvcConfigurerAdapter接口在Spring已经被标记为Deprecated(已弃用)。推荐使用实现WebMvcConfigurer接口这种方式。 ```java package com.fly.config;

import org.springframework.context.annotation.Configuration; import org.springframework.web.servlet.config.annotation.CorsRegistry; import org.springframework.web.servlet.config.annotation.WebMvcConfigurer;

@Configuration public class CorsConfig implements WebMvcConfigurer { @Override public void addCorsMappings(CorsRegistry registry) { /**

  1. * addMapping:用于配置可以被跨域的路径,可以任意配置,可以具体到直接请求路径。
  2. * /**表示允许所有路径跨域。
  3. *
  4. * allowedMethods:允许所有的请求方法访问该跨域资源服务器,如:POST、GET、PUT、DELETE等。
  5. *
  6. * allowCredentials:是否允许请求携带证书
  7. *
  8. * allowedOrigins:允许所有的请求域名访问的跨域资源,可以固定单条或者多条内容,
  9. * 如:“http://www.aaa.com”,只有该域名可以访问跨域资源。
  10. *
  11. *
  12. * allowedHeaders:允许请求header的信息,*表示允许所有的请求header访问。
  13. * 可以自定义设置任意请求头信息。
  14. *
  15. * maxAge:设置再次发起预检请求的过期时间
  16. */
  17. registry.addMapping("/**")
  18. .allowedOrigins("*")
  19. .allowedMethods("GET","POST","HEAD","PUT","DELETE","OPTIONS")
  20. .allowCredentials(true)
  21. .maxAge(3600)
  22. .allowedHeaders("*");
  23. }

}

<a name="l6kMC"></a>
#### 3.2 基于Filter接口解决跨域
```java
package com.fly.config;

import org.springframework.context.annotation.Configuration;
import org.springframework.web.servlet.config.annotation.CorsRegistry;
import org.springframework.web.servlet.config.annotation.WebMvcConfigurer;

import javax.servlet.*;
import javax.servlet.annotation.WebFilter;
import javax.servlet.http.HttpServletResponse;
import java.io.IOException;

@WebFilter(filterName = "CorsFilter")
@Configuration
public class CorsConfig implements Filter {

    @Override
    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-Credentials","true");
        response.setHeader("Access-Control-Allow-Methods","POST,GET,DELETE,PUT");
        response.setHeader("Access-Control-Max-Age","3600");
        response.setHeader("Access-Control-Allow-Headers","Origin,X-Requested-With,Content-Type,Accept");
        /**
         * chain表示当前Filter链对象,doFilter方法用于通知Web容器把请求交给Filter链中的下一个           * Filter去处理,如果当前调用此方法的 Filter 对象是Filter 链中的最后一个 Filter,
         * 那么将把请求交给目标 Servlet 程序去处理。
         */
        chain.doFilter(req, res);    
    }
}

3.3 基于@CrossOrigin注解解决跨域

在控制器上或控制器的方法加上@CrossOrigin并指定允许跨域的请求即可。@CrossOrigin如果标识在类上,那么它会作用于该类的每一个方法,@CrossOrigin标识在方法上仅会作用于当前方法。使用@CrossOrigin注解时在每个控制器方法都应明确请求类型。

@CrossOrigin用在类上的示例:

package com.fly.controller;
import lombok.AllArgsConstructor;
import lombok.Data;
import lombok.NoArgsConstructor;
import org.springframework.web.bind.annotation.*;
import org.springframework.web.multipart.MultipartFile;

import java.io.File;

@CrossOrigin(origins = "http://localhost:3000",allowedHeaders = "*",allowCredentials = "true",
        maxAge = 3600,methods = {
        RequestMethod.POST,
        RequestMethod.DELETE,
        RequestMethod.HEAD,
        RequestMethod.PUT,
        RequestMethod.PATCH})
@RestController
public class UploadController {
    // 上传文件存放目录
    public final static String UPLOAD_DIR=System.getProperty("user.dir")+"/upload";

    @Data
    @AllArgsConstructor
    @NoArgsConstructor
    public static class Result{
        private Integer code;
        private String msg;
        private Object data;
        static Result success(String message,Object data){
            return new Result(200,message,data);
        }
        static Result error(String message){
            return new Result(201,message,null);
        }
    }

    @PostMapping("/upload/single")
    public Object uploadSingleFile(@RequestParam("file") MultipartFile file){
        if(file.isEmpty()){
            return Result.error("请选择文件");
        }
        String fileName=file.getOriginalFilename();
        File file1=new File(UPLOAD_DIR+"/"+fileName);
        try {
            // 转移文件到file1进行存储
            file.transferTo(file1);
            return Result.success("文件上传成功","文件地址:"+file1.getPath());
        }catch (Exception e){
            e.printStackTrace();
        }
        return Result.error("文件上传失败");
    }
}

@CrossOrigin用在方法上的示例:

public class GoodsController{

    @CrossOrigin(origins = "http://localhost:8080")
    @RequestMapping(method =RequestMethod.GET,value="/queryGoodsList")
    public Response queryGoodsList(){
        return null;    
    }
}

常用第一、二种方式,第三方粒度更加细,一般不用。