Spring Boot - 跨域资源共享(CORS)

同源策略

在浏览器中,如果我们直接使用 AJAX 发送一个对其他网站的请求(跨域请求),默认情况下是无法获取到响应的。
这是因为浏览器内置的 同源策略 对客户端脚本的限制。

默认情况下,同源策略 只允许脚本请求同源资源,而对于请求不同源的脚本在没有明确授权的情况下,无法读取对方资源。

同源 指的是:协议域名端口 三者都相同

同源策略是浏览器内置的一个最核心,也是最基础的安全功能,它保障了用户的上网安全。

但是,如果我们确信某个非同源网站是安全的,我们希望能够对其资源进行访问,那么,就需要通过相应的机制进行跨域请求。

最常见的前端跨域请求解决方案是 JSONP,它的原理是借助script标签不受浏览器同源策略限制,允许跨域请求资源,因此可以通过script标签的src属性,进行跨域访问。如下代码所示:

// 1. 前端定义一个 回调函数 handleResponse 用来接收后端返回的数据
function handleResponse(data) {
    console.log(data);
};

// 2. 动态创建一个 script 标签,并且告诉后端回调函数名叫 handleResponse
var body = document.getElementsByTagName('body')[0];
var script = document.gerElement('script');
script.src = 'http://www.laixiangran.cn/json?callback=handleResponse';
body.appendChild(script);

// 3. 通过 script.src 请求 `http://www.laixiangran.cn/json?callback=handleResponse`,
// 4. 后端能够识别这样的 URL 格式并处理该请求,然后返回 handleResponse({"name": "laixiangran"}) 给浏览器
// 5. 浏览器在接收到 handleResponse({"name": "laixiangran"}) 之后立即执行 ,也就是执行 handleResponse 方法,获得后端返回的数据,这样就完成一次跨域请求了。

虽然 JSONP 可以完成跨域请求,但是它只支持GET请求方式,限制非常大。
于是,为了更好地支持跨域资源请求,W3C 标准就发布了一套浏览器跨域资源共享标准:CORS(Cross-origin resource sharing,跨域资源共享)

CORS(跨域资源共享)

CORS 支持多种 HTTP 请求,它其实就是定义了一套跨域资源请求时,浏览器与服务器之间的交互方式。基本的原理就是通过自定义的 HTTP 请求头来传递信息,进行验证。

浏览器中,将 CORS 请求分为两种类型:

  • 简单请求:同时满足以下两大条件的请求,即为简单请求:

    1. 请求的方法是HEADGET或者是POST三种之一
    2. 请求头不超出以下几种字段:AcceptAccept-LanguageContent-LanguageLast-Event-IDContent-Type(其值为:application/x-www-form-urlencodedmultipart/form-datatext/plain三者之一)
  • 非简单请求:不是简单请求的都属于非简单请求。

浏览器对于 简单请求 和 非简单请求 的 CORS 处理机制不一样,具体如下:

  • 简单请求:对于简单请求的 CORS,浏览器的处理机制流程如下:

    1. 浏览器会在请求头添加一个额外的Origin头部,其值为当前请求页面的源信息(即:协议 + 域名 + 端口)。如下所示:
    GET /cors HTTP/1.1
    Origin: http://api.bob.com
    Host: api.alice.com
    Accept-Language: en-US
    Connection: keep-alive
    User-Agent: Mozilla/5.0...
    
    1. 服务器接收到请求后,查看到Origin头部指定的源信息,如果同意该请求,就会为下发的响应添加头部Access-Control-Allow-Origin,其值为请求的源信息(或者是*,表示允许任意源信息)。如下所示:
    Access-Control-Allow-Origin: http://api.bob.com
    Access-Control-Allow-Credentials: true
    Access-Control-Expose-Headers: FooBar
    Content-Type: text/html; charset=utf-8
    
    1. 浏览器接收到响应后,会查看下是否有Access-Control-Allow-Origin头部信息,如果没有或者其值不匹配当前源信息,那么浏览器就会禁止响应该 CORS 请求,当前页面的 AJAX 请求的onerror函数会得到回调。
      反之,如果浏览器验证通过,则跨域请求成功。

    :CORS 请求默认不发送 Cookie 和 HTTP 认证信息,如果需要把 Cookie 发送给服务器,则 AJAX 和 服务器必须同时打开 Credentials 字段,如下所示:

    • 服务器需设置:Access-Control-Allow-Credentials: true
    • AJAX 需设置:new XMLHttpRequest().withCredentials = true;

    :如果 AJAX 发送了 Cookie,那么服务器的Access-Control-Allow-Origin则不能设置为*,必须指定该明确的、与请求网页一致的域名。

  • 非简单请求:非简单请求是那种对服务器有特殊要求的请求,比如PUTDELETE请求,或者是Content-type: application/json请求...
    浏览器检测到非简单请求的 CORS 时,在正式发送请求前,会先进行一次探测请求(preflight),通过才会发送正式请求,具体过程如下:

    1. 浏览器检测到非简单 CORS 请求,则先发送一个探测请求,请求方式为OPTIONS,如下所示:
    OPTIONS /cors HTTP/1.1
    Origin: http://api.bob.com
    Access-Control-Request-Method: PUT
    Access-Control-Request-Headers: X-Custom-Header
    Host: api.alice.com
    Accept-Language: en-US
    Connection: keep-alive
    User-Agent: Mozilla/5.0...
    

    可以看到OPTIONS请求,除了携带Origin请求头外,还额外携带了以下几个请求头:

    • Access-Control-Request-Method:该字段必须携带,表示 CORS 请求使用的 HTTP 请求方法
    • Access-Control-Request-Headers:可选字段,表示 CORS 请求发送的自定义头部信息,多个头部以逗号进行分隔
    1. 服务器收到浏览器发送的探测请求后,检测OriginAccess-Control-Request-MethodAccess-Control-Request-Headers都在自己的许可名单时,就会允许跨域请求,返回响应。如下所示:
    HTTP/1.1 200 OK
    Date: Mon, 01 Dec 2008 01:15:39 GMT
    Server: Apache/2.0.61 (Unix)
    Access-Control-Allow-Origin: http://api.bob.com
    Access-Control-Allow-Methods: GET, POST, PUT
    Access-Control-Allow-Headers: X-Custom-Header
    Access-Control-Max-Age: 1728000
    Content-Type: text/html; charset=utf-8
    Content-Encoding: gzip
    Content-Length: 0
    Keep-Alive: timeout=2, max=100
    Connection: Keep-Alive
    Content-Type: text/plain
    

    响应主要包含如下请求头信息:

    • Access-Control-Allow-Origin:表示允许进行跨域请求的域
    • Access-Control-Allow-Methods:必须字段,表示允许 CORS 请求的方法
    • Access-Control-Allow-Headers:表示允许 CORS 请求的头部
    • Access-Control-Max-Age:表示探测请求缓存时间(单位:秒)
    1. 一旦浏览器通过探测请求,以后每次进行 CORS 请求时,就重复简单请求步骤(直至探测请求缓存过期)。
      而如果探测请求通不过(即响应没有任何 CORS 相关的头部信息字段),浏览器就知道服务器会拒绝该 CORS 请求,于是就直接触发一个错误,回调给 AJAX 请求的onerror方法。

Spring Boot 配置支持 CORS

一个很幸运的事情就是:浏览器会自动帮我们完成 CORS 相关操作,用户完全无感知。
对于开发者来说,前端代码无需修改,如果是 CORS 请求,浏览器会自动帮我们加上相应的请求头进行请求...

因此,实现 CORS 通信需要配置的就只是服务器端。

下面介绍下在 Spring Boot 中配置 CORS 通信,主要介绍几种常用的配置方法,如下所示:

  • @CrossOrigin:该注解可用于方法和类上,注解在方法上,表示对该方法的请求进行 CORS 校验,注解在类上(即Controller上),表示该类内的方法都遵循该 CORS 校验。如下所示:

    :前端页面 AJAX 请求源码可查看 附录 内容。

    @Slf4j
    @RestController
    @RequestMapping("cors")
    @CrossOrigin(
            value = "http://127.0.0.1:5500",
            maxAge = 1800,
            allowedHeaders = "*")
    public class CorsController {
    
        @PostMapping("/")
        public String add(@RequestParam("name") String name,
                          @RequestHeader("Origin") String origin) {
            log.info("Request Header ==> Origin: " + origin);
            return "add successfully: " + name;
    
        }
    
        @DeleteMapping("/{id}")
        public String delete(@PathVariable("id") Long id) {
            return String.valueOf(id) + " deleted!";
        }
    }
    

    上述代码在Controller类上使用@CrossOrigin进行注解配置 CORS,这样前端页面就可以进行 CORS 请求当前Controller下的所有接口。

    其中,@CrossOrigin注解可选参数如下:

    方法 作用
    value 表示支持的域,即Access-Control-Allow-Origin的值
    origins 表示支持的域数组
    methods 表示支持的 CORS 请求方法,即Access-Control-Allow-Methods的值。
    其默认值与绑定的控制器方法一致
    maxAge 表示探测请求缓存时间(单位:秒),即Access-Control-Max-Age的值。
    其默认值为1800,也即 30 分钟
    allowedHeaders 表示允许的请求头,即Access-Control-Allow-Headers的值
    默认情况下,支持所有请求头
    exposedHeaders 表示下发其他响应头字段给浏览器,即Access-Control-Expose-Headers的值。
    默认不下发暴露字段
    allowCredentials 表示是否支持浏览器发送认证信息(比如 Cookie),即Access-Control-Allow-Credentials的值。
    默认不支持接收认证信息
  • 全局配置:如果想全局配置 CORS 通信,只需添加一个配置类。如下所示:

    @Configuration
    public class WebMvcConfig implements WebMvcConfigurer {
        @Override
        public void addCorsMappings(CorsRegistry registry) {
            registry.addMapping("/**")     //设置允许跨域的路径
                    .allowedOrigins("*")
                    .allowedMethods("*")
                    .allowedHeaders("*")
                    .maxAge(1800)
                    .allowCredentials(true);
        }
    }
    

    只需创建一个配置类实现接口WebMvcConfigurer,然后覆写方法addCorsMappings即可。
    addCorsMappings方法中,registry.addMapping用于设置可以进行跨域请求的路径,比如/cors/**表示路径/cors/下的所有路由都支持 CORS 请求。其他的设置与注解@CrossOrigin一样,无需介绍。

    :这里也可以直接通过注入一个WebMvcConfigurer的 Bean 实例,自定义跨域规则:

    @Bean
    public WebMvcConfigurer corsConfigurer() {
        return new WebMvcConfigurer() {
            @Override
            public void addCorsMappings(CorsRegistry registry) {
                registry.addMapping("/**")
                        .allowedOrigins("*")
                        .allowedMethods("GET", "PUT", "POST", "PATCH", "DELETE", "OPTIONS");
            }
        };
    }
    
  • 通过Filter配置:通过过滤器Filter可以让我们手动控制响应,自然就能完成 CORS 配置。如下所示:

    @Component
    public class CorsFilter implements Filter {
    
        @Override
        public void doFilter(ServletRequest servletRequest, ServletResponse servletResponse, FilterChain filterChain) throws IOException, ServletException {
            HttpServletResponse response = (HttpServletResponse) servletResponse;
            response.setHeader("Access-Control-Allow-Origin", "*");
            response.setHeader("Access-Control-Allow-Methods", "POST, GET, OPTIONS, DELETE, HEAD");
            response.setHeader("Access-Control-Max-Age", "3600");
            response.setHeader("Access-Control-Allow-Headers", "access-control-allow-origin, authority, content-type, version-info, X-Requested-With");
    
            filterChain.doFilter(servletRequest, servletResponse);
        }
    }
    

附录

  • CORS 前端页面 AJAX 请求源码如下所示:

    
      
      
        
        
    
        
      
    
    

    :前端页面运行在本地:http://127.0.0.1:5500

  • Spring Security 配置跨域:如果项目中使用了 Spring Security 框架,那么也可以直接配置 Spring Security 支持跨域即可:

    @Configuration
    public class SecurityConfiguration extends WebSecurityConfigurerAdapter {
    
        @Override
        protected void configure(HttpSecurity http) throws Exception {
            // 允许跨域资源请求
            // by default uses a Bean by the name of corsConfigurationSource
            http.cors(Customizer.withDefaults());
        }
    
        @Bean
        CorsConfigurationSource corsConfigurationSource() {
            CorsConfiguration configuration = new CorsConfiguration();
            configuration.setAllowedOrigins(Arrays.asList("*"));
            configuration.setAllowedMethods(Arrays.asList("GET","POST","OPTIONS"));
    
            UrlBasedCorsConfigurationSource source = new UrlBasedCorsConfigurationSource();
            // 所有 url 都使用 configuration 定制的跨域规则
            source.registerCorsConfiguration("/**", configuration);
            return source;
        }
    }
    

参考

  • 阮一峰 - 跨域资源共享 CORS 详解
  • 浏览器同源策略及跨域的解决方法
  • Spring Boot+Vue全栈开发实战
  • SpringBoot配置CORS解决跨域访问的几种实现方式

你可能感兴趣的:(Spring Boot - 跨域资源共享(CORS))