N种方法解决跨域问题

目录

1 为什么会有跨域问题

2 辨别是否跨域

3 解决跨域的方案

3.1 JSONP

3.2 PostMessage

3.3 Websocket

3.4 Nginx代理

3.5 document.domain

3.6 CORS解决跨域

3.6.1 什么是CORS?

3.6.2 原理(简单请求、复杂请求)

3.6.3 重新注入CorsFilter解决跨域

 3.7 实现 WebMvcConfigurer#addCorsMappings 的方法

  3.8 创建一个 filter 解决跨域

3.9 @CrossOrigin注解


1 为什么会有跨域问题

  • 同源策略(SOP Same origin policy):是一种约定,由Netscape公司1995年引入浏览器,它是浏览器最核心也是最基本的安全功能,如果缺少同源策略,浏览器很容易受到XSS、CSFR等攻击。所谓同源是指"协议+域名+端口"三者相同,即便两个不同的域名指向同一个ip,也非同源。
  • 跨域(Cross Origin)指浏览器不允许当前页面所在的源去请求另一个源的数据。(要理解当前页面的源是什么?就是当前页面部署的服务器路径)跨域不一定都会有跨域问题。因为跨域问题是浏览器对于ajax请求的一种安全限制:一个页面发起的ajax请求,只能是与当前页域名相同的路径,这能有效的阻止跨站攻击。因此:跨域问题 是针对ajax的一种限制。 

2 辨别是否跨域

URL说明是否属于跨域

http://www.benjamin.com/a.jpg

http://www.baidu.com/a.jpg

域名不同YES

http://www.benjamin.com/a.jpg

http://www.benjamin.com/b.jpg

相同域名,不同路径NO

http://www.benjamin.com:8888/a.jpg

http://www.benjamin.com:9999/a.jpg

相同域名,不同端口YES

http://www.benjamin.com:8888/a.jpg

https://www.benjamin.com:8888/a.jpg

域名端口相同,不同协议YES

http://www.benjamin.com:8888/a.jpg

http://192.168.12.143:8888/a.jpg

域名和域名对应的ipYES

http://www.benjamin.com:8888/a.jpg

http://pm.benjamin.com:8888/a.jpg

主域名相同,子域名不同YES,COOKIE不可访问

http://www.benjamin.com:8888/a.jpg

http://www.ben.com:8888/a.jpg

二级域名不同YES

3 解决跨域的方案

3.1 JSONP

最早的解决方案,利用script标签可以跨域的原理实现
    限制:

  •  需要服务的支持
  • 只能发起GET请求

  步骤:

  1. 本站的脚本创建一个 元素,src 地址指向跨域请求数据的服务器
  2. 提供一个回调函数来接受数据,函数名可以通过地址参数传递进行约定
  3. 服务器收到请求后,返回一个包装了 JSON 数据的响应字符串,类似这样:callback({...})

浏览器接受响应后就会去执行回调函数 callback,传递解析后的 JSON 对象作为参数,这样我们就可以在 callback 里处理数据了。实际开发中,会遇到回调函数名相同的情况,可以简单封装一个 JSONP 函数:   

function jsonp({ url, params, callback }) {  
  return new Promise((resolve, reject) => {  
    // 创建一个临时的 script 标签用于发起请求  
    const script = document.createElement('script');  
    // 将回调函数临时绑定到 window 对象,回调函数执行完成后,移除 script 标签  
    window[callback] = data => {  
      resolve(data);  
      document.body.removeChild(script);  
    };  
    // 构造 GET 请求参数,key=value&callback=callback  
    const formatParams = { ...params, callback };  
    const requestParams = Object.keys(formatParams)  
      .reduce((acc, cur) => {  
        return acc.concat([`${cur}=${formatParams[cur]}`]);  
      }, [])  
   .join('&');  
 // 构造 GET 请求的 url 地址  
    const src = `${url}?${requestParams}`;  
    script.setAttribute('src', src);  
    document.body.appendChild(script);  
  });  
}  
  
// 调用时  
jsonp({  
  url: 'https://xxx.xxx',  
  params: {...},  
  callback: 'func',  
})  

我们用 Promise 封装了请求,使异步回调更加优雅,但是别看楼上的洋洋洒洒写了一大段,其实本质上就是:

<script src='https://xxx.xxx.xx?key=value&callback=xxx'><script> 

3.2 PostMessage

PostMessage 是 Html5 XMLHttpRequest Level 2 中的 API,它可以实现跨文档通信(Cross-document messaging)。兼容性上,IE8+,Chrome,Firfox 等主流浏览器都支持,可以放心用😊,如何理解跨文档通信?你可以类比设计模式中的发布-订阅模式,在这里,一个窗口发送消息,另一个窗口接受消息,之所以说类似发布-订阅模式,而不是观察者模式,是因为这里两个窗口间没有直接通信,而是通过浏览器这个第三方平台。

发送:

window.postMessage(message, origin, [transfer])

 postMessage 方法三个参数,要发送的消息接收消息的源和一个可选的 Transferable 对象

参数说明
window/otherWindow其他窗口的一个引用,比如 iframe 的 contentWindow 属性、执行 window.open 返回的窗口对象、或者是命名过或数值索引的 window.frames。
message将要发送到其他 window的数据。
targetOrigin指定哪些窗口能接收到消息事件,其值可以是 *(表示无限制)或者一个 URI。
transfer可选,是一串和 message 同时传递的 Transferable 对象。这些对象的所有权将被转移给消息的接收方,而发送一方将不再保有所有权。

接收:需要一个监听

window.addEventListener("message", function receiveMessage(event) {}, false); // 推荐,兼容性更好  
  
window.onmessage = function receiveMessage(event) {} // 不推荐,这是一个实验性的功能,兼容性不如上面的方法  

 接收到消息后,消息对象 event 中包含了三个属性:source,origin,data,其中 data 就是我们发送的 message。此外,除了实现窗口通信,postMessage 还可以同 Web Worker 和 Service Work 进行通信。

  • event.source – 消息源,消息的发送窗口/iframe。
  • event.origin – 消息源的 URI(可能包含协议、域名和端口),用来验证数据源。
  • event.data – 发送过来的数据。

3.3 Websocket

Websocket 是 HTML5 的一个持久化的协议,它实现了浏览器与服务器的全双工通信,同时也是跨域的一种解决方案。什么是全双工通信 ?简单来说,就是在建立连接之后,server 与 client 都能主动向对方发送或接收数据。原生的 WebSocket API 使用起来不太方便,我们一般会选择自己封装一个 Websocket或者使用已有的第三方库,我们这里以第三方库 ws 为例:

const WebSocket = require('ws');  
  
const ws = new WebSocket('ws://www.host.com/path');  
  
ws.on('open', function open() {  
  ws.send('something');  
});  
  
ws.on('message', function incoming(data) {  
  console.log(data);  
});  
... ...  

 需要注意的是,Websocket 属于长连接,在一个页面建立多个 Websocket 连接可能会导致性能问题。

3.4 Nginx代理

先看下跨域nginx配置文件:此时页面所在的源是manage.leyou.com:80,而后端代码服务的源是api.leyou.com:80,域名不同,域名对应的ip和port以及协议相同,也属于跨域。


#user  nobody;
worker_processes  1;

#error_log  logs/error.log;
#error_log  logs/error.log  notice;
#error_log  logs/error.log  info;

#pid        logs/nginx.pid;


events {
    worker_connections  1024;
}


http {
    include       mime.types;
    default_type  application/octet-stream;

    #log_format  main  '$remote_addr - $remote_user [$time_local] "$request" '
    #                  '$status $body_bytes_sent "$http_referer" '
    #                  '"$http_user_agent" "$http_x_forwarded_for"';

    #access_log  logs/access.log  main;

    sendfile        on;
    #tcp_nopush     on;

    #keepalive_timeout  0;
    keepalive_timeout  65;

    #gzip  on;

    server {
        listen       80;
        server_name  manage.leyou.com;

        #charset koi8-r;

        #access_log  logs/host.access.log  main;

	#前台页面
        location / {
	    proxy_pass http://127.0.0.1:9001;
            #root   html;
            #index  index.html index.htm;
        }
		
   
        error_page   500 502 503 504  /50x.html;
        location = /50x.html {
            root   html;
        }	
		
    }
	
	server {
		listen 80;
		server_name api.leyou.com;
		
		proxy_set_header X-Forward-Host $host;
		proxy_set_header X-Forward-Server $host;
		proxy_set_header X-Forward-For $proxy_add_x_forwarded_for;
		
		#gateway
		location /api {
			proxy_pass http://127.0.0.1:10010;
			proxy_connect_timeout 600;
			proxy_read_timeout 600;
		}
		
	
	
	}
	

    # HTTPS server
    #
    #server {
    #    listen       443 ssl;
    #    server_name  localhost;

    #    ssl_certificate      cert.pem;
    #    ssl_certificate_key  cert.key;

    #    ssl_session_cache    shared:SSL:1m;
    #    ssl_session_timeout  5m;

    #    ssl_ciphers  HIGH:!aNULL:!MD5;
    #    ssl_prefer_server_ciphers  on;

    #    location / {
    #        root   html;
    #        index  index.html index.htm;
    #    }
    #}

}

可以把后端服务配置和页面的源,配置在一起,根据路径的不同进行区分,ajax访问后台代码路径也是manage.leyou.com:80/api/***这样和页面同属于一个源,不属于跨域,只是路径不同,协议端口域名都相同。


#user  nobody;
worker_processes  1;

#error_log  logs/error.log;
#error_log  logs/error.log  notice;
#error_log  logs/error.log  info;

#pid        logs/nginx.pid;


events {
    worker_connections  1024;
}


http {
    include       mime.types;
    default_type  application/octet-stream;

    #log_format  main  '$remote_addr - $remote_user [$time_local] "$request" '
    #                  '$status $body_bytes_sent "$http_referer" '
    #                  '"$http_user_agent" "$http_x_forwarded_for"';

    #access_log  logs/access.log  main;

    sendfile        on;
    #tcp_nopush     on;

    #keepalive_timeout  0;
    keepalive_timeout  65;

    #gzip  on;

    server {
        listen       80;
        server_name  manage.leyou.com;

        #charset koi8-r;

        #access_log  logs/host.access.log  main;

		#前台页面
        location / {
			proxy_pass http://127.0.0.1:9001;
            #root   html;
            #index  index.html index.htm;
        }
		
		
		#gateway
		location /api {
			proxy_pass http://127.0.0.1:10010;
			proxy_connect_timeout 600;
			proxy_read_timeout 600;
		}
		
        error_page   500 502 503 504  /50x.html;
        location = /50x.html {
            root   html;
        }
		
    }

    # HTTPS server
    #
    #server {
    #    listen       443 ssl;
    #    server_name  localhost;

    #    ssl_certificate      cert.pem;
    #    ssl_certificate_key  cert.key;

    #    ssl_session_cache    shared:SSL:1m;
    #    ssl_session_timeout  5m;

    #    ssl_ciphers  HIGH:!aNULL:!MD5;
    #    ssl_prefer_server_ciphers  on;

    #    location / {
    #        root   html;
    #        index  index.html index.htm;
    #    }
    #}

}

3.5 document.domain

二级域名相同的情况下,设置 document.domain 就可以实现跨域。如何实现跨域 ?

document.domain = 'test.com' // 设置 domain 相同  
  
// 通过 iframe 嵌入跨域的页面  
const iframe = document.createElement('iframe')  
iframe.setAttribute('src', 'b.test.com/xxx.html')  
iframe.onload = function() {  
  // 拿到 iframe 实例后就可以直接访问 iframe 中的数据  
  console.log(iframe.contentWindow.xxx)  
}  
document.appendChild(iframe)  

3.6 CORS解决跨域

3.6.1 什么是CORS?

CORS是一个W3C标准,全称是"跨域资源共享"(Cross-origin resource sharing)。它允许浏览器向跨源服务器,发出XMLHttpRequest请求,从而克服了AJAX只能同源使用的限制。

CORS需要浏览器和服务器同时支持。目前,所有浏览器都支持该功能,IE浏览器不能低于IE10

1)浏览器端

目前,所有浏览器都支持该功能(IE10以下不行)。整个CORS通信过程,都是浏览器自动完成,不需要用户参与。

2)服务端

CORS通信与AJAX没有任何差别,因此你不需要改变以前的业务逻辑。只不过,浏览器会在请求中携带一些头信息,我们需要以此判断是否允许其跨域,然后在响应头中加入一些信息即可。这一般通过过滤器完成即可。

3.6.2 原理(简单请求、复杂请求)

浏览器会将ajax请求分为两类,其处理方案略有差异:简单请求、特殊请求。不要认为简单请求一定不跨域,复杂请求一定跨域,要弄明白什么跨域?简单请求也有可能跨域,复杂请求有可能不跨域,跨域或者不跨域看是否是同源。

3.6.2.1 简单请求

什么是简单请求?

(1)请求方法是以下三种方法之一:
    HEAD、 GET、POST
(2)HTTP的头信息不超出以下几种字段:
    Accept
    Accept-Language
    Content-Language
    Last-Event-ID
    Content-Type:只限于三个值application/x-www-form-urlencoded、multipart/form-data、text/plain

当浏览器发现发起的ajax请求是简单请求时,会在请求头中携带一个字段:Origin.
Origin中会指出当前请求属于哪个域(协议+域名+端口)和Request URL进行对比。服务会根据这个值决定是否允许其跨域。

如果服务器允许跨域,需要在返回的响应头中携带下面信息:
    Access-Control-Allow-Origin: http://manage.leyou.com
    Access-Control-Allow-Credentials: true
    Content-Type: text/html; charset=utf-8

Access-Control-Allow-Origin:可接受的域,是一个具体域名或者*(代表任意域名)
Access-Control-Allow-Credentials:是否允许携带cookie,默认情况下,cors不会携带cookie,除非这个值是true。
要想操作cookie,需要满足3个条件

  •     服务的响应头中需要携带Access-Control-Allow-Credentials并且为true。
  •     浏览器发起ajax需要指定withCredentials 为true
  •     响应头中的Access-Control-Allow-Origin一定不能为*,必须是指定的域名

3.6.2.2 复杂请求

预检请求(preflight):特殊请求会在正式通信之前,增加一次HTTP查询请求,称为"预检"请求。

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

一个“预检”请求的样板:

与简单请求相比,除了Origin以外,多了两个请求头:

Access-Control-Request-Method:接下来会用到的请求方式,比如PUT

Access-Control-Request-Headers:会额外用到的头信息

预检请求的响应

 

如果服务允许跨域,除了Access-Control-Allow-Origin和Access-Control-Allow-Credentials以外,这里又额外多出3个头:

Access-Control-Allow-Methods:允许访问的方式;

Access-Control-Allow-Headers允许携带的头;

Access-Control-Max-Age:本次许可的有效时长,单位是秒,过期之前的ajax请求就无需再次进行预检了。

如果浏览器得到上述响应,则认定为可以跨域,后续就跟简单请求的处理是一样的了。

3.6.3 重新注入CorsFilter解决跨域

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;

/**
 * 跨域:指浏览器不允许当前页面所在的源去请求另一个源的数据。(要理解当前页面源是什么?)
 * 同源策略(SOP Same origin policy):是一种约定,由Netscape公司1995年引入浏览器,
 * 它是浏览器最核心也是最基本的安全功能,如果缺少同源策略,浏览器很容易受到XSS、CSFR
 * 等攻击。所谓同源是指"协议+域名+端口"三者相同,即便两个不同的域名指向同一个ip,也非同源。
 */
@Configuration
public class LeyouCorsConfiguration {

    @Bean
    public CorsFilter corsFilter() {

        // 初始化cros配置对象
        CorsConfiguration corsConfiguration = new CorsConfiguration();
        // 允许跨域的域名,如果要携带cookie,不能写*。*:代表所有的域名都可以跨域访问
        // 可以添加多个域名(源:你页面所在的源)
        corsConfiguration.addAllowedOrigin("http://manage.leyou.com");

        // 允许携带cookie
        corsConfiguration.setAllowCredentials(true);

        // 允许跨域的请求方法,*代表所有方法。可以添加多个
        corsConfiguration.addAllowedMethod("*");

        // 允许跨域携带的头信息,*代表所有头。可以添加多个
        corsConfiguration.addAllowedHeader("*");

        // 本次许可的有效时间,单位秒,过期之前的ajax请求就无需再次进行预检啦
        // 默认是1800s,此处设置1h
        corsConfiguration.setMaxAge(3600L);

        // 初始化cors配置源对象
        UrlBasedCorsConfigurationSource urlBasedCorsConfigurationSource = new UrlBasedCorsConfigurationSource();
        // 拦截一切路径
        urlBasedCorsConfigurationSource.registerCorsConfiguration("/**", corsConfiguration);

        // 返回新的CorsFilter
        return new CorsFilter(urlBasedCorsConfigurationSource);
    }
}

 3.7 实现 WebMvcConfigurer#addCorsMappings 的方法

注意注意注意:addMapping()配置应用路径(server.servlet.context-path)是不能进行拦截的,拦截的而是Controller配置确切的路径。一般配置/**即可,即拦截所有Controller配置的路径 

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) {
        // 拦截的路径
        // 注意注意注意:addMapping()配置应用路径(server.servlet.context-path)是不能进行拦            
        // 截的,拦截的而是Controller配置确切的路径
        // 一般配置/**即可,即拦截所有Controller配置的路径
        registry.addMapping("/**")
                // 允许跨域的域名,*:代表所有。允许携带cookie不能为*
                .allowedOrigins("*")
                // 允许跨域的请求方法
                .allowedMethods("GET", "HEAD", "POST", "PUT", "DELETE", "OPTIONS")
                // 是否允许跨域携带cookie,为true允许跨域的域名需要指定,不能为*
                .allowCredentials(false)
                // 本次许可的有效时间,单位秒,过期之前的ajax请求就无需再次进行预检
                // 默认是1800s,此处设置1h
                .maxAge(3600)
                // 允许跨域携带的头信息,*代表所有头。可以添加多个
                .allowedHeaders("*");
    }
}

  3.8 创建一个 filter 解决跨域

import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.springframework.stereotype.Component;

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

@Component
@WebFilter(urlPatterns = { "/*" }, filterName = "headerFilter")
public class HeaderFilter implements Filter {

    private static final Logger logger = LoggerFactory.getLogger(HeaderFilter.class);

    @Override
    public void doFilter(ServletRequest servletRequest, ServletResponse servletResponse,
                         FilterChain filterChain) throws IOException, ServletException {
       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 {
        logger.info("-----------------cross origin filter start-------------------");
    }

    @Override
    public void destroy() {
        logger.info("-----------------cross origin filter end-------------------");
    }
}

3.9 @CrossOrigin注解

@CorssOrigin一个注解轻松解决,可以用在类上和方法。

### 解决Node.js中的问题 在Node.js中解决资源共享(CORS)问题可以通过多种方式实现。以下是两种常见的解决方案:一种是通过手动设置HTTP响应头,另一种是借助`cors`库。 #### 方法一:手动设置HTTP响应头 可以直接在Node.js的服务端代码中设置HTTP响应头来允许请求。这种方式适合简单的应用场景,不需要额外安装依赖库。以下是一个示例: ```javascript let http = require("http"); http.createServer(function (request, response) { // 设置 CORS 响应头,允许所有来源的请求 response.writeHead(200, { 'Access-Control-Allow-Origin': '*', // 允许任何名访问 'Access-Control-Allow-Methods': '*' // 允许所有的 HTTP 请求方法 }); response.write("Hello World\n"); response.end(); }).listen(8082); ``` 此代码片段展示了如何通过设置`Access-Control-Allow-Origin`和`Access-Control-Allow-Methods`头部字段来支持请求[^2]。 #### 方法二:使用`cors`库 对于更复杂的场景或者需要更多自定义选项的情况,推荐使用专门的`cors`库。该库提供了灵活的方式来管理CORS行为,并且易于集成到基于Express框架的应用程序中。 ##### 安装`cors`库 首先需要安装`cors`模块: ```bash npm install cors ``` ##### 配置并应用`cors` 下面是如何在Express应用程序中配置和使用`cors`的例子: ```javascript // 1. 引入必要的模块 const express = require('express'); const cors = require('cors'); // 引入 cors 库 const app = express(); // 2. 注册全局 cors 中间件 app.use(cors()); // 3. 配置路由 app.get('/get', function (req, res) { res.json({ message: 'This is a cross-origin request!' }); }); // 启动服务器监听指定端口 app.listen(3000, function () { console.log('Server running on port 3000...'); }); ``` 在这个例子中,我们引入了`cors`库并通过调用`app.use(cors())`将其作为中间件应用于整个Express应用实例。这样就可以自动处理来自客户端的所有请求[^4]。 如果希望进一步细化控制哪些特定的源可以访问资源,则可以在初始化`cors()`时传入选项对象来进行定制化配置。例如只允许某些特定网站发起请求而不是全部开放给互联网上的每一个站点。 --- ### 总结 无论是采用手动调整响应头的方式还是利用第三方插件如`cors`都可以有效地应对Node.js环境下的挑战。前者简单直接适用于小型项目;后者功能强大更适合大型复杂系统的构建需求。
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值