Access-Control-Allow-Methods配置踩坑记那些年被跨域请求方法困扰的日子
Access-Control-Allow-Methods被搞砸了
今天又被跨域问题给折腾得够呛,这次是Access-Control-Allow-Methods相关的。本来以为这种问题早就不是事儿了,结果一个配置错误让我折腾了快两个小时,真是服了。
问题复现过程
前端发请求的时候突然报错:
The value of the ‘Access-Control-Allow-Methods’ header in the response must not be ‘*’ when the request’s credentials mode is ‘include’.
大概意思就是说,当请求带credentials的时候,不能用*通配符来设置允许的方法。这倒是个新知识,之前没太注意这个限制。
我的前端代码大概是这样的:
fetch('https://jztheme.com/api/user', {
method: 'POST',
credentials: 'include',
headers: {
'Content-Type': 'application/json'
},
body: JSON.stringify({
username: 'test',
password: '123456'
})
})
后端呢,之前为了省事,Access-Control-Allow-Methods直接设成了*,觉得这样所有方法都能通过,结果就踩了这个坑。
折腾过程和各种尝试
一开始我也没太在意错误信息的具体内容,先试了下把credentials改成omit或者same-origin,确实不报错了,但这显然不是解决办法,因为我要的就是携带cookie。
然后我就去查MDN文档,发现这里有个安全限制:当请求的credentials模式是include的时候,响应头中的Access-Control-Allow-Methods不能是*,必须明确指定具体的方法。
这里我想到了几种解决办法:
- 把Access-Control-Allow-Methods设成具体的值
- 改请求的credentials配置
- 调整后端的CORS策略
第二种方案明显不行,第三种需要改动的地方比较多。最后还是选择了第一种,把*改成具体的方法列表。
真正的问题所在
但是这里我又遇到了另一个问题。如果我把Access-Control-Allow-Methods设成固定的几个方法,比如GET, POST, PUT,那万一前端要用DELETE或者其他方法怎么办?
这里我踩了个比较大的坑。一开始想当然地认为应该把所有可能用到的方法都写上去:
// 这种写法是不对的
res.setHeader('Access-Control-Allow-Methods', 'GET, POST, PUT, DELETE, PATCH, OPTIONS');
但实际上这样会有安全风险,因为你把所有方法都开放了,即使有些方法当前并不需要。更好的做法应该是根据实际请求的method来动态设置。
折腾了半天发现,可以用Access-Control-Request-Method这个请求头来判断前端想要用什么方法,然后动态返回对应的Allow-Methods。
最终解决方案
经过一番研究和测试,最终的解决方案如下。这里分两种情况:预检请求和普通请求。
对于预检请求(OPTIONS),需要根据Access-Control-Request-Method来决定返回什么方法:
app.use((req, res, next) => {
// 设置基本的CORS头
res.header('Access-Control-Allow-Origin', 'http://localhost:3000'); // 生产环境要换成具体的域名
res.header('Access-Control-Allow-Credentials', 'true');
if (req.method === 'OPTIONS') {
// 获取前端请求的方法
const requestedMethod = req.headers['access-control-request-method'];
if (requestedMethod) {
// 根据请求方法返回对应的允许方法
// 这里可以根据业务需求进行过滤
const allowedMethods = getValidatedMethods(requestedMethod);
res.header('Access-Control-Allow-Methods', allowedMethods);
} else {
// 如果没有指定请求方法,默认返回常用的方法
res.header('Access-Control-Allow-Methods', 'GET, POST, PUT, DELETE');
}
res.header('Access-Control-Allow-Headers',
req.headers['access-control-request-headers'] || 'Content-Type, Authorization');
res.sendStatus(200);
return;
}
next();
});
function getValidatedMethods(requestedMethod) {
// 定义合法的方法列表
const validMethods = ['GET', 'POST', 'PUT', 'DELETE', 'PATCH'];
// 验证并返回对应的方法
if (validMethods.includes(requestedMethod.toUpperCase())) {
return requestedMethod;
}
// 如果不在白名单内,返回默认的安全方法
return 'GET, POST';
}
对于Node.js Express框架,也可以用cors中间件配合自定义逻辑:
const cors = require('cors');
const corsOptions = {
origin: 'http://localhost:3000',
credentials: true,
methods: ['GET', 'POST'], // 默认方法
optionsSuccessStatus: 200
};
app.use((req, res, next) => {
if (req.method === 'OPTIONS' && req.headers['access-control-request-method']) {
// 动态处理OPTIONS请求
const requestedMethod = req.headers['access-control-request-method'].toUpperCase();
// 验证请求的方法是否在允许范围内
if (isValidMethod(requestedMethod)) {
res.header('Access-Control-Allow-Methods', requestedMethod);
} else {
res.header('Access-Control-Allow-Methods', 'GET, POST');
}
}
next();
});
app.use(cors(corsOptions));
PHP版本的处理也很类似:
<?php
header('Access-Control-Allow-Origin: http://localhost:3000');
header('Access-Control-Allow-Credentials: true');
if ($_SERVER['REQUEST_METHOD'] === 'OPTIONS') {
$requestedMethod = $_SERVER['HTTP_ACCESS_CONTROL_REQUEST_METHOD'] ?? '';
if ($requestedMethod && isValidMethod($requestedMethod)) {
header("Access-Control-Allow-Methods: $requestedMethod");
} else {
header('Access-Control-Allow-Methods: GET, POST, PUT, DELETE');
}
$requestedHeaders = $_SERVER['HTTP_ACCESS_CONTROL_REQUEST_HEADERS'] ?? 'Content-Type, Authorization';
header("Access-Control-Allow-Headers: $requestedHeaders");
http_response_code(200);
exit();
}
function isValidMethod($method) {
$allowedMethods = ['GET', 'POST', 'PUT', 'DELETE', 'PATCH'];
return in_array(strtoupper($method), $allowedMethods);
}
?>
需要注意的安全细节
这里特别要注意的是,不要盲目相信前端传来的Access-Control-Request-Method。一定要在服务端进行验证,确保只有预设的方法才能被允许。
另外,Access-Control-Allow-Methods设置成具体值而不是*,其实也是为了安全考虑。*意味着所有方法都可以,这可能会带来意想不到的安全风险。
我之前就遇到过一个问题,某个恶意脚本利用了通配符设置,发送了一些原本不应该被允许的HTTP方法,虽然最终没有造成严重后果,但也提醒我这个配置的重要性。
调试小技巧
在调试这类问题的时候,Chrome DevTools的Network面板特别有用。可以看到具体的请求头和响应头,以及预检请求的过程。
如果是在本地开发环境,可以直接看控制台的错误信息。但线上环境的话,可能需要通过日志来排查,这时候就要注意记录CORS相关的信息了。
还有一个小技巧是,在后端加一些日志输出,看看实际收到的请求是什么样的:
console.log('Requested method:', req.headers['access-control-request-method']);
console.log('Credentials mode:', req.headers['with-credentials'] ? 'include' : 'omit');
这样可以更好地理解前端的实际请求情况。
以上是我踩坑后的总结,如果你有更好的方案欢迎评论区交流。

暂无评论