HTTP OPTIONS 预检请求详解
HTTP OPTIONS 预检请求详解
(一)一、基本概念
1.1.1 什么是预检请求?
OPTIONS 预检请求是浏览器在发送某些“非简单请求”前自动触发的安全检查机制,属于 CORS(跨源资源共享)规范 的核心组成部分。其本质是浏览器向目标服务器发起一次 OPTIONS 方法的请求,验证服务器是否允许当前跨源请求,以避免跨站请求伪造(CSRF)等安全风险。
2.1.2 触发条件(非简单请求)
浏览器仅对以下“非简单请求”触发预检:
• 请求方法非简单方法:使用 PUT、DELETE、CONNECT、OPTIONS、TRACE、PATCH 等非简单方法(简单方法为 GET、POST、HEAD)。
• 自定义请求头:请求头包含非标准头(如 X-Audit-Reason、X-Idempotency-Key 等)。
• 特殊 Content-Type:请求体的 Content-Type 不属于 application/x-www-form-urlencoded、multipart/form-data 或 text/plain(如 application/json、application/xml)。
(二)二、预检请求的工作流程
1.2.1 流程示意图
sequenceDiagram
Browser->>Server: 发送 OPTIONS 预检请求(检查是否允许跨源)
Server-->>Browser: 返回 CORS 策略响应(允许/拒绝)
alt 服务器允许访问
Browser->>Server: 发送实际请求(如 POST/PUT)
Server-->>Browser: 返回实际业务数据
else 服务器拒绝访问
Browser->>Browser: 拦截实际请求,抛出 CORS 错误
end
2.2.2 关键步骤说明
1. 预检请求发送:浏览器自动构造 OPTIONS 请求,携带跨源相关信息(如 Origin、Access-Control-Request-Method 等)。
2. 服务器响应验证:服务器返回 204 No Content 状态码,并通过 Access-Control-Allow-* 头声明允许的跨源策略。
3. 实际请求处理:若预检通过,浏览器发送实际业务请求;若拒绝,实际请求被拦截。
(三)三、预检请求的特征与响应
1.3.1 请求特征(浏览器自动添加)
• 方法:固定为 OPTIONS。
• 关键请求头:
Origin: http://frontend.example.com # 前端源地址(跨源检查核心)
Access-Control-Request-Method: POST # 实际请求将使用的方法
Access-Control-Request-Headers: x-audit-reason, x-idempotency-key # 实际请求携带的自定义头
2.3.2 服务器正确响应示例
服务器需返回以下关键头信息以允许跨源请求:
HTTP/1.1 204 No Content
Access-Control-Allow-Origin: http://frontend.example.com # 允许的前端源(或 * 表示所有)
Access-Control-Allow-Methods: GET, POST, PUT, DELETE # 允许的请求方法
Access-Control-Allow-Headers: x-audit-reason, x-idempotency-key # 允许的自定义头
Access-Control-Max-Age: 86400 # 预检结果缓存时间(秒,减少重复预检)
Vary: Origin # 告知缓存服务器根据 Origin 头区分响应
(四)四、案例问题分析:中文请求头导致预检失败
1.4.1 问题现象
当请求头(如 X-Audit-Reason)包含中文(非 ASCII 字符)时,预检请求失败,实际请求被浏览器拦截。
2.4.2 根本原因
1. HTTP 规范限制:根据 RFC 7230 规范,HTTP 请求头的字段值必须为 ASCII 字符(0x00-0x7F)。非 ASCII 字符(如中文)直接传输会违反规范,导致浏览器拒绝发送预检请求。
2. 浏览器安全策略:浏览器检测到非 ASCII 头值时,会认为请求不合法,直接拦截预检,不会发送到服务器。
(五)五、解决方案对比
|
方案 |
实施位置 |
核心逻辑 |
优点 |
缺点 |
|
前端编码 |
客户端(前端) |
对非 ASCII 头值进行 URL 编码(如 encodeURIComponent) |
符合 HTTP 规范,通用性强 |
需要修改前端代码 |
|
后端宽松处理 |
服务端 |
后端手动解码非 ASCII 头值(如 urllib.parse.unquote) |
无需前端改动 |
违反 HTTP 规范,可能被防火墙拦截 |
|
强制 UTF-8 配置 |
服务端(框架) |
通过框架配置(如 Django 的 DEFAULT_CHARSET)强制解析非 ASCII 头 |
一劳永逸 |
可能影响其他接口兼容性 |
推荐方案:优先选择 前端编码,既符合规范,又避免后端猜测编码方式。
(六)六、企业级最佳实践
1.6.1 前端统一编码规范
封装头值编码函数,确保所有非 ASCII 头值自动编码,避免遗漏。
// 工具函数:安全编码非 ASCII 头值
const encodeHeaderValue = (value) => {
// 检查是否为纯 ASCII 字符(0x00-0x7F)
const isAscii = /^[\x00-\x7F]*$/.test(value);
return isAscii ? value :
encodeURIComponent(value).replace(/%../g, (match) => match.toLowerCase()); // 统一小写编码
};
// 使用示例:发送含中文头的请求
fetch('https://api.example.com/data', {
method: 'POST',
headers: {
'X-Audit-Reason': encodeHeaderValue('测试安全头功能'), // 编码后:%e6%b5%8b%e8%af%95%e5%ae%89%e5%85%a8%e5%a4%b4%e5%8a%9f%e8%83%bd
'X-Idempotency-Key': 'unique-key-123'
},
body: JSON.stringify({ data: 'payload' })
});
2.6.2 后端 CORS 完整配置(以 Django 为例)
在服务端明确声明允许的跨源策略,并添加中间件处理编码头值。
# settings.py(Django 配置)
CORS_ALLOWED_ORIGINS = [
'http://frontend.example.com', # 允许的前端源
]
CORS_ALLOW_METHODS = [
'GET', 'POST', 'PUT', 'DELETE', # 允许的请求方法
]
CORS_ALLOW_HEADERS = [
'x-audit-reason', 'x-idempotency-key', # 允许的自定义头
'content-type', 'accept'
]
CORS_MAX_AGE = 86400 # 预检结果缓存 1 天(减少预检频率)
# middleware.py(解码非 ASCII 头中间件)
from urllib.parse import unquote
from django.utils.deprecation import MiddlewareMixin
class HeaderDecodingMiddleware(MiddlewareMixin):
def process_request(self, request):
# 解码自定义头(如 X-Audit-Reason)
for header in ['HTTP_X_AUDIT_REASON', 'HTTP_X_IDEMPOTENCY_KEY']:
if header in request.META:
encoded_value = request.META[header]
request.META[header] = unquote(encoded_value) # URL 解码
return None
3.6.3 监控与调试
在服务端添加日志监控,记录预检请求细节,方便排查问题。
# views.py(Django 视图)
import logging
logger = logging.getLogger(__name__)
class DataAPIView(APIView):
def options(self, request, *args, **kwargs):
# 记录预检请求的源、方法、头信息
logger.info(f"预检请求 - Origin: {request.META.get('HTTP_ORIGIN')}")
logger.info(f"预检请求 - 期望方法: {request.META.get('HTTP_ACCESS_CONTROL_REQUEST_METHOD')}")
logger.info(f"预检请求 - 期望头: {request.META.get('HTTP_ACCESS_CONTROL_REQUEST_HEADERS')}")
return super().options(request, *args, **kwargs)
(七)七、关键结论
1. 预检是强制安全机制:无法绕过预检请求,必须通过服务器正确响应 Access-Control-* 头来允许跨源。
2. 非 ASCII 头必须编码:HTTP 规范要求头值为 ASCII 字符,非 ASCII 字符需通过 URL 编码传输(如 encodeURIComponent)。
3. 推荐前端编码方案:前端编码既符合规范,又减少后端复杂度,是最通用的解决方案。
4. 生产环境需完善配置:结合 CORS 策略配置、中间件解码和日志监控,确保系统支持多语言头值并保持安全性。
通过以上实践,系统可可靠处理跨源请求,兼容多语言头值,同时满足安全性和性能要求。
浙公网安备 33010602011771号