跨域和状态管理 ✅
本章节深入探讨 HTTP 中的跨域资源共享、会话管理、客户端交互技术,帮助你全面理解现代 Web 应用的状态管理和安全机制。
什么是跨域资源共享 (CORS)?它用于解决什么问题?
答案
CORS(Cross-Origin Resource Sharing,跨域资源共享)是一种用于让浏览器绕过同源策略限制,实现跨域访问资源的机制。在浏览器端,JavaScript 的跨域请求必须要经过浏览器的同源策略限制,即只能向同一域名下的服务器发送请求,而不能向其它域名的服务器发送请求。CORS 提供了一种通过在服务端设置响应头的方式来实现浏览器端跨域请求的机制。
基本概念
-
预检请求(Preflight Request):在实际请求之前,浏览器会发送一个预检请求OPTIONS,来确认服务端是否接受实际请求。
-
简单请求(Simple Request):满足以下条件的请求为简单请求:
- 请求方法为GET、HEAD或POST
- HTTP头信息不超出Accept、Accept-Language、Content-Language、Content-Type、Last-Event-ID、DPR、Save-Data、Viewport-Width、Width
- Content-Type的值仅限于text/plain、multipart/form-data、application/x-www-form-urlencoded
-
非简单请求(Non-simple Request):不满足简单请求条件的请求。
-
CORS安全规则(CORS Safelisting Rules):指的是CORS中服务端响应的Access-Control-Allow-Origin,指定是否允许跨域请求的源。
-
withCredentials:指的是XMLHttpRequest中的一个属性,用于在请求中携带cookie信息。
-
暴露Header(Exposed Headers):在CORS响应中,Access-Control-Expose-Headers头用于暴露哪些响应头给客户端使用。
-
存储 Cookies(Cookie Storage):跨域请求中,浏览器默认不会发送cookie信息,需要在服务端设置Access-Control-Allow-Credentials和客户端设置withCredentials为true才能实现。
-
跨域请求中的安全问题(CORS Security Issues):CORS的出现,引入了一些安全问题,例如CSRF、XSS等,需要在开发中做好防范措施。
如何实现跨域请求
在 HTTP 请求中,使用了 CORS 标准头部来告诉浏览器该请求是跨域请求,并且在服务端设置 Access-Control-Allow-Origin 头部来允许指定的域名访问资源。
客户端 CORS 标准头部:
Origin
:表示请求来自哪个域名Access-Control-Request-Method
:表示请求的方法类型(比如 GET、POST 等)Access-Control-Request-Headers
:表示请求头中的额外信息(比如 Content-Type 等)
服务端返回的响应头部:
Access-Control-Allow-Origin
:表示允许的域名访问该资源,可以设置为*表示任何域名都可以访问Access-Control-Allow-Credentials
:表示是否允许浏览器携带 Cookie 和认证信息等,默认为 falseAccess-Control-Allow-Methods
:表示允许的请求方法类型Access-Control-Allow-Headers
:表示允许的请求头中的额外信息
通过在服务端设置这些头部,可以实现跨域请求的授权和安全验证。
预检请求的作用
答案
预检请求(Preflight Request)是CORS中的一种特殊请求,主要用于在实际请求之前,增加一次HTTP查询请求,以检查实际请求是否可以被服务器接受。
为什么需要预检请求
在CORS中,有些HTTP请求是简单请求(Simple Request),比如GET和POST请求,可以直接发送。而对于一些复杂请求,比如请求方法为PUT、DELETE、PATCH等,或者Content-Type类型为application/json、application/xml等,会在发送真正请求之前,增加一次HTTP查询请求,以便服务器能够知道是否允许该请求。这个查询请求就是预检请求,用来查询服务器是否支持该请求,并给出支持的条件。
预检请求的工作机制
-
预检请求包含的信息:预检请求中包含了一些额外的HTTP头信息,比如Origin、Access-Control-Request-Method、Access-Control-Request-Headers等,这些信息告诉服务器实际请求中会包含哪些信息,并请求服务器在实际请求中是否能够接受这些信息。
-
服务器响应处理:服务器接收到预检请求后,会根据请求头中的信息来判断是否允许实际请求。如果允许,会在响应头中加入一些额外的信息,比如Access-Control-Allow-Origin、Access-Control-Allow-Methods、Access-Control-Allow-Headers等,告诉浏览器实际请求可以被接受。
-
请求结果处理:如果不允许,则不会发送实际请求,而是直接返回一个错误响应。
预检请求的意义
- 安全性保障:确保只有被明确允许的跨域请求才能被执行
- 服务器保护:防止恶意的跨域请求对服务器造成损害
- 协商机制:让客户端和服务器在正式请求前进行"协商"
- 性能优化:避免发送可能被拒绝的复杂请求,节省网络资源
如何避免 CORS 中的安全问题
答案
在CORS中有一些安全问题,例如CSRF(跨站点请求伪造)攻击和CORS劫持。以下是避免这些问题的一些方法:
1. 防范CSRF攻击
- 使用CSRF令牌:使用CSRF令牌来验证请求,这样只有在正确的来源站点上发出的请求才会被视为有效请求。
- 验证来源:严格验证请求的来源,确保请求来自可信的源。
- 状态检查:在敏感操作前进行状态检查和用户身份验证。
2. 防范CORS劫持
- 精确设置Access-Control-Allow-Origin:在响应中添加Access-Control-Allow-Origin标头,并设置为信任的站点,避免使用通配符"*"。
- 使用Content-Security-Policy:也可以使用Content-Security-Policy标头来限制JavaScript的执行。
- 域名白名单:维护明确的域名白名单,只允许列表中的域名进行跨域访问。
3. 敏感数据保护
- 避免传输敏感凭据:永远不要在CORS请求中使用敏感凭据(例如Cookie和HTTP身份验证信息),除非绝对必要且有充分的安全保障。
- 数据脱敏:对于必须跨域传输的数据,进行适当的脱敏处理。
4. 访问控制限制
- 限制跨域请求范围:只允许特定的来源站点进行跨域访问,不要过于宽泛。
- 方法限制:通过Access-Control-Allow-Methods精确控制允许的HTTP方法。
- 头部限制:通过Access-Control-Allow-Headers限制允许的请求头。
5. 基础安全防护
- 服务器安全措施:在服务器上使用防火墙和其他安全措施来保护应用程序。
- SSL/TLS加密:使用HTTPS确保数据传输过程中的安全性。
- HSTS策略:启用HTTP Strict Transport Security(HSTS)强制使用HTTPS。
6. 监控和审计
- 访问日志:记录和监控跨域请求的访问日志。
- 异常检测:建立异常请求检测机制。
- 定期审计:定期审计CORS配置和安全策略。
最佳实践建议
- 采用最小权限原则,只开放必要的跨域访问
- 定期更新和审核CORS策略
- 结合其他安全机制(如身份验证、授权等)
- 进行安全测试和渗透测试
总之,应该采取适当的安全措施来防止CORS相关的安全问题,确保Web应用的安全性。
Cookie 详解
答案
在 HTTP 协议中,Cookie 是一种包含在请求和响应报文头中的数据,用于在客户端存储和读取信息。Cookie 是由服务器发送的,客户端可以使用浏览器 API 将 Cookie 存储在本地进行后续使用。
Cookie 的组成部分
一个 Cookie 通常由以下几个部分组成:
- 名称:Cookie 的名称(键),通常是一个字符串
- 值:Cookie 的值,通常也是一个字符串
- 失效时间:Cookie 失效的时间,过期时间通常存储在一个
expires
属性中,以便浏览器自动清除失效的 Cookie - 作用路径:Cookie 的作用路径,只有在指定路径下的请求才会携带该 Cookie
- 作用域:Cookie 的作用域,指定了该 Cookie 绑定的域名,可以使用
domain
属性来设置
Cookie 设置示例
以下是一个设置了名称为 "user"、值为 "john"、失效时间为 2022 年 1 月 1 日,并且作用于全站的 Cookie:
Set-Cookie: user=john; expires=Sat, 01 Jan 2022 00:00:00 GMT; path=/; domain=example.com
其中,Set-Cookie
是响应报文头,用于设置 Cookie。在该响应报文中,将 Cookie 数据设置为 "user=john",失效时间为 "2022年1月1日",作用路径为全站,作用域为 "example.com" 的域名。
会话 Cookie 的失效时间
如果 Cookie 没有设置 max-age
,它通常被视为会话 Cookie,其失效时间的计算方式如下:
1. 浏览器关闭时失效
一般情况下,当用户关闭浏览器时,会话 Cookie 会被删除。这意味着只要浏览器处于打开状态,并且用户在与同一个网站进行交互,会话 Cookie 就会一直有效。
- 例如,你在浏览一个在线购物网站时,登录后服务器设置了一个未设置
max-age
的 Cookie 用于标识你的登录状态。只要你不关闭浏览器,这个 Cookie 就会一直有效,让你在不同页面之间切换时保持登录状态。但是一旦你关闭浏览器,再次打开并访问该网站时,你可能需要重新登录。
2. 特殊情况处理
有些浏览器可能会在一段时间内保留会话 Cookie,以便在用户重新打开浏览器时恢复会话状态。然而,这种行为并不是标准的,不同的浏览器可能有不同的处理方式。
Cookie 的跨域限制
默认情况下,Cookie 不能在不同的顶级域名之间共享数据。
但是,如果两个域名属于同一主域名下的子域名,并且您设置了正确的 Domain
属性,那么在这些子域名之间是可以共享 Cookie 的。
示例:
- 对于
sub1.example.com
和sub2.example.com
这样的子域名,如果设置 Cookie 的Domain
属性为.example.com
,那么在这两个子域名之间,这个 Cookie 是可以共享和访问的。 - 然而,如果是完全不同的顶级域名,如
example.com
和anotherdomain.com
之间,Cookie 是不能直接共享的。
安全注意事项
-
安全性考虑:由于会话 Cookie 在浏览器关闭时会被删除,相对来说比较安全。但是,如果在公共计算机上使用会话 Cookie,仍然存在被他人获取敏感信息的风险。
-
功能影响:对于依赖会话 Cookie 来保持用户状态或提供个性化体验的网站,用户关闭浏览器后可能会导致一些功能失效。
-
属性影响:还需要注意 Cookie 的
Path
属性、安全属性(Secure
)、HttpOnly
属性等,这些属性也会影响 Cookie 的使用范围和方式。
站点如何保持登录状态
答案
虽然 HTTP 是无状态协议,但可以通过以下几种方式来保持登录状态:
一、Cookie
1. 工作原理
- 当用户成功登录后,服务器在响应中设置一个 Cookie,通常包含用户的身份标识、会话信息等。
- 客户端(浏览器)会存储这个 Cookie,并在后续的请求中自动将其发送给服务器。
- 服务器通过检查 Cookie 中的信息来识别用户并确定其登录状态。
2. 实现示例
- 用户登录时,服务器生成一个唯一的会话 ID,并将其存储在数据库中,同时在响应中设置一个名为"session_id"的 Cookie,值为该会话 ID。
- 后续请求中,浏览器自动发送包含"session_id"的 Cookie,服务器根据这个会话 ID 查找对应的用户信息,从而确定用户已登录。
二、Session
1. 结合 Cookie 使用
- 服务器端创建一个会话(Session)对象来存储用户的登录状态和其他相关信息。
- 与 Cookie 类似,服务器在用户登录成功后设置一个包含会话 ID 的 Cookie,客户端在后续请求中携带这个 Cookie。
- 服务器根据会话 ID 查找对应的 Session 对象,以确定用户的登录状态。
2. 优点
相比直接使用 Cookie 存储用户信息,Session 更加安全,因为敏感信息存储在服务器端,而不是在客户端的 Cookie 中。
三、Token(令牌)
1. JWT(JSON Web Token)
- 用户登录成功后,服务器生成一个包含用户信息和签名的 JWT 令牌,并将其返回给客户端。
- 客户端在后续请求中,将 JWT 令牌作为请求头或参数发送给服务器。
- 服务器通过验证令牌的签名和有效性来确定用户的登录状态。
2. 优点
- 无状态:服务器不需要存储会话信息,只需要验证令牌的有效性,因此可以轻松地进行水平扩展。
- 跨域支持:JWT 令牌可以在不同的域之间传递,适用于前后端分离的架构。
四、HTTP 基本认证和摘要认证
1. 基本认证
- 客户端在请求中包含用户名和密码,经过 Base64 编码后作为请求头的一部分发送给服务器。
- 服务器验证用户名和密码的正确性,如果正确则认为用户已登录。
- 缺点是密码以明文形式传输(虽然经过 Base64 编码,但仍然可以被轻易解码),不安全。
2. 摘要认证
- 是对基本认证的改进,通过使用哈希函数对密码进行加密,提高了安全性。
- 但仍然存在一些安全风险,并且在每次请求中都需要发送用户名和密码的哈希值,不够便捷。
最佳实践建议
- 安全性优先:优先使用 HTTPS 来保护数据传输
- 会话管理:合理设置会话超时时间
- 令牌安全:对于 JWT,注意密钥管理和令牌刷新策略
- 多因素认证:在敏感场景中考虑使用多因素认证
- 监控和日志:记录登录行为,便于安全审计
Session 会话管理详解
答案
Session 是服务器端用来跟踪用户会话状态的机制,它在 Web 应用中扮演着重要角色。
Session 的基本概念
Session 是服务器为每个用户分配的一个唯一标识,用来在多个 HTTP 请求之间维持用户的状态信息。由于 HTTP 协议本身是无状态的,Session 提供了一种在服务器端存储用户信息的方式。
Session 的工作原理
-
Session 创建
- 当用户首次访问应用或登录时,服务器为该用户创建一个新的 Session
- 服务器生成一个唯一的 Session ID(通常是一个随机字符串)
- 将 Session ID 通过 Cookie 发送给客户端
-
Session 存储
- 服务器将用户的状态信息(如登录状态、购物车内容、用户偏好等)存储在服务器端
- 存储方式可以是内存、文件、数据库或分布式缓存(如 Redis)
-
Session 识别
- 客户端在后续请求中通过 Cookie 自动发送 Session ID
- 服务器根据 Session ID 查找对应的 Session 数据
- 恢复用户的状态信息
Session 的存储方式
-
内存存储
- 优点:访问速度快
- 缺点:重启后数据丢失,不支持集群
-
文件存储
- 优点:持久化存储
- 缺点:I/O 开销大,不便于集群共享
-
数据库存储
- 优点:持久化,支持集群
- 缺点:数据库访问开销
-
分布式缓存(推荐)
- 优点:高性能,支持集群,持久化可选
- 缺点:需要额外的缓存服务器
Session 的生命周期管理
-
创建时机:用户首次访问或登录
-
销毁时机:
- 用户主动注销
- Session 超时
- 服务器重启(内存存储)
-
超时设置:
- 绝对超时:固定时间后过期
- 相对超时:无活动一定时间后过期
Session 安全性考虑
-
Session ID 安全
- 使用足够长的随机字符串
- 避免 Session ID 泄露
- 定期更换 Session ID
-
传输安全
- 使用 HTTPS 传输
- 设置 Cookie 的 HttpOnly 和 Secure 属性
-
存储安全
- 敏感信息加密存储
- 定期清理过期 Session
Session vs Cookie vs Token
特性 | Session | Cookie | Token |
---|---|---|---|
存储位置 | 服务器端 | 客户端 | 客户端 |
安全性 | 高 | 中等 | 高 |
服务器负载 | 高 | 低 | 低 |
跨域支持 | 需配置 | 受限 | 支持 |
扩展性 | 需要共享存储 | 好 | 好 |
实际应用建议
- 小型应用:可以使用基于内存的 Session
- 中型应用:建议使用数据库或文件存储
- 大型分布式应用:推荐使用 Redis 等分布式缓存
- 微服务架构:考虑使用无状态的 Token 方案
AJAX 工作原理
答案
AJAX(Asynchronous JavaScript and XML,异步 JavaScript 和 XML)是一种在不重新加载整个页面的情况下与服务器交换数据的技术。
AJAX 的核心概念
AJAX 不是一种新的编程语言,而是一种使用现有技术的新方法:
- JavaScript:用于处理用户交互和控制请求
- XMLHttpRequest 对象:用于与服务器进行异步通信
- DOM:用于动态更新页面内容
- CSS:用于改善用户界面
- XML/JSON:作为数据交换格式
AJAX 工作流程
-
创建 XMLHttpRequest 对象
const xhr = new XMLHttpRequest();
-
配置请求
xhr.open('GET', '/api/data', true);
xhr.setRequestHeader('Content-Type', 'application/json'); -
设置回调函数
xhr.onreadystatechange = function() {
if (xhr.readyState === 4 && xhr.status === 200) {
// 处理响应数据
const data = JSON.parse(xhr.responseText);
updatePage(data);
}
}; -
发送请求
xhr.send(JSON.stringify(requestData));
XMLHttpRequest 对象的关键属性
-
readyState:请求状态
- 0:请求未初始化
- 1:服务器连接已建立
- 2:请求已接收
- 3:请求处理中
- 4:请求已完成,响应已就绪
-
status:HTTP 状态码(200、404、500 等)
-
responseText:响应数据(文本格式)
-
responseXML:响应数据(XML 格式)
详细的 AJAX 学习参考:MDN AJAX Guide
JSONP 原理和回调过程详解
答案
JSONP(JSON with Padding)是一种跨域数据请求技术,它利用 <script>
标签不受同源策略限制的特性来实现跨域数据获取。
JSONP 的实现原理
JSONP 的实现原理是通过添加一个 <script>
标签,指定 src
属性为跨域请求的 URL,而这个 URL 返回的不是 JSON 数据,而是一段可执行的 JavaScript 代码,这段代码会调用一个指定的函数,并且将 JSON 数据作为参数传入函数中。
基本实现示例
假设我们从 http://example.org
域名下请求数据,我们可以通过在页面中添加如下代码实现 JSONP 请求:
function handleData(data) {
// 处理获取到的数据
console.log('接收到数据:', data);
}
const script = document.createElement('script');
script.src = 'http://example.org/api/data?callback=handleData';
document.head.appendChild(script);
服务器端响应
服务器端返回的数据将会被包装在回调函数中,例如:
handleData({ name: 'John', age: 30 });
获取 JSONP 响应结果的方法
有两种方式获取 JSONP 响应结果:
方法一:通过回调函数参数获取(推荐)
function handleResponse(data) {
console.log(data.name); // Alice
console.log(data.age); // 20
}
// 创建 script 标签
const script = document.createElement('script');
script.src = 'http://example.com/api?callback=handleResponse';
// 插入到文档中开始加载数据
document.body.appendChild(script);
方法二:通过全局变量获取
let responseData = null;
function handleResponse(data) {
responseData = data;
console.log('数据已接收:', responseData);
}
// 创建 script 标签
const script = document.createElement('script');
script.src = 'http://example.com/api?callback=handleResponse';
// script 标签加载完成后处理
script.onload = () => {
if (responseData) {
// 使用接收到的数据
processData(responseData);
}
// 清理:删除 script 标签
document.body.removeChild(script);
};
document.body.appendChild(script);
JSONP 的局限性
需要注意的是,使用 JSONP 时要注意安全问题,应该对返回的数据进行验证,避免接收到恶意代码。此外,JSONP 只能发送 GET 请求,无法发送 POST 请求,也无法使用 HTTP 请求头和请求体传递数据。