Spring Boot 接口本地正常,异地前端却报跨域?用 cpolar 跑通 CORS 预检与白名单

Spring Boot 接口本地正常,异地前端却报跨域?用 cpolar 跑通 CORS 预检与白名单

前言

后端在终端里请求接口,JSON 正常返回;前端同事换个网络打开页面,控制台却是一片跨域报错。先别把白名单改成星号,也别急着关浏览器安全检查:终端请求成功,只说明那次 HTTP 请求成功,不代表浏览器允许页面读取响应。

这篇用一个只返回虚构数据的 Spring Boot 接口,把本机验证、cpolar HTTPS 入口、OPTIONS 预检和浏览器调用串起来。我们不接数据库、不用 Cookie,只带一个临时 Bearer Token,方便把网络、跨域和鉴权三个问题分开看。

1 分清前端 Origin 和接口地址

Origin 由协议、主机、端口组成。http://localhost:5173 与 http://127.0.0.1:5173 不是同一个 Origin,端口换成 5174 也不同。

本文约定:后端电脑运行 127.0.0.1:8080;异地同事在自己的电脑上打开 http://localhost:5173 页面,再请求 cpolar 分配的 HTTPS API 地址。两个 localhost 分别属于两台电脑,前端不会直接请求后端的回环地址。

浏览器地址栏直接打开接口,与网页脚本读取接口,也不是同一种检查场景。地址栏能看见数据,不等于页面里的请求已经通过跨域校验。排错时把页面地址和接口地址分别记录下来,后面每一次比较都有依据。

白名单填页面的 Origin,不是 API 的公网域名。 请求带 Authorization 时,浏览器会先发 OPTIONS,询问服务端是否允许这个来源、方法和请求头,再决定是否发实际 GET。

图1:异地浏览器经 cpolar 访问本地 Spring Boot:页面 Origin 与 API 目标地址不同(教学示意)。

图里要区分两条信息:请求目标是 API 地址,Origin 则来自页面。cpolar 负责把请求送到本机,不替应用决定哪些网页能读取响应。

2 准备一个独立的测试工程

后端准备 JDK 17、Maven 3.6.3 或以上版本,前端电脑准备 Python 3 和浏览器。以下终端命令使用 Bash;先检查工具,别等写完代码才发现 Maven 指向了另一套 JDK。

bash 复制代码
java -version
mvn -v
python3 --version
mkdir -p cors-demo/src/main/java/demo
cd cors-demo

示例固定使用 Spring Boot 4.1.1 ,与官方系统要求及入门示例对齐。创建 pom.xml:

xml 复制代码
<project xmlns="http://maven.apache.org/POM/4.0.0"
         xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
         xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 https://maven.apache.org/xsd/maven-4.0.0.xsd">
  <modelVersion>4.0.0</modelVersion>
  <parent>
    <groupId>org.springframework.boot</groupId>
    <artifactId>spring-boot-starter-parent</artifactId>
    <version>4.1.1</version>
    <relativePath/>
  </parent>
  <groupId>demo</groupId>
  <artifactId>cors-demo</artifactId>
  <version>1.0.0</version>
  <properties><java.version>17</java.version></properties>
  <dependencies>
    <dependency>
      <groupId>org.springframework.boot</groupId>
      <artifactId>spring-boot-starter-webmvc</artifactId>
    </dependency>
  </dependencies>
  <build><plugins><plugin>
    <groupId>org.springframework.boot</groupId>
    <artifactId>spring-boot-maven-plugin</artifactId>
  </plugin></plugins></build>
</project>

这里只引入 MVC,不引入 Spring Security。已有项目若带安全过滤器,不要直接照搬后就认定配置完成;预检还必须先于认证处理,避免 OPTIONS 被要求携带业务 Token。

3 创建只读接口,配置精确白名单

新建 src/main/java/demo/App.java,把启动类、接口和跨域配置放在一起,便于对照。不要再叠加 @CrossOrigin,免得局部配置和全局配置合并后扩大来源范围。

java 复制代码
package demo;

import java.time.Instant;
import java.util.Map;
import org.springframework.beans.factory.annotation.Value;
import org.springframework.boot.SpringApplication;
import org.springframework.boot.autoconfigure.SpringBootApplication;
import org.springframework.http.ResponseEntity;
import org.springframework.web.bind.annotation.GetMapping;
import org.springframework.web.bind.annotation.RequestHeader;
import org.springframework.web.bind.annotation.RestController;
import org.springframework.web.servlet.config.annotation.CorsRegistry;
import org.springframework.web.servlet.config.annotation.WebMvcConfigurer;

@SpringBootApplication
@RestController
public class App implements WebMvcConfigurer {
    @Value("${demo.token}")
    private String token;
    private final Instant expiresAt = Instant.now().plusSeconds(1800);

    public static void main(String[] args) {
        SpringApplication.run(App.class, args);
    }

    @Override
    public void addCorsMappings(CorsRegistry registry) {
        registry.addMapping("/api/**")
            .allowedOrigins("http://localhost:5173")
            .allowedMethods("GET")
            .allowedHeaders("Authorization")
            .allowCredentials(false)
            .maxAge(0);
    }

    @GetMapping("/api/demo")
    public ResponseEntity<Map<String, String>> demo(
            @RequestHeader(value = "Authorization", required = false) String auth) {
        if (token.isBlank() || !Instant.now().isBefore(expiresAt)
                || !("Bearer " + token).equals(auth)) {
            return ResponseEntity.status(401)
                .body(Map.of("error", "unauthorized"));
        }
        return ResponseEntity.ok(Map.of("name", "demo-user", "status", "ok"));
    }
}

这里有三个提醒。allowedMethods("GET") 指实际业务方法,Spring MVC 会处理符合条件的 OPTIONS 预检,不需要另写 OPTIONS Controller。Authorization 必须明确允许;本文不发 Cookie,保持 allowCredentials(false)。

maxAge(0) 是为了调试时不复用预检结果,不是生产调优建议。Token 在应用启动后 30 分钟失效;这是独立演示接口的简化鉴权,不是生产账号系统。

在工程目录生成随机 Token 并启动应用:

bash 复制代码
export DEMO_TOKEN="$(python3 -c 'import secrets; print(secrets.token_hex(24))')"
printf '%s\n' "$DEMO_TOKEN"
mvn spring-boot:run \
  -Dspring-boot.run.arguments="--server.address=127.0.0.1 --server.port=8080"

把输出的 Token 只交给参与联调的同事,不要放进截图或仓库。环境变量 DEMO_TOKEN 对应代码中的 demo.token,不要把密钥写入 Java 文件。

另开 Bash 终端,按提示输入刚才的 Token,再验证本机接口:

bash 复制代码
read -r -s -p '输入测试 Token: ' DEMO_TOKEN; printf '\n'
curl -i http://127.0.0.1:8080/api/demo \
  -H "Authorization: Bearer $DEMO_TOKEN"

验收结果是 HTTP 200 和包含 demo-user、ok 的 JSON,字段顺序不影响结果。如果返回 401,先核对 Token 和半小时有效期;连接失败则先查启动日志及 8080 端口,不要直接修改 CORS。

4 用 cpolar 给本地 API 建立 HTTPS 入口

本机通过后再开公网入口,这一步是为了确认跨网链路,不是用隧道"修复跨域"。在 cpolar 官网注册账号,从官方下载页按系统下载安装客户端。

本文只用前台临时隧道,不安装后台服务、不开放管理页面。已有客户端直接检查版本,再从账号后台获取认证 Token:

bash 复制代码
cpolar version
read -r -s -p '输入 cpolar 认证 Token: ' CPOLAR_TOKEN; printf '\n'
cpolar authtoken "$CPOLAR_TOKEN"
unset CPOLAR_TOKEN
cpolar http 8080

注意区分两个 Token:cpolar Token 绑定客户端账号,DEMO_TOKEN 校验接口请求,不能互换。已有后台实例时不要重复启动前台实例,应沿用现有实例管理隧道,避免登录实例冲突。

从终端复制实际生成的 HTTPS 公网入口 ,不要抄别人的示例域名。保持该终端运行,在测试终端输入地址,末尾不要带 /api/demo:

bash 复制代码
read -r -p '粘贴 HTTPS 公网入口: '![Spring Boot 接口本地正常,异地前端却报跨域?用 cpolar 跑通 CORS 预检与白名单 - 发布前检查图](https://i-blog.csdnimg.cn/direct/9d29500fc1004641bf5636319a32ef5a.png)

 API
API="${API%/}"
curl -i "$API/api/demo" -H "Authorization: Bearer $DEMO_TOKEN"

如果本机返回 JSON、公网却不通,先检查隧道是否在线、映射端口是否为 8080、地址是否仍是当前地址。返回 HTML 而不是 JSON 时,先核对响应内容与入口,不能把所有失败都当成 CORS。

5 对照允许和拒绝两种 OPTIONS 请求

公网 GET 成功后,再模拟浏览器预检。这里故意不带业务 Token:浏览器预检声明的是即将发送的请求头名称,不会把那个 Bearer Token 一起发送。

bash 复制代码
curl -i -X OPTIONS "$API/api/demo" \
  -H 'Origin: http://localhost:5173' \
  -H 'Access-Control-Request-Method: GET' \
  -H 'Access-Control-Request-Headers: authorization'

检查响应头:Access-Control-Allow-Origin 应精确对应 http://localhost:5173,允许方法包含 GET,允许请求头包含 Authorization。HTTP 头名称大小写不敏感,别把大小写差异当故障。

别只看状态码就结束。成功响应如果缺少匹配的允许来源头,浏览器仍不会把响应交给页面代码;允许请求头里缺少认证头,预检也无法通过。终端不会替浏览器做这些判断,所以这里需要逐项对照响应头,而不是把终端打印的内容直接当成跨域成功证据。

再故意换成没有放行的来源,确认白名单真的生效:

bash 复制代码
curl -i -X OPTIONS "$API/api/demo" \
  -H 'Origin: http://localhost:5174' \
  -H 'Access-Control-Request-Method: GET' \
  -H 'Access-Control-Request-Headers: authorization'

在这份 MVC 配置下,被拒绝的预检返回 403,不会获得允许该来源的响应头。不要只检查成功用例;错误来源也能通过,就要回头找通配规则和重复配置。

图2:OPTIONS 预检允许 localhost:5173、拒绝 localhost:5174 的响应对照(教学示意)。

这张图应展示请求 Origin 与对应响应头,隐藏公网入口的敏感信息和所有 Token。预检通过只表示跨域规则通过,不代表实际接口鉴权成功。

6 在异地电脑用浏览器完成联调

请同事在自己的电脑新建空目录,在其中启动静态服务:

bash 复制代码
mkdir -p cors-front
cd cors-front
python3 -m http.server 5173 --bind 127.0.0.1

浏览器打开 http://localhost:5173,不要双击本地文件,也别改成 127.0.0.1。打开开发者工具的 Network 和 Console,在这个页面的 Console 执行:

javascript 复制代码
const api = prompt('输入 cpolar HTTPS 入口').replace(/\/+$/, '');
const token = prompt('输入短期测试 Token');
fetch(`${api}/api/demo`, {
  method: 'GET',
  credentials: 'omit',
  headers: { Authorization: `Bearer ${token}` }
}).then(async response => {
  console.log(response.status, await response.text());
}).catch(console.error);

Network 中检查 OPTIONS 和 GET,Console 应显示 200 与虚构数据。这里使用手动设置的 Authorization,但不让浏览器附带 Cookie,因此不需要开启跨源 Cookie 凭据支持。

图3:浏览器 Network 同时检查 OPTIONS、GET 与 JSON 响应(教学示意,非实测截图)。

图中要同时保留预检和实际请求,不要只截一个 200。若 OPTIONS 成功、GET 返回 401,检查 Token;若 OPTIONS 403,比较页面的 location.origin 与白名单;若出现连接或证书错误,先处理网络入口。

建议每轮只改一个条件,保留同一个接口路径和请求方法。来源、密钥、端口一起改,看到失败时就无法判断是哪条规则起作用。浏览器里找不到预检记录时,先清空网络面板记录、刷新页面,再重新执行请求,并确认没有启用只显示某一类请求的筛选。

还可以把代码里的 Token 临时改成错误值:允许来源应读到 401 和 unauthorized。再把浏览器页面改为 http://127.0.0.1:5173,重新执行请求,应触发来源拒绝。不要用 mode: 'no-cors' 绕过去,它不能让业务代码正常读取这个跨域 JSON。

随机入口变动后,更新前端 api 和终端 API。只有页面自己的协议、域名或端口变化,才需要修改后端 Origin 白名单并重启;API 域名变化本身不是增加白名单的理由。

7 总结:按链路排错,联调后关闭入口

按上面的验收顺序,我们把本机接口、公网连通性、预检规则与实际鉴权拆开检查,异地前端也有了明确的成功和失败对照,不必靠反复加星号碰运气。

  • 本机 GET 先过,再用 cpolar 建立 HTTPS 入口。
  • 白名单只填页面 Origin,明确限制 GET 和 Authorization。
  • 同时检查允许来源、拒绝来源与错误 Token 三种结果。

CORS 不是防止他人调用接口的认证机制,命令行客户端也不受浏览器同源策略约束。联调结束,在 cpolar、Spring Boot 和静态服务终端分别按 Ctrl+C,清除测试 Token;下一次联调重新生成密钥。要接入正式项目,再沿着同样顺序检查安全过滤器与正式鉴权,比把演示接口长期挂在公网更稳妥。

参考:Spring Boot 系统要求、官方入门工程、Spring MVC CORS、cpolar 官方文档。

相关推荐
wuminyu1 小时前
Virtual Thread重投递至ForkJoinPool任务队列过程解析
java·linux·c语言·jvm·c++
小蒜学长1 小时前
在线保险服务与管理平台的设计与实现(代码+数据库+LW)
java·数据库·spring boot·后端·服务平台·在线保险
掉进电商坑三年没爬出来的东叔1 小时前
青龙面板进阶:一个面板聚合多平台签到,所有积分自动领
java·开发语言
殷色玫瑰2 小时前
C++ STL:stack、queue、priority_queue 与容器适配器详解
java·开发语言·数据结构·c++·算法·visualstudio
IT方大同2 小时前
java输入输出+方法
java·开发语言·python
Yyyyyy~2 小时前
【Java】String
java
Wang's Blog3 小时前
Java框架 SpringCloud 快速入门: 服务拆分之服务远程调用
java·开发语言·spring cloud
Wang's Blog3 小时前
Java框架 SpringCloud 快速入门: 路由断言工厂
java·spring·spring cloud
ym hyd 1113 小时前
门诊挂号系统源码 Java+SpringBoot+Vue3 前后分离
java·vue.js·spring boot·毕设