编程技术文章分享与教程

网站首页 > 技术文章 正文

从零搭建开发脚手架跨域请求原理、同源协议以及常见跨域请求方案

hmc789 2024-11-22 15:28:02 技术文章 2 ℃

原文链接:https://mp.weixin.qq.com/s/Z-PyTuCvFWiNVZBcKN7I6Q

原作者:Java大厂面试官

现在的开发架构大部分都是前后端分离架构,跨域请求也变成了常见的高频问题,这里分析下跨域请求原理,总结下常见的几种跨域解决方案。

什么是跨域、同源协议

说道跨域,先看下浏览器的同源协议(Same-Origin-Policy),它是浏览器的默认安全措施,一般一个网址通常由 protocoldomainport 三个部分所组成,根据SOP同源协议如果一个网址只要至少一个部分的不符合,便不能进入到先前进入的非同源地址,这种行为就是跨域

非同源限制

  • 无法读取非同源网页的 Cookie、LocalStorage 和 IndexedDB
  • 无法接触非同源网页的 DOM
  • 无法向非同源地址发送 AJAX 请求

如果没有浏览器的同源协议,那么web安全就乱套了,你可以随意攻击别人的网站。另外这个同源协议是防止跨站请求伪造(CSRF)攻击,具体可参考跨站请求伪造(CSRF)示例、原理及其防御措施

