美文网首页
Spring Boot - 跨域资源共享(CORS)

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

作者: Whyn | 来源:发表于2020-08-18 02:35 被阅读0次

    同源策略

    在浏览器中,如果我们直接使用 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 请求源码如下所示:

      <html lang="en">
        <!-- ... -->
        <body>
          <button id="cors_post">CORS - POST</button>
          <button id="cors_delete">CORS - DELETE</button>
      
          <script>
            const BASE_URL = 'http://localhost:8080/cors/';
            const postBtn = document.querySelector('#cors_post');
            postBtn.addEventListener('click', async () => {
              // 简单请求
              const response = await fetch(BASE_URL, {
                method: 'POST',
                headers: {
                  'Content-Type': 'application/x-www-form-urlencoded;charset=UTF-8',
                },
                body: 'name=Whyn',
              });
              response.text().then((text) => console.log(text));
            });
      
            const delBtn = document.querySelector('#cors_delete');
            delBtn.addEventListener('click', async () => {
              // 非简单请求
              const response = await fetch(BASE_URL + '1', {
                method: 'DELETE',
              });
              response.text().then((text) => console.log(text));
            });
          </script>
        </body>
      </html>
      

      :前端页面运行在本地: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;
          }
      }
      

    参考

    相关文章

      网友评论

          本文标题:Spring Boot - 跨域资源共享(CORS)

          本文链接:https://www.haomeiwen.com/subject/ksvcjktx.html