简介:跨域资源共享(CORS)是现代Web开发中的关键技术,用于解决浏览器同源策略限制下的跨域请求问题。本文通过一个基于Servlet实现的HTTPS跨域POST提交实例,详细讲解如何处理从HTTP页面向HTTPS服务器发起的跨域POST请求。内容涵盖CORS机制、预检请求(OPTIONS)处理、响应头配置、POST数据解析与返回结果封装,并提供对JSONP等兼容方案的支持说明。配套的TestWeb项目为开发者提供了可运行的实践案例,有助于深入理解HTTPS与Servlet在跨域场景下的协同工作原理。
1. 跨域资源共享(CORS)基本概念
同源策略是浏览器保障网络安全的核心机制,它限制了来自不同源的脚本对文档和资源的访问权限。然而,在现代Web应用开发中,前后端分离架构日益普及,前端页面常部署在独立域名下,需向后端API服务发起HTTP请求,由此引发大量跨域需求。跨域资源共享(Cross-Origin Resource Sharing, CORS)作为一种W3C标准,通过在HTTP响应头中添加特定字段,如 Access-Control-Allow-Origin 、 Access-Control-Allow-Methods 等,明确告知浏览器允许哪些外部源访问当前资源,从而实现安全可控的跨域通信。
HTTP/1.1 200 OK
Access-Control-Allow-Origin: https://frontend.example.com
Access-Control-Allow-Methods: POST, GET, OPTIONS
Access-Control-Allow-Headers: Content-Type, Authorization
上述响应头表明,仅允许来自 https://frontend.example.com 的前端发起指定类型的跨域请求。CORS机制根据请求是否包含自定义头或复杂数据类型,自动区分“简单请求”与需预先探测的“预检请求”(Preflight),后者通过发送 OPTIONS 方法确认服务器策略,确保安全性。对于HTTPS环境下的POST请求,还需结合SSL加密与CORS配置协同工作,防止混合内容(Mixed Content)被浏览器拦截,为后续在Servlet容器中构建安全可靠的跨域接口提供基础支撑。
2. 同源策略与跨域请求限制
浏览器作为现代Web应用的主要运行环境,其安全模型在很大程度上依赖于“同源策略”这一核心机制。该策略由Netscape Navigator 2.0首次引入,至今仍是防范跨站脚本攻击(XSS)、跨站请求伪造(CSRF)等常见Web安全威胁的基石。随着前后端分离架构、微服务部署和第三方集成场景的普及,开发者频繁面临资源跨域访问的需求。然而,浏览器出于安全考虑,默认禁止此类操作,由此引发了大量开发实践中的技术挑战。理解同源策略的本质及其对不同类型请求的具体限制,是构建安全、可靠跨域通信方案的前提。
2.1 浏览器同源策略的定义与作用
同源策略(Same-Origin Policy)是一种由浏览器强制执行的安全机制,用于隔离来自不同源(origin)的文档或脚本,防止恶意文档窃取敏感数据或冒充用户执行非法操作。所谓“源”,是由协议(scheme)、主机名(host)和端口(port)三部分组成的唯一标识。只有当两个URL在这三个维度上完全一致时,才被视为“同源”。任何一项不匹配——无论是从 http 切换到 https ,还是域名从 api.example.com 变为 www.example.com ,亦或是端口号由 8080 改为 9000 ——都会触发跨域行为,进而受到浏览器的严格管控。
2.1.1 协议、域名、端口三要素判定机制
判断两个资源是否同源的标准极为精确,必须同时满足以下三项条件:
- 协议相同 :如
https://与http://被视为不同源; - 域名相同 :
a.example.com与b.example.com属于子域差异,非同源; - 端口相同 :若未显式指定端口,则使用默认值(HTTP为80,HTTPS为443),否则需完全匹配。
例如,当前页面位于 https://admin.example.com:8443/dashboard.html ,则以下对比可帮助明确同源边界:
| 目标URL | 是否同源 | 原因分析 |
|---|---|---|
https://admin.example.com:8443/profile | ✅ 是 | 协议、域名、端口均一致 |
http://admin.example.com:8443/profile | ❌ 否 | 协议不同(HTTP vs HTTPS) |
https://api.example.com:8443/data | ❌ 否 | 域名不同(子域差异) |
https://admin.example.com:9000/status | ❌ 否 | 端口不同(8443 vs 9000) |
https://admin.example.com/dashboard | ✅ 是 | 默认端口匹配HTTPS 443?⚠️注意! |
⚠️ 特别说明:尽管
https://admin.example.com看似省略了端口,但实际连接的是443端口,而原始地址使用的是8443,因此仍属不同源。这一点常被忽视,导致线上调试失败。
为了更直观地展示判定流程,下图通过Mermaid语法描述了浏览器如何逐级判断两个URL是否同源:
graph TD
A[开始比较两个URL] --> B{协议相同?}
B -->|否| C[判定为不同源]
B -->|是| D{域名相同?}
D -->|否| C
D -->|是| E{端口相同?}
E -->|否| C
E -->|是| F[判定为同源]
上述流程清晰地揭示了同源判定的逻辑层级:只有当所有三个条件都通过后,浏览器才会允许跨文档交互。这种设计确保了即使攻击者控制了一个子域(如 evil.example.com ),也无法直接读取主站的数据,除非存在明确的跨域授权机制(如CORS)。
2.1.2 同源策略对DOM访问、Cookie传递及AJAX请求的约束
同源策略的影响范围广泛,主要体现在三个方面:DOM访问控制、Cookie与认证信息传递、以及XMLHttpRequest/Fetch API的网络请求限制。
DOM访问限制
JavaScript无法跨源访问另一个页面的DOM元素。例如,若主窗口加载了 https://a.com ,其中嵌套了一个 <iframe src="https://b.com"> ,则父页面脚本不能调用 iframe.contentDocument.getElementById() 来获取子页面内容,否则会抛出类似“Blocked a frame with origin… from accessing a cross-origin frame”的错误。
// 假设 iframe 指向不同源
const iframe = document.getElementById('external-frame');
try {
const innerDoc = iframe.contentDocument; // 抛出 SecurityError
} catch (e) {
console.error("无法访问跨域iframe内容:", e.message);
}
代码逻辑解析 :
第1行获取iframe元素引用;第3行尝试访问其contentDocument属性,但由于目标URL与当前页面不同源,浏览器阻止该操作并抛出异常。此机制有效防止了钓鱼页面通过iframe嵌套银行网站后窃取表单字段的行为。
Cookie与身份凭证传递
浏览器仅在同源请求中自动携带Cookie。当发起跨域AJAX请求时,默认情况下不会发送当前域的Cookie,除非显式设置 withCredentials=true ,且服务器响应头包含 Access-Control-Allow-Credentials: true 。这既是保护机制,也带来了登录态共享的复杂性。
fetch('https://api.example.com/user', {
method: 'GET',
credentials: 'include' // 显式要求携带Cookie
});
参数说明 :
-credentials: 'include':指示浏览器在跨域请求中附带凭据(Cookie、HTTP认证等);
- 若服务器未返回Access-Control-Allow-Credentials: true,浏览器将拒绝解析响应,即使HTTP状态码为200。
AJAX请求限制
使用 XMLHttpRequest 或 fetch() 向非同源地址发送请求时,浏览器会在预检阶段检查响应头是否包含合法的CORS头部。若缺少 Access-Control-Allow-Origin 或值不匹配,即使服务器成功处理请求,浏览器也会拦截响应体,并在控制台报错:“No ‘Access-Control-Allow-Origin’ header is present”。
fetch('https://other-domain.com/api/data')
.then(res => res.json())
.catch(err => console.error('跨域请求被阻断:', err));
执行逻辑说明 :
尽管该请求可能已到达服务器并获得JSON响应,但由于响应头未声明允许当前源访问,浏览器主动丢弃数据流,开发者只能看到失败提示。这是同源策略在网络层的体现,强调“信任必须由服务器明确授予”。
2.2 跨域请求的典型场景与安全风险
在真实的开发环境中,跨域问题并非理论假设,而是高频出现的技术痛点。尤其在前后端解耦、云原生部署和开放平台集成的趋势下,跨域已成为常态而非例外。然而,每一次绕过同源策略的操作,也都伴随着潜在的安全隐患,必须谨慎权衡便利性与安全性。
2.2.1 前后端分离项目中的跨域调用
现代前端框架(如React、Vue、Angular)通常独立部署于CDN或静态服务器(如 https://frontend.company.com ),而后端API运行在另一台服务器上(如 https://api.company.com )。此时,前端应用需通过AJAX向后端发起请求,形成典型的跨域通信场景。
+------------------+ AJAX Request +--------------------+
| | -------------------> | |
| Frontend App | | Backend API |
| https://fe.com | | https://api.com |
| | <------------------- | |
+------------------+ JSON Response +--------------------+
在此架构中,若后端未配置CORS策略,所有请求都将被浏览器拦截。解决方式包括:
- 后端添加 Access-Control-Allow-Origin: https://fe.com ;
- 使用反向代理统一入口(如Nginx将 /api/* 代理至后端);
- 开发阶段启用代理中间件(如Webpack DevServer的proxy选项)。
示例:Spring Boot中配置CORS
@Configuration
public class CorsConfig {
@Bean
public CorsFilter corsFilter() {
UrlBasedCorsConfigurationSource source = new UrlBasedCorsConfigurationSource();
CorsConfiguration config = new CorsConfiguration();
config.setAllowedOriginPatterns(Arrays.asList("https://frontend.company.com"));
config.setAllowedMethods(Arrays.asList("GET", "POST", "PUT", "DELETE"));
config.setAllowCredentials(true);
config.addAllowedHeader("*");
source.registerCorsConfiguration("/api/**", config);
return new CorsFilter(source);
}
}
逐行解读 :
1. 创建UrlBasedCorsConfigurationSource用于注册路径级别的CORS规则;
2. 构建CorsConfiguration对象,设定允许的源(支持通配符模式);
3. 指定允许的HTTP方法;
4. 允许携带凭证(Cookie);
5. 允许所有请求头;
6. 将规则绑定到/api/**路径;
7. 返回过滤器实例供Spring容器管理。
2.2.2 第三方API集成时的身份验证问题
当Web应用需要集成第三方服务(如微信登录、支付宝支付、地图SDK)时,往往涉及跨域身份验证。这类场景下,OAuth 2.0授权流程常采用重定向方式完成,但前端仍需通过AJAX获取access_token或用户信息,容易触发CORS限制。
例如,在调用微信JS-SDK时,前端需先请求自身后端获取 jsapi_ticket ,再发起跨域请求至 https://api.weixin.qq.com 。若直接从前端调用,除非微信服务器设置了适当的CORS头,否则请求会被拦截。
安全建议:
- 敏感接口(如获取token)应由后端代理调用,避免密钥暴露;
- 前端仅负责UI交互和结果展示;
- 使用短生命周期的临时凭证降低泄露风险。
2.2.3 恶意站点伪造请求带来的CSRF隐患
跨域限制虽能防数据窃取,却无法阻止某些类型的写操作。攻击者可利用用户的登录态,在恶意页面中构造隐藏表单或图像标签,诱导浏览器向目标站点发送请求,从而实现跨站请求伪造(CSRF)。
<!-- 攻击者页面 -->
<img src="https://bank.com/transfer?to=attacker&amount=1000" width="0" height="0">
当用户已登录网银且Cookie未过期时,该请求会携带有效会话凭证,可能导致资金被转移。
防御机制对比表:
| 防御手段 | 实现方式 | 有效性 | 缺陷 |
|---|---|---|---|
| CSRF Token | 服务端生成一次性令牌,前端提交时验证 | 高 | 增加前后端耦合度 |
| SameSite Cookie | 设置 Set-Cookie: session=abc; SameSite=Lax/Strict | 中高 | 不兼容旧版浏览器 |
| Referer Check | 服务端校验请求来源 | 中 | 可被篡改或屏蔽 |
| 双重提交Cookie | 将Token存入Cookie并在请求头中重复 | 高 | 需客户端支持 |
Set-Cookie: XSRF-TOKEN=abcdef123; Path=/; Secure; HttpOnly=false; SameSite=Strict
此Cookie设置使得前端可通过JavaScript读取
XSRF-TOKEN并放入自定义头X-XSRF-TOKEN,配合服务端验证,形成双重防护。
2.3 HTTPS与混合内容导致的跨域难题
随着网络安全意识提升,HTTPS已成为生产环境的标准配置。然而,这也带来了新的跨域挑战,尤其是在混合内容(Mixed Content)场景下,浏览器采取更为严格的拦截策略。
2.3.1 HTTP页面无法向HTTPS接口发起POST请求的限制
根据现代浏览器规范, 主动混合内容 (Mixed Active Content)——即HTTP页面加载HTTPS之外的脚本、CSS、XHR等资源——将被全面阻止。这意味着一个运行在 http://localhost:3000 的开发页面,无法向 https://api.example.com 发送POST请求。
sequenceDiagram
participant Browser
participant HTTP_Page as http://app.com
participant HTTPS_API as https://api.com
HTTP_Page->>HTTPS_API: POST /login
HTTPS_API-->>HTTP_Page: 200 OK + CORS headers
Browser->>Browser: 阻止响应解析!
Note right of Browser: 混合内容策略拒绝非安全上下文访问安全资源
即使API正确返回了
Access-Control-Allow-Origin: *,浏览器依然因“降级风险”而拒绝执行。
解决方案:
- 所有前端资源升级为HTTPS;
- 使用本地开发证书(如mkcert)模拟HTTPS环境;
- 配置反向代理统一协议。
2.3.2 浏览器对混合主动内容(Mixed Active Content)的拦截行为
Chrome等主流浏览器将混合内容分为两类:
| 类型 | 示例 | 处理方式 |
|---|---|---|
| 被动内容(Passive) | <img src="http://..."> | 警告但仍加载 |
| 主动内容(Active) | <script> , fetch() , POST form | 直接阻止 |
// 在HTTPS页面中执行
fetch('http://insecure-api.com/data', { method: 'POST' })
// 控制台输出:
// Mixed Content: The page at 'https://...' was loaded over HTTPS, but requested an insecure resource 'http://...'
// The request has been blocked.
此类错误不可通过CORS绕过,必须修复协议一致性。
2.3.3 SSL证书有效性对跨域通信的影响分析
即使协议一致,若SSL证书无效(自签名、过期、域名不匹配),浏览器仍将终止连接。此时不仅CORS失效,整个TLS握手都会失败。
常见错误类型:
| 错误现象 | 可能原因 | 排查方法 |
|---|---|---|
| NET::ERR_CERT_INVALID | 自签名证书未被信任 | 导入CA证书至系统信任库 |
| ERR_SSL_PROTOCOL_ERROR | TLS版本不兼容 | 检查服务器支持的TLS版本 |
| CERT_COMMON_NAME_INVALID | 证书CN与访问域名不符 | 使用SAN扩展或多域名证书 |
开发建议:
- 使用Let’s Encrypt等免费CA签发正式证书;
- 内部系统可搭建私有PKI并批量安装根证书;
- 避免在生产环境使用
ignore-certificate-errors等危险标志。
2.4 突破跨域限制的技术选型对比
面对跨域需求,开发者有多种技术路径可选。每种方案各有优劣,适用于不同的业务场景和技术栈。
2.4.1 CORS vs JSONP vs 代理服务器的优劣比较
| 方案 | 支持方法 | 数据格式 | 安全性 | 兼容性 | 维护成本 |
|---|---|---|---|---|---|
| CORS | 所有HTTP方法 | 任意 | 高(需服务器配置) | 现代浏览器 | 中 |
| JSONP | 仅GET | JavaScript函数调用 | 低(易受XSS影响) | IE6+ | 低 |
| 代理服务器 | 所有方法 | 任意 | 高(集中控制) | 无限制 | 高 |
CORS优势:
- W3C标准,原生支持;
- 支持复杂请求头和凭据;
- 可精细控制访问权限。
JSONP局限:
- 仅支持GET,无法上传文件;
- 回调函数暴露全局命名空间;
- 服务端需动态生成JS代码。
代理服务器优点:
- 完全规避浏览器限制;
- 可做请求改写、日志审计;
- 适合微服务网关场景。
2.4.2 各方案在安全性、兼容性与维护成本上的权衡
综合来看, CORS应作为首选方案 ,尤其适用于可控的服务端环境。对于遗留系统或仅需GET查询的场景,JSONP仍具实用价值。而在大型分布式系统中,借助Nginx或API Gateway实现反向代理,既能统一安全策略,又能简化前端逻辑。
最终选择应基于以下决策树:
graph LR
A[是否需要POST/PUT等方法?] -- 是 --> B[CORS or Proxy]
A -- 否 --> C{是否支持现代浏览器?}
C -- 是 --> B
C -- 否 --> D[JSONP]
B --> E{能否控制服务端配置?}
E -- 能 --> F[启用CORS]
E -- 不能 --> G[使用代理服务器]
无论采用何种方案,始终遵循最小权限原则:只允许可信源访问必要接口,定期审查CORS策略,避免使用 * 通配符开放全部域名。
3. POST请求的预检机制与OPTIONS处理
在现代Web应用开发中,前后端分离架构已成为主流模式。前端通过JavaScript发起AJAX请求调用部署在不同域名下的后端API服务时,浏览器会依据同源策略对跨域请求进行严格限制。虽然CORS(Cross-Origin Resource Sharing)标准为合法跨域通信提供了安全通道,但并非所有请求都能直接发送。对于某些特定类型的POST请求,浏览器会在正式请求前自动插入一个 预检请求(Preflight Request) ,以确认目标服务器是否允许该跨域操作。理解这一机制的工作原理、触发条件以及如何在Servlet容器中正确响应预检请求,是实现稳定跨域交互的关键环节。
3.1 简单请求与非简单请求的判断标准
浏览器根据请求的方法类型、请求头字段和请求体内容等特征,将跨域请求划分为“简单请求”和“非简单请求”。只有当请求满足一系列严格的条件时,才会被视为简单请求,从而跳过预检流程;否则,必须先执行一次OPTIONS请求作为预检。
3.1.1 方法类型(GET/POST/HEAD)与Content-Type限制
所谓“简单请求”,是指那些不会对服务器状态造成副作用、且数据格式受限的请求。W3C CORS规范定义了以下三个核心判定条件:
-
请求方法必须是以下之一 :
-GET
-POST
-HEAD -
只能包含被允许的CORS安全请求头字段 ,如:
-Accept
-Accept-Language
-Content-Language
-Content-Type(仅限于三种值):-
application/x-www-form-urlencoded -
multipart/form-data -
text/plain
-
-
XMLHttpRequest Upload对象未监听任何事件 (如progress、load等)
- 请求未使用ReadableStream读取请求体
若以上任一条件不满足,则视为非简单请求,需触发预检。
| 请求示例 | 是否为简单请求 | 原因 |
|---|---|---|
POST /api/login Content-Type: application/json | 否 | Content-Type超出允许范围 |
POST /upload Content-Type: multipart/form-data | 是 | 符合简单请求规范 |
PUT /user/123 | 否 | 方法不在GET/POST/HEAD范围内 |
GET /data X-Custom-Header: test | 否 | 包含自定义请求头 |
从上表可见,尽管 POST 本身属于简单方法,但一旦其 Content-Type 设置为 application/json (常用于JSON数据提交),即不再符合简单请求定义,必须经历预检过程。
实际场景中的典型问题
假设前端使用 fetch 向后端发送用户登录信息:
fetch('https://api.example.com/login', {
method: 'POST',
headers: {
'Content-Type': 'application/json'
},
body: JSON.stringify({ username: 'alice', password: 'secret' })
})
虽然这是一个常见的POST请求,但由于 Content-Type: application/json 不属于简单类型,浏览器将不会立即发送该请求,而是首先发送一个 OPTIONS 请求来探测服务器是否接受此类跨域操作。
3.1.2 自定义请求头触发预检的条件
除了 Content-Type 外,任何添加到请求中的 自定义请求头 (即不属于CORS安全列表的头部字段)都将强制触发预检机制。例如,在JWT认证系统中,前端通常需要携带身份令牌:
fetch('/api/profile', {
method: 'GET',
headers: {
'Authorization': 'Bearer eyJhbGciOiJIUzI1NiIs...'
}
})
尽管这是一个 GET 请求,理论上可以是简单的,但由于引入了 Authorization 这一非标准CORS头部(即使它是广泛使用的认证头),浏览器依然会发起预检请求。
预检触发逻辑流程图
graph TD
A[发起跨域请求] --> B{是否满足简单请求条件?}
B -- 是 --> C[直接发送实际请求]
B -- 否 --> D[自动发送OPTIONS预检请求]
D --> E[服务器返回CORS响应头]
E --> F{是否允许该请求?}
F -- 是 --> G[发送原始请求]
F -- 否 --> H[浏览器抛出CORS错误]
该流程清晰展示了浏览器在遇到非简单请求时的行为路径:必须先通过 OPTIONS 探针验证权限,才能继续执行主请求。这种设计确保了服务器对复杂或潜在危险的操作具有完全控制权。
关键参数说明
- Access-Control-Allow-Methods :服务器应在响应中明确列出允许的HTTP方法。
- Access-Control-Allow-Headers :声明客户端可使用的自定义请求头。
- Vary: Origin :建议配合使用,防止CDN缓存导致CORS策略错乱。
这些响应头将在后续章节详细讨论,但在判断是否进入预检阶段时,它们的作用尤为关键——浏览器正是基于这些信息决定是否放行原始请求。
3.2 预检请求(Preflight Request)的交互流程
当浏览器识别出某次跨域请求为非简单请求时,它不会立即发送原始请求,而是先发起一次 OPTIONS 方法的预检请求,目的是获取服务器对该类请求的许可策略。这个过程完全由浏览器自动完成,开发者无法绕过,也无法感知(除非查看网络面板)。
3.2.1 OPTIONS请求的自动发送时机与目的
预检请求的发送遵循以下规则:
- 仅针对跨域请求
- 仅当请求为非简单请求时触发
- 每次页面加载后首次符合条件的请求都会触发
- 可通过
Access-Control-Max-Age缓存结果,避免重复发送
其主要目的在于让服务器声明:
- 是否允许当前源(Origin)访问资源
- 是否支持请求所用的HTTP方法
- 是否接受请求中包含的自定义头部字段
以一个典型的RESTful API调用为例:
OPTIONS /api/users HTTP/1.1
Host: api.example.com
Origin: https://frontend.example.org
Access-Control-Request-Method: POST
Access-Control-Request-Headers: content-type, authorization
此请求表示:“来自 https://frontend.example.org 的脚本想要使用 POST 方法,并带上 content-type 和 authorization 头,请问你是否允许?”
服务器若同意,则应返回如下响应:
HTTP/1.1 204 No Content
Access-Control-Allow-Origin: https://frontend.example.org
Access-Control-Allow-Methods: POST, GET, OPTIONS
Access-Control-Allow-Headers: Content-Type, Authorization
Access-Control-Max-Age: 86400
此时浏览器确认权限无误,随即发出原始POST请求。
浏览器行为解析
值得注意的是, OPTIONS 请求并不会携带原始请求体(body),也不执行实际业务逻辑。它的唯一职责是“探路”。如果服务器未能正确响应此请求(如返回404、500或缺少必要CORS头),浏览器将中断整个流程并报错:
CORS preflight did not succeed
这意味着即使后端API功能正常,只要预检失败,前端也无法调用。
3.2.2 Access-Control-Request-Method与Access-Control-Request-Headers字段解析
预检请求中两个最关键的头部字段是:
| 头部名称 | 作用 |
|---|---|
Access-Control-Request-Method | 指明即将使用的HTTP方法(如PUT、DELETE) |
Access-Control-Request-Headers | 列出将要发送的所有自定义请求头,逗号分隔 |
这两个字段均由浏览器自动生成,无需开发者手动设置。
示例代码分析
考虑以下Java Servlet片段,用于处理预检请求:
@Override
protected void doOptions(HttpServletRequest req, HttpServletResponse resp) {
String origin = req.getHeader("Origin");
resp.setHeader("Access-Control-Allow-Origin", origin);
resp.setHeader("Access-Control-Allow-Methods", "GET, POST, PUT, DELETE, OPTIONS");
resp.setHeader("Access-Control-Allow-Headers", "Content-Type, Authorization");
resp.setHeader("Access-Control-Max-Age", "86400"); // 缓存24小时
resp.setStatus(HttpServletResponse.SC_NO_CONTENT); // 返回204
}
逐行逻辑解读:
-
String origin = req.getHeader("Origin");
获取请求来源,用于回写Allow-Origin,实现动态授权。 -
resp.setHeader("Access-Control-Allow-Origin", origin);
明确允许该源访问资源。注意:不能使用通配符*同时启用凭据传递。 -
resp.setHeader("Access-Control-Allow-Methods", "...");
声明支持的方法集,必须覆盖原始请求所需方法。 -
resp.setHeader("Access-Control-Allow-Headers", "Content-Type, Authorization");
列出自定义头白名单。若原始请求头未在此列,预检失败。 -
resp.setHeader("Access-Control-Max-Age", "86400");
设置预检结果缓存时间为86400秒(24小时),减少重复请求。 -
resp.setStatus(SC_NO_CONTENT);
正确做法是返回204 No Content,表示成功但无响应体。
表格:预检相关头部字段对照
| 请求头(客户端发送) | 响应头(服务器返回) | 说明 |
|---|---|---|
Origin | Access-Control-Allow-Origin | 控制哪些源可访问 |
Access-Control-Request-Method | Access-Control-Allow-Methods | 允许的HTTP方法 |
Access-Control-Request-Headers | Access-Control-Allow-Headers | 允许的自定义头 |
| —— | Access-Control-Max-Age | 预检结果缓存时间(秒) |
通过合理配置上述头部,服务器即可安全地授权复杂的跨域请求,同时保留对非法访问的拦截能力。
3.3 Servlet中拦截并响应OPTIONS请求
在基于Java Servlet的Web应用中,若未显式处理 OPTIONS 请求,Tomcat等容器可能默认返回 HTTP 405 Method Not Allowed ,导致预检失败。因此,必须通过过滤器或重写Servlet的 doOptions 方法,主动响应预检请求。
3.3.1 使用Filter统一处理预检请求
推荐做法是在全局 Filter 中统一处理CORS相关逻辑,包括预检响应。这样可避免每个Servlet重复编码。
@WebFilter("/*")
public class CorsFilter implements Filter {
@Override
public void doFilter(ServletRequest servletRequest,
ServletResponse servletResponse,
FilterChain chain) throws IOException, ServletException {
HttpServletRequest request = (HttpServletRequest) servletRequest;
HttpServletResponse response = (HttpServletResponse) servletResponse;
// 动态获取Origin
String origin = request.getHeader("Origin");
if (origin != null && !origin.isEmpty()) {
response.setHeader("Access-Control-Allow-Origin", origin);
}
// 若为预检请求,直接响应并终止链
if ("OPTIONS".equalsIgnoreCase(request.getMethod())) {
response.setHeader("Access-Control-Allow-Methods", "GET, POST, PUT, DELETE, OPTIONS");
response.setHeader("Access-Control-Allow-Headers", "Content-Type, Authorization");
response.setHeader("Access-Control-Max-Age", "86400");
response.setStatus(HttpServletResponse.SC_NO_CONTENT);
return; // 不再继续调用后续Filter或Servlet
}
chain.doFilter(request, response);
}
}
逻辑分析:
- 动态Origin处理 :从请求头提取
Origin并原样回写,支持多域名接入。 - 拦截OPTIONS请求 :检测到
OPTIONS时提前结束过滤链,防止到达业务Servlet。 - 设置完整CORS头 :涵盖方法、头部、缓存三项关键策略。
- 返回204状态码 :符合HTTP语义,表示“预检通过,无内容返回”。
注意事项:
- 必须在
web.xml或使用@WebFilter注册该Filter。 - 若项目使用Spring等框架,仍建议保留底层Filter以应对非Spring管理的请求。
3.3.2 设置Access-Control-Allow-Origin、Access-Control-Allow-Methods等关键响应头
以下是各CORS响应头的具体含义及设置建议:
| 响应头 | 推荐值 | 说明 |
|---|---|---|
Access-Control-Allow-Origin | https://yourdomain.com 或动态 Origin | 生产环境禁用 * (尤其带凭据时) |
Access-Control-Allow-Methods | GET, POST, PUT, DELETE, OPTIONS | 覆盖所有实际使用的方法 |
Access-Control-Allow-Headers | Content-Type, Authorization, X-Requested-With | 包含前端所需自定义头 |
Access-Control-Allow-Credentials | true (可选) | 允许携带Cookie,需与具体Origin配合 |
Access-Control-Max-Age | 86400 | 减少预检频率 |
安全性提醒:
- 当设置
Access-Control-Allow-Credentials: true时,Access-Control-Allow-Origin不能为*,必须指定具体源。 - 避免开放过多方法或头部,遵循最小权限原则。
3.3.3 返回HttpServletResponse.SC_NO_CONTENT状态码的规范实践
在预检响应中,应始终返回 204 No Content 状态码,而非 200 OK 或其他状态。
response.setStatus(HttpServletResponse.SC_NO_CONTENT);
原因如下:
- 语义正确 :
204表示“请求已成功处理,但无响应体”,恰好匹配预检请求的性质。 - 避免误解 :若返回
200且带有HTML内容,某些浏览器可能误认为是有效资源而渲染。 - 性能优化 :无需输出任何内容,节省带宽。
错误示例对比
❌ 错误写法:
response.getWriter().println("OK");
response.setStatus(200);
会导致浏览器收到非空响应体,违反CORS规范。
✅ 正确写法:
response.setStatus(HttpServletResponse.SC_NO_CONTENT);
// 不调用getWriter()或getOutputStream()
保持响应体为空,符合RFC7231标准。
3.4 避免重复预检的缓存优化策略
频繁的预检请求会增加网络开销,影响用户体验。幸运的是,CORS规范提供了一种机制——通过 Access-Control-Max-Age 头部缓存预检结果,使浏览器在一定时间内复用之前的授权决策。
3.4.1 Access-Control-Max-Age头部设置建议
该头部指定预检结果可缓存的时间(单位:秒)。推荐设置为:
Access-Control-Max-Age: 86400
即24小时。对于大多数应用场景而言,此值足够长,能显著降低预检请求频次。
设置方式:
response.setHeader("Access-Control-Max-Age", "86400");
注意事项:
- 时间不宜过长(如超过一周),不利于策略变更后的及时生效。
- 若值为 0 ,表示禁用缓存,每次都需重新预检。
- 某些旧版浏览器(如IE)最大只支持300秒。
3.4.2 不同浏览器对预检结果缓存时间的实际差异
尽管规范允许长达24小时的缓存,但各浏览器实现存在差异:
| 浏览器 | 最大缓存时间 | 特殊行为 |
|---|---|---|
| Chrome | 24小时 | 支持完整Max-Age |
| Firefox | 24小时 | 同上 |
| Safari | 5分钟(早期版本)→ 现代版本支持更长 | 曾有限制较严 |
| Edge | 24小时 | 基于Chromium内核 |
| IE 11 | 最多300秒 | 不遵守高数值 |
实测建议:
- 开发阶段可设为
300以便快速调试; - 生产环境建议设为
86400,兼顾性能与灵活性; - 若需即时更新策略,可临时降为
0,待部署完成后再恢复。
缓存效果验证
可通过Chrome开发者工具观察:
- 第一次访问:出现 OPTIONS 请求;
- 刷新页面后:若仍在缓存期内, OPTIONS 请求消失,仅见主请求。
这表明预检已被缓存,提升了接口响应效率。
综上所述,深入理解预检机制不仅有助于解决CORS报错问题,更能指导我们在Servlet层面构建高效、安全的跨域通信体系。下一章将进一步探讨如何在Filter中统一封装完整的CORS支持方案。
4. Servlet中实现CORS支持的完整方案
在现代Web应用架构中,前后端分离已成为主流开发模式。前端通常运行于独立域名(如 http://localhost:3000 ),而后端服务部署在另一地址(如 https://api.example.com:8443 )。这种部署方式天然形成了跨域场景,若不进行妥善处理,浏览器将依据同源策略拦截请求。虽然CORS标准为跨域通信提供了安全可控的解决方案,但在基于Java Servlet的传统Web容器(如Tomcat、Jetty)中,开发者需手动构建完整的CORS支持机制。本章深入探讨如何在Servlet环境中通过过滤器统一管理CORS头、支持凭据传递、正确读取复杂请求体数据,并结合JSON序列化工具实现高效的数据解析与响应封装。
4.1 Filter过滤器统一注入CORS响应头
使用Filter是实现全局CORS控制的最佳实践之一。它能够在请求到达具体Servlet之前统一添加必要的响应头信息,避免每个接口重复编写相同逻辑,提升代码可维护性与一致性。
4.1.1 编写CORSFilter类实现javax.servlet.Filter接口
创建一个自定义的 CORSFilter 类并实现 javax.servlet.Filter 接口,是实现集中式跨域处理的核心步骤。该类负责拦截所有进入系统的HTTP请求,并根据请求类型决定是否附加CORS相关头部。
import javax.servlet.*;
import javax.servlet.http.HttpServletRequest;
import javax.servlet.http.HttpServletResponse;
import java.io.IOException;
public class CORSFilter implements Filter {
@Override
public void init(FilterConfig filterConfig) throws ServletException {
// 初始化阶段可加载配置参数,例如允许的源列表
System.out.println("CORSFilter initialized.");
}
@Override
public void doFilter(ServletRequest req, ServletResponse res, FilterChain chain)
throws IOException, ServletException {
HttpServletRequest request = (HttpServletRequest) req;
HttpServletResponse response = (HttpServletResponse) res;
// 设置CORS响应头
response.setHeader("Access-Control-Allow-Origin", "https://example.com");
response.setHeader("Access-Control-Allow-Methods", "GET, POST, PUT, DELETE, OPTIONS");
response.setHeader("Access-Control-Allow-Headers",
"Content-Type, Authorization, X-Requested-With");
response.setHeader("Access-Control-Expose-Headers", "X-Auth-Token");
response.setHeader("Access-Control-Allow-Credentials", "true");
// 处理预检请求
if ("OPTIONS".equalsIgnoreCase(request.getMethod())) {
response.setStatus(HttpServletResponse.SC_NO_CONTENT);
return;
}
// 继续执行后续链路
chain.doFilter(req, res);
}
@Override
public void destroy() {
// 资源释放逻辑(如有)
}
}
代码逻辑逐行解读分析:
| 行号 | 说明 |
|---|---|
| 1-6 | 导入必需的Servlet API类,包括Filter、ServletRequest、ServletResponse等 |
| 9-12 | 定义 CORSFilter 类并实现 Filter 接口,表明其作为过滤器的身份 |
| 15-18 | init() 方法用于初始化配置,可用于从web.xml读取允许的域或方法列表 |
| 21-22 | 将原始请求和响应对象转换为HTTP专用类型以便操作Header |
| 25-31 | 添加关键CORS响应头字段: Access-Control-Allow-Origin 指定允许访问资源的源; Allow-Methods 声明支持的方法; Allow-Headers 列出客户端可使用的自定义头; Expose-Headers 允许前端JS读取特定响应头; Allow-Credentials 启用Cookie传输 |
| 34-37 | 若请求为 OPTIONS ,即预检请求,则直接返回204 No Content状态码,终止后续处理 |
| 40 | 调用 chain.doFilter() 放行请求至下一个过滤器或目标Servlet |
参数说明 :
-Access-Control-Allow-Origin: 必须明确指定具体域名(不能为*)当启用凭据时。
-Access-Control-Allow-Credentials: 设为"true"才能允许携带Cookie,但此时Origin不可为通配符。
-SC_NO_CONTENT: HTTP状态码204,表示成功响应无内容体,符合CORS预检规范。
4.1.2 在doFilter方法中动态设置Access-Control-Allow-Origin策略
静态配置CORS来源存在局限性,尤其在多环境或多租户系统中。理想做法是在运行时动态判断请求来源是否合法,从而增强安全性与灵活性。
以下示例展示如何基于白名单机制动态设置 Access-Control-Allow-Origin :
private static final Set<String> ALLOWED_ORIGINS = Set.of(
"https://example.com",
"https://admin.example.com",
"https://dev-client.internal"
);
@Override
public void doFilter(ServletRequest req, ServletResponse res, FilterChain chain)
throws IOException, ServletException {
HttpServletRequest request = (HttpServletRequest) req;
HttpServletResponse response = (HttpServletResponse) res;
String origin = request.getHeader("Origin");
if (origin != null && ALLOWED_ORIGINS.contains(origin)) {
response.setHeader("Access-Control-Allow-Origin", origin);
response.setHeader("Access-Control-Allow-Credentials", "true");
} else {
// 非法来源拒绝跨域
response.setStatus(HttpServletResponse.SC_FORBIDDEN);
response.getWriter().write("{\"error\":\"Origin not allowed\"}");
return;
}
// 其他CORS头设置...
response.setHeader("Access-Control-Allow-Methods", "GET, POST, PUT, DELETE, OPTIONS");
response.setHeader("Access-Control-Allow-Headers", "Content-Type, Authorization");
if ("OPTIONS".equalsIgnoreCase(request.getMethod())) {
response.setStatus(HttpServletResponse.SC_NO_CONTENT);
return;
}
chain.doFilter(req, res);
}
动态校验流程图(Mermaid)
graph TD
A[收到HTTP请求] --> B{是否包含Origin头?}
B -- 否 --> C[视为同源, 不设CORS头]
B -- 是 --> D[提取Origin值]
D --> E{是否在白名单内?}
E -- 否 --> F[返回403 Forbidden]
E -- 是 --> G[设置Access-Control-Allow-Origin=Origin]
G --> H[设置其他CORS头]
H --> I{是否为OPTIONS?}
I -- 是 --> J[返回204 No Content]
I -- 否 --> K[放行至业务Servlet]
白名单配置对比表
| 策略类型 | 安全性 | 灵活性 | 适用场景 |
|---|---|---|---|
| 固定域名 | 高 | 低 | 单一前端应用 |
| 星号 * | 低 | 高 | 公共API(不允许credentials) |
| 白名单匹配 | 高 | 中 | 多子系统集成 |
| 正则表达式匹配 | 高 | 高 | 多租户SaaS平台 |
该设计确保只有受信任的前端才能发起跨域请求,防止任意站点滥用接口,同时保持良好的扩展能力。
4.2 支持凭据传递(withCredentials)的安全配置
许多应用场景要求跨域请求携带身份凭证(如Session Cookie、Bearer Token),例如用户登录后的状态维持。此时必须启用 withCredentials 选项,并在服务端做出相应配置。
4.2.1 允许跨域携带Cookie的前提条件
要在Ajax请求中发送Cookie,前端必须显式设置 withCredentials: true :
fetch('https://api.example.com/login', {
method: 'POST',
credentials: 'include', // 关键配置
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify({ username: 'user', password: 'pass' })
})
对应地,服务端必须满足以下三个条件:
-
Access-Control-Allow-Origin不能为*,必须为具体的协议+域名+端口; -
Access-Control-Allow-Credentials必须设为"true"; - 响应头中声明暴露的自定义头(如有)需通过
Access-Control-Expose-Headers明确列出。
否则浏览器会因安全策略拒绝接收响应或阻止Cookie写入。
4.2.2 Access-Control-Allow-Credentials与Access-Control-Allow-Origin协同设置规则
这两者之间的配合极为关键。常见错误是将 Allow-Origin 设为 * 并开启 Allow-Credentials ,这会导致浏览器报错:
“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’.”
正确配置如下所示:
String origin = request.getHeader("Origin");
if (ALLOWED_ORIGINS.contains(origin)) {
response.setHeader("Access-Control-Allow-Origin", origin); // 精确匹配
response.setHeader("Access-Control-Allow-Credentials", "true"); // 开启凭据
response.setHeader("Access-Control-Expose-Headers", "Set-Cookie, X-Auth-Token");
}
凭据传递验证流程(Mermaid)
sequenceDiagram
participant Browser
participant Server
Browser->>Server: POST /login (withCredentials=true)
Server-->>Browser: Set-Cookie=sessionid=abc123; Secure; SameSite=None
Browser->>Server: GET /profile (携带Cookie)
Server-->>Browser: 返回用户信息
⚠️ 注意事项:
- Cookie必须标记Secure属性(仅HTTPS传输);
- 推荐设置SameSite=None以兼容跨站请求;
- 若使用Session认证,确保JSESSIONID未被CORS过滤干扰。
4.3 处理复杂请求体数据的输入流读取
对于POST提交JSON数据等非表单格式请求,传统 getParameter() 方法无法获取内容,必须通过输入流解析原始字节流。
4.3.1 使用getInputStream()获取原始JSON数据流
@Override
protected void doPost(HttpServletRequest request, HttpServletResponse response)
throws ServletException, IOException {
StringBuilder jsonBody = new StringBuilder();
BufferedReader reader = request.getReader();
String line;
while ((line = reader.readLine()) != null) {
jsonBody.append(line);
}
String payload = jsonBody.toString();
System.out.println("Received JSON: " + payload);
// 后续交由Jackson/Gson反序列化
}
输入流读取过程分析:
| 步骤 | 操作 | 说明 |
|---|---|---|
| 1 | 调用 request.getReader() | 获取字符流,适用于UTF-8编码文本 |
| 2 | 循环读取每行内容 | 构建完整JSON字符串 |
| 3 | 存储为String变量 | 便于后续JSON库处理 |
❗ 重要提醒 :一旦调用了
getInputStream()或getReader(),就不能再调用getParameter(),反之亦然。因为Servlet规范规定两者互斥,底层流只能消费一次。
4.3.2 防止getParameter()与getInputStream()冲突的技术要点
若需同时处理查询参数和JSON体,建议统一采用流读取方式,并自行解析URL参数。
替代方案:使用装饰器模式包装 HttpServletRequestWrapper ,缓存输入流内容供多次读取:
public class CachedBodyHttpServletRequest extends HttpServletRequestWrapper {
private byte[] cachedBody;
public CachedBodyHttpServletRequest(HttpServletRequest request) throws IOException {
super(request);
InputStream inputStream = request.getInputStream();
this.cachedBody = inputStream.readAllBytes(); // Java 9+
}
@Override
public ServletInputStream getInputStream() {
ByteArrayInputStream byteArrayInputStream = new ByteArrayInputStream(cachedBody);
return new ServletInputStream() {
@Override
public boolean isFinished() { return true; }
@Override
public boolean isReady() { return true; }
@Override
public int available() { return cachedBody.length; }
@Override
public int read() { return byteArrayInputStream.read(); }
};
}
@Override
public BufferedReader getReader() {
return new BufferedReader(new InputStreamReader(getInputStream()));
}
}
此包装类可在Filter中提前缓存请求体,使后续多次读取成为可能。
4.4 JSON数据解析与响应封装实践
结构化数据交换依赖JSON格式,合理选择解析库并设计统一响应结构对提升接口质量至关重要。
4.4.1 利用Jackson或Gson库反序列化请求体对象
引入Jackson依赖后,可通过 ObjectMapper 实现自动映射:
<!-- Maven -->
<dependency>
<groupId>com.fasterxml.jackson.core</groupId>
<artifactId>jackson-databind</artifactId>
<version>2.15.2</version>
</dependency>
反序列化示例:
ObjectMapper mapper = new ObjectMapper();
UserLoginRequest loginReq = mapper.readValue(payload, UserLoginRequest.class);
static class UserLoginRequest {
private String username;
private String password;
// getter/setter省略
}
Jackson核心功能对比表
| 特性 | Jackson | Gson |
|---|---|---|
| 性能 | 更快 | 稍慢 |
| 泛型支持 | 强大 | 一般 |
| 注解丰富度 | 高(@JsonProperty等) | 中 |
| Spring默认集成 | 是 | 否 |
| Android兼容性 | 较差 | 优秀 |
推荐在服务器端优先选用Jackson。
4.4.2 构建统一返回格式(如Result )提升接口可维护性
定义通用响应结构,便于前端统一处理:
public class Result<T> {
private int code;
private String message;
private T data;
public static <T> Result<T> success(T data) {
Result<T> r = new Result<>();
r.code = 200;
r.message = "OK";
r.data = data;
return r;
}
public static Result<Void> error(int code, String msg) {
Result<Void> r = new Result<>();
r.code = code;
r.message = msg;
return r;
}
// getter/setter...
}
响应输出:
response.setContentType("application/json;charset=UTF-8");
mapper.writeValue(response.getWriter(), Result.success(user));
该模式显著降低前后端联调成本,增强接口健壮性与可观测性。
5. 基于Servlet的HTTPS跨域POST接口开发实战
在现代Web应用架构中,前后端分离已成为主流模式。前端通过独立部署于 https://frontend.example.com 等域名下的单页应用(SPA)向后端API服务发起请求,而后端通常运行在 https://api.backend.com:8443 这样的HTTPS地址上。这种场景下,浏览器出于安全考虑会触发同源策略限制,导致跨域POST请求被拦截。尤其当涉及用户登录、数据提交等敏感操作时,必须确保通信过程既满足CORS规范,又建立在SSL加密通道之上。
本章将围绕一个典型的用户登录功能展开,构建一个完整的基于Servlet的HTTPS跨域POST接口实战案例。我们将从项目结构设计入手,逐步实现支持跨域的身份认证接口,并深入探讨如何在Tomcat容器中配置SSL以启用HTTPS访问。整个流程涵盖Maven依赖管理、Filter过滤器注册、JSON参数解析、统一响应封装以及多环境测试验证等多个关键环节,最终形成一套可复用、高安全性的后端接口开发模板。
5.1 TestWeb示例项目结构设计
为清晰展示跨域POST接口的实现路径,我们构建一个名为TestWeb的Maven Web工程。该项目采用标准目录布局,便于集成到主流IDE和构建工具中。其核心目标是提供一个轻量级、无框架依赖的Servlet-based后端服务,专注于处理跨域HTTPS请求。
5.1.1 Maven工程目录组织与依赖配置(servlet-api、jackson-databind)
TestWeb项目的Maven pom.xml 文件定义了必要的编译和运行时依赖。以下是关键依赖项的声明:
<dependencies>
<!-- Servlet API -->
<dependency>
<groupId>javax.servlet</groupId>
<artifactId>javax.servlet-api</artifactId>
<version>4.0.1</version>
<scope>provided</scope>
</dependency>
<!-- Jackson for JSON serialization/deserialization -->
<dependency>
<groupId>com.fasterxml.jackson.core</groupId>
<artifactId>jackson-databind</artifactId>
<version>2.15.2</version>
</dependency>
</dependencies>
<build>
<finalName>testweb</finalName>
<plugins>
<plugin>
<groupId>org.apache.maven.plugins</groupId>
<artifactId>maven-compiler-plugin</artifactId>
<version>3.8.1</version>
<configuration>
<source>1.8</source>
<target>1.8</target>
</configuration>
</plugin>
</plugins>
</build>
上述配置说明如下:
- javax.servlet-api :版本4.0.1对应Servlet 4.0规范,支持HTTP/2及异步处理特性。使用 provided 范围表示该库由Servlet容器(如Tomcat)提供,不打包进WAR。
- jackson-databind :用于自动序列化Java对象为JSON字符串,反序列化前端传入的JSON请求体。它是Jackson库的核心模块。
- 编译插件设置Java版本为1.8,确保兼容性。
项目目录结构遵循标准Maven WebApp布局:
testweb/
├── src/
│ └── main/
│ ├── java/
│ │ └── com/example/servlet/
│ │ ├── LoginServlet.java
│ │ └── CORSFilter.java
│ ├── resources/
│ └── webapp/
│ ├── WEB-INF/
│ │ └── web.xml
│ └── index.html
└── pom.xml
其中 webapp/WEB-INF/web.xml 是部署描述符,负责注册Servlet和Filter组件。
表格:核心依赖功能对照表
| 依赖名称 | 作用 | 使用场景 |
|---|---|---|
javax.servlet-api | 提供HttpServlet、HttpServletRequest、Filter等基础类 | 所有Servlet开发必备 |
jackson-databind | 实现POJO与JSON之间的双向转换 | 处理JSON格式请求/响应 |
tomcat-servlet-api (运行时) | 容器内置,无需显式引入 | 运行期间由Tomcat加载 |
该结构简洁明了,避免引入Spring等重型框架,突出原生Servlet的能力边界与灵活性。
5.1.2 web.xml中Servlet与Filter的注册方式
web.xml 作为Java EE的传统部署描述符,在本项目中承担着组件注册职责。以下为其完整内容:
<?xml version="1.0" encoding="UTF-8"?>
<web-app xmlns="http://xmlns.jcp.org/xml/ns/javaee"
xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
xsi:schemaLocation="http://xmlns.jcp.org/xml/ns/javaee
http://xmlns.jcp.org/xml/ns/javaee/web-app_4_0.xsd"
version="4.0">
<!-- 注册CORS Filter -->
<filter>
<filter-name>CORSFilter</filter-name>
<filter-class>com.example.servlet.CORSFilter</filter-class>
</filter>
<filter-mapping>
<filter-name>CORSFilter</filter-name>
<url-pattern>/*</url-pattern>
</filter-mapping>
<!-- 注册Login Servlet -->
<servlet>
<servlet-name>LoginServlet</servlet-name>
<servlet-class>com.example.servlet.LoginServlet</servlet-class>
</servlet>
<servlet-mapping>
<servlet-name>LoginServlet</servlet-name>
<url-pattern>/login</url-pattern>
</servlet-mapping>
<!-- 启用会话跟踪 -->
<session-config>
<session-timeout>30</session-timeout>
</session-config>
</web-app>
逻辑分析:
- <filter> 标签声明了一个名为 CORSFilter 的过滤器,其实现类位于指定包路径下。
- <filter-mapping> 将其映射至 /* 路径,意味着所有请求都会经过此Filter预处理,适用于全局CORS控制。
- <servlet> 注册了 LoginServlet ,对外暴露 /login 端点。
- <session-config> 设置了会话超时时间为30分钟,有助于后续实现基于Session的认证状态管理。
Mermaid流程图:请求生命周期中的组件调用顺序
sequenceDiagram
participant Browser
participant Tomcat
participant CORSFilter
participant LoginServlet
Browser->>Tomcat: POST /login (JSON)
Tomcat->>CORSFilter: doFilter()
alt 是OPTIONS预检?
CORSFilter-->>Browser: 返回204 + CORS头
Browser->>Tomcat: 实际POST请求
else 不是预检
CORSFilter->>LoginServlet: 调用service()
end
LoginServlet-->>Browser: 200 OK + JSON响应
该图展示了浏览器发送跨域POST请求时,Tomcat容器如何依次调用Filter和Servlet的执行流程。特别地,若为复杂请求(如携带自定义头),则先触发OPTIONS预检,成功后再进行实际POST调用。
5.2 开发支持跨域的LoginServlet doPost方法
LoginServlet 是本项目的核心业务组件,负责接收前端提交的用户名密码并模拟认证逻辑。
5.2.1 接收前端JSON格式用户名密码参数
由于前端使用 fetch 或 axios 发送 Content-Type: application/json 请求,传统 request.getParameter() 无法读取数据。必须通过输入流获取原始JSON内容。
@Override
protected void doPost(HttpServletRequest request, HttpServletResponse response)
throws ServletException, IOException {
// 获取输入流并读取JSON字符串
StringBuilder jsonBuffer = new StringBuilder();
String line;
try (BufferedReader reader = request.getReader()) {
while ((line = reader.readLine()) != null) {
jsonBuffer.append(line);
}
}
String jsonString = jsonBuffer.toString();
if (jsonString.isEmpty()) {
response.setStatus(HttpServletResponse.SC_BAD_REQUEST);
return;
}
// 使用Jackson反序列化为User对象
ObjectMapper mapper = new ObjectMapper();
User user = mapper.readValue(jsonString, User.class);
// 输出调试信息
System.out.println("Received login attempt: " + user.getUsername());
}
代码逐行解读:
1. request.getReader() :获取字符输入流,适用于 text/* 或 application/json 类型请求体。
2. 循环读取每一行拼接成完整JSON字符串,确保处理大体量请求体时不丢失数据。
3. 检查空值防止后续空指针异常。
4. ObjectMapper 来自Jackson库,自动将JSON字段映射到 User 类属性(需保证命名一致或使用注解)。
5. User.class 是一个简单的POJO,包含 username 和 password 字段。
参数说明:
-
jsonBuffer:累积请求体内容,应对分块传输。 -
mapper.readValue():泛型反序列化方法,支持嵌套对象结构。 - 异常捕获应在生产环境中完善,此处简化处理。
5.2.2 实现业务逻辑处理与模拟认证流程
完成参数解析后,执行模拟认证:
// 模拟数据库校验
boolean authenticated = "admin".equals(user.getUsername())
&& "123456".equals(user.getPassword());
Result result;
if (authenticated) {
// 创建会话并存储用户信息
HttpSession session = request.getSession();
session.setAttribute("user", user.getUsername());
result = Result.success("登录成功", Map.of("token", "fake-jwt-token"));
} else {
result = Result.fail("用户名或密码错误");
}
// 序列化结果并输出
ObjectMapper mapper = new ObjectMapper();
String jsonResponse = mapper.writeValueAsString(result);
response.setContentType("application/json;charset=UTF-8");
response.getWriter().write(jsonResponse);
逻辑分析:
- 使用硬编码判断模拟认证过程,实际项目应对接数据库或OAuth服务。
- 成功时创建HttpSession保存用户上下文,可用于后续权限校验。
- Result 是一个通用响应包装类,统一返回结构。
- 最终通过 PrintWriter 写回JSON响应。
5.3 返回标准化JSON响应结果
为提升接口可维护性与前端解析效率,所有响应应遵循统一格式。
5.3.1 设置Content-Type: application/json;charset=UTF-8
正确设置响应头至关重要:
response.setContentType("application/json;charset=UTF-8");
response.setCharacterEncoding("UTF-8");
-
Content-Type告知浏览器数据类型为JSON,避免XSS风险。 - 显式声明UTF-8编码防止中文乱码。
5.3.2 通过PrintWriter输出序列化后的响应数据
如前所述,使用 PrintWriter 写入已序列化的JSON字符串:
PrintWriter out = response.getWriter();
out.print(jsonResponse);
out.flush(); // 确保缓冲区立即输出
注意:不可同时调用 getOutputStream() 和 getWriter() ,否则抛出 IllegalStateException 。
5.4 HTTPS环境下的部署与测试
5.4.1 使用Tomcat配置SSL连接器启用HTTPS
编辑 conf/server.xml ,添加SSL连接器:
<Connector port="8443" protocol="org.apache.coyote.http11.Http11NioProtocol"
maxThreads="150" SSLEnabled="true">
<SSLHostConfig>
<Certificate certificateKeystoreFile="conf/.keystore"
type="RSA" />
</SSLHostConfig>
</Connector>
生成密钥库命令:
keytool -genkey -alias tomcat -keyalg RSA -keystore conf/.keystore -validity 365 -storepass changeit
重启Tomcat后可通过 https://localhost:8443/testweb/login 访问。
5.4.2 通过Postman与浏览器双重验证跨域POST提交功能
Postman测试:
- 方法:POST
- URL: https://localhost:8443/testweb/login
- Body → raw → JSON:
{"username": "admin", "password": "123456"}
- Headers添加:
Content-Type: application/json
预期返回:
{
"code": 200,
"message": "登录成功",
"data": { "token": "fake-jwt-token" }
}
浏览器测试:
创建HTML页面置于不同域(如 http://localhost:3000 ):
<script>
fetch('https://localhost:8443/testweb/login', {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify({ username: 'admin', password: '123456' })
})
.then(res => res.json())
.then(console.log);
</script>
观察开发者工具Network面板,确认:
- OPTIONS预检返回204且含CORS头;
- 实际POST请求返回200;
- 响应头包含 Access-Control-Allow-Origin 等字段。
至此,完整实现了基于Servlet的HTTPS跨域POST接口,具备安全性、可扩展性和可测试性,适用于企业级微服务网关或独立认证中心建设。
6. JSONP原理解析及其在Servlet中的兼容实现
6.1 JSONP的执行机制与浏览器安全边界
JSONP(JSON with Padding)是一种利用HTML中 <script> 标签跨域特性的“变通”方案。由于同源策略并不限制脚本标签加载外部JavaScript资源,开发者可以动态创建 <script> 元素,将src属性指向目标API地址,并附带一个回调函数名作为查询参数:
<script src="https://api.example.com/user?callback=handleResponse"></script>
当服务器接收到该请求后,不会返回原始JSON数据,而是将其封装在一个JavaScript函数调用中:
handleResponse({
"id": 1001,
"name": "Alice",
"email": "alice@example.com"
});
浏览器加载此脚本后会立即执行 handleResponse() 函数,从而完成跨域数据传递。
这种技术绕过了XMLHttpRequest的同源限制,但其本质并非真正的“跨域AJAX”,而是一种基于DOM操作的数据注入方式。由于返回内容是可执行的JavaScript代码,若未对 callback 参数进行严格校验,极易引发XSS攻击。例如,恶意请求如下:
https://api.example.com/data?callback=alert(document.cookie)
如果服务端不加过滤地回写该参数,则响应体变为:
alert(document.cookie)({"data": "sensitive"});
这将在用户上下文中执行任意脚本,造成严重的安全漏洞。
| 特性 | JSONP | CORS |
|---|---|---|
| 请求方法支持 | 仅GET | 所有HTTP方法 |
| 数据格式 | JavaScript函数调用 | 原始数据(JSON等) |
| 错误处理能力 | 无法捕获网络或语法错误 | 可通过XHR监听error事件 |
| 安全性 | 易受XSS影响 | 支持凭据、自定义头,更可控 |
| 浏览器兼容性 | IE6+ 兼容良好 | 需XMLHttpRequest Level 2支持 |
6.2 Servlet中实现JSONP接口的具体步骤
为使传统Servlet应用兼容JSONP调用,需在原有业务逻辑基础上增加对 callback 参数的解析与响应包装逻辑。以下是一个完整的 UserServlet 示例:
@WebServlet("/user")
public class UserServlet extends HttpServlet {
private static final long serialVersionUID = 1L;
// 模拟用户数据
private Map<Integer, User> userMap = new HashMap<>();
@Override
public void init() throws ServletException {
userMap.put(1001, new User(1001, "Alice", "alice@example.com"));
userMap.put(1002, new User(1002, "Bob", "bob@example.com"));
userMap.put(1003, new User(1003, "Charlie", "charlie@example.com"));
userMap.put(1004, new User(1004, "Diana", "diana@example.com"));
userMap.put(1005, new User(1005, "Eve", "eve@example.com"));
userMap.put(1006, new User(1006, "Frank", "frank@example.com"));
userMap.put(1007, new User(1007, "Grace", "grace@example.com"));
userMap.put(1008, new User(1008, "Henry", "henry@example.com"));
userMap.put(1009, new User(1009, "Ivy", "ivy@example.com"));
userMap.put(1010, new User(1010, "Jack", "jack@example.com"));
}
protected void doGet(HttpServletRequest request, HttpServletResponse response)
throws ServletException, IOException {
// 获取callback参数
String callback = request.getParameter("callback");
// 设置响应类型
if (callback != null && !callback.isEmpty()) {
// JSONP模式
response.setContentType("text/javascript;charset=UTF-8");
} else {
// 普通JSON模式
response.setContentType("application/json;charset=UTF-8");
}
PrintWriter out = response.getWriter();
// 构造响应数据
List<User> users = new ArrayList<>(userMap.values());
ObjectMapper mapper = new ObjectMapper();
String jsonData = mapper.writeValueAsString(users);
if (callback != null && isValidCallbackName(callback)) {
// 输出函数调用格式:callback({...})
out.printf("%s(%s);", callback, jsonData);
} else {
// 直接输出JSON
out.print(jsonData);
}
out.flush();
}
// 校验callback参数合法性,防止XSS
private boolean isValidCallbackName(String callback) {
return Pattern.matches("^[a-zA-Z_][a-zA-Z0-9_]*$", callback);
}
}
代码解释说明:
-
getParameter("callback"):获取前端传入的回调函数名。 -
response.setContentType("text/javascript"):符合JSONP规范的内容类型。 -
isValidCallbackName():使用正则校验函数名是否合法,避免非法字符导致脚本注入。 -
out.printf("%s(%s);", callback, jsonData):将数据包裹在指定函数调用中并以分号结尾,确保语法正确。
6.3 JSONP调用的前端实现与调试技巧
前端可通过动态创建 <script> 标签或使用jQuery的 $.ajax({ dataType: 'jsonp' }) 来发起请求:
function loadUserData() {
const script = document.createElement('script');
script.src = 'https://localhost:8443/TestWeb/user?callback=renderUsers';
document.head.appendChild(script);
}
function renderUsers(data) {
console.log('Received users:', data);
data.forEach(user => {
console.log(`ID: ${user.id}, Name: ${user.name}`);
});
}
开发者工具中查看Network面板时,应确认:
- 请求URL包含有效的 callback 参数;
- 响应状态码为200;
- 返回内容为合法JS语句,如 renderUsers([...]); ;
- Content-Type为 text/javascript 或 application/javascript 。
6.4 JSONP与CORS的综合对比分析
虽然JSONP在某些受限环境中仍具价值,但从现代Web开发角度看,其劣势明显:
graph TD
A[跨域方案选择] --> B{是否需要POST/PUT?}
B -->|是| C[CORS]
B -->|否| D{是否需支持IE8以下?}
D -->|是| E[JSONP]
D -->|否| F[CORS]
A --> G{是否涉及敏感数据?}
G -->|是| H[CORS + HTTPS + withCredentials]
G -->|否| I[视情况选用]
综上所述,JSONP适用于只读数据暴露、老旧系统集成等特定场景,但在安全性、功能完整性和维护成本方面均不如CORS。
简介:跨域资源共享(CORS)是现代Web开发中的关键技术,用于解决浏览器同源策略限制下的跨域请求问题。本文通过一个基于Servlet实现的HTTPS跨域POST提交实例,详细讲解如何处理从HTTP页面向HTTPS服务器发起的跨域POST请求。内容涵盖CORS机制、预检请求(OPTIONS)处理、响应头配置、POST数据解析与返回结果封装,并提供对JSONP等兼容方案的支持说明。配套的TestWeb项目为开发者提供了可运行的实践案例,有助于深入理解HTTPS与Servlet在跨域场景下的协同工作原理。
1万+

被折叠的 条评论
为什么被折叠?



