为什么会出现跨域?
跨域(Cross-Origin Resource Sharing,简称 CORS)是一种安全策略,用于限制一个域的网页如何与另一个域的资源进行交互。这是浏览器实现的同源策略(Same-Origin Policy)的一部分,旨在防止恶意网站通过一个域的网页访问另一个域的敏感数据。
由于浏览器实施的同源策略(Same Origin Policy),这是一种基本的安全协议,它确保了浏览器的稳定运行。没有同源策略,浏览器的许多功能可能无法正常工作。整个Web体系建立在同源策略之上,浏览器是这一策略的具体实现。该策略禁止来自不同域的JavaScript脚本与另一个域的资源进行交互。所谓同源,指的是两个页面必须具有相同的协议(protocol)、域名(host)和端口号(port)。
如何判断是否跨域?
当一个请求url的协议、域名、端口三者之间任意一个与当前页面url不同即为跨域
当前页面url 被请求的url 是否跨域 原因 http://www.test.com http://www.test.com/api/test 否 同源(协议、域名、端口号相同) http://www.test.com https://www.test.com 是 协议不同(http/https) http://www.test.com http://www.baidu.com 是 主域名不同(test/baidu) http://www.test.com http://blog.test.com 是 子域名不同(www/blog) http://www.test.com:8080 http://www.test.com:8081 是 端口号不同(8080/8081)
跨域解决方案
方案一:JSONP
特点:兼容性好(兼容低版本IE浏览器),但缺点是只支持GET请求不支持POST等其他类型的请求,所以在新项目中,尽量使用CORS或者代理服务器,JSONP仅作为兼容旧系统的备选
原理:浏览器的同源策略禁止跨域ajax请求,但是允许加载跨域脚本(如script标签),JSONP利用这一特性,将数据请求伪装成脚本加载
实现步骤
- 客户端定义一个全局函数(如
handleResponse) - 生成一个调用服务器端的脚本,将此函数名作为脚本路径src值的参数(如
callback=handleResponse)发送给服务器 - 服务器将数据包裹在该函数调用中返回(如
handleResponse({"name": "zhangsan"})) - 客户端执行第2步的脚本时,自动触发回调函数处理数据
- 客户端定义一个全局函数(如
示例代码
客户端
jsfunction handleResponse(data) { alert(`当前姓名:${data.name}`); } const script = document.createElement("script"); script.src = `https://api.com/getName?其他参数&callback=handleResponse`; document.body.appendChild(script);服务器端(Node.js为例)
jsconst 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);
安全性问题的解决
CSRF攻击
说明:攻击者可以伪造一个钓鱼网站,让用户向目标服务器的JSONP接口发送请求,收集服务端的敏感信息
解决方案:
- 服务器端校验JSONP的调用来源(
Referer)是否是白名单 + 临时token校验 - 同时也可以设置Cookie的SameSite属性,限制跨站请求携带Cookie,这里不作演示
- 服务器端校验JSONP的调用来源(
代码示例
客户端
jsfunction 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}×tamp=${timestamp}`; document.body.appendChild(script);服务器端(Node.js为例)
jsconst 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);
XSS漏洞
- 说明:既然JSONP的请求可以是 "https://api.com/getName?allback=handleResponse" 格式的,那么用户也可以请求 "https://api.com/getName?allback=script标签脚本",如果没有对callback方法参数进行字符转义,就是XSS了
- 解决方案:对callback方法参数进行字符转义
方案二:CORS
原理:通过设置HTTP响应头
Access-Control-Allow-Origin告诉浏览器允许哪些请求的域名可以跨过同源策略nodejs解决跨域
方式一:node.js使用中间件cors实现允许所有源跨域
shellnpm i cors -Sjs// 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"; } }方式五:自定义过滤器
javaimport 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,移动应用,桌面应用)
不受同源策略的限制。因为这些场景不涉及浏览器环境,无需安全隔离
参考文档