场景:

业务要求从把系统B嵌入到系统A中,系统A和系统B是完成不同的两个域名,前端同事完成系统嵌入后,从A系统内部调用B系统的接口时候发现跨域错误(CORS error),如下:
在这里插入图片描述

什么是跨域?

跨域(Cross Origin)指浏览器不允许当前页面所在的源去请求另一个源的数据,跨域也就是跨源的意思。

什么是同源?

同源策略(SOP Same origin policy):是一种约定,由Netscape公司1995年引入浏览器,它是浏览器最核心也是最基本的安全功能,如果缺少同源策略,浏览器很容易受到XSS、CSFR等攻击,所谓同源是指"协议+域名+端口"三者相同,必须满足这三个条件,才算做同源。

跨域具体场景举例:

当前页面 url 地址被请求页面 url 地址是否跨域原因
https://www.aaa.comhttps://www.aaa.com/index协议+域名+端口 三者相同
https://www.aaa.comhttp://www.aaa.com协议不同,http、https
https://www.aaa.comhttps://www.aaa.com主域名不同
https://www.aaa.comhttps://hy.aaa.com子域名不同
https://www.aaa.com:10000https://www.aaa.com:10010端口不同

什么原因导致浏览器报跨域错误?

发起ajax请求的那个页面的地址 和 ajax接口地址 不在同一个域中,直接导致了跨域问题,也就是说跨域问题发生在浏览器。

跨域问题解决方案:

Nginx 反向代理解决跨域

Nginx 反向代理解决跨域,只需要在 nginx 上增加配置文件,即可解决跨域问题,如下:

server {
      listen       80;
      listen       443;
      server_name  xxx.test.com;

      root /usr/share/nginx/html;
      index  index.html index.htm;
	
	  #跨域配置	
      add_header Access-Control-Allow-Methods GET,POST,PUT,OPTIONS,DELETE,PATCH;
      add_header Cache-Control no-cache;
      add_header Access-Control-Allow-Origin *;
      add_header Access-Control-Allow-Headers *; 

      #access_log   /data/logs/nginx/nginx_elk_log/test.com.access.log nginx-json-log;
      #error_log   /data/logs/nginx/nginx_elk_log/test.com.error.log;

      if ($request_method !~* GET|POST|HEAD) {
          return 403;
      }

      location / {
	  
		#跨域配置(和上面的跨域配置二选一即可)
		add_header Access-Control-Allow-Origin *;
		add_header Access-Control-Allow-Headers *;
		add_header_Access-Control-Allow-MethodS GET,PUT,DELETE,POST,OPTIONS;
	
		#另外一种配置方式 预检请求直接返回 否则可能代码中有鉴权过不去 还要额外处理
		if ($request_method = 'OPTIONS'){
		  add_header 'Access-Control-Allow-Origin' '*';
		  add_header 'Access-Control-Allow-Credentials' 'true';           
		  add_header Access-Control-Allow-Methods GET,POST,PUT,DELETE,OPTIONS;
		  add_header 'Access-Control-Allow-Headers' 'DNT,X-Mx-ReqToken,Keep-Alive,User-Agent,X-Requested-With,If-Modified-Since,Cache-Control,Content-Type';
		  return 204;
		}
		
        #root   /data/web/html/zteam;
        root   /usr/share/nginx/html;
        index  index.html index.htm;
        try_files $uri $uri/ /index.html;

      }

      location ^~ /WEB-INF{
          deny all;
      }

      error_page   500 502 503 504  /50x.html;
      location = /50x.html {
          root   html;
      }

    }

Nginx 增加配置解决跨域问题,只使用一种解决问题即可,不要同时配置多个。

Nginx 知识传送门:

Nginx 故障排查之斜杠(/) --(附 Nginx 常用命令)

服务端解决跨域问题

解决 CORS 跨域问题,就是在服务器端给响应添加头信息,解释如下:

Access-Control-Allow-Origin 允许请求的域
Access-Control-Allow-Methods 允许请求的方法
Access-Control-Allow-Headers 预检请求后,告知发送请求需要有的头部
Access-Control-Allow-Credentials 表示是否允许发送cookie,默认false;
Access-Control-Max-Age 本次预检的有效期,单位:秒;

1、使用过滤器解决跨域问题,注意该方案需要在启动类加注解:@ServletComponentScan({“com.my.study.main.filter”}

package com.my.study.main.filter;

import lombok.extern.slf4j.Slf4j;
import org.springframework.stereotype.Component;

import javax.servlet.*;
import javax.servlet.annotation.WebFilter;
import javax.servlet.http.HttpServletRequest;
import javax.servlet.http.HttpServletResponse;
import java.io.IOException;


@Slf4j
@Component
@WebFilter(urlPatterns = { "/*" })
public class MyCorsFilter implements Filter {
    @Override
    public void doFilter(ServletRequest servletRequest, ServletResponse servletResponse,
                         FilterChain filterChain) throws IOException, ServletException {
        HttpServletRequest request = (HttpServletRequest) servletRequest;
        log.info("进入了过滤器,请求路径为:{}",request.getRequestURL());
        HttpServletResponse httpServletResponse = (HttpServletResponse)servletResponse;
        // 允许跨域的域名,*:代表所有域名
        httpServletResponse.setHeader("Access-Control-Allow-Origin", "*");
        // 允许跨域请求的方法
        httpServletResponse.setHeader("Access-Control-Allow-Methods",  "POST, PUT, GET, OPTIONS, DELETE");
        // 本次许可的有效时间,单位秒,过期之前的ajax请求就无需再次进行预检啦
        // 默认是1800s,此处设置1h
        httpServletResponse.setHeader("Access-Control-Max-Age", "3600");
        // 允许的响应头
        httpServletResponse.setHeader("Access-Control-Allow-Headers", "Origin, X-Requested-With, Content-Type, Accept, client_id, uuid, Authorization");
        // 支持HTTP 1.1.
        httpServletResponse.setHeader("Cache-Control", "no-cache, no-store, must-revalidate");
        // 支持HTTP 1.0. response.setHeader("Expires", "0");
        httpServletResponse.setHeader("Pragma", "no-cache");
        // 编码
        httpServletResponse.setCharacterEncoding("UTF-8");
        // 放行
        filterChain.doFilter(servletRequest, servletResponse);
    }


    @Override
    public void init(FilterConfig filterConfig) throws ServletException {

    }

    @Override
    public void destroy() {

    }

}

2、过滤器解决跨域的另外一种实现方式。

package com.my.study.main.configurer;

import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.springframework.web.cors.CorsConfiguration;
import org.springframework.web.cors.UrlBasedCorsConfigurationSource;
import org.springframework.web.filter.CorsFilter;


@Configuration
public class CorsConfig {

    private CorsConfiguration buildConfig() {
        CorsConfiguration corsConfiguration = new CorsConfiguration();
        corsConfiguration.addAllowedOrigin("*");
        corsConfiguration.addAllowedHeader("*");
        corsConfiguration.addAllowedMethod("*");
        corsConfiguration.setMaxAge(3600L);         
        corsConfiguration.setAllowCredentials(true);
        return corsConfiguration;
    }

    @Bean
    public CorsFilter corsFilter() {
        UrlBasedCorsConfigurationSource source = new UrlBasedCorsConfigurationSource();
        source.registerCorsConfiguration("/**", buildConfig());
        return new CorsFilter(source);
    }
}

2、添加 @Configuration 注解,实现 WebMvcConfigurer 接口,解决跨域问题。

package com.my.study.main.configurer;

import org.springframework.context.annotation.Configuration;
import org.springframework.web.servlet.config.annotation.CorsRegistry;
import org.springframework.web.servlet.config.annotation.WebMvcConfigurer;
 
@Configuration 
public class CorsConfig implements WebMvcConfigurer {
    @Override
    public void addCorsMappings(CorsRegistry registry) {
        registry.addMapping("/**") // 所有接口
                .allowCredentials(true) // 是否发送 Cookie
                .allowedOriginPatterns("*") // 支持域
                .allowedMethods(new String[]{"GET", "POST", "PUT", "DELETE"}) // 支持方法
                .allowedHeaders("*")
                .exposedHeaders("*");
    }

跨域解决方案总结:

推荐使用 Nginx 处理跨域问题,只需要在nginx 上增加配置即可解决问题,而服务端解决跨域问题,或多或少都需要写代码,本着少改代码的原则,强烈建议使用 Nginx 的方式解决跨域问题,不管使用哪种方式解决跨域问题,只需要使用一种即可,不要多种方式叠加使用。

温馨提示:

跨域问题通常是伴随多个系统一起出现了,也就是出现了跨系统调用,可能会出现跨域问题,这个时候要主要多个系统的权限认证是否通用,如果权限认证不通用,要优先解决权限认证的问题,否则也是提示跨域问题,浏览器端常见错误如下:
在这里插入图片描述

什么是预检(OPTIONS)请求?

浏览器使用 OPTIONS 方法发起一个预检请求(preflight request),来感知服务端是否允许该跨域请求,服务器确认允许之后,才发起实际的 HTTP 请求,OPTIONS 请求没有附带请求数据,响应体也为空,简单来说就是一种探测,这就是预检请求,是浏览器的一种保护机制。

预检(OPTIONS)请求的作用?

  • 跨域场景中使用了预检请求,跨域请求失败产生错误,代码层无法获知感知错误发生的地方,这时候可以查看浏览器的控制台来查询错误信息。
  • 检测服务器支持的请求方法。

什么时候会触发预检(OPTIONS)请求?

非简单请求时候会触发预检请求。

简单请求与非简单请求:

简单请求:

  • 请求方法是 GET、HEAD、POST 中的一种。
  • HTTP的头信息只能是 Accept、Accept-Language、Content-Language、Last-Event-ID、Content-Type 中的某几个,不能超出这个范围。
  • Content-Type 的值只能是 text\plain、multipart/form-data、application/x-www-form-urlencoded 中的一种。
  • 请求中没有使用 XMLHttpRequestUpload 对象。
  • 请求中没有使用 ReadableStream 对象。

非简单请求:
简单请求的对立面就是非简单请求,也就是说不能同时满足简单请求条件的请求就是非简单请求,就可能会触发预检(OPTIONS)请求。

如有错误的地方欢迎指出纠正。

Logo

旨在为数千万中国开发者提供一个无缝且高效的云端环境,以支持学习、使用和贡献开源项目。

更多推荐