如何从一个域重定向到另一个域并为另一个域设置 cookie 或标头?

2025-01-06 08:32:00
admin
原创
118
摘要:问题描述:我正在使用 FastAPI RedirectResponse,并尝试将用户从一个应用程序(域)重定向到另一个应用程序(域),并在其中设置一些 cookie Response;但是,cookie 总是被删除(而不是传输)。如果我尝试添加一些标头,我添加的所有标头RedirectResponse也不会被...

问题描述:

我正在使用 FastAPI RedirectResponse,并尝试将用户从一个应用程序(域)重定向到另一个应用程序(域),并在其中设置一些 cookie Response;但是,cookie 总是被删除(而不是传输)。如果我尝试添加一些标头,我添加的所有标头RedirectResponse也不会被传输。

@router.post("/callback")
async def sso_callback(request: Request):
   jwt_token = generate_token(request)
   redirect_response = RedirectResponse(url="http://192.168.10.1/app/callback", 
                             status_code=303)
   redirect_response.set_cookie(key="accessToken", value=jwt_token, httponly=True)
   redirect_response.headers["Authorization"] = str(jwt_token)
   return redirect_response

我该如何解决这个问题?提前感谢你的帮助。


解决方案 1:

如此处所述,无论您使用哪种语言或框架,都无法使用自定义标头集重定向到另一个域。HTTP 协议中的重定向基本上是Location与响应关联的标头(即),并且不允许添加任何指向目标位置的标头。当您Authorization在示例中添加标头时,您基本上是为指示浏览器重定向的响应设置该标头,而不是为重定向本身设置该标头。换句话说,您正在将该标头发送回客户端

至于HTTP cookies,浏览器会将服务器发送的 cookies 与响应一起存储(使用Set-Cookie标头),然后将这些 cookies 与对同一服务器的请求一起发送到 HTTP 标头中Cookie。根据文档:

HTTPSet-Cookie响应标头用于将 Cookie 从服务器发送到用户代理,以便用户代理稍后将其发送回服务器。要发送多个 Cookie,Set-Cookie
应在同一响应中发送多个标头。

因此,如果这是从一个应用程序(具有子域,例如abc.example.test)到另一个应用程序(具有子域,例如xyz.example.test)的重定向,并且两个应用程序都具有相同的(父)域(并且在创建 cookie 时domain将标志设置为example.test),则 cookie 将在两个应用程序之间成功共享(好像指定domain了,那么子域始终包含在内)。 无论使用哪种协议(HTTP/HTTPS)或端口,浏览器都会向给定的域(包括任何子域)提供 cookie。 您可以使用domainpath标志限制 cookie 的可用性,也可以使用和标志限制对 cookie 的访问(请参阅此处和此处,以及Starlette 文档)。 如果未设置标志,潜在的攻击者可以通过 JavaScript(JS)读取和修改信息,而具有属性的 cookie 只会发送到服务器,并且客户端的 JS 无法访问。secure`httpOnlyhttpOnlyhttpOnly`

但是,您不能为不同的域设置 cookie。如果允许这样做,将带来巨大的安全漏洞。因此,由于您“试图将用户从一个应用程序(域)重定向到另一个设置了某些 cookie 的应用程序……”,因此它不会起作用,因为 cookie 只会随对同一域的请求一起发送。

解决方案 1

如此处所述,一种解决方案是让域(应用程序)A 将用户重定向到域(应用程序)B,并将access-tokenURL 作为查询参数传入。然后,域 B 将读取令牌并设置自己的 cookie,以便浏览器将存储该 cookie 并在每次对域 B 的后续请求中发送该 cookie。

请注意,您应考虑使用安全 (HTTPS) 通信,以便令牌以加密方式传输,并secure在创建 cookie 时设置标志。另请注意,在查询字符串中包含令牌 会带来严重的安全风险,因为敏感数据绝不能通过查询字符串传递。这是因为查询字符串(URL 的一部分)出现在浏览器的地址栏中;因此,允许用户查看和收藏包含令牌的 URL(意味着它保存在磁盘上)。此外,URL 会进入浏览历史记录,这意味着它无论如何都会写入磁盘并出现在History选项卡中(按下Ctrl+H可查看浏览器的历史记录)。以上两种情况都会允许攻击者(以及与您共享计算机/移动设备的人)窃取此类敏感数据。此外,许多浏览器插件/扩展会跟踪用户的浏览活动 - 您访问的每个 URL 都会发送到他们的服务器进行分析,以便检测恶意网站并提前警告您。因此,在使用以下方法之前,您应该考虑以上所有因素(有关此主题的相关帖子,请参见此处、此处和此处)。

为了防止在地址栏中显示 URL,下面的方法也使用了域 B 内的重定向。一旦域 B 收到/submit以令牌作为查询参数的路由请求,域 B 就会重定向到其中没有令牌的裸 URL(即其home页面)。由于这种重定向,其中包含令牌的 URL 不会出现在浏览历史记录中。虽然这可以在一定程度上防止前面描述的某些攻击,但这并不意味着浏览器扩展等将无法捕获其中包含令牌的 URL。

如果您在 localhost 上测试此操作,则需要为应用程序 B 指定一个不同的域名;否则,如前所述,cookie 将在具有相同域的应用程序之间共享,因此,您最终将收到为域 A 设置的 cookie,并且无法判断该方法是否有效。为此,您必须编辑文件/etc/hosts(在 Windows 上,它位于C:WindowsSystem32driversetc)并为 分配主机名127.0.0.1。例如:

127.0.0.1 example.test

请勿将方案或端口添加到域名,也不要使用常见扩展名,例如.com.net等,否则可能会与访问互联网上的其他网站发生冲突。

在下面访问域 A 后,您需要单击按钮以向路由submit执行请求以开始重定向。请求的唯一原因是因为您在示例中使用它,并且我假设您必须发布一些。否则,您也可以使用请求。在应用程序 B 中,当执行从路由(即)到路由(即)的 时,响应状态代码将更改为,如此处、此处和此处所述。应用程序 A 正在监听端口,而应用程序 B 正在监听端口。POST`/submitPOSTform-dataGETRedirectResponsePOST/submitGET/status.HTTP_303_SEE_OTHER8000`8001

运行以下两个应用程序,然后通过http://127.0.0.1:8000/访问域A。

应用程序A.py

from fastapi import FastAPI
from fastapi.responses import RedirectResponse, HTMLResponse
import uvicorn

app = FastAPI()
           
@app.get('/', response_class=HTMLResponse)
def home():
    return """
    <!DOCTYPE html>
    <html>
       <body>
          <h2>Click the "submit" button to be redirected to domain B</h2>
          <form method="POST" action="/submit">
             <input type="submit" value="Submit">
          </form>
       </body>
    </html>
    """
        
@app.post("/submit")
def submit():
    token = 'MTQ0NjJkZmQ5OTM2NDE1ZTZjNGZmZjI3'
    redirect_url = f'http://example.test:8001/submit?token={token}'
    response = RedirectResponse(redirect_url)
    response.set_cookie(key='access-token', value=token, httponly=True)  # set cookie for domain A too
    return response
 
if __name__ == '__main__':
    uvicorn.run(app, host='0.0.0.0', port=8000)

应用程序B.py

from fastapi import FastAPI, Request, status
from fastapi.responses import RedirectResponse
import uvicorn

app = FastAPI()

@app.get('/')
def home(request: Request):
    token = request.cookies.get('access-token')
    print(token)
    return 'You have been successfully redirected to domain B!' \n           f' Your access token ends with: {token[-4:]}'
 
@app.post('/submit')
def submit(request: Request, token: str):
    redirect_url = request.url_for('home')
    response = RedirectResponse(redirect_url, status_code=status.HTTP_303_SEE_OTHER)
    response.set_cookie(key='access-token', value=token, httponly=True)
    return response
 
if __name__ == '__main__':
    uvicorn.run(app, host='0.0.0.0', port=8001)

解决方案 2

另一个解决方案是使用Window.postMessage(),它允许对象cross-origin之间进行通信Window;例如,页面与其pop-up生成的 之间,或页面与其iframe嵌入的 之间。有关如何添加事件侦听器和窗口之间通信的示例可在此处找到。要遵循的步骤如下:

步骤 1:将域 A 添加到域 B 的隐藏iframe。例如:

<iframe id="cross_domain_page" src="http://example.test:8001" frameborder="0" scrolling="no" style="background:transparent;margin:auto;display:block"></iframe>

步骤2:从对域A的异步JS请求Authorization的headers中获取token后,立即将其发送给域B。例如:

document.getElementById('cross_domain_page').contentWindow.postMessage(token,"http://example.test:8001");

步骤3:在域B中,通过接收令牌window.addEventListener("message", (event) ...,并将其存储在localStorage

localStorage.setItem('token', event.data);

或者,在 cookie 中使用 JS(不推荐,请参阅下面的注释):

document.cookie = `token=${event.data}; path=/; SameSite=None; Secure`;

步骤4:向域A发送消息,告知其令牌已存储,然后将用户重定向到域B。

注意 1:步骤 3 演示了如何使用 JS 设置 cookie,但当您要存储此类敏感信息时,您实际上不应该使用JS,因为通过 JS 创建的 cookie 不能包含HttpOnly标志,这有助于缓解跨站点脚本 ( XSS ) 攻击。这意味着可能已将恶意脚本注入您网站的攻击者将能够访问该 cookie。您应该让服务器设置 cookie(通过fetch请求),包括HttpOnly标志(如下例所示),从而使 JS Document.cookieAPI 无法访问该 cookie。localStorage也容易受到 XSS 攻击,因为数据也可以通过 JS 访问(例如localStorage.getItem('token'))。

注 2:要使此解决方案起作用,用户必须Allow all cookies在其浏览器中启用该选项(许多用户没有这样做,并且某些浏览器默认排除第三方cookie(众所周知,Safari 和 Chrome 的私人模式默认拒绝这些 cookie))- 因为内容是iframe从不同的域加载到的,因此 cookie 被归类为第三方cookie。这同样适用于使用localStorage(即Allow all cookies必须启用才能通过使用它iframe)。但是,在这种情况下使用 cookie,您还需要将SameSite标志设置为None,并且 cookie 应包含Secure标志,这是使用所必需的。这意味着 cookie 将仅通过SameSite=NoneHTTPS连接发送;这不会减轻与跨站点访问相关的所有风险,但它将提供针对网络攻击的保护(如果您的服务器不通过 HTTPS 运行,仅出于演示目的,您可以在 Chrome 浏览器中使用'Insecure origins treated as secure'实验性功能)。设置意味着 cookie将不受保护以防止外部访问,因此您在使用它之前应该意识到风险。chrome://flags/`SameSite=None`

使用cookieiframe的示例SameSite=None; Secure; HttpOnly

运行以下两个应用程序,然后通过http://127.0.0.1:8000/访问域A。

应用程序A.py

from fastapi import FastAPI, Request, Response
from fastapi.responses import HTMLResponse

app = FastAPI()

@app.get('/', response_class=HTMLResponse)
def home():
    return """
    <!DOCTYPE html>
    <html>
       <body>
          <h2>Click the "submit" button to be redirected to domain B</h2>
          <input type="button" value="Submit" onclick="submit()"><br>
          <iframe id="cross_domain_page" src="http://example.test:8001/iframe"  frameborder="0" scrolling="no" style="background:transparent;margin:auto;display:block"></iframe>
          <script>
             function submit() {
                fetch('/submit', {
                     method: 'POST',
                  })
                  .then(res => {
                     authHeader = res.headers.get('Authorization');
                     if (authHeader.startsWith("Bearer "))
                        token = authHeader.substring(7, authHeader.length);
                     return res.text();
                  })
                  .then(data => {
                     document.getElementById('cross_domain_page').contentWindow.postMessage(token, "http://example.test:8001");
                  })
                  .catch(error => {
                     console.error(error);
                  });
             }
             
             window.addEventListener("message", (event) => {
                if (event.origin !== "http://example.test:8001")
                  return;
             
                if (event.data == "cookie is set")
                  window.location.href = 'http://example.test:8001/';
             }, false);
          </script>
       </body>
    </html>
    """

@app.post('/submit')
def submit():
    token = 'MTQ0NjJkZmQ5OTM2NDE1ZTZjNGZmZjI3'
    headers = {'Authorization': f'Bearer {token}'}
    response = Response('success', headers=headers)
    response.set_cookie(key='access-token', value=token, httponly=True)  # set cookie for domain A too
    return response
    
if __name__ == '__main__':
    import uvicorn
    uvicorn.run(app, host='0.0.0.0', port=8000)

应用程序B.py

from fastapi import FastAPI, Request, Response
from fastapi.responses import HTMLResponse

app = FastAPI()

@app.get('/iframe', response_class=HTMLResponse)
def iframe():
    return """
    <!DOCTYPE html>
    <html>
       <head>
          <script>
             window.addEventListener("message", (event) => {
                if (event.origin !== "http://127.0.0.1:8000")
                   return;
             
                fetch('/submit', {
                      method: 'POST',
                      headers: {
                         'Authorization': `Bearer ${event.data}`
                      }
                   })
                   .then(res => res.text())
                   .then(data => {
                      event.source.postMessage("cookie is set", event.origin);
                   })
                   .catch(error => {
                      console.error(error);
                   })
             }, false);
          </script>
       </head>
    </html>
    """
    
@app.get('/')
def home(request: Request):
    token = request.cookies.get('access-token')
    print(token)
    return 'You have been successfully redirected to domain B!' \n           f' Your access token ends with: {token[-4:]}'

@app.post('/submit')
def submit(request: Request):
    authHeader = request.headers.get('Authorization')
    if authHeader.startswith("Bearer "):
        token = authHeader[7:]
    response = Response('success')
    response.set_cookie(key='access-token', value=token, samesite='none', secure=True, httponly=True) 
    return response
    
if __name__ == '__main__':
    import uvicorn
    uvicorn.run(app, host='0.0.0.0', port=8001)

iframe使用和的示例localStorage

此示例演示了使用此时间来存储令牌的方法localStorage。令牌存储后,域 A 将用户重定向到/redirect域 B 的路由;域 B 然后从 检索令牌localStorage(随后将其从 中删除localStorage),然后将其发送到自己的/submit路由,以便为 设置httpOnlycookie access-token。最后,用户被重定向到域 B 的主页。

应用程序A.py

from fastapi import FastAPI, Request, Response
from fastapi.responses import HTMLResponse

app = FastAPI()
    
@app.get('/', response_class=HTMLResponse)
def home():
    return """
    <!DOCTYPE html>
    <html>
       <body>
          <h2>Click the "submit" button to be redirected to domain B</h2>
          <input type="button" value="Submit" onclick="submit()"><br>
          <iframe id="cross_domain_page" src="http://example.test:8001/iframe"  frameborder="0" scrolling="no" style="background:transparent;margin:auto;display:block"></iframe>
          <script>
             function submit() {
                fetch('/submit', {
                     method: 'POST',
                  })
                  .then(res => {
                     authHeader = res.headers.get('Authorization');
                     if (authHeader.startsWith("Bearer "))
                        token = authHeader.substring(7, authHeader.length);
                     return res.text();
                  })
                  .then(data => {
                     document.getElementById('cross_domain_page').contentWindow.postMessage(token, "http://example.test:8001");
                  })
                  .catch(error => {
                     console.error(error);
                  });
             }
             
             window.addEventListener("message", (event) => {
                if (event.origin !== "http://example.test:8001")
                  return;
             
                if (event.data == "token stored")
                  window.location.href = 'http://example.test:8001/redirect';
             }, false);
                
          </script>
       </body>
    </html>
    """

@app.post('/submit')
def submit():
    token = 'MTQ0NjJkZmQ5OTM2NDE1ZTZjNGZmZjI3'
    headers = {'Authorization': f'Bearer {token}'}
    response = Response('success', headers=headers)
    response.set_cookie(key='access-token', value=token, httponly=True)  # set cookie for domain A too
    return response
    
if __name__ == '__main__':
    import uvicorn
    uvicorn.run(app, host='0.0.0.0', port=8000)

应用程序B.py

from fastapi import FastAPI, Request, Response
from fastapi.responses import HTMLResponse

app = FastAPI()

@app.get('/iframe', response_class=HTMLResponse)
def iframe():
    return """
    <!DOCTYPE html>
    <html>
       <head>
          <script>
             window.addEventListener("message", (event) => {
                if (event.origin !== "http://127.0.0.1:8000")
                   return;
             
                localStorage.setItem('token', event.data);
                event.source.postMessage("token stored", event.origin);
             }, false);
          </script>
       </head>
    </html>
    """
 
@app.get('/redirect', response_class=HTMLResponse)
def redirect():
    return """
    <!DOCTYPE html>
    <html>
       <head>
          <script>
            const token = localStorage.getItem('token');
            localStorage.removeItem("token");   
            fetch('/submit', {
                  method: 'POST',
                  headers: {
                     'Authorization': `Bearer ${token}`
                  }
               })
               .then(res => res.text())
               .then(data => {
                  window.location.href = 'http://example.test:8001/';
               })
               .catch(error => {
                  console.error(error);
               })
          </script>
       </head>
    </html>
    """
    
    
@app.get('/')
def home(request: Request):
    token = request.cookies.get('access-token')
    print(token)
    return 'You have been successfully redirected to domain B!' \n           f' Your access token ends with: {token[-4:]}'

@app.post('/submit')
def submit(request: Request):
    authHeader = request.headers.get('Authorization')
    if authHeader.startswith("Bearer "):
        token = authHeader[7:]
    response = Response('success')
    response.set_cookie(key='access-token', value=token, httponly=True) 
    return response
  
   
if __name__ == '__main__':
    import uvicorn
    uvicorn.run(app, host='0.0.0.0', port=8001)

解决方案 3

查看这里和这里了解 StackExchange 的自动登录如何工作。例如,当您已经登录 StackOverflow(SO)时,StackExchange(SE)会自动登录。简而言之,他们不使用第三方 cookie,而是localStorage与其集中式域http://stackauth.com上的全局身份验证相结合。即使不使用第三方 cookie,他们也会使用(如此处iframe所述)将令牌存储在中。这意味着,这种方法只有在用户的浏览器接受第三方cookie 时才会起作用(如解决方案 2 中所述)。如解决方案 2 中所述,即使您正在访问而不是在主窗口中嵌入,您仍然需要用户在其浏览器设置中启用它;否则,它将不起作用,如果用户尝试访问SE 网络中的任何其他站点,它将被要求再次登录。localStorage`localStoragedocument.cookieiframe`Allow all cookies

更新

上面描述的方法是 SE 自动登录过去的工作方式。如今,方法略有不同,如最近的一篇文章中所述,该文章实际上描述了 SE 通用登录现在的工作方式(您可以在登录 SE 站点之一时,通过检查浏览器 DevTools 中的网络活动来验证该过程;例如,SO)。

其工作方式是,<img>当您登录 SE 站点之一时,注入指向其他 Stack Exchange 站点(即 serverfault.com、superuser.com 等)的标签。src这些 <img>标签的 URL 包含一个唯一的authToken查询参数,该参数由通用身份验证系统生成并通过请求获取XMLHttpRequest POST。这些标签的示例<img>如下:

<img src="https://serverfault.com/users/login/universal.gif?authToken=<some-token-value>&nonce=<some-nonce-value>" />

src然后,您的浏览器会将该URL(其中包含)发送authToken到其他每个网站(您当前不在的),并且作为对该图像的响应,每个给定的域/网站将返回两个 cookie:provacct。当您稍后切换到其他 SE 网站之一时,您的浏览器将发送您之前收到的 cookieprovacct,以便该网站验证令牌并(如果有效)让您登录。

注意:要使其正常工作,您的浏览器需要接受第三方cookie(如前所述),因为必须使用SameSite=None; Secure标志设置 cookie(请注意上述风险)。如果不允许第三方 cookie(以及不通过 HTTPS 运行服务器),通用自动登录将无法工作。此外,您尝试为其设置 cookie 的其他域需要启用CORSimg ,因为当从其他域加载时,会执行跨域authToken请求。此外,由于这种方法会在 URL 的查询参数中发送(即使它发生在后台而不是浏览器的地址栏中),您应该注意解决方案 1 中前面描述的风险。

下面使用<img>指向域 B 的标签。 img URL 不必是服务器接收的实际图像access-token,因此,您可以使用该.onerror()函数来检查请求何时实际完成(意味着服务器已使用Set-Cookie标头做出响应),以便您可以将用户重定向到域 B。

也可以使用在标头中fetch包含 的对域 B 的请求,服务器可以做出类似响应以设置 cookie。在这种情况下,请确保使用和,并在服务器端明确指定允许的来源,如此处所述。access-token`Authorizationcredentials: 'include'mode: 'cors'`

运行以下两个应用程序,然后通过http://127.0.0.1:8000/访问域A。

应用程序A.py

from fastapi import FastAPI, Response
from fastapi.responses import HTMLResponse

app = FastAPI()

@app.get('/', response_class=HTMLResponse)
def home():
    return """
    <!DOCTYPE html>
    <html>
       <body>
          <h2>Click the "submit" button to be redirected to domain B</h2>
          <input type="button" value="Submit" onclick="submit()"><br>
          <script>
            function submit() {
               fetch('/submit', {
                     method: 'POST',
                  })
                  .then(res => {
                     authHeader = res.headers.get('Authorization');
                     if (authHeader.startsWith("Bearer "))
                        token = authHeader.substring(7, authHeader.length);

                     return res.text();
                  })
                  .then(data => {
                     var url = 'http://example.test:8001/submit?token=' + encodeURIComponent(token);
                     var img = document.createElement('img');
                     img.style = 'display:none';
                     img.crossOrigin = 'use-credentials'; // needed for CORS
                     img.onerror = function(){
                        window.location.href = 'http://example.test:8001/';
                     }
                     img.src = url;
                  })
                  .catch(error => {
                     console.error(error);
                  });
            }
          </script>
       </body>
    </html>
    """
    
@app.post('/submit')
def submit():
    token = 'MTQ0NjJkZmQ5OTM2NDE1ZTZjNGZmZjI3'
    headers = {'Authorization': f'Bearer {token}'}
    response = Response('success', headers=headers)
    response.set_cookie(key='access-token', value=token, httponly=True)  # set cookie for domain A too
    return response
    
if __name__ == '__main__':
    import uvicorn
    uvicorn.run(app, host='0.0.0.0', port=8000)

应用程序B.py

from fastapi import FastAPI, Request, Response
from fastapi.responses import RedirectResponse
from fastapi.middleware.cors import CORSMiddleware

app = FastAPI()

origins = ['http://localhost:8000', 'http://127.0.0.1:8000',
           'https://localhost:8000', 'https://127.0.0.1:8000'] 

app.add_middleware(
    CORSMiddleware,
    allow_origins=origins,
    allow_credentials=True,
    allow_methods=["*"],
    allow_headers=["*"],
)

@app.get('/')
def home(request: Request):
    token = request.cookies.get('access-token')
    print(token)
    return 'You have been successfully redirected to domain B!' \n           f' Your access token ends with: {token[-4:]}'
 
@app.get('/submit')
def submit(request: Request, token: str):
    response = Response('success')
    response.set_cookie(key='access-token', value=token, samesite='none', secure=True, httponly=True) 
    return response

if __name__ == '__main__':
    import uvicorn
    uvicorn.run(app, host='0.0.0.0', port=8001)
相关推荐
  政府信创国产化的10大政策解读一、信创国产化的背景与意义信创国产化,即信息技术应用创新国产化,是当前中国信息技术领域的一个重要发展方向。其核心在于通过自主研发和创新,实现信息技术应用的自主可控,减少对外部技术的依赖,并规避潜在的技术制裁和风险。随着全球信息技术竞争的加剧,以及某些国家对中国在科技领域的打压,信创国产化显...
工程项目管理   2577  
  为什么项目管理通常仍然耗时且低效?您是否还在反复更新电子表格、淹没在便利贴中并参加每周更新会议?这确实是耗费时间和精力。借助软件工具的帮助,您可以一目了然地全面了解您的项目。如今,国内外有足够多优秀的项目管理软件可以帮助您掌控每个项目。什么是项目管理软件?项目管理软件是广泛行业用于项目规划、资源分配和调度的软件。它使项...
项目管理软件   1553  
  IPD(Integrated Product Development)流程作为一种先进的产品开发管理模式,在众多企业中得到了广泛应用。其中,技术评审与决策评审是IPD流程中至关重要的环节,它们既有明显的区别,又存在紧密的协同关系。深入理解这两者的区别与协同,对于企业有效实施IPD流程,提升产品开发效率与质量具有重要意义...
IPD管理流程   26  
  本文介绍了以下10款项目管理软件工具:禅道项目管理软件、ClickUp、Freshdesk、GanttPRO、Planview、Smartsheet、Asana、Nifty、HubPlanner、Teamwork。在当今快速变化的商业环境中,项目管理软件已成为企业提升效率、优化资源分配和确保项目按时交付的关键工具。然而...
项目管理系统   21  
  建设工程项目质量关乎社会公众的生命财产安全,也影响着企业的声誉和可持续发展。高质量的建设工程不仅能为使用者提供舒适、安全的环境,还能提升城市形象,推动经济的健康发展。在实际的项目操作中,诸多因素会对工程质量产生影响,从规划设计到施工建设,再到后期的验收维护,每一个环节都至关重要。因此,探寻并运用有效的方法来提升建设工程...
工程项目管理制度   18  
热门文章
项目管理软件有哪些?
曾咪二维码

扫码咨询,免费领取项目管理大礼包!

云禅道AD
禅道项目管理软件

云端的项目管理软件

尊享禅道项目软件收费版功能

无需维护,随时随地协同办公

内置subversion和git源码管理

每天备份,随时转为私有部署

免费试用