帧同步入门之日遭遇CORS(跨域资源共享)问题的暴击

前言

工作以来的每个游戏项目都是用的状态同步,所以一直想找时间看一下帧同步的实现细节,网上搜到了一些开源项目,有cocos的、有Unity的,有原生C#的,大多数项目作为入门资料来看都比较重,虽然定点数计算、UDP加速这些已经成为了帧同步策略的标配,但是对于一个初学者来说,这些内容的引入无疑增加了学习成本,所以我还是想找一款帧同步纯逻辑实现的代码,后来发现 LockstepDemo 这个项目还不错,那就从它开始学吧。

可以学习的帧同步项目

LockstepDemo

这个项目比较轻量,如何运行Readme中写的很清楚:

  • 服务端

    1
    2
    3
    $ cd server
    $ npm install
    $ node app.js
  • 客户端(也可使用任意方法启动一个web服务)

    1
    2
    $ cd client
    $ python3 -m http.server 8080
  • 启动完成后访问:http://127.0.0.1:8080/

大概的原理

利用node运行app.js监听3000作为服务器,然后启动监听8080端口的web服务,浏览器访问web页面作为客户端,来展示CS架构下的帧同步

打开两个浏览器都访问这个页面,登录后就匹配成功了,操作简单,演示方便,真是入门必备读物,但是到这里不出意外的情况下,要出意外了

我打开界面输入用户名之后提示我“连接服务器失败!”,真是醉了

查找问题

幸好我还有一段网站开发的经历,所以熟练的按下F12打开了浏览器的调试页面,看到了如下报错

Access to XMLHttpRequest at ‘http://127.0.0.1:3000/socket.io/?EIO=3&transport=polling&t=OyAd7iL‘ from origin ‘http://127.0.0.1:8080‘ has been blocked by CORS policy: No ‘Access-Control-Allow-Origin’ header is present on the requested resource.
GET http://127.0.0.1:3000/socket.io/?EIO=3&transport=polling&t=OyAd7iL net::ERR_FAILED 400 (Bad Request)

这个报错什么意思呢?就是说从’http://127.0.0.1:8080‘ 发起的原始请求在访问’http://127.0.0.1:3000‘ 时,其地址并不在访问资源允许的地址范围内,这个问题有个很常见的名字——跨域问题

好在前段时间做小游戏解决过跨域问题(其实第一次遇到这个问题在小游戏之前,先积累了经验才在小游戏中解决了这个问题),基本上知道朝哪个方向修改,所以一切还在可控范围之内,不过为了记录的全面一点,还是单独解释下跨域问题

CORS跨域问题

跨域问题的英文简写是 “CORS”,全称是 “Cross-Origin Resource Sharing”,是一种用于在不同源之间共享资源的机制。”源”是由协议(如 http 或 https)、主机名和端口号组成的组合,如果两个 URL 的这三个部分都相同,那么它们属于相同的源。

在 Web 开发中,浏览器会实施同源策略,这是一种安全机制,用于防止在一个网页的上下文中加载的资源(例如 JavaScript、CSS、图像等)去访问另一个不同源的资源。换句话说,网页只能访问与其自身相同源的资源,不能直接访问其他源的资源。

CORS 提供了一种机制,允许服务器声明哪些源的请求是被允许的,以及允许在响应中附加一些特定的头信息来告诉浏览器是否允许跨源请求。如果服务器允许跨源请求,浏览器将允许网页访问该资源,并将响应头中包含的额外信息传递给网页的 JavaScript。

跨域限制是浏览器行为,不是服务器行为(看你个人理解,服务器只是配合,可以什么都不做),通过代理服务器,或者其他工具发送请求就能轻松绕过。

因为浏览器使用门槛非常低,为了防止别有用心的人攻击普通用户,所以引入同源策略。服务器间没有跨域这种说法,使用和破解难度较大,就交给用户自己防备了。

浏览器的同源策略,对于不同源的站点之间的相互请求会做限制,具体限制了以下行为:

  • Cookie、LocalStorage 和 IndexDB 无法读取
  • DOM 和 JS 对象无法获取
  • Ajax请求发送不出去

跨域资源共享(CORS)的目的之一就是在允许跨源资源共享的同时保证安全性。如果不限制跨域,可能会引发以下安全问题:

  • 跨站脚本攻击(XSS): 恶意网站可以在用户访问时向另一个网站发送请求,并执行在受害者浏览器中执行恶意脚本,从而窃取用户的敏感信息、篡改页面内容或执行其他恶意操作。
  • 跨站请求伪造(CSRF): 恶意网站可以伪造请求,利用用户在其他网站上的登录状态,发送未经用户授权的请求,例如修改用户信息、发起转账等。
  • 信息泄露: 恶意网站可以发送跨域请求,并读取其他网站的敏感信息,例如 Cookie、本地存储、IndexedDB 中的数据,从而获取用户的个人信息或其他敏感数据。
  • 点击劫持: 恶意网站可以通过嵌入透明的 iframe 将另一个网站的页面覆盖在自己的页面上,并诱使用户在不知情的情况下点击覆盖的页面上的按钮或链接,执行未经用户授权的操作。
  • 缓存投毒: 恶意网站可以通过发送恶意请求并利用缓存投毒技术来污染浏览器的缓存,从而在用户之后访问相同资源时执行恶意代码。
  • 数据冒充和篡改: 恶意网站可以发送伪造的请求,并篡改或冒充其他网站的数据,例如修改在线购物网站的购物车内容。

说到这里可能还迷迷糊糊的,举个具体点的例子吧,如果觉得不对尽可以来喷,我改就是了:

假如你访问了一个恶意网站,如果没有同源策略,恶意网站可以向一个你已经登录的敏感信息上网站发送http请求,因为带着你的cookie信息,可以直接获取敏感数据,但有同源策略,这种行为就被浏览器限制住了,在服务器端不需要做任何设置就能实现。

但是如果A网站和B网站是兄弟网站,B网站的资源允许A网站访问,那么就可以在B网站的服务器上将A网站域名配置成可以跨域访问的origin,这样就可以了

解决跨域问题

说了这么多,下面要开始解决问题了,从前面解释来看,这个问题应该在服务器上处理,也就是在 http://127.0.0.1:3000 上允许 http://127.0.0.1:8080 就行了

找到 LockstepDemo/server/app.js 文件,其中服务器部分是这样定义的

1
2
var server = require('http').Server()
var io = require('socket.io')(server)

改为如下写法,允许所有跨域请求,注意 origin 字段的值:

1
2
3
4
5
6
7
8
var server = require('http').Server()
var io = require("socket.io")(server, {
allowEIO3: true,
cors: {
origin: "*",
methods: ["GET", "POST"]
}
});

依旧报错

Access to XMLHttpRequest at ‘http://127.0.0.1:3000/socket.io/?EIO=3&transport=polling&t=OyAeQnU‘ from origin ‘http://127.0.0.1:8080‘ has been blocked by CORS policy: The value of the ‘Access-Control-Allow-Origin’ header in the response must not be the wildcard ‘*‘ when the request’s credentials mode is ‘include’. The credentials mode of requests initiated by the XMLHttpRequest is controlled by the withCredentials attribute.
GET http://127.0.0.1:3000/socket.io/?EIO=3&transport=polling&t=OyAeQnU net::ERR_FAILED 200 (OK)

根据CORS规范,当请求中包含凭据时,响应中的Access-Control-Allow-Origin头不能设置为通配符*,而必须指定允许请求的具体来源。

换句话说,如果你在客户端发起了带有凭据的请求(例如cookies或HTTP认证信息),服务器在响应中必须指定确切的来源,而不能使用通配符。

要解决这个问题,你需要在服务器端设置Access-Control-Allow-Origin头,将其设置为允许请求的确切来源。如果你的请求是来自特定的域(例如http://127.0.0.1:8080) ,那么你的服务器应该将Access-Control-Allow-Origin头设置为该域,修改如下:

1
2
3
4
5
6
7
8
var server = require('http').Server()
var io = require("socket.io")(server, {
allowEIO3: true,
cors: {
origin: "http://127.0.0.1:8080",
methods: ["GET", "POST"]
}
});

还是报错

Access to XMLHttpRequest at ‘http://127.0.0.1:3000/socket.io/?EIO=3&transport=polling&t=OyAgWOx‘ from origin ‘http://127.0.0.1:8080‘ has been blocked by CORS policy: The value of the ‘Access-Control-Allow-Credentials’ header in the response is ‘’ which must be ‘true’ when the request’s credentials mode is ‘include’. The credentials mode of requests initiated by the XMLHttpRequest is controlled by the withCredentials attribute.
socket.io.js:1456
GET http://127.0.0.1:3000/socket.io/?EIO=3&transport=polling&t=OyAgWOx net::ERR_FAILED 200 (OK)

这个错误表明在响应的Access-Control-Allow-Credentials头中设置了空字符串,而应该设置为true。这是因为当请求中包含凭据(例如使用了withCredentials属性),并且服务器允许使用凭据时,响应的Access-Control-Allow-Credentials头必须设置为true,修改如下:

1
2
3
4
5
6
7
8
9
var server = require('http').Server()
var io = require("socket.io")(server, {
allowEIO3: true,
cors: {
origin: "http://127.0.0.1:8080",
methods: ["GET", "POST"],
credentials: true // 设置为true以允许使用凭据
}
});

这次再刷新页面,控制台打印信息“Socket连接成功: 0LLd8Rm9mO3zbOkZAAAD”

这个问题算解决了,这里指定了 http://127.0.0.1:8080 可以跨域访问,如果不是通过web服务,而是使用浏览器直接打开index.html文件,地址为 file:///E:/WorkSpace/opensource/LockstepDemo/client/index.html,还会出现以下错误:

Access to XMLHttpRequest at ‘http://127.0.0.1:3000/socket.io/?EIO=3&transport=polling&t=OyAg_nn‘ from origin ‘null’ has been blocked by CORS policy: The ‘Access-Control-Allow-Origin’ header has a value ‘http://127.0.0.1:8080‘ that is not equal to the supplied origin.

相信你也知道怎么改了,把 “null” 加到允许列表就行了

1
2
3
4
5
6
7
8
9
var server = require('http').Server()
var io = require("socket.io")(server, {
allowEIO3: true,
cors: {
origin: ["http://127.0.0.1:8080", "null"],,
methods: ["GET", "POST"],
credentials: true // 设置为true以允许使用凭据
}
});

演示界面如下,还挺不错的,今天就先写到这,以后抽时间再分析代码

demo

总结

  • ookcode/LockstepDemo 项目可作为帧同步入门教程
  • 跨域限制是浏览器行为,不是服务器行为,服务器只是配合,可以什么都不做,想实现跨域可以让服务器参与配置
  • Access-Control-Allow-OriginAccess-Control-Allow-Origin 都是跨域问题处理中常见字段,但不同语言中字段赋值形式不同
  • 浏览器的同源策略是保护小白使用者的,在服务器间访问资源并不用遵守同源策略
  • 这次尝试有一个问题没解决,就是怎样允许所有的跨域行为,origin配置成 *不行,说是客户端带有认证信息,但是我没找到在哪
  • 客户端请求连接很简单 socket = io.connect('http://127.0.0.1:3000'),并未发现认证信息,有知道的大佬欢迎指出

==>> 反爬链接,请勿点击,原地爆炸,概不负责!<<==

不同的维度品不同的人生,你认为你还在,我看你已经无了~

2024-4-23 21:33:52

Albert Shi wechat
欢迎您扫一扫上面的微信公众号,订阅我的博客