很多开发者在接到“前端直传OBS附件”这类需求时,都会在联调阶段被一条报错拦住:Access to XMLHttpRequest at 'https://xxx.obs.cn-north-4.myhuaweicloud.com/xxx' from origin 'https://your-web.com' has been blocked by CORS policy: No 'Access-Control-Allow-Origin' header is present on the requested resource.
这条报错一出现,第一反应往往是去改后端、改Nginx,但折腾一圈发现根本没动到根子。搞清CORS机制、华为云OBS的配置入口,再加上一套可以直接抄作业的前后端方案,这个问题半小时内就能收工。这篇文章就围绕“对接华为云OBS上传附件时遇到的CORS报错”,把排查思路、配置细节和落地代码完整梳理一遍。
1. 先把CORS报错的根因讲透
1.1 浏览器同源策略和“预检请求”是怎么一回事
浏览器在发跨域请求时有一套自己的安全规矩:只要请求的协议、域名、端口和当前页面任一不同,就属于跨域请求,默认会被拦截。但在拦截之前,浏览器通常会先发一个OPTIONS请求,去问服务器“允不允许我这个来源、用这些请求头、用这个方法访问你”,这个OPTIONS请求就叫“预检请求”(Preflight Request)。
服务端(这里是华为云OBS)必须在响应里明确返回Access-Control-Allow-Origin、Access-Control-Allow-Methods、Access-Control-Allow-Headers这几个头,而且要和前端请求携带的Origin、Method、Headers精确匹配,预检才算通过。任何一个头缺失,浏览器控制台就会给出has been blocked by CORS policy这类提示。
把这件事套到OBS上传场景里:页面在https://your-web.com下,上传接口指向https://your-bucket.obs.cn-north-4.myhuaweicloud.com,两者域名不同,跨域问题天然存在。而PUT上传或自定义Content-Type请求头又都不属于“简单请求”,必然触发预检流程,所以报错几乎是必然的,除非后端走全代理模式。
1.2 为什么有时候GET能访问、上传却报错
不少读者会遇到一个奇怪的现象:浏览器直接打开OBS的图片URL能看,但代码里用fetch上传就是报CORS错。原因很简单:
- 直接打开图片URL是浏览器导航行为,不经过JavaScript的CORS校验。
- 上传接口从前端JavaScript发起,必然受同源策略管控。
- 对于
GET请求,OBS桶如果配置了“公共读”,OBS会直接返回图片内容;但对于上传这种写操作,OBS必须先校验CORS配置,同时还要校验签名和权限。
换句话说,CORS是浏览器层面的“准入检查”,而OBS的权限机制(IAM策略、桶策略)是云平台层面的“操作许可”。两者都通过,请求才能真正写进桶里。联调时经常出现“CORS报错下面还夹着403错误”,说明问题往往是叠加的。
1.3 一个容易误判的点:CORS配置和“跨域”不是一回事
有的同学在OBS控制台找了一圈没看到“跨域设置”这个入口,就怀疑是自己的账号类型不对。实际上,华为云OBS控制台里的菜单叫“权限管理”或“CORS规则”,不同的控制台版本入口略有差异,但核心配置项是统一的。CORS规则本质是给OBS桶附加一段XML配置,告诉OBS“哪些来源的跨域请求可以被放行”。
这里必须明确一个概念:CORS配置不会“解除”桶的私有权限,它只是允许浏览器把跨域请求发出去并把响应返回给JavaScript。如果桶是私有的,前端请求还得带上签名信息,否则即使CORS放行,OBS依然会返回403。理解了这一点,就不会把CORS和“桶策略改成公共读”混为一谈,后者纯属为了排查问题临时开的,生产环境绝不能这么干。
需要模型API调用? 免费领10W Token,多模型网关一键接入 Claude、DeepSeek 等主流模型。
2. 华为云OBS控制台的CORS规则配置细节
2.1 CORS规则配置入口和参数说明
登录华为云控制台,进入OBS服务,找到对应的桶:
- 在桶列表中点击桶名称,进入桶详情页。
- 找到“权限管理”页签,点击左侧“CORS规则”。
- 点击“创建”按钮,系统会弹出规则编辑框。
华为云OBS的CORS规则支持两种编辑方式:可视化表单和XML编辑。可视化表单里的关键字段如下:
| 字段 | 必须填写 | 说明 | 推荐值 |
|---|---|---|---|
| 允许的来源(AllowedOrigin) | 是 | 合法的请求来源,必须是协议+域名+端口 | https://your-web.com |
| 允许的方法(AllowedMethod) | 是 | 允许的HTTP方法,支持GET、PUT、POST、DELETE、HEAD | 根据上传方式选择PUT |
| 允许的消息头(AllowedHeader) | 否 | 允许携带的自定义请求头,多个用逗号分隔 | * 或精确指定 |
| 暴露的消息头(ExposeHeader) | 否 | 允许JavaScript读取的响应头 | ETag |
| 缓存时间(MaxAgeSeconds) | 否 | 预检结果缓存时间,单位秒 | 3600 |
特别注意“允许的来源”这一栏,默认值如果是*,虽然能匹配任意来源,但如果你按需上传对象时使用了Authorization请求头,浏览器预检阶段就会要求Access-Control-Allow-Headers里必须包含Authorization。因此调试时用通配符方便,生产环境建议写死为业务域名的完整三元组。
2.2 XML配置示例和常见规则配置组合
如果你更习惯直接写XML,在CORS规则页面切换到XML视图,配置结构大致如下:
xml复制<CORSConfiguration>
<CORSRule>
<AllowedOrigin>https://your-web.com</AllowedOrigin>
<AllowedOrigin>http://localhost:8080</AllowedOrigin>
<AllowedMethod>PUT</AllowedMethod>
<AllowedMethod>GET</AllowedMethod>
<AllowedMethod>POST</AllowedMethod>
<AllowedHeader>*</AllowedHeader>
<ExposeHeader>ETag</ExposeHeader>
<MaxAgeSeconds>3600</MaxAgeSeconds>
</CORSRule>
</CORSConfiguration>
这套规则覆盖了三种常见场景:GET用于读取/预览文件,PUT用于浏览器直传,POST用于表单上传。ExposeHeader配ETag是因为很多前端直传逻辑需要拿到服务端返回的ETag去校验上传完整性。MaxAgeSeconds配3600可以减少重复预检,提升上传体验。
2.3 关于“允许的消息头”配置的几个坑
有一种很常见的报错场景:配置里允许了来源和方法,但预检依然失败,报错提示Request header field authorization is not allowed by Access-Control-Allow-Headers in preflight response。
原因就是AllowedHeader没配好。OBS内置的CORS校验逻辑要求预检的Access-Control-Request-Headers里的每一项都必须出现在AllowedHeader中。如果你不确定前端SDK会带哪些请求头,最省事的方案是直接写*。当然,*在生产环境会有一定宽泛性,但OBS本身还会校验签名,仅凭CORS配置绕过不了权限校验,所以这个通配符的安全风险是可控的。
另一个关联问题是:有的前端代码用fetch直接PUT文件,请求体中不带任何额外请求头,此时预检请求里可能就不存在Access-Control-Request-Headers,那后台只配AllowedHeader为空都能过。但一旦用OBS官方SDK,SDK会自动追加一堆请求头(如x-obs-content-sha256、host等),所以强烈建议直接配*,避免上线时因为SDK版本升级导致请求头增删引发CORS校验失败。
2.4 老版本控制台的“隐藏入口”
部分华为云账号(尤其是IAM子账号)在OBS桶详情页找不到“CORS规则”入口,大概率是权限问题。确认子账号是否拥有obs:bucket:PutBucketCORS和obs:bucket:GetBucketCORS权限。可以在IAM策略中单独放行这两个权限,或者直接使用OBS Operator这个系统预置角色,它能覆盖大部分桶配置操作。
还有一种情况:你用的是“OBS Browser+”这类桌面工具连接桶,CORS规则配置入口并不在这个工具里,而是必须走网页控制台。桌面工具只能配置桶的存储类别、生命周期、策略等,跨域规则属于控制台专属能力。遇到入口缺失时先换网页端,再排查IAM权限,顺序不要反。
3. 前端直传OBS的完整接入方案
3.1 方案选型:前端直传还是后端中转
处理附件上传,架构上通常有两种选型:
- 前端直传(浏览器直接向OBS桶发起请求):链路短,服务器压力小,适合大文件、高并发。但受CORS影响大。
- 后端中转(前端把文件传给后端,后端再写OBS):实现简单、安全可控,但会增加带宽消耗和服务器压力。
如果公司有文件大小限制(比如不超过20MB),我更倾向于后端中转,代码逻辑清晰,也不容易踩CORS的坑。但如果是视频、安装包这类动辄几百MB的大文件,后端中转会让应用服务器带宽被吃满,费用也很不划算,这时候必须采用前端直传方案。
前端直传有三种常见做法:
| 做法 | 原理 | CORS影响 | 适用场景 |
|---|---|---|---|
| 预签名URL直传 | 后端调用SDK生成临时PUT URL,前端直接PUT | 仍需配置CORS | 最常用、最推荐 |
| 表单POST直传 | OBS支持POST表单上传,携带策略和签名 | 需配POST方法 | 浏览器兼容性较好 |
| 服务端中转 | 前端传后端,后端再传OBS | 无跨域问题 | 小文件、安全要求高 |
3.2 后端生成预签名URL的标准Java实现
后端生成预签名URL的过程本身不受CORS影响,因为它是服务端到服务端的调用,不经过浏览器。真正的CORS风险只存在于前端拿着签名URL去PUT文件的阶段。
下面是一段基于华为云OBS Java SDK生成预签名URL的标准代码:
java复制import com.obs.services.ObsClient;
import com.obs.services.model.PutObjectRequest;
import com.obs.services.model.PutObjectResult;
import com.obs.services.model.HttpMethodEnum;
import com.obs.services.model.TemporarySignatureRequest;
import com.obs.services.model.TemporarySignatureResponse;
import java.util.Date;
import java.util.HashMap;
import java.util.Map;
public class ObsPresignService {
private static final String ENDPOINT = "https://obs.cn-north-4.myhuaweicloud.com";
private static final String AK = "your-access-key";
private static final String SK = "your-secret-key";
private static final String BUCKET_NAME = "your-bucket";
public String createPresignedPutUrl(String objectKey, long expiresInSeconds) {
// 1. 创建ObsClient,推荐使用临时AK/SK + SecurityToken 的方式
ObsClient obsClient = new ObsClient(AK, SK, ENDPOINT);
// 实际生产环境请使用临时凭证:
// ObsClient obsClient = new ObsClient(ak, sk, securityToken, endpoint);
try {
TemporarySignatureRequest request = new TemporarySignatureRequest();
request.setBucketName(BUCKET_NAME);
request.setObjectKey(objectKey);
request.setMethod(HttpMethodEnum.PUT);
// 2. 设置签名有效期,一般按业务需求设置,建议不超过10分钟
Date expiry = new Date(System.currentTimeMillis() + expiresInSeconds * 1000);
request.setExpires(expiry);
// 3. 如果要限制文件类型,可以在这里设置请求头
Map<String, Object> headers = new HashMap<>();
headers.put("Content-Type", "application/octet-stream");
request.setHeaders(headers);
// 4. 生成预签名URL
TemporarySignatureResponse response = obsClient.createTemporarySignature(request);
return response.getSignedUrl();
} finally {
obsClient.close();
}
}
}
3.3 前端PUT直传及处理预检请求的关键代码
前端拿到预签名URL之后,直接用fetch或axios发送PUT请求即可:
javascript复制async function uploadToObs(file, presignedUrl) {
const response = await fetch(presignedUrl, {
method: 'PUT',
// 注意:如果后端生成URL时已经签名了Content-Type,那么这一行必须留;
// 如果生成URL时没有指定Content-Type,这里也建议显式带上,否则可能被OBS判定为不一致而拒绝
headers: {
'Content-Type': file.type || 'application/octet-stream',
},
body: file,
});
if (!response.ok) {
throw new Error(`OBS上传失败,HTTP状态码: ${response.status}`);
}
// 可以从响应头获取ETag做完整性校验
const etag = response.headers.get('ETag');
console.log('上传成功,ETag:', etag);
}
这段代码本身不复杂,真正的坑在于Content-Type。OBS预签名URL签名时如果带了Content-Type,前端PUT时就必须用一模一样的值,否则OBS会返回签名不匹配的403。很多CORS报错在此时被“伪”报成跨域问题,实际上一旦CORS配置正确后底层403就会暴露出来。
3.4 规避自定义Header触发的预检
浏览器判断是否为“简单请求”的条件之一就是有没有自定义请求头。像Authorization、x-custom-header这类非标准请求头都会直接触发预检。如果预检失败,后续真正的PUT请求根本不会发出。
做前端直传时,有两种思路减少预检干扰:
- 不主动添加自定义Header,仅使用
Content-Type和标准Header。 - 所有额外信息放到URL查询参数里传递,让请求回到“简单请求”或至少减少预检检查项。
预签名URL方式天然适合第二种思路,因为签名信息就在URL里,前端不需要额外塞Header,CORS配置的压力就小了很多。如果你选用的是axios,注意拦截器是否会在PUT请求上自动追加X-Requested-With之类的Header,这个Header会触发预检,需要显式清除。
4. 后端中转模式如何绕开CORS
4.1 前端传后端、后端写OBS的流程
如果不想碰CORS,最直接的办法就是后端中转。前端把文件POST到自己的后端接口,后端用SDK把文件流转存到OBS。
前端部分非常常规:
javascript复制const formData = new FormData();
formData.append('file', file);
const response = await fetch('/api/upload/obs', {
method: 'POST',
body: formData,
});
后端接口使用Spring Boot实现,接收MultipartFile后写入OBS:
java复制@PostMapping("/api/upload/obs")
public Map<String, String> uploadToObs(@RequestParam("file") MultipartFile file) {
String objectKey = "uploads/" + System.currentTimeMillis() + "_" + file.getOriginalFilename();
ObsClient obsClient = new ObsClient(AK, SK, ENDPOINT);
try {
PutObjectRequest request = new PutObjectRequest(BUCKET_NAME, objectKey);
request.setInput(new ByteArrayInputStream(file.getBytes()));
request.setMetadata(new ObjectMetadata());
request.getMetadata().setContentLength(file.getSize());
request.getMetadata().setContentType(file.getContentType());
PutObjectResult result = obsClient.putObject(request);
Map<String, String> resultMap = new HashMap<>();
resultMap.put("objectKey", objectKey);
resultMap.put("etag", result.getEtag());
return resultMap;
} catch (IOException e) {
throw new RuntimeException("文件读取失败", e);
} finally {
obsClient.close();
}
}
4.2 后端中转的优缺点和取舍建议
后端中转最大的优点就是不需要配CORS,也不需要预签名URL,因为浏览器和OBS根本不直接交互。并且文件的权限校验可以完全纳入业务系统,比如“只有VIP用户才能上传超过100MB的附件”这类逻辑,在后端可以轻松实现。
缺点也很明显:所有文件都要过一遍应用服务器,带宽瓶颈会被放大。如果业务峰值在每天几万次上传且单个文件较大,你的服务器带宽费用会非常可观。这时候就得评估一下是加带宽划得来,还是走前端直传省成本。
我的建议是:小文件(5MB以下)走后端中转,大文件(超过50MB)走预签名URL直传,中间段按实际带宽和并发评估。很多团队一开始图省事全走后端中转,等流量起来后再切成直传,业务上也是能平滑过渡的。
5. 实际场景中的CORS排查实录
5.1 排查流程:从控制台报错到问题定位
在对接华为云OBS过程中,我碰到过四种典型的CORS报错变种,这里整理成一张速查表:
| 报错信息 | 根因 | 解决方案 |
|---|---|---|
No 'Access-Control-Allow-Origin' header is present |
CORS规则未配置或来源不匹配 | 核对AllowedOrigin |
Response to preflight request doesn't pass access control check |
预检失败,方法或请求头不匹配 | 核对AllowedMethod和AllowedHeader |
Request header field authorization is not allowed by Access-Control-Allow-Headers |
允许的消息头没包含Authorization | AllowedHeader改为* |
| 配置了CORS后仍报403 | 签名无效或桶权限不足 | 检查AK/SK、临时凭证、桶策略 |
5.2 一个特殊的坑:临时签名URL和自定义Header组
某个项目里,前端上传附件时需要在Header中携带x-obs-storage-class(指定存储类型为低频访问存储),结果加上之后发现PUT请求直接报CORS错误。去掉这个Header之后一切正常。
定位步骤是:
- 打开浏览器开发者工具,查看“网络”面板,发现失败的请求也是
OPTIONS预检。 - 查看预检响应,发现
Access-Control-Allow-Headers的值里确实没有x-obs-storage-class。 - 去OBS控制台把AllowedHeader改为
*,重新触发请求,CORS通过。 - 随后OBS返回403签名错误,因为预签名URL生成时没有把这个Header加入签名范围。
- 调整后端生成URL逻辑,把自定义Header也带入签名。
这个案例说明CORS预检和签名校验是两个独立环节,千万不要以为CORS报错的原因只能在控制台上找。很多时候CORS只是“第一层烟雾弹”,签名才是真正的拦路虎。
5.3 本地开发环境如何配置多个来源
开发时前端往往跑在http://localhost:3000或http://127.0.0.1:5173,后端预签名URL可能指向测试环境的OBS桶。这种情况下,一定要在OBS控制台把本地来源加进AllowedOrigin里,否则页面一打开,第一个预检请求就失败。
注意一点:http://localhost:3000和http://127.0.0.1:3000在CORS规则中是两个完全不同的Origin,必须分别添加。我见过有同事在localhost上排查了半小时,换127.0.0.1一测才发现是来源不匹配。
5.4 HTTPS页面访问HTTP接口的“混合内容”报错
在一个内网部署的系统里,页面是https协议,前端直接对接OBS的http公网地址,浏览器控制台报错类似Mixed Content: The page at 'https://...' was loaded over HTTPS, but requested an insecure XMLHttpRequest endpoint 'http://...'。
这个报错和CORS其实不是一回事,属于浏览器的混合内容拦截机制。解决方案只能是让OBS桶域名也走HTTPS。华为云OBS默认提供了HTTPS访问能力,你只需要在预签名URL生成时把endpoint从http://换成https://,链路就全绿了。
5.5 自定义域名绑定OBS时的CORS配置注意项
很多品牌系统会给OBS桶绑定自定义域名,比如files.example.com对应到your-bucket.obs.cn-north-4.myhuaweicloud.com。绑定之后,前端请求地址变成了https://files.example.com,这时CORS规则的AllowedOrigin是“页面来源”,和桶域名是否自定义没有直接关系。
但绑定自定义域名后有一个新风险:如果该自定义域名也配置了CDN加速,CDN层面可能会缓存错误响应,导致CORS规则修改后浏览器依然拿到旧的错误响应头。此时需要在CDN控制台刷新缓存,或者在CDN配置里针对OPTIONS请求设置缓存时间为0。
6. 排查工具与验证命令
6.1 用curl手动模拟预检请求
有时候页面开了DevTools也看不出问题,因为浏览器在真正发请求前,把预检请求和业务请求合并展示,叠加了太多信息。更直接的方式是用curl手动模拟预检:
bash复制curl -i -X OPTIONS \
'https://your-bucket.obs.cn-north-4.myhuaweicloud.com/test.txt' \
-H 'Origin: https://your-web.com' \
-H 'Access-Control-Request-Method: PUT' \
-H 'Access-Control-Request-Headers: content-type'
观察响应头中是否包含:
http复制Access-Control-Allow-Origin: https://your-web.com
Access-Control-Allow-Methods: PUT
Access-Control-Allow-Headers: content-type
如果响应头缺失,说明OBS上的CORS规则没生效或者不匹配。如果响应头齐全,问题大概率出在前端代码或者网络层。
6.2 验证实际PUT上传是否被CORS拦截
预检通过后,再手动发一个PUT请求看看实际结果:
bash复制curl -i -X PUT \
'https://your-bucket.obs.cn-north-4.myhuaweicloud.com/test.txt' \
-H 'Origin: https://your-web.com' \
-H 'Content-Type: text/plain' \
--data-binary 'hello obs'
注意:直接用curl发PUT缺少签名,OBS会返回403,这并不代表CORS有问题。403响应里如果能看到Access-Control-Allow-Origin头,说明CORS已经放行,剩下的是签名校验问题。反之如果响应里没有CORS头,说明请求还在OBS网关层就被拦截了。
6.3 浏览器开发者工具怎么看CORS头
在Chrome开发者工具中:
- 打开“网络”标签页,筛选
Fetch/XHR。 - 找到失败的请求,点击它。
- 切换到“响应头”面板,搜索
Access-Control-开头的字段。 - 如果找不到任何
Access-Control-Allow-*,说明服务端没有返回CORS头,去检查OBS规则。 - 如果头齐全但浏览器依然报错,对比一下
Access-Control-Allow-Headers和预检请求的Access-Control-Request-Headers是否完全覆盖。
这个方法能快速区分“服务端没有配置CORS”和“配置了但没匹配上”两种情形。
7. 经验总结
这次对接华为云OBS上传附件的踩坑经历,让我对CORS有了更务实的理解。CORS不是后端接口层面能屏蔽的问题,它取决于浏览器、服务端和请求本身三方共同作用。处理这类问题,最怕的就是拿着前端报错一头扎进代码里翻找,却忘了先去OBS控制台看一眼CORS规则。
我个人在实际操作中的体会是:先确认架构模式(直传还是中转),再配CORS规则,最后调试签名。顺序一旦搞反,很容易把时间浪费在无意义的代码改动上。另外,开发环境和生产环境的CORS规则最好分开维护,不要图省事把AllowedOrigin全部配成*,否则日志排查时会很难受。
最后再分享一个小技巧:给后端生成预签名URL的接口加一个debug参数,当debug=true时把生成的签名URL原样返回给前端并打印到控制台,这样前端可以直接用这个URL在curl里做验证,把CORS问题和签名问题彻底拆开排查。这个习惯帮我省掉了大量联调时间,你有类似需求时也可以试试。
