Springboot-跨域

93 阅读9分钟

同源策略

1995年,同源政策由 Netscape 公司引入浏览器。目前,所有浏览器都实行这个政策

最初,它的含义是指,A 网页设置的 Cookie,B 网页不能打开,除非这两个网页“同源”。所谓“同源”指的是“三个相同”:

  • 协议相同
  • 域名相同
  • 端口相同

同源政策的目的:是为了保证用户的信息安全,防止恶意的网站窃取数据

设想这样一种情况:

A 网站是一家银行网站,用户登录以后,A 网站在用户的机器上设置了一个 Cookie,包含了一些隐私信息(比如存款总额)。用户离开 A 网站以后,又去访问 B 网站,如果没有同源限制,B 网站可以读取 A 网站的 Cookie,那么隐私信息就会泄漏。更可怕的是,Cookie 往往用来保存用户的登录状态,如果用户没有退出登录,其他网站就可以冒充用户,为所欲为。因为浏览器同时还规定,提交表单不受同源政策的限制

浏览器允许通过表单提交来实现跨域请求,即使目标地址与当前页面的域名不同,也可以正常进行表单提交。这是因为表单提交是由浏览器直接发起的,而非通过 javaScript 或 ajax 等方式。需要注意的是,跨域表单提交仅适用于普通的 HTML 表单提交,对于使用 ajax 或 XMLHttpRequest 发起的异步请求,仍然受到同源策略的限制。为了安全考虑,服务器端应该对接收的表单数据进行合法性验证和过滤,以防止恶意提交和攻击

由此可见,同源政策是必需的,否则 Cookie 可以共享,互联网就毫无安全可言了。

随着互联网的发展,同源政策越来越严格。目前,如果非同源,共有三种行为受到限制

  1. 无法获取非同源网页的 cookie、localstorage 和 indexedDB
  2. 无法访问非同源网页的 DOM (iframe)
  3. 无法向非同源地址发送 AJAX 请求 或 fetch 请求(可以发送,但浏览器拒绝接受响应)

Ajax跨域

浏览器的同源策略会导致跨域,也就是说,如果协议、域名或者端口任意有一个不同,都被当作是不同的域,就不能使用 Ajax 向不同源的服务器发送 HTTP 请求。首先我们要明确一个问题,请求跨域了,请求到底发出去没有?答案是肯定发出去了,但是浏览器拦截了响应。

跨域的意义

Ajax 的同源策略主要是为了防止 CSRF(跨站请求伪造) 攻击,如果没有 AJAX 同源策略,相当危险,我们发起的每一次 HTTP 请求都会带上请求地址对应的 cookie,那么可以做如下攻击行为

  1. 用户登录了自己的银行页面 mybank.commybank.com向用户的cookie中添加用户标识。
  1. 用户浏览了恶意页面 evil.com,执行了页面中的恶意AJAX请求代码。
  1. evil.commybank.com发起AJAX HTTP请求,请求会默认把mybank.com对应cookie也同时发送过去。
  1. 银行页面从发送的cookie中提取用户标识,验证用户无误,response中返回请求数据。此时数据就泄露了。
  1. 而且由于Ajax在后台执行,用户无法感知这一过程。

DOM同源策略也一样,如果 iframe 之间可以跨域访问,可以这样攻击:

  1. 做一个假网站,里面用iframe嵌套一个银行网站 mybank.com
  2. 把iframe宽高啥的调整到页面全部,这样用户进来除了域名,别的部分和银行的网站没有任何差别。
  3. 这时如果用户输入账号密码,我们的主网站可以跨域访问到mybank.com的dom节点,就可以拿到用户的输入了,那么就完成了一次攻击。

所以说有了跨域跨域限制之后,我们才能更安全的上网了

跨域的解决方式

CORS

CORS 是一个 W3C 标准,全称是跨域资源共享(Cross-origin resource sharing),它允许浏览器向跨源服务器,发出XMLHttpRequest请求。

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

服务器端配置

CORS常用的配置项有以下几个:

  • Access-Control-Allow-Origin(必含) – 允许的域名,只能填 *(通配符)或者单域名。
  • Access-Control-Allow-Methods(必含) – 这允许跨域请求的 http 方法(常见有 POST、GET、OPTIONS)。
  • Access-Control-Allow-Headers(当预请求中包含 Access-Control-Request-Headers 时必须包含) – 这是对预请求当中 Access-Control-Request-Headers 的回复,和上面一样是以逗号分隔的列表,可以返回所有支持的头部。
  • Access-Control-Allow-Credentials(可选) – 表示是否允许发送Cookie,只有一个可选值:true(必为小写)。如果不包含cookies,请略去该项,而不是填写false。
  • Access-Control-Max-Age(可选) – 以秒为单位的缓存时间。在有效时间内,浏览器无须为同一请求再次发起预检请求。
