跨域是什么?如何解决跨域
跨域是什么
名词解释:
同源策略
(Same-Origin Policy,简称 SOP)是浏览器的一种重要安全机制,用于防止一个来源的恶意脚本访问另一个来源的敏感资源。这个策略由 Netscape 公司在 1995 年引入,现已成为所有现代浏览器的基础安全功能。
什么是“同源”
一个 URL 的“源”由协议(scheme)、主机(host)和端口(port)三个部分组成。只有当两个 URL 的这三个部分完全相同时,它们才被认为是同源的。
- 协议:例如 HTTP、HTTPS。
- 主机:例如 www.example.com。
- 端口:例如 80、443。
同源策略的限制
同源策略主要限制了以下几种行为:
- Cookie、LocalStorage 和 IndexDB 等存储内容的读取。
- DOM 和 JavaScript 对象的访问。
- AJAX 请求的发送和接收。
产生跨域问题的主要原因
-
不满足同源策略
跨域问题是在Web开发中常见的一个问题,通常在浏览器访问一个与当前页面不同的域名、协议或端口的资源时产生。这是因为浏览器出于安全考虑,默认采用同源策略(Same-Origin Policy),限制了从一个源加载的脚本如何能够与来自另一个源的资源进行交互。-
不同的域名
例如,从
http://example.com
访问http://another-domain.com
的资源。 -
不同的子域名
例如,从
http://sub.example.com
访问http://example.com
的资源。 -
不同的协议
例如,从
http://example.com
访问https://example.com
的资源。 -
不同的端口
例如,从
http://example.com:8080
访问http://example.com:3000
的资源。
-
-
多次添加 cors头部
有的小伙伴说我加了允许跨域 ,为什么还是会报跨域错误呢?
原因是多个地方添加了允许跨域的header 导致响应头中有多个Access-Control-Allow-Origin 这时候也会报跨域错误。
如何解决跨域
对于第一种跨域问题(违反同源策略)
1.nginx
-
前端访问后端服务
将请求发送只同一地址和端口 让nginx反向代理 就不违反同源策略
server { listen 80; server_name frontend.com; location / { root /path/to/your/frontend; try_files $uri $uri/ /index.html; } # 其实这时已经符合同源策略 不需要加上允许跨域的请求头了 所以下方 proxy_set_header 都可加可不加 location /api { # 这里不想要转发/api 可以用rewrite 去掉/api proxy_pass http://backend.com/api; # 将原始请求的主机名传递给后端服务器。如果后端服务器需要知道原始的主机名,这条指令是有用的。 proxy_set_header Host $host; # 将客户端的真实 IP 地址传递给后端服务器。在日志记录和 IP 限制等情况下是有用的。 proxy_set_header X-Real-IP $remote_addr; # 用于传递客户端的原始IP地址 $proxy_add_x_forwarded_for包含了客户端的IP地址和经过的代理服务器的IP地址。 proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for; # 传递原始请求的协议(HTTP 或 HTTPS)。这在后端服务器需要知道请求的原始协议时是有用的。 proxy_set_header X-Forwarded-Proto $scheme; } }
-
访问第三方服务
有时我们的服务需要访问第三方服务 这时也可能会报跨域 这时我们要注意 如果第三方接口允许了跨域 我们再自己加上允许跨域 就会导致第二种跨域报错
server { listen 80; server_name yourdomain.com; location /api { proxy_pass http://external-api.com/api; # 处理 OPTIONS 预检请求 if ($request_method = 'OPTIONS') { # 添加 CORS 头部 # 允许任何来源(域)访问资源。 add_header 'Access-Control-Allow-Origin' '*'; # 指定允许的 HTTP 请求方法。 add_header 'Access-Control-Allow-Methods' 'GET, POST, OPTIONS, PUT, DELETE'; # 指定允许的 HTTP 请求头部字段。 add_header 'Access-Control-Allow-Headers' 'Origin, Content-Type, Accept, Authorization, X-Custom-Header'; # 指定是否允许发送凭据(如 Cookie 和 HTTP 认证信息)。 add_header 'Access-Control-Allow-Credentials' 'true'; return 204; # No Content } } }
2.springboot配置cors
-
使用springMVC的WebMvcConfigurer
@Configuration public class CorsConfig implements WebMvcConfigurer { @Override public void addCorsMappings(CorsRegistry registry) { registry.addMapping("/**") .allowedOrigins("*") // 可以指定具体的域名,替换 "*" 为 "http://example.com" .allowedMethods("GET", "POST", "PUT", "DELETE", "OPTIONS") .allowedHeaders("*") .allowCredentials(true); } }
-
使用spring的@CrossOrigin注解
可以加载类 或者方法上
@RestController @RequestMapping("/api") public class ApiController { // 可直接加在类上 @CrossOrigin(origins = "*") // 可以指定具体的域名,替换 "*" 为 "http://example.com" @GetMapping("/data") public String getData() { return "Data from server"; } }
3.使用Servlet 过滤器
-
自定义过滤器继承Filter
public class CorsFilter implements Filter { @Override public void init(FilterConfig filterConfig) throws ServletException { } @Override public void doFilter(ServletRequest request, ServletResponse response, FilterChain chain) throws IOException, ServletException { HttpServletResponse httpResponse = (HttpServletResponse) response; httpResponse.setHeader("Access-Control-Allow-Origin", "*"); // 可以指定具体的域名,替换 "*" 为 "http://example.com" httpResponse.setHeader("Access-Control-Allow-Methods", "GET, POST, PUT, DELETE, OPTIONS"); httpResponse.setHeader("Access-Control-Allow-Headers", "Origin, Content-Type, Accept, Authorization"); httpResponse.setHeader("Access-Control-Allow-Credentials", "true"); // 对于预检请求,直接返回状态码 204 if ("OPTIONS".equalsIgnoreCase(((HttpServletRequest) request).getMethod())) { httpResponse.setStatus(HttpServletResponse.SC_NO_CONTENT); return; } chain.doFilter(request, response); } @Override public void destroy() { } }
-
注册自定义过滤器
@Configuration public class FilterConfig { @Bean public FilterRegistrationBean<CorsFilter> corsFilter() { FilterRegistrationBean<CorsFilter> registrationBean = new FilterRegistrationBean<>(); registrationBean.setFilter(new CorsFilter()); registrationBean.addUrlPatterns("/*"); registrationBean.setOrder(0); return registrationBean; } }
4.Spring Security 配置 CORS
如果项目中使用了Spring Security 。可在代码中添加
@Configuration
@EnableWebSecurity
public class SecurityConfig extends WebSecurityConfigurerAdapter {
@Override
protected void configure(HttpSecurity http) throws Exception {
http.cors().and().csrf().disable();
}
@Bean
public CorsConfigurationSource corsConfigurationSource() {
CorsConfiguration configuration = new CorsConfiguration();
// 可以指定具体的域名,替换 "*" 为 "http://example.com"
configuration.setAllowedOrigins(Arrays.asList("*"));
configuration.setAllowedMethods(Arrays.asList("GET", "POST", "PUT", "DELETE", "OPTIONS"));
configuration.setAllowedHeaders(Arrays.asList("Origin", "Content-Type", "Accept", "Authorization"));
configuration.setAllowCredentials(true);
UrlBasedCorsConfigurationSource source = new UrlBasedCorsConfigurationSource();
source.registerCorsConfiguration("/**", configuration);
return source;
}
}
5.使用 Spring 拦截器配置 CORS
拦截器适用于需要在请求处理前后进行拦截和处理的场景。可以通过实现 HandlerInterceptor
接口来配置 CORS。
需要注意
拦截器默认不拦截静态资源请求 如果需要也可以配置,但是会带来不必要的性能开销
-
自定义CORS拦截器
@Component public class CorsInterceptor implements HandlerInterceptor { @Override public boolean preHandle(HttpServletRequest request, HttpServletResponse response, Object handler) throws Exception { response.setHeader("Access-Control-Allow-Origin", "*"); response.setHeader("Access-Control-Allow-Methods", "GET, POST, PUT, DELETE, OPTIONS"); response.setHeader("Access-Control-Allow-Headers", "Origin, Content-Type, Accept, Authorization"); response.setHeader("Access-Control-Allow-Credentials", "true"); // 对于预检请求,直接返回状态码 204 if ("OPTIONS".equalsIgnoreCase(request.getMethod())) { response.setStatus(HttpServletResponse.SC_NO_CONTENT); return false; } return true; } }
-
注册,指定拦截路径
@Configuration public class WebConfig implements WebMvcConfigurer { @Autowired private CustomInterceptor customInterceptor; @Override public void addInterceptors(InterceptorRegistry registry) { registry.addInterceptor(customInterceptor).addPathPatterns("/**"); // 拦截所有路径 } }
6.使用 Spring Cloud Gateway 配置 CORS
如果项目使用了微服务架构。用到了网关gateway
-
配置 CORS 过滤器
本质还是配置过滤器
@Configuration public class CorsConfig { @Bean public CorsWebFilter corsWebFilter() { UrlBasedCorsConfigurationSource source = new UrlBasedCorsConfigurationSource(); CorsConfiguration config = new CorsConfiguration(); // 设置允许哪些url访问跨域资源,*表示全部允许 config.addAllowedOrigin("*"); // 允许访问的头信息,*表示全部 config.addAllowedHeader("*"); // 允许所有请求方法(GET,POST等)访问跨域资源 config.addAllowedMethod("*"); config.setAllowCredentials(Boolean.TRUE); source.registerCorsConfiguration("/**", config); return new CorsWebFilter(source); } }
-
配置文件配置
更加灵活
spring: cloud: gateway: globalcors: corsConfigurations: '[/**]': allowedOrigins: "*" allowedMethods: - GET - POST - PUT - DELETE - OPTIONS allowedHeaders: "*" allowCredentials: true
-
gateway局部过滤器
这种方式更加灵活 可以对部分代理的接口单独添加跨域
spring: cloud: gateway: routes: - id: add_cors_headers_route # 目标地址 uri: https://example.org predicates: - Path=/api/** filters: - name: AddResponseHeader args: Access-Control-Allow-Origin: "*" Access-Control-Allow-Methods: "GET,POST,PUT,DELETE,OPTIONS" Access-Control-Allow-Headers: "Origin,Content-Type,Accept,Authorization" Access-Control-Allow-Credentials: "true"
对与第二种跨域问题(请求头中重复添加Access-Control-Allow-Origin)
多个地方添加了允许跨域的请求头
如果是自己的服务可以通过排查整体请求链路
- 查看整个请求的链路 去掉多余添加请求头的地方,保证保留一个。
如果是调用第三方接口,或者说不想去排查服务
-
gateway的DedupeResponseHeader
DedupeResponseHeader 剔除重复的响应头 保留一个,接受一个name参数和一个可选strategy参数。name可以包含以空格分隔的标题名称列表。
spring: cloud: gateway: routes: - id: dedupe_response_header_route uri: https://example.org filters: - name: DedupeResponseHeader args: name: Access-Control-Allow-Credentials Access-Control-Allow-Origin strategy: RETAIN_FIRST
-
nginx的proxy_hide_header
location / { proxy_pass http://example.org; # 删除重复的响应头 proxy_hide_header Access-Control-Allow-Credentials; proxy_hide_header Access-Control-Allow-Origin; }
-
使用filter自定义逻辑
@WebFilter("/api/*") public class RemoveDuplicateCorsHeadersFilter implements Filter { private static final List<String> CORS_HEADERS = Arrays.asList( "Access-Control-Allow-Origin", "Access-Control-Allow-Credentials" ); @Override public void init(FilterConfig filterConfig) throws ServletException { // 初始化逻辑,可选实现 } @Override public void doFilter(ServletRequest request, ServletResponse response, FilterChain chain) throws IOException, ServletException { HttpServletRequest httpRequest = (HttpServletRequest) request; Map<String, List<String>> headerMap = new HashMap<>(); // 收集请求头 Enumeration<String> headerNames = httpRequest.getHeaderNames(); while (headerNames.hasMoreElements()) { String headerName = headerNames.nextElement(); Enumeration<String> headers = httpRequest.getHeaders(headerName); List<String> headerValues = Collections.list(headers); headerMap.put(headerName, headerValues); } // 移除重复的 CORS 相关请求头 removeDuplicateCorsHeaders(headerMap); // 应用修改后的请求头 applyModifiedHeaders(httpRequest, headerMap); // 继续请求链 chain.doFilter(httpRequest, response); } private void removeDuplicateCorsHeaders(Map<String, List<String>> headerMap) { // 遍历请求头,找出重复的 CORS 相关请求头 for (String headerName : CORS_HEADERS) { if (headerMap.containsKey(headerName)) { List<String> values = headerMap.get(headerName); if (values != null && values.size() > 1) { // 有两个值 只保留一个 List<String> newValues = new ArrayList<String>(); newValues.add(values.get(0)); headerMap.put(headerName,newValues); } } } } private void applyModifiedHeaders(HttpServletRequest request, Map<String, List<String>> headerMap) { // 移除需要移除的请求头 for (String headerToRemove : CORS_HEADERS) { request.removeHeader(headerToRemove); } // 将修改后的请求头重新设置到请求对象中 headerMap.forEach((headerName, headerValues) -> { for (String headerValue : headerValues) { request.addHeader(headerName, headerValue); } }); } @Override public void destroy() { // 销毁逻辑,可选实现 } }