基于Servlet的HTTPS跨域POST提交完整实例解析

部署运行你感兴趣的模型镜像

本文还有配套的精品资源,点击获取 menu-r.4af5f7ec.gif

简介:跨域资源共享(CORS)是现代Web开发中的关键技术,用于解决浏览器同源策略限制下的跨域请求问题。本文通过一个基于Servlet实现的HTTPS跨域POST提交实例,详细讲解如何处理从HTTP页面向HTTPS服务器发起的跨域POST请求。内容涵盖CORS机制、预检请求(OPTIONS)处理、响应头配置、POST数据解析与返回结果封装,并提供对JSONP等兼容方案的支持说明。配套的TestWeb项目为开发者提供了可运行的实践案例,有助于深入理解HTTPS与Servlet在跨域场景下的协同工作原理。
https跨域POST提交实例(基于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规范定义了以下三个核心判定条件:

  1. 请求方法必须是以下之一
    - GET
    - POST
    - HEAD

  2. 只能包含被允许的CORS安全请求头字段 ,如:
    - Accept
    - Accept-Language
    - Content-Language
    - Content-Type (仅限于三种值):

    • application/x-www-form-urlencoded
    • multipart/form-data
    • text/plain
  3. XMLHttpRequest Upload对象未监听任何事件 (如progress、load等)

  4. 请求未使用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
}
逐行逻辑解读:
  1. String origin = req.getHeader("Origin");
    获取请求来源,用于回写 Allow-Origin ,实现动态授权。

  2. resp.setHeader("Access-Control-Allow-Origin", origin);
    明确允许该源访问资源。注意:不能使用通配符 * 同时启用凭据传递。

  3. resp.setHeader("Access-Control-Allow-Methods", "...");
    声明支持的方法集,必须覆盖原始请求所需方法。

  4. resp.setHeader("Access-Control-Allow-Headers", "Content-Type, Authorization");
    列出自定义头白名单。若原始请求头未在此列,预检失败。

  5. resp.setHeader("Access-Control-Max-Age", "86400");
    设置预检结果缓存时间为86400秒(24小时),减少重复请求。

  6. 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);

原因如下:

  1. 语义正确 204 表示“请求已成功处理,但无响应体”,恰好匹配预检请求的性质。
  2. 避免误解 :若返回 200 且带有HTML内容,某些浏览器可能误认为是有效资源而渲染。
  3. 性能优化 :无需输出任何内容,节省带宽。
错误示例对比

❌ 错误写法:

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' })
})

对应地,服务端必须满足以下三个条件:

  1. Access-Control-Allow-Origin 不能为 * ,必须为具体的协议+域名+端口;
  2. Access-Control-Allow-Credentials 必须设为 "true"
  3. 响应头中声明暴露的自定义头(如有)需通过 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。

本文还有配套的精品资源,点击获取 menu-r.4af5f7ec.gif

简介:跨域资源共享(CORS)是现代Web开发中的关键技术,用于解决浏览器同源策略限制下的跨域请求问题。本文通过一个基于Servlet实现的HTTPS跨域POST提交实例,详细讲解如何处理从HTTP页面向HTTPS服务器发起的跨域POST请求。内容涵盖CORS机制、预检请求(OPTIONS)处理、响应头配置、POST数据解析与返回结果封装,并提供对JSONP等兼容方案的支持说明。配套的TestWeb项目为开发者提供了可运行的实践案例,有助于深入理解HTTPS与Servlet在跨域场景下的协同工作原理。


本文还有配套的精品资源,点击获取
menu-r.4af5f7ec.gif

您可能感兴趣的与本文相关的镜像

Stable-Diffusion-3.5

Stable-Diffusion-3.5

图片生成
Stable-Diffusion

Stable Diffusion 3.5 (SD 3.5) 是由 Stability AI 推出的新一代文本到图像生成模型,相比 3.0 版本,它提升了图像质量、运行速度和硬件效率

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值