CORS跨域的判定流程
  1. 浏览器先根据同源策略对前端页面和后台交互地址做匹配,若同源,则直接发送数据请求;若不同源,则发送跨域请求。

  2. 服务器收到浏览器跨域请求后,根据自身配置返回对应文件头。若未配置过任何允许跨域,则文件头里不包含 Access-Control-Allow-origin 字段,若配置过域名,则返回 Access-Control-Allow-origin + 对应配置规则里的域名的方式

  3. 浏览器根据接受到的 响应头里的 Access-Control-Allow-origin 字段做匹配,若无该字段,说明不允许跨域,从而抛出一个错误;若有该字段,则对字段内容和当前域名做比对,如果同源,则说明可以跨域,浏览器接受该响应;若不同源,则说明该域名不可跨域,浏览器不接受该响应,并抛出一个错误

    服务器配置CORS

    HTTP/1.1 200
    Vary: Origin
    Vary: Access-Control-Request-Method
    Vary: Access-Control-Request-Headers
    Access-Control-Allow-Origin: http://localhost:63342
    Access-Control-Allow-Credentials: true
    Content-Type: application/json
    Transfer-Encoding: chunked
    Date: Sat, 09 Sep 2023 03:03:03 GMT
    Keep-Alive: timeout=60
    Connection: keep-alive
    

    服务器未配置CORS

    HTTP/1.1 200
    Content-Type: application/json
    Transfer-Encoding: chunked
    Date: Sat, 09 Sep 2023 02:58:42 GMT
    Keep-Alive: timeout=60
    Connection: keep-alive
    

    服务器配置CORS,但是不匹配

    HTTP/1.1 403
    Vary: Origin
    Vary: Access-Control-Request-Method
    Vary: Access-Control-Request-Headers
    Transfer-Encoding: chunked
    Date: Sat, 09 Sep 2023 03:06:16 GMT
    Keep-Alive: timeout=60
    Connection: keep-alive
    
简单请求

实际上浏览器将CORS请求分成两类:简单请求(simple request)和非简单请求(not-so-simple request

简单请求是指满足以下条件的(一般只考虑前面两个条件即可):

  1. 使用 GET、POST、HEAD 其中一种请求方法

  2. HTTP的头信息不超出以下几种字段:

    • Accept
    • Accept-Language
    • Content-Language
    • Last-Event-ID
    • Content-Type:只限于三个值 application/x-www-form-urlencoded、multipart/form-data、text/plai
  3. 请求中的任意XMLHttpRequestUpload 对象均没有注册任何事件监听器;

  4. XMLHttpRequestUpload 对象可以使用 XMLHttpRequest.upload 属性访问。 请求中没有使用 ReadableStream 对象

对于简单请求,浏览器直接发起 CORS 请求,具体来说就是服务器端会根据请求头信息中的 origin 字段(包括了协议 + 域名 + 端口),来决定是否同意这次请求

如果 origin 指定的源在许可范围内,服务器返回的响应,会多出几个头信息字段:

HTTP/1.1 200
Vary: Origin
Vary: Access-Control-Request-Method
Vary: Access-Control-Request-Headers
Access-Control-Allow-Origin: http://localhost:63342
Access-Control-Allow-Credentials: true
Content-Type: application/json
Transfer-Encoding: chunked
Date: Sat, 09 Sep 2023 03:03:03 GMT
Keep-Alive: timeout=60
Connection: keep-alive
非简单请求

非简单请求时指那些对服务器有特殊要求的请求,比如请求方法是 putdelete,或者 content-type 的类型是 application/json。其实简单请求之外的都是非简单请求了。

非简单请求的 CORS 请求,会在正式通信之前,使用 OPTIONS 方法发起一个预检(preflight)请求到服务器,浏览器先询问服务器,当前网页所在的域名是否在服务器的许可名单之中,以及可以使用哪些 HTTP 动词和头信息字段。只有得到肯定答复,浏览器才会发出正式的 XMLHttpRequest 请求,否则就报错。

img

下面是一个预检请求的头部:

OPTIONS /test HTTP/1.1
Accept: */*
Accept-Encoding: gzip, deflate, br
Accept-Language: zh-CN,zh;q=0.9
Access-Control-Request-Headers: content-type
Access-Control-Request-Method: POST
Connection: keep-alive
Host: 127.0.0.1:9999
Origin: http://localhost:63342
Referer: http://localhost:63342/
Sec-Fetch-Dest: empty
Sec-Fetch-Mode: cors
Sec-Fetch-Site: cross-site
User-Agent: Mozilla/5.0 (Macintosh; Intel Mac OS X 10_15_7) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/115.0.0.0 Safari/537.36

一旦服务器通过了"预检"请求,以后每次浏览器正常的CORS请求,就都跟简单请求一样了。

image-20230909144300481

关于为什么要有简单请求和非简单请求,可参考知乎上的一个回答 为什么跨域的post请求区分为简单请求和非简单请求和content-type相关?

处理跨域

Filter
package com.smile.wechat.application.filters;
​
import org.springframework.stereotype.Component;
​
import javax.servlet.Filter;
import javax.servlet.FilterChain;
import javax.servlet.ServletException;
import javax.servlet.ServletRequest;
import javax.servlet.ServletResponse;
import javax.servlet.annotation.WebFilter;
import javax.servlet.http.HttpServletResponse;
import java.io.IOException;
​
/**
 * @author: smile
 * @title:
 * @projectName:
 * @description: TODO
 * @date: 2023/9/9 2:55 下午
 */
@Component
@WebFilter(urlPatterns = {"/*"}, filterName = "corsFilter")
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, PUT, GET, OPTIONS, DELETE");
​
        // 设置过期时间会
        response.setHeader("Access-Control-Max-Age", "86400");
        response.setHeader("Access-Control-Allow-Headers", "Origin, X-Requested-With, Content-Type, Accept, Authorization");
        // 支持 HTTP 1.1
        response.setHeader("Cache-Control", "no-cache, no-store, must-revalidate");
        // 支持 HTTP 1.0. response.setHeader("Expires", "0");
        response.setHeader("Pragma", "no-cache");
        // 编码
        response.setCharacterEncoding("UTF-8");
​
        filterChain.doFilter(servletRequest, servletResponse);
    }
}
@CrossOrigin 局部跨域
@CrossOrigin(origins = "*", allowedHeaders = "*", maxAge = 86400)
@PostMapping("/login")
public String login(@RequestBody User user) {
  TODO..
}
实现WebMvcConfigurer接口
package com.smile.wechat.application.config;
​
import org.springframework.context.annotation.Configuration;
import org.springframework.web.servlet.config.annotation.CorsRegistry;
import org.springframework.web.servlet.config.annotation.WebMvcConfigurer;
​
/**
 * @author: smile
 * @title: 跨域配置
 * @projectName: wechat
 * @description: 跨域配置
 * @date: 2023/9/6 4:44 下午
 */
@Configuration
public class CorsConfiguration implements WebMvcConfigurer {
    @Override
    public void addCorsMappings(CorsRegistry registry) {
        // 设置允许跨域的路径
        registry.addMapping("/**")
                // 设置允许跨域请求的域名
                .allowedOriginPatterns("*")
                // 是否允许证书 不再默认开启
                .allowCredentials(true)
                // 设置允许的方法
                .allowedMethods("*")
                // 设置允许的头
                .allowedHeaders("*")
                // 跨域允许时间
                .maxAge(1);
    }
}
注入 CorsFilter 过滤器
@Configuration
public class CorsFilterConfiguration {
​
    @Bean
    public CorsFilter corsFilter() {
        // 创建 CorsConfiguration 对象后添加配置
        CorsConfiguration corsConfiguration = new CorsConfiguration();
        // 设置放行哪些原始域
        corsConfiguration.addAllowedOrigin("*");
        // 放行哪些原始请求头部信息
        corsConfiguration.addAllowedHeader("*");
        // 放行哪些请求方法
        corsConfiguration.addAllowedMethod("*");
​
        // 添加映射路径
        UrlBasedCorsConfigurationSource source = new UrlBasedCorsConfigurationSource();
        source.registerCorsConfiguration("/**", corsConfiguration);
​
        return new CorsFilter(source);
    }
​
}

参考

juejin.cn/post/684490…

juejin.cn/post/705375…

juejin.cn/post/709416…

juejin.cn/post/685041…