跨域请求是HTTP请求,其中请求的源和目标是不同的。例如,从浏览器访问一个域(前端服务https://www.site.com)其提供Web应用程序页面并且浏览器向另一个域(后端接口服务https://www.api.com)中的服务器发送AJAX请求数据。

下面是常见的跨域错误截图,从我们的网站http://localhost:3001http://www.google.com发出GET请求,Chrome浏览器发生的错误如下:

跨域场景总结

几种跨域场景总结如下:

只要协议、域名、端口有任何一个不同,就是跨域。

https://www.baidu.com/index.html进行跨域比较:

URL是否跨域原因https://www.baidu.com/more/index.html不跨域三要素相同https://map.baidu.com/跨域域名不同http://www.baidu.com/index.html跨域协议不同https://www.baidu.com:81/index.html跨域端口号不同

什么是跨域资源共享(CORS)

cors相关内容来自:https://www.baeldung.com/cs/cors-preflight-requests

CORS(Cross-origin resource sharing)是为了满足访问第三方API的需求,CORS策略确定了一个来源提供的脚本如何请求另一个来源上的资源,是W3C标准,是一种机制。

CORS定义了需要包含在请求/响应交互中的特定HTTP标头,允许服务器传达允许来自哪个来源的请求。然后,浏览器通过允许或阻止脚本访问响应来强制执行此操作。

简单来说就是为了解决跨域请求而制定的一种机制、标准。

当涉及跨域请求时,浏览器可以处理三种类型:

简单请求

符合简单请求的条件:

  • 请求类型:GET、POST或者HEAD
  • 请求头:仅发送自动用户代理标头或CORS安全列出的头,例如AcceptAccept-LanguageContent-LanguageContent-Type
  • Content-Type:只限于三个值application/x-www-form-urlencoded、multipart/form-data、text/plain

举例:在浏览器访问前端https://www.site.com后端接口服务https://www.api.com

  • 首先,浏览器将带有标识原始来源的origin标头的请求(origin: https://www.site.com)发送到https://www.api.com服务器
  • 服务器以请求的数据作为响应,并且还包含一个 设置为https://www.site.comaccess-control-allow-origin头,该响应头向浏览器指示服务器允许该来源的请求

access-control-allow-origin标头是主要的CORS头,一台服务器可以用它来显示什么允许的域。此标头的值可以是一个单一的来源,以告诉浏览器允许访问该特定来源,也可以是*,它指示浏览器允许任何来源。

access-control-allow-origin是CORS的重要响应头,如果服务器不响应此标头,或者标头值是与请求的来源不匹配的域origin != access-control-allow-origin),浏览器将阻止将响应传递回脚本。这可能会导致控制台发生如下错误。

非简单请求

任何不是简单请求的请求都将被视为非简单请求或预检请求。浏览器对这类请求的处理略有不同。在发送实际请求之前,浏览器将发送我们称为预检请求的内容,以与服务器进行检查,以确认是否允许这种类型的请求。预检请求是一个OPTIONS请求,其中包括以下标头:

  • origin –告诉服务器请求的来源
  • access-control-request-method –告诉服务器请求包含哪种HTTP方法
  • access-control-request-headers –告诉服务器请求包含哪些头

服务器可以通过响应以下标头来决定是否接受来自此来源的此类请求:

  • access-control-allow-origin –服务器允许的来源
  • access-control-allow-methods –服务器允许的方法的逗号分隔列表
  • access-control-allow-headers -服务器将允许的逗号分隔的头列表
  • access-control-max-age –告诉浏览器将对预检请求的响应缓存多长时间(以秒为单位)
  • MDN Web文档中列出了可能的CORS响应标头的完整列表。

与简单请求类似,如果服务器不包含任何CORS标头,则浏览器将假定该服务器不允许此请求,并且不会继续实际请求。

举例:添加一个自定义标头custom-header,变为非简单请求。

浏览器会将该请求标识为非简单请求,并将向服务器发起预检请求,以检查其是否允许该请求。让我们看一下如果https://www.api.com服务器允许这种请求,则这种交互的流程:

img

服务器以正确的头响应,浏览器继续发出实际请求。如果服务器响应时没有正确的标头,则浏览器将阻止该请求发出。

在这里,我们有一个浏览器示例,其中我试图通过一个包含自定义标头的非简单请求来访问Google Book API。我们可以在浏览器控制台中看到一个略有不同的错误,因为API并未使用所需的标头来响应预检请求:

img

凭证请求

凭据可以是cookie,授权标头或TLS客户端证书。默认情况下,除非两个请求都包括一个包含凭据的标记,并且服务器以将access-control-allow-credentials设置为true进行响应,否则CORS策略不允许在跨域请求中包含凭据

要将凭证包含在我们的请求中,让我们通过将withCredentials属性设置为true来更新XMLHttpRequest

const xhr = new XMLHttpRequest();
const url = 'https://www.api.com?q=test';
xhr.open('GET', url);
xhr.withCredentials = true;
xhr.send();

如果服务器响应中不包含设置为true的access-control-allow-credentials和与请求来源相同的access-control-allow-origin头,浏览器将阻止我们的请求。下面显示了一个示例,在该示例中,我们尝试向不允许凭据的Google Book API发出相同的请求:

如何解决跨域请求

一、反向代理

例如使用Nginx反向代理。

前端:https://www.site.com

后端接口服务:https://www.api.com

nginx配置示例:

server {
        location /api/{
          proxy_pass https://www.api.com/;
        }
        ...

反向代理前

浏览器访问的时序:

  • 1.https://www.site.com
  • 2.https://www.api.com/getinfo

反向代理后

浏览器访问的时序:

  • 1.https://www.site.com
  • 2.https://www.site.com/api/getinfo

即把不同的域,通过反向代理变为相同的域。

二、跨域资源共享(CORS)

看了前面的一堆介绍,只要我们的服务器端设置好CORS相关的相应头即可实现跨域

        httpServletResponse.setHeader("Access-Control-Allow-Origin", "*");
        httpServletResponse.setHeader("Access-Control-Allow-Credentials", "true");
        httpServletResponse.setHeader("Access-Control-Allow-Methods", "POST, GET, PATCH, DELETE, PUT, OPTIONS");
        httpServletResponse.setHeader("Access-Control-Max-Age", "3600");
        httpServletResponse.setHeader("Access-Control-Allow-Headers", "Origin, X-Requested-With, Content-Type, Accept");

可以在每个请求的相应头去这样设置,但是这种做法有点蠢,所以,一般都是使用filter来做这件事。

以下几种方式原理都是依托上面的原理实现,在实际开发中,按照需求任选其一即可。

1、CorsFilter

1.自己写过滤器在response中写入这些响应头

@Order(value = Ordered.HIGHEST_PRECEDENCE)
@Component
@WebFilter(filterName = "CorsFilter", urlPatterns = "/*")
public class CorsFilter extends OncePerRequestFilter {
    @Override
    protected void doFilterInternal(HttpServletRequest httpServletRequest, HttpServletResponse httpServletResponse, FilterChain filterChain) throws ServletException, IOException {
        httpServletResponse.setHeader("Access-Control-Allow-Origin", "*");
        httpServletResponse.setHeader("Access-Control-Allow-Credentials", "true");
        httpServletResponse.setHeader("Access-Control-Allow-Methods", "POST, GET, PATCH, DELETE, PUT, OPTIONS");
        httpServletResponse.setHeader("Access-Control-Max-Age", "3600");
        httpServletResponse.setHeader("Access-Control-Allow-Headers", "Origin, X-Requested-With, Content-Type, Accept");
        filterChain.doFilter(httpServletRequest, httpServletResponse);
    }
}

2.使用SpringMvc的CorsFilter

其实SpringMvc中已经有CorsFilter了,可以直接拿来用。

org.springframework.web.filter.CorsFilter

//@Configuration
public class CorsConfig {
    @Bean
    public CorsFilter corsFilter() {
        UrlBasedCorsConfigurationSource source = new UrlBasedCorsConfigurationSource();
        CorsConfiguration corsConfiguration = new CorsConfiguration();
        corsConfiguration.setAllowCredentials(true);
        corsConfiguration.addAllowedOrigin("*");
        corsConfiguration.addAllowedHeader("*");
        corsConfiguration.addAllowedMethod("*");
        source.registerCorsConfiguration("/**", corsConfiguration);
        return new CorsFilter(source);
    }
}

3.使用tomcat的CorsFilter

tomcat中也已经有实现好了的过滤器。

org.apache.catalina.filters.CorsFilter

@Configuration
public class TomcatCorsFilterConfig {
    @Bean
    public FilterRegistrationBean<CorsFilter> corsFilter() {
        FilterRegistrationBean<CorsFilter> registration = new FilterRegistrationBean<>();
        registration.setFilter(new CorsFilter());
        //Defaults: false
        registration.addInitParameter(CorsFilter.PARAM_CORS_SUPPORT_CREDENTIALS, "false");
        //这个默认是"",不允许访问的,可直接设置成 *
        //Defaults: The empty String. (No origin is allowed to access the resource).  
        registration.addInitParameter(CorsFilter.PARAM_CORS_ALLOWED_ORIGINS, "*");
        //Defaults: GET, POST, HEAD, OPTIONS 我测试的tomcat9.0.41 不支持 * 写法
        registration.addInitParameter(CorsFilter.PARAM_CORS_ALLOWED_METHODS, "GET, POST, HEAD, OPTIONS");
        //Defaults: Origin, Accept, X-Requested-With, Content-Type, Access-Control-Request-Method, Access-Control-Request-Headers
        registration.addInitParameter(CorsFilter.PARAM_CORS_ALLOWED_HEADERS, "*");
        //Defaults: 1800。3600表示一个小时
        registration.addInitParameter(CorsFilter.PARAM_CORS_PREFLIGHT_MAXAGE, "3600");
        registration.setName("TomcatCorsFilter"); //过滤器名称
        registration.addUrlPatterns("/*");//过滤路径  
        registration.setOrder(1);//设置顺序  
        return registration;
    }
} 

注意过滤器的顺序。

2 、@CrossOrigin注解

@RestController
@CrossOrigin
public class ResourceController {

    @GetMapping("/user")
    @CrossOrigin
    public String user(Principal principal) {
        return principal.getName();
    }
}

可以精确控制到某个类或者某个方法跨域。

3、Spring Boot 全局配置CorsRegistry

@Configuration
public class CorsConfig implements WebMvcConfigurer {

    @Override
    public void addCorsMappings(CorsRegistry registry) {
        registry.addMapping("/**")
                .allowedOrigins("*")
                .allowedMethods("GET", "HEAD", "POST", "PUT", "DELETE", "OPTIONS")
                .allowCredentials(true)
                .maxAge(3600)
                .allowedHeaders("*");
    }
}

三、Jsonp

JSONP 是服务器与客户端跨源通信的常用方法之一。最大特点就是简单适用,兼容性好(兼容低版本IE),缺点是只支持get请求,所以一般我们也不用它

核心思想:网页通过添加一个<script>元素,向服务器请求 JSON 数据,服务器收到请求后,将数据放在一个指定名字的回调函数的参数位置传回来。

前端实现:

<script src="http://localhost:8080/map?callback=dosomething"></script>
// 向服务器发出请求,该请求的查询字符串有一个callback参数,用来指定回调函数的名字
// 处理服务器返回回调函数的数据
<script type="text/javascript">
    function dosomething(res){
        console.log(res)// 处理获得的数据
    }
</script>

后端实现

@RestController
@Slf4j
public class MapController {
    @RequestMapping(value = "/map")
    public String transfer(String callback) {
        log.info(callback);
        return callback + "('lakertest')";
    }
}

总结

一般项目开发针对跨域请求的解决方案选择Nginx反向代理或者CORSFilter+域的黑白名单方式。

以上相关内容我都做了代码验证,代码位置:https://gitee.com/lakernote/lakernote

Tags:

标签列表
最新留言