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 请求源码如下所示:

    <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;
        }
    }
    

参考

最后编辑于
©著作权归作者所有,转载或内容合作请联系作者
  • 序言:七十年代末,一起剥皮案震惊了整个滨河市,随后出现的几起案子,更是在滨河造成了极大的恐慌,老刑警刘岩,带你破解...
    沈念sama阅读 217,542评论 6 504
  • 序言:滨河连续发生了三起死亡事件,死亡现场离奇诡异,居然都是意外死亡,警方通过查阅死者的电脑和手机,发现死者居然都...
    沈念sama阅读 92,822评论 3 394
  • 文/潘晓璐 我一进店门,熙熙楼的掌柜王于贵愁眉苦脸地迎上来,“玉大人,你说我怎么就摊上这事。” “怎么了?”我有些...
    开封第一讲书人阅读 163,912评论 0 354
  • 文/不坏的土叔 我叫张陵,是天一观的道长。 经常有香客问我,道长,这世上最难降的妖魔是什么? 我笑而不...
    开封第一讲书人阅读 58,449评论 1 293
  • 正文 为了忘掉前任,我火速办了婚礼,结果婚礼上,老公的妹妹穿的比我还像新娘。我一直安慰自己,他们只是感情好,可当我...
    茶点故事阅读 67,500评论 6 392
  • 文/花漫 我一把揭开白布。 她就那样静静地躺着,像睡着了一般。 火红的嫁衣衬着肌肤如雪。 梳的纹丝不乱的头发上,一...
    开封第一讲书人阅读 51,370评论 1 302
  • 那天,我揣着相机与录音,去河边找鬼。 笑死,一个胖子当着我的面吹牛,可吹牛的内容都是我干的。 我是一名探鬼主播,决...
    沈念sama阅读 40,193评论 3 418
  • 文/苍兰香墨 我猛地睁开眼,长吁一口气:“原来是场噩梦啊……” “哼!你这毒妇竟也来了?” 一声冷哼从身侧响起,我...
    开封第一讲书人阅读 39,074评论 0 276
  • 序言:老挝万荣一对情侣失踪,失踪者是张志新(化名)和其女友刘颖,没想到半个月后,有当地人在树林里发现了一具尸体,经...
    沈念sama阅读 45,505评论 1 314
  • 正文 独居荒郊野岭守林人离奇死亡,尸身上长有42处带血的脓包…… 初始之章·张勋 以下内容为张勋视角 年9月15日...
    茶点故事阅读 37,722评论 3 335
  • 正文 我和宋清朗相恋三年,在试婚纱的时候发现自己被绿了。 大学时的朋友给我发了我未婚夫和他白月光在一起吃饭的照片。...
    茶点故事阅读 39,841评论 1 348
  • 序言:一个原本活蹦乱跳的男人离奇死亡,死状恐怖,灵堂内的尸体忽然破棺而出,到底是诈尸还是另有隐情,我是刑警宁泽,带...
    沈念sama阅读 35,569评论 5 345
  • 正文 年R本政府宣布,位于F岛的核电站,受9级特大地震影响,放射性物质发生泄漏。R本人自食恶果不足惜,却给世界环境...
    茶点故事阅读 41,168评论 3 328
  • 文/蒙蒙 一、第九天 我趴在偏房一处隐蔽的房顶上张望。 院中可真热闹,春花似锦、人声如沸。这庄子的主人今日做“春日...
    开封第一讲书人阅读 31,783评论 0 22
  • 文/苍兰香墨 我抬头看了看天上的太阳。三九已至,却和暖如春,着一层夹袄步出监牢的瞬间,已是汗流浃背。 一阵脚步声响...
    开封第一讲书人阅读 32,918评论 1 269
  • 我被黑心中介骗来泰国打工, 没想到刚下飞机就差点儿被人妖公主榨干…… 1. 我叫王不留,地道东北人。 一个月前我还...
    沈念sama阅读 47,962评论 2 370
  • 正文 我出身青楼,却偏偏与公主长得像,于是被迫代替她去往敌国和亲。 传闻我的和亲对象是个残疾皇子,可洞房花烛夜当晚...
    茶点故事阅读 44,781评论 2 354