Skip to content

为什么会出现跨域?

  • 跨域(Cross-Origin Resource Sharing,简称 CORS)是一种安全策略,用于限制一个域的网页如何与另一个域的资源进行交互。这是浏览器实现的同源策略(Same-Origin Policy)的一部分,旨在防止恶意网站通过一个域的网页访问另一个域的敏感数据。

    由于浏览器实施的同源策略(Same Origin Policy),这是一种基本的安全协议,它确保了浏览器的稳定运行。没有同源策略,浏览器的许多功能可能无法正常工作。整个Web体系建立在同源策略之上,浏览器是这一策略的具体实现。该策略禁止来自不同域的JavaScript脚本与另一个域的资源进行交互。所谓同源,指的是两个页面必须具有相同的协议(protocol)、域名(host)和端口号(port)。

如何判断是否跨域?

跨域解决方案

  • 方案一:JSONP

    • 特点:兼容性好(兼容低版本IE浏览器),但缺点是只支持GET请求不支持POST等其他类型的请求,所以在新项目中,尽量使用CORS或者代理服务器,JSONP仅作为兼容旧系统的备选

    • 原理:浏览器的同源策略禁止跨域ajax请求,但是允许加载跨域脚本(如script标签),JSONP利用这一特性,将数据请求伪装成脚本加载

    • 实现步骤

      1. 客户端定义一个全局函数(如handleResponse
      2. 生成一个调用服务器端的脚本,将此函数名作为脚本路径src值的参数(如callback=handleResponse)发送给服务器
      3. 服务器将数据包裹在该函数调用中返回(如handleResponse({"name": "zhangsan"})
      4. 客户端执行第2步的脚本时,自动触发回调函数处理数据
    • 示例代码

      1. 客户端

        js
        function handleResponse(data) {
          alert(`当前姓名:${data.name}`);
        }
        
        const script = document.createElement("script");
        script.src = `https://api.com/getName?其他参数&callback=handleResponse`;
        document.body.appendChild(script);
      2. 服务器端(Node.js为例)

        js
        const http = require('http');
        const url = require('url');
        
        http.createServer((req, res) => {
          const { query } = url.parse(req.url, true);
          const data = JSON.stringify({ name: 'zhangsan' });
          res.writeHead(200, {'Content-Type': 'application/javascript'});
          res.end(`${query.callback}(${data})`);
        }).listen(3000);
    • 安全性问题的解决

      1. CSRF攻击

        • 说明:攻击者可以伪造一个钓鱼网站,让用户向目标服务器的JSONP接口发送请求,收集服务端的敏感信息

        • 解决方案:

          • 服务器端校验JSONP的调用来源(Referer)是否是白名单 + 临时token校验
          • 同时也可以设置Cookie的SameSite属性,限制跨站请求携带Cookie,这里不作演示
        • 代码示例

          • 客户端

            js
            function handleResponse(data) {
              alert(`当前姓名:${data.name}`);
            }
            
            // 共享秘钥,与服务器端一致
            const SECRET_KEY = "qnmdmyy";
            // 时间戳
            const timestamp = Date.now().toString();
            // 组合时间戳和密钥,生成签名
            const hash = crypto.createHash("sha256");
            hash.update(timestamp + SECRET_KEY);
            const token = hash.digest("hex"); // 返回十六进制哈希值
            // 执行脚本
            const script = document.createElement("script");
            script.src = `https://api.com/getName?其他参数&callback=handleResponse&token=${token}&timestamp=${timestamp}`;
            document.body.appendChild(script);
          • 服务器端(Node.js为例)

            js
            const http = require('http');
            const url = require('url');
            const crypto = require('crypto');
            
            // 共享密钥,与客户端一致
            const SECRET_KEY = "qnmdmyy";
            // 允许的临时token有效期
            const ALLOWED_TIME_WINDOW = 5 * 60 * 1000; // 5分钟
            // Referer白名单
            const ALLOWED_WHITE_LIST = ["https://www.trusted-site.com"];
            
            http.createServer((req, res) => {
                const { query } = url.parse(req.url, true);
                const { callback, token, timestamp } = query;
                const referer = req.headers.referer || "";
                
                // 1. 校验 Referer
                const isRefererValid = ALLOWED_WHITE_LIST.some(domain => referer.startsWith(domain));
            
                
                // 2. 校验时间戳
                const currentTime = Date.now();
                const requestTime = parseInt(timestamp, 10);
                if (isNaN(requestTime)) {
                    res.end(`${callback}({ error: "无效时间戳" })`);
                    return;
                }
                if (Math.abs(currentTime - requestTime) > ALLOWED_TIME_WINDOW) {
                	res.end(`${callback}({ error: "请求已过期" })`);
                    return;
              	}
                
                // 3. 校验临时token的有效性
                const expectedSignature = crypto
                                            .createHash('sha256')
                                            .update(timestamp + SECRET_KEY)
                                            .digest('hex');
                if (signature !== expectedSignature) {
                    res.end(`${callback}({ error: "签名验证失败" })`);
                    return;
                }
                
            	// 3. 返回数据
                const data = JSON.stringify({ name: 'zhangsan' });
                res.writeHead(200, {'Content-Type': 'application/javascript'});
                res.end(`${query.callback}(${data})`);
            }).listen(3000);
      2. XSS漏洞

  • 方案二:CORS

    • 原理:通过设置HTTP响应头 Access-Control-Allow-Origin 告诉浏览器允许哪些请求的域名可以跨过同源策略

    • nodejs解决跨域

      • 方式一:node.js使用中间件cors实现允许所有源跨域

        shell
        npm i cors -S
        js
        // app.js
        const express = require('express');
        const cors = require('cors');
        const = app = express();
        
        // 为所有源启用跨域
        app.use(cors());
        
        app.listen(81, function() {
            console.log('服务端启动成功...')
        });
      • 方式二:使用路由拦截实现允许所有源跨域

        js
        // app.js
        const express = require('express');
        const cors = require('cors');
        const = app = express();
        
        // 解决跨域
        app.all('*', function (req, res, next) {
            // 设置允许跨域的域名, *代表任意域名
            res.header('Access-Control-Allow-Origin', '*');
            // 允许的请求头
            res.header('Access-Control-Allow-Headers', 'Content-Type');
            // 跨域允许的请求方式, *代表任意方式
            res.header('Access-Control-Allow-Methods', '*');
            res.header('Content-Type', 'application/json;charset=utf-8');
            next()
        })
        
        app.listen(81, () => {
          console.log('服务端启动成功...')
        })
    • springboot解决跨域

      • 方式一:返回新的CorsFilter**(全局跨域)**

        java
        @Configuration
        public class GlobalCorsConfig {
            @Bean
            public CorsFilter corsFilter() {
                // 1. CORS配置信息
                CorsConfiguration config = new CorsConfiguration();
                // 设置允许跨域的域名
                config.addAllowedOrigin("*");
                // 是否发送Cookie
                config.setAllowCredentials(true);
                // 跨域允许的请求方式
                config.addAllowedMethod("*");
                // 允许的请求头
                config.addAllowedHeader("*");
                // 暴露哪些头部信息 高版本spring-web5.2.4不需要此项
                config.addExposedHeader("*");
                
                // 2. 添加映射路径
                UrlBasedCorsConfigurationSource corsConfigurationSource = new UrlBasedCorsConfigurationSource();
                corsConfigurationSource.registerCorsConfiguration("/**",config);
                
                // 3. 返回新的CorsFilter
                return new CorsFilter(corsConfigurationSource);
            }
        }
      • 方式二:重写WebMvcConfigurer**(全局跨域)**

        java
        @Configuration
        public class CorsConfig implements WebMvcConfigurer {
            @Override
            public void addCorsMappings(CorsRegistry registry) {
                registry.addMapping("/**")
                    	// 是否发送Cookie
                        .allowCredentials(true)
                    	// 设置允许跨域的域名
                        .allowedOrigins("http://localhost:8080")
                    	// 跨域允许的请求方式
                        .allowedMethods(new String[]{"GET", "POST", "PUT", "DELETE"})
                    	// 允许的请求头
                        .allowedHeaders("*")
                    	// 暴露哪些头部信息
                        .exposedHeaders("*");
            }
        }
      • 方式三:使用@CrossOrgin注解**(局部跨域)**

        java
        // 应用到类上,表示该类的所有方法都允许跨域
        @RestController
        @CrossOrigin(origins = "*")
        public class HelloController {
            @GetMapping("/hello")
            public String hello() {
                return "hello world";
            }
        }
        java
        // 只某个方法允许跨域
        @RestController
        public class HelloController {
            @GetMapping("/hello")
            @CrossOrigin(origins = "*")
            public String hello() {
                return "hello world";
            }
        }
      • 方式四:手动设置响应头**(局部跨域)**

        java
        @RestController
        public class HelloController {
            @GetMapping("/hello")
            public String hello(HttpServletResponse response) {
                response.addHeader("Access-Allow-Control-Origin","*");
                return "hello world";
            }
        }
      • 方式五:自定义过滤器

        java
        import org.springframework.stereotype.Component;
        import javax.servlet.*;
        import javax.servlet.http.HttpServletResponse;
        import java.io.IOException;
        
        @WebFilter(filterName = "CorsFilter")
        @Configuration
        public class CoresFilter implements Filter {
            @Override
            public void init(FilterConfig filterConfig) throws ServletException {
        
            }
        
            @Override
            public void destroy() {
        
            }
        
            @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, GET, OPTIONS, DELETE, HEAD");
                response.setHeader("Access-Control-Max-Age", "3600");
                response.setHeader("Access-Control-Allow-Headers", "access-control-allow-origin, authority, content-type, version-info, X-Requested-With");
                filterChain.doFilter(servletRequest, servletResponse);
            }
        }
      • 注意:局部跨域会覆盖全局跨域的规则!!!

    • CORS各项配置解释

      配置项说明
      Access-Control-Allow-Origin允许哪些域名访问资源,如果值为 * 时,不能与 Access-Control-Allow-Credentials: true 同时使用
      Access-Control-Allow-Methods允许客户端使用的HTTP方法,值为用逗号分隔的方法列表(如res.header("Access-Control-Allow-Methods", "GET, POST, PUT, DELETE"); )
      Access-Control-Allow-Headers服务端通过此项告知浏览器客户端可以携带哪些请求头,可以是常见的(Content-Type, Authorization),也可以是自定义的(如 test-token)
      Access-Control-Allow-Credentials是否允许客户端携带凭据(如 Cookie、HTTP 认证信息),默认是false,若为true,则Access-Control-Allow-Origin必须指定具体的域名
      Access-Control-Expose-Headers允许客户端访问的响应头(默认只能访问简单响应头:Cache-Control, Content-Language, Content-Type, Expires, Last-Modified, Pragma
      Access-Control-Max-Age预检请求(OPTIONS 请求)的缓存时间(单位:),减少重复预检
      Vary 头告知浏览器响应内容会根据 Origin 头的不同而变化,避免缓存问题(如res.header("Vary", "Origin"); )
  • WebSocket

    • 原理:Websocket 是 HTML5 的一个持久化的协议,它实现了浏览器与服务器的全双工通信,同时也是跨域的一种解决方案。WebSocket 和 HTTP 都是应用层协议,都基于 TCP 协议。但是 WebSocket 是一种双向通信协议,在建立连接之后,WebSocket 的 服务器与 客户端都能主动向对方发送或接收数据。同时,WebSocket 在建立连接时需要借助 HTTP 协议,连接建立好了之后 client 与 server 之间的双向通信就与 HTTP 无关了。
  • nginx反向代理:nginx还没熟悉,暂时搁置

个人对跨域的理解

  • 跨域问题的核心是浏览器的同源策略,它限制了前端脚本和后端资源交互的能力
  • 后端服务之间的调用(如服务器A -> 服务器B)或者非浏览器环境(如Postman,curl,移动应用,桌面应用)不受同源策略的限制。因为这些场景不涉及浏览器环境,无需安全隔离

参考文档

MIT版权,未经许可禁止任何形式的转载