Access-Control-Allow-Methods配置踩坑记那些年被跨域请求方法困扰的日子

♫小汐 安全 阅读 545
赞 4 收藏
二维码
手机扫码查看
反馈

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

这样可以更好地理解前端的实际请求情况。

以上是我踩坑后的总结,如果你有更好的方案欢迎评论区交流。

本文章不代表JZTHEME立场,仅为作者个人观点 / 研究心得 / 经验分享,旨在交流探讨,供读者参考。
发表评论

暂无评论