'Access-Control-Allow-Origin' 标头如何工作?
- 2024-11-02 21:00:00
- admin 原创
- 43
问题描述:
显然,我完全误解了它的语义。我想到的是这样的:
客户端从源
http://siteA
下载JavaScript 代码 MyCode.js 。MyCode.js 的响应头包含Access-Control-Allow-Origin:
http://siteB
,我认为这意味着允许 MyCode.js 对站点 B 进行跨域引用。客户端触发 MyCode.js 的某些功能,进而向 发出请求
http://siteB
,尽管是跨源请求,但这应该没问题。
好吧,我错了。它根本不是这样工作的。因此,我阅读了跨源资源共享,并尝试阅读w3c 建议中的跨源资源共享。
有一件事是肯定的——我仍然不明白该如何使用这个标题。
我对站点 A 和站点 B 都有完全控制权。如何使从站点 A 下载的 JavaScript 代码使用此标头访问站点 B 上的资源?
PS:我不想使用JSONP。
解决方案 1:
Access-Control-Allow-Origin是一个CORS(跨域资源共享)标头。
当站点 A 尝试从站点 B 获取内容时,站点 B 可以发送Access-Control-Allow-Origin响应标头,以告知浏览器此页面的内容可供某些来源访问。(来源是域加上方案和端口号。)默认情况下,站点 B 的页面无法被任何其他来源访问;使用Access-Control-Allow-Origin标头为特定请求来源的跨源访问打开了一扇大门。
对于站点 B 想要让站点 A 访问的每个资源/页面,站点 B 应该使用响应标头提供其页面:
Access-Control-Allow-Origin: http://siteA.com
现代浏览器不会直接阻止跨域请求。如果站点 A 请求站点 B 的页面,浏览器实际上会在网络级别获取所请求的页面,并检查响应标头是否将站点 A 列为允许的请求者域。如果站点 B 没有表明允许站点 A 访问此页面,则浏览器将触发XMLHttpRequest
'serror
事件并拒绝向请求的 JavaScript 代码发送响应数据。
非简单请求
网络层面发生的事情可能比上面解释的稍微复杂一些。如果请求是“非简单”请求,浏览器首先发送一个无数据的“预检”OPTIONS请求,以验证服务器是否会接受该请求。当使用以下任一(或两者)时,请求是非简单请求:
GET 或 POST 之外的 HTTP 动词(例如 PUT、DELETE);
非简单请求标头;唯一的简单请求标头是:
Accept
;Accept-Language
;Content-Language
;Content-Type
(仅当其值为application/x-www-form-urlencoded
、multipart/form-data
或 时才是简单的text/plain
)。
Access-Control-Allow-Headers
如果服务器使用与非简单动词和/或非简单标头匹配的适当响应标头(对于非简单标头,Access-Control-Allow-Methods
对于非简单动词)响应 OPTIONS 预检,则浏览器发送实际请求。
假设站点 A 想要发送一个 PUT 请求/somePage
,其值为非简单Content-Type
值application/json
,则浏览器将首先发送预检请求:
OPTIONS /somePage HTTP/1.1
Origin: http://siteA.com
Access-Control-Request-Method: PUT
Access-Control-Request-Headers: Content-Type
请注意,Access-Control-Request-Method
和Access-Control-Request-Headers
是由浏览器自动添加的;您不需要添加它们。此 OPTIONS 预检获取成功的响应标头:
Access-Control-Allow-Origin: http://siteA.com
Access-Control-Allow-Methods: GET, POST, PUT
Access-Control-Allow-Headers: Content-Type
发送实际请求时(预检完成后),其行为与处理简单请求的方式相同。换句话说,预检成功的非简单请求将被视为与简单请求相同(即,服务器仍必须Access-Control-Allow-Origin
再次发送实际响应)。
浏览器发送实际请求:
PUT /somePage HTTP/1.1
Origin: http://siteA.com
Content-Type: application/json
{ "myRequestContent": "JSON is so great" }
然后服务器返回一个Access-Control-Allow-Origin
,就像一个简单的请求一样:
Access-Control-Allow-Origin: http://siteA.com
有关非简单请求的更多信息,请参阅理解 CORS 上的 XMLHttpRequest 。
解决方案 2:
跨域资源共享CORS
(又称跨域 AJAX 请求)是大多数 Web 开发人员可能会遇到的问题,根据同源策略,浏览器将客户端 JavaScript 限制在安全沙箱中,通常 JS 无法直接与不同域的远程服务器通信。过去开发人员创建了许多巧妙的方法来实现跨域资源请求,最常用的方法有:
使用 Flash/Silverlight 或服务器端作为“代理”与远程通信。
带填充的 JSON(JSONP)。
在 iframe 中嵌入远程服务器并通过 fragment 或 window.name 进行通信,请参阅此处。
这些棘手的方法或多或少都存在一些问题,例如,如果开发人员简单地“评估”它,JSONP 可能会导致安全漏洞,而上面的第 3 条,虽然它可以工作,但两个域之间都应该建立严格的契约,在我看来,它既不灵活也不优雅:)
W3C 引入了跨域资源共享(CORS)作为标准解决方案,为解决该问题提供了一种安全、灵活且值得推荐的标准方法。
机制
从高层次来看,我们可以简单地将 CORS 视为来自域 A 的客户端 AJAX 调用与托管在域 B 上的页面之间的契约,典型的跨域请求/响应将是:
DomainA AJAX 请求标头
Host DomainB.com
User-Agent Mozilla/5.0 (Windows NT 6.1; WOW64; rv:2.0) Gecko/20100101 Firefox/4.0
Accept text/html,application/xhtml+xml,application/xml;q=0.9,*/*;q=0.8,application/json
Accept-Language en-us;
Accept-Encoding gzip, deflate
Keep-Alive 115
Origin http://DomainA.com
DomainB 响应标头
Cache-Control private
Content-Type application/json; charset=utf-8
Access-Control-Allow-Origin DomainA.com
Content-Length 87
Proxy-Connection Keep-Alive
Connection Keep-Alive
上面我标注的蓝色部分是核心内容,“Origin”请求头表示跨域请求或者预检请求来源于哪里,“Access-Control-Allow-Origin”响应头表示这个页面允许来自DomainA的远程请求(如果值为*则表示允许来自任何域的远程请求)。
正如我上面提到的,W3 建议浏览器在提交真正的跨域 HTTP 请求之前执行“预检请求”,简而言之,它是一个 HTTPOPTIONS
请求:
OPTIONS DomainB.com/foo.aspx HTTP/1.1
如果 foo.aspx 支持 OPTIONS HTTP 动词,它可能会返回如下响应:
HTTP/1.1 200 OK
Date: Wed, 01 Mar 2011 15:38:19 GMT
Access-Control-Allow-Origin: http://DomainA.com
Access-Control-Allow-Methods: POST, GET, OPTIONS, HEAD
Access-Control-Allow-Headers: X-Requested-With
Access-Control-Max-Age: 1728000
Connection: Keep-Alive
Content-Type: application/json
只有当响应包含“Access-Control-Allow-Origin”并且其值为“*”或包含提交 CORS 请求的域时,满足此强制条件浏览器才会提交真正的跨域请求,并将结果缓存在“ Preflight-Result-Cache ”中。
我三年前写过一篇关于 CORS 的博客:AJAX 跨域 HTTP 请求
解决方案 3:
根据Mozilla 开发者网络的这篇文章,
当资源从与第一个资源本身所服务的域或端口不同的域或端口请求资源时,该资源会发出跨源 HTTP 请求。
由 提供的HTML 页面对http://domain-a.com
发出<img>
src 请求http://domain-b.com/image.jpg
。如今,网络上的许多页面都从不同的域加载CSS 样式表、图像和脚本等资源(因此这应该很酷)。
同源策略
出于安全原因,浏览器会限制从脚本内部发起的跨源 HTTP请求。例如,和遵循同源策略。因此,使用或的Web 应用程序只能向其自己的域发出HTTP 请求。XMLHttpRequest
`FetchXMLHttpRequest
Fetch`
跨域资源共享 (CORS)
为了改进网络应用程序,开发人员要求浏览器供应商允许跨域请求。
跨域资源共享 (CORS)机制为 Web 服务器提供了跨域访问控制,从而实现了安全的跨域数据传输。现代浏览器在 API 容器(例如XMLHttpRequest
或fetch
)中使用 CORS 来减轻跨域 HTTP 请求的风险。
CORS 的工作原理(Access-Control-Allow-Origin标头)
维基百科:
CORS 标准描述了新的 HTTP 标头,它为浏览器和服务器提供了一种只有在获得权限时才请求远程 URL 的方法。
尽管服务器可以执行某些验证和授权,但通常浏览器有责任支持这些标头并遵守它们施加的限制。
例子
浏览器发送
OPTIONS
带有Origin HTTP
标头的请求。此标头的值是为父页面提供服务的域。当来自 的页面
http://www.example.com
尝试访问 中的用户数据时service.example.com
,将向 发送以下请求标头service.example.com
:
lang-none Origin: http://www.example.com
服务器
service.example.com
可能会响应:
响应中的 (ACAO)标
Access-Control-Allow-Origin
头指示允许哪些源站点。例如:
Access-Control-Allow-Origin: http://www.example.com
如果服务器不允许跨源请求,则显示错误页面
带有通配符的 (ACAO)标
Access-Control-Allow-Origin
头允许所有域:
Access-Control-Allow-Origin: *
解决方案 4:
每当我开始考虑 CORS 时,我对哪个站点托管标头的直觉都是错误的,就像您在问题中描述的那样。对我来说,思考同源策略的目的会有所帮助。
同源策略的目的是保护您免受 siteA.com 上的恶意 JavaScript 访问您选择仅与 siteB.com 共享的私人信息的侵害。如果没有同源策略,siteA.com 的作者编写的 JavaScript 可能会让您的浏览器使用您对 siteB.com 的身份验证 cookie 向 siteB.com 发出请求。这样,siteA.com 就可以窃取您与 siteB.com 共享的秘密信息。
有时你需要跨域工作,这就是 CORS 的用武之地。CORS 放宽了 siteB.com 的同源策略,使用Access-Control-Allow-Origin
标头列出其他可信任运行可与 siteB.com 交互的 JavaScript 的域(siteA.com)。
要了解哪个域应该提供 CORS 标头,请考虑以下情况。您访问malware.com ,其中包含一些尝试向mybank.com发出跨域请求的 JavaScript 。应该由mybank.com(而不是malware.com)决定是否设置放宽同源策略的 CORS 标头,以允许来自malware.com的 JavaScript与其交互。如果malicous.com可以设置自己的 CORS 标头,允许自己的 JavaScript 访问mybank.com,这将完全取消同源策略。
我认为我直觉不好的原因在于我开发网站时的观点。这是我的网站,里面有我所有的JavaScript。因此,它没有做任何恶意的事情,我应该指定我的 JavaScript 可以与哪些其他网站交互。而事实上我应该思考:哪些其他网站的 JavaScript 正在尝试与我的网站交互,我应该使用 CORS 来允许它们吗?
解决方案 5:
根据我自己的经验,很难找到一个简单的解释为什么 CORS 甚至是一个值得关注的问题。
一旦你理解了它为什么在那里,标题和讨论就会变得更加清晰。我会用几行代码来演示一下。
一切都与 Cookie 有关。Cookie 由其域存储在客户端上。
举个例子:在你的电脑上,有一个 的 cookie
yourbank.com
。也许你的会话就在那里。
关键点:当客户端向服务器发出请求时,它会发送该请求的域下存储的 cookie。
您已在浏览器中登录
yourbank.com
。您请求查看所有帐户,并向 发送 cookieyourbank.com
。yourbank.com
接收一堆 cookie 并发回其响应(您的帐户)。
如果另一个客户端向服务器发出跨源请求,这些 cookie 也会像以前一样被发送。Ruh roh。
您浏览到
malicious.com
。恶意向不同的银行发出一堆请求,其中包括yourbank.com
。
由于 Cookie 已按预期通过验证,因此服务器将授权响应。
这些饼干被收集起来并寄出去——现在,
malicious.com
收到了来自的回复yourbank
。
哎呀。
现在,一些问题和答案变得明显了:
“我们为什么不直接阻止浏览器这样做呢?”是的。这就是 CORS。
“我们该如何解决这个问题?”让服务器告知请求 CORS 是可以的。
解决方案 6:
使用React和Axios,将代理链接加入到 URL 并添加标头,如下所示:
https://cors-anywhere.herokuapp.com/
+Your API URL
只需添加代理链接即可,但也可能再次引发“无访问权限”错误。因此,最好添加如下所示的标头。
axios.get(`https://cors-anywhere.herokuapp.com/[YOUR_API_URL]`,{headers: {'Access-Control-Allow-Origin': '*'}})
.then(response => console.log(response:data);
}
警告:不得在生产中使用
这只是一个快速解决方案。如果您不知道为什么无法获得回复,可以使用它。但这并不是生产的最佳答案。
解决方案 7:
1. 客户端从http://siteA(源站)下载 javascript 代码 MyCode.js。
执行下载的代码(您的 html 脚本标记或来自 javascript 的 xhr 或其他代码)来自http://siteZ。并且,当浏览器请求 MyCode.js 时,它会发送一个 Origin: 标头,显示“Origin: http://siteZ ”,因为它可以看到您正在向 siteA 发出请求,并且 siteZ != siteA。(您无法停止或干扰此操作。)
2. MyCode.js 的响应头包含 Access-Control-Allow-Origin: http://siteB,我认为这意味着允许 MyCode.js 对站点 B 进行跨域引用。
不。这意味着,只有 siteB 被允许执行此请求。因此,您从 siteZ 请求 MyCode.js 会出错,浏览器通常不会给您任何内容。但是,如果您让服务器返回 ACAO: siteZ,您将获得 MyCode.js 。或者如果它发送 '*',那将起作用,这将让每个人都能进入。或者,如果服务器始终发送来自 Origin: 标头的字符串...但是...为了安全起见,如果您担心黑客,您的服务器应该只允许候选名单上的来源发出这些请求。
然后,MyCode.js 来自 siteA。当它向 siteB 发出请求时,它们都是跨源的,浏览器发送 Origin: siteA,而 siteB 必须接受 siteA,识别出它在允许的请求者名单中,并返回 ACAO: siteA。只有这样,浏览器才会让您的脚本获得这些请求的结果。
解决方案 8:
我使用过Express.js 4、Node.js 7.4 和Angular,也遇到了同样的问题。这对我有帮助:
a)服务器端:在文件app.js中,我为所有响应添加了标题,例如:
app.use(function(req, res, next) {
res.header('Access-Control-Allow-Origin', req.headers.origin);
res.header("Access-Control-Allow-Headers", "Origin, X-Requested-With, Content-Type, Accept");
next();
});
这必须在所有路线之前。
我看到很多添加了这个标题:
res.header("Access-Control-Allow-Headers","*");
res.header('Access-Control-Allow-Credentials', true);
res.header('Access-Control-Allow-Methods', 'GET,PUT,POST,DELETE');
但我不需要那个,
b) 客户端:通过Ajax发送时,需要添加“withCredentials: true”,例如:
$http({
method: 'POST',
url: 'url',
withCredentials: true,
data : {}
}).then(function(response){
// Code
}, function (response) {
// Code
});
解决方案 9:
如果您使用 PHP,请尝试在 php 文件的开头添加以下代码:
如果您使用的是本地主机,请尝试以下操作:
header("Access-Control-Allow-Origin: *");
如果您正在使用外部域(如服务器),请尝试以下操作:
header("Access-Control-Allow-Origin: http://www.website.com");
解决方案 10:
如果您只是想测试浏览器阻止您的请求的跨域应用程序,那么您只需在不安全模式下打开浏览器并测试您的应用程序,而无需更改您的代码,也不会使您的代码变得不安全。
在macOS中,您可以通过终端行执行此操作:
open -a Google Chrome --args --disable-web-security --user-data-dir
解决方案 11:
在 Python 中,我一直在使用Flask-CORS库,并且取得了巨大的成功。它使处理 CORS 变得非常简单和轻松。我从下面的库文档中添加了一些代码。
安装:
pip install -U flask-cors
允许所有路由上的所有域使用 CORS 的简单示例:
from flask import Flask
from flask_cors import CORS
app = Flask(__name__)
CORS(app)
@app.route("/")
def helloWorld():
return "Hello, cross-origin-world!"
有关更具体的示例,请参阅文档。我已使用上面的简单示例来解决我正在构建的Ionic应用程序中的 CORS 问题,该应用程序必须访问单独的 flask 服务器。
解决方案 12:
只需将以下代码粘贴到您的web.config文件中。
请注意,您必须将以下代码粘贴到<system.webServer>
标签下
<httpProtocol>
<customHeaders>
<add name="Access-Control-Allow-Origin" value="*" />
<add name="Access-Control-Allow-Headers" value="Content-Type" />
<add name="Access-Control-Allow-Methods" value="GET, POST, PUT, DELETE, OPTIONS" />
</customHeaders>
</httpProtocol>
解决方案 13:
我无法在后端服务器上对其进行配置,但使用浏览器中的这些扩展,它可以为我工作:
对于 Firefox:
无处不在的 CORS
对于 Google Chrome:
允许 CORS:Access-Control-Allow-Origin
注意:CORS 适用于以下配置:
解决方案 14:
对于跨源共享,设置标头:'Access-Control-Allow-Origin':'*';
PHP:header('Access-Control-Allow-Origin':'*');
节点:app.use('Access-Control-Allow-Origin':'*');
这将允许不同域共享内容。
解决方案 15:
Nginx 和 Apache
作为对apsiller 答案的补充,我想添加一个wiki 图表,显示请求是否简单(以及 OPTIONS 预检请求是否发送)
对于简单请求(例如热链接图像),您不需要更改服务器配置文件,但您可以在应用程序中添加标头(托管在服务器上,例如在PHP中),就像 Melvin Guerrero 在他的回答中提到的那样 - 但请记住:如果您在服务器(配置)中添加完整的 CORS 标头,同时在应用程序(例如 PHP)中允许简单的 CORS,那么这将根本不起作用。
以下是两种流行服务器的配置:
在 Nginx 上启用CORS(nginx.conf文件)
显示代码片段
location ~ ^/index.php(/|$) {
...
add_header 'Access-Control-Allow-Origin' "$http_origin" always; # if you change "$http_origin" to "*" you shoud get same result - allow all domain to CORS (but better change it to your particular domain)
add_header 'Access-Control-Allow-Credentials' 'true' always;
if ($request_method = OPTIONS) {
add_header 'Access-Control-Allow-Origin' "$http_origin"; # DO NOT remove THIS LINES (doubled with outside 'if' above)
add_header 'Access-Control-Allow-Credentials' 'true';
add_header 'Access-Control-Max-Age' 1728000; # cache preflight value for 20 days
add_header 'Access-Control-Allow-Methods' 'GET, POST, OPTIONS'; # arbitrary methods
add_header 'Access-Control-Allow-Headers' 'My-First-Header,My-Second-Header,Authorization,Content-Type,Accept,Origin'; # arbitrary headers
add_header 'Content-Length' 0;
add_header 'Content-Type' 'text/plain charset=UTF-8';
return 204;
}
}
Run code snippetHide resultsExpand snippet
在 Apache 上启用CORS (.htaccess文件)
显示代码片段
# ------------------------------------------------------------------------------
# | Cross-domain Ajax requests |
# ------------------------------------------------------------------------------
# Enable cross-origin Ajax requests.
# http://code.google.com/p/html5security/wiki/CrossOriginRequestSecurity
# http://enable-cors.org/
# change * (allow any domain) below to your domain
Header set Access-Control-Allow-Origin "*"
Header always set Access-Control-Allow-Methods "POST, GET, OPTIONS, DELETE, PUT"
Header always set Access-Control-Allow-Headers "My-First-Header,My-Second-Header,Authorization, content-type, csrf-token"
Header always set Access-Control-Allow-Credentials "true"
Run code snippetHide resultsExpand snippet
解决方案 16:
Access-Control-Allow-Origin 响应标头指示是否可以与来自给定来源的请求代码共享响应。
Header type Response header ------------------------------------------- Forbidden header name no
告诉浏览器允许来自任何来源的代码访问资源的响应将包括以下内容:
Access-Control-Allow-Origin: *
有关更多信息,请访问Access-Control-Allow-Origin ...
解决方案 17:
TLDR: CORS 是一种服务器设置,允许所有或某些受信任的域与服务器交互。
以下是针对虚构网站 mysite.com 使用两种不同场景的简短解释:
场景 A:CORS 设置将仅允许源域与服务器交互。场景 B:通过将 external.com 添加到允许源列表,CORS 设置将允许 external.com 与服务器交互。
对于场景 A,用户访问 mysite.com,其浏览器查询服务器并得到响应。一切正常,因为请求是通过 mysite.com 域发出的。
同样对于场景 A,external.com 尝试向您的服务器发出请求以在其网站上显示。也许他们想显示您的徽标并添加一个带有 src="mysite.com/logo.svg" 的 html 元素。您的服务器可以看到发送到 mysite.com/logo.svg 的请求来自 external.com。由于 CORS 策略只允许来源 mysite.com,因此该请求将被阻止,并且 external.com 将收到 CORS 错误。
对于场景 B,external.com 被添加为 mysite.com 服务器上的受信任来源。当 external.com 向“mysite.com/logo.svg”发送请求时,服务器可以看到域 external.com 被允许并返回资源。图像 logo.svg 显示在 external.com 上,没有任何问题。
如果您使用通配符 * 允许所有来源,则任何站点都可以按照与场景 B 相同的方式与您的服务器进行交互。
解决方案 18:
对于带有 Angular 的 .NET Core 3.1 API
Startup.cs:添加 CORS
//SERVICES
public void ConfigureServices(IServiceCollection services){
//CORS (Cross Origin Resource Sharing)
//=====================================
services.AddCors();
}
//MIDDLEWARES
public void Configure(IApplicationBuilder app, IWebHostEnvironment env)
{
app.UseRouting();
//ORDER: CORS -> Authentication -> Authorization)
//CORS (Cross Origin Resource Sharing)
//=====================================
app.UseCors(x=>x.AllowAnyHeader().AllowAnyMethod().WithOrigins("http://localhost:4200"));
app.UseHttpsRedirection();
}
}
控制器:为授权控制器启用 CORS
//Authorize all methods inside this controller
[Authorize]
[EnableCors()]
public class UsersController : ControllerBase
{
//ActionMethods
}
解决方案 19:
注意:仅为测试临时解决方案
对于那些无法控制后端的人来说Options 405 Method Not Allowed
,这里有一个针对 Chrome 浏览器的解决方法。
在命令行中执行:
"C:Program Files (x86)GoogleChromeApplicationchrome.exe" --disable-web-security --user-data-dir="path_to_profile"
例子:
"C:Program Files (x86)GoogleChromeApplicationchrome.exe" --disable-web-security --user-data-dir="C:UsersitalAppDataLocalGoogleChromeUser DataProfile 2"
解决方案 20:
在客户端应用程序端解决此 CORS 错误(“缺少 CORS 标头‘Access-Control-Allow-Origin’”)的另一种方法是通过 XHR 授权机制。例如,
如何在 angular 6 中将 JWT 令牌作为授权标头发送
1. Ensure that your OIDC (Ex. Keycloak) allows you in, and check "allowed-origins" or "web-origins". This "allowed-origins" need to contain your frontend application url (Ex. http://host1:4200)
2. Into the client XHR call, add a request header with a valid JWT token to access backend application url (Ex. http://host2:8080)
Authorization: Bearer .....
3. The HttpClient, XHR call using header form step 2. will accept CORS if JWT attribute "allowed-origins" contains client url, (Ex. http://host1:4200), please check your JWT token
使用https://jwt.io/检查你的 JWT 令牌
完成这些步骤后,CORS 就可以在客户端正常工作了。
解决方案 21:
这很奇怪,但 CORS 问题可能发生,因为您尝试通过客户端 ajax 从作为前端基本库的 react、angular、jquery 应用程序发出请求。
您必须从后端应用程序发出请求。
您正在尝试从前端 API 发出请求,但您尝试使用的 API 期望该请求由后端应用程序发出,并且它永远不会接受客户端请求。
我尝试从前端发出请求,但没有成功,然后我使用 curl 发出相同的请求,并且成功了。
- 2024年20款好用的项目管理软件推荐,项目管理提效的20个工具和技巧
- 2024年开源项目管理软件有哪些?推荐5款好用的项目管理工具
- 项目管理软件有哪些?推荐7款超好用的项目管理工具
- 项目管理软件哪个最好用?盘点推荐5款好用的项目管理工具
- 项目管理软件有哪些最好用?推荐6款好用的项目管理工具
- 项目管理软件有哪些,盘点推荐国内外超好用的7款项目管理工具
- 2024项目管理软件排行榜(10类常用的项目管理工具全推荐)
- 项目管理软件排行榜:2024年项目经理必备5款开源项目管理软件汇总
- 2024年常用的项目管理软件有哪些?推荐这10款国内外好用的项目管理工具
- 项目管理必备:盘点2024年13款好用的项目管理软件