为跨域请求设置Cookie

262
如何在跨域情况下共享cookies?更具体地说,如何将Set-Cookie头与Access-Control-Allow-Origin头一起使用?
以下是我的情况说明:
我正试图为一个运行在localhost:4000上的API设置cookie,在托管在localhost:3000上的Web应用程序中。
在浏览器中似乎收到了正确的响应头,但不幸的是它们没有任何效果。这些是响应头:
HTTP/1.1 200 OK
Access-Control-Allow-Origin: http://localhost:3000
Vary: Origin, Accept-Encoding
Set-Cookie: token=0d522ba17e130d6d19eb9c25b7ac58387b798639f81ffe75bd449afbc3cc715d6b038e426adeac3316f0511dc7fae3f7; Max-Age=86400; Domain=localhost:4000; Path=/; Expires=Tue, 19 Sep 2017 21:11:36 GMT; HttpOnly
Content-Type: application/json; charset=utf-8
Content-Length: 180
ETag: W/"b4-VNrmF4xNeHGeLrGehNZTQNwAaUQ"
Date: Mon, 18 Sep 2017 21:11:36 GMT
Connection: keep-alive
此外,当我使用Chrome开发者工具的网络选项卡检查流量时,我可以在“响应cookie”下看到cookie。然而,在“存储/ cookie”下的应用程序选项卡中,我看不到设置cookie。我没有看到任何CORS错误,所以我认为我可能错过了其他东西。
有什么建议吗?
更新I:
我在React-Redux应用程序中使用request模块向服务器的“/ signin”端点发出请求。对于服务器,我使用express。
Express服务器:
res.cookie('token', 'xxx-xxx-xxx', { maxAge: 86400000, httpOnly: true, domain: 'localhost:3000' })
浏览器中的请求:
request.post({uri:'/ signin',json:{userName:'userOne',password:'123456'}},(err,response,body)=> { // doing stuff })
更新II:

我现在正在疯狂地设置请求和响应头,确保它们存在于请求和响应中。下面是截图。请注意头部Access-Control-Allow-CredentialsAccess-Control-Allow-HeadersAccess-Control-Allow-MethodsAccess-Control-Allow-Origin。查看我在Axios的github上发现的问题,我认为现在已经设置了所有必需的头部。然而,仍然没有成功...

enter image description here


5
@PimHeijden,看一下这个链接:https://developer.mozilla.org/zh-CN/docs/Web/API/XMLHttpRequest/withCredentials 或许使用 withCredentials 是你所需要的?(原文已被翻译为中文) - Kalamarico
2
好的,你正在使用请求,但我认为这不是最好的选择,请查看此帖子和答案,我认为axios对你有用。https://dev59.com/QVkS5IYBdhLWcg3wemz6 - Kalamarico
1
谢谢!我没有注意到request模块不适用于浏览器。Axios目前似乎做得很好。我现在收到了头部信息:Access-Control-Allow-Credentials:trueAccess-Control-Allow-Origin:http://localhost:3000(用于启用CORS)。这看起来没问题,但是Set-Cookie头部却没有任何作用... - Pim van der Heijden
@PimHeijden 有什么进展吗?我也遇到了类似的问题。请问你是如何解决的? - 1nstinct
1
@1nstinct请阅读被接受的答案。 - Pim van der Heijden
显示剩余3条评论
15个回答

408

跨站点处理

为了成功地通过CORS请求接收和发送cookie,请执行以下操作:

后端(服务器)HTTP头设置:

有关在express js中设置CORS的更多信息,请在此处阅读文档

Cookie设置:Chrome和Firefox在2021年的更新中的cookie设置如下:

  • SameSite=None
  • Secure

在执行SameSite=None时,设置Secure是必需的。有关SameSite的文档以及Secure要求的文档,请参见文档。还请注意,Chrome devtools现在在网络选项卡和应用程序选项卡中改进了cookie问题的过滤和突出显示。

前端(客户端):XMLHttpRequest.withCredentials标志设置为true,可以通过不同的请求-响应库以不同的方式实现:

  • ES6 fetch()这是HTTP的首选方法。 使用credentials:'include'

  • jQuery 1.5.1因遗留目的而提到。 使用xhrFields:{withCredentials:true}

  • axios作为一个流行的NPM库的示例。 使用withCredentials:true

代理方法

完全避免进行跨站点(CORS)操作。 您可以使用代理来实现这一点。 只需将所有流量发送到相同的顶级域名,并使用DNS(子域名)和/或负载平衡进行路由。 使用Nginx这相对较少的工作量。

此方法非常适合JAMStack。 JAMStack设计API和Webapp代码完全分离。 越来越多的用户阻止第三方cookie。 如果API和Webapp可以轻松在同一主机上提供,则第三方问题(跨站点/ CORS)将消失。 在此处阅读有关JAMStack的更多信息herehere

附注

原来Chrome在设置cookie时,如果域名中包含端口号,则无法设置。但对于不带端口号的localhost是没有问题的。非常感谢Erwin提供的这个提示!


2
我认为你遇到这个问题只是因为localhost,请查看此处:https://dev59.com/anNA5IYBdhLWcg3wAItP#1188145,同时这篇文章可能对你有所帮助(https://stackoverflow.com/questions/50966861/how-to-correct-set-the-cookies-for-a-subdomain-in-jsp/50988268#50988268)。 - Edwin
8
这个答案对我帮助很大!花了很长时间才找到它。但我认为答案应该提到,要将 Access-Control-Allow-Origin 设置为一个明确的域名,而不仅仅是 "*" 是必须的。这样它就会成为完美的答案。 - e.dan
6
这是个好的答案,它涵盖了CORS、头信息、后端和前端相关内容,并且避免使用localhost,而是通过在本地使用真实子域名覆盖/etc/hosts。但是我发现Postman显示响应头中有SET-COOKIE,但Chrome调试时并没有显示,并且在Chrome中该cookie实际上也没有被设置。你还有其他检查的想法吗? - bjm88
1
@bjm88 你最终解决了这个问题吗?我处于完全相同的情况。当从localhost:3010连接到localhost:5001时,cookie设置正确,但是从localhost:3010连接到fakeremote:5001(在我的hosts文件中指向127.0.0.1)时不起作用。当我在真实服务器上使用自定义域托管我的服务器时(从localhost:3010连接到mydomain.com),情况完全相同。我已经做了所有推荐的事情,并尝试了很多其他方法。 - Form
1
是的,谢谢,答案在这里。我不完全理解你的问题,但我会尽快回答。Cookie 不考虑端口。只需从 cookie 域中省略端口即可。端口本来就不是域概念的一部分。如果涉及真正不同的域,请尽可能使用代理。这样,您就可以完全避免 CORS 问题。 - Pim van der Heijden
显示剩余44条评论

38

注意:适用于2020年发布的Chrome浏览器。

未来版本的Chrome浏览器仅会在设置有 SameSite=NoneSecure 的情况下,才会在跨站点请求中发送Cookies。

因此,如果您的后端服务器未设置 SameSite=None,则Chrome将默认使用 SameSite=Lax,并且不会将此Cookie与 { withCredentials: true } 请求一起使用。

更多信息请参见https://www.chromium.org/updates/same-site

Firefox和Edge的开发人员也计划在未来发布此功能。

规范详见:https://datatracker.ietf.org/doc/html/draft-west-cookie-incrementalism-01#page-8


13
提供samesite = none和secure标志需要HTTPS。如何在本地系统中实现此目标,而HTTPS不是可选项?我们能否绕过它?答案:要提供samesite=none和secure标志,需要使用HTTPS。如果本地系统无法使用HTTPS,那么无法实现该功能。无法绕过此限制。 - nirmal patel
@nirmalpatel 只需在Chrome开发者控制台中删除“Lax”值。 - LennyLip
@LennyLip 我认为如果你移除"Lax"的值,它实际上仍然默认为Lax而不是None。(根据https://web.dev/samesite-cookies-explained/和MDN。) - Matt Browne
@nirmalpatel 只是提醒一下,你可以在本地系统上使用 Let's Encrypt 证书来启用 HTTPS。如果需要的话,可以查一下它们。 - Syed M. Sannan
我在这里遇到了相同的问题,但涉及https协议。 请在此处查看:https://stackoverflow.com/questions/76148572/cross-domain-httponly-cookie-not-being-set - Mussa Charles

20

为了让客户端能够从跨域请求中读取cookie,您需要做到以下两点:

  1. 服务器的所有响应头都需要包含以下内容:

    Access-Control-Allow-Credentials: true

  2. 在客户端发送请求时需要添加 withCredentials: true 选项。

在我使用 Angular 7 和 Spring Boot 进行实现时,可以通过以下方式实现:


服务器端:

@CrossOrigin(origins = "http://my-cross-origin-url.com", allowCredentials = "true")
@Controller
@RequestMapping(path = "/something")
public class SomethingController {
  ...
}

origins = "http://my-cross-origin-url.com"将在每个服务器的响应头中添加Access-Control-Allow-Origin: http://my-cross-origin-url.com

allowCredentials = "true"将在每个服务器的响应头中添加Access-Control-Allow-Credentials: true,这是客户端读取 cookie 所需的内容。


客户端:

import { HttpInterceptor, HttpXsrfTokenExtractor, HttpRequest, HttpHandler, HttpEvent } from "@angular/common/http";
import { Injectable } from "@angular/core";
import { Observable } from 'rxjs';

@Injectable()
export class CustomHttpInterceptor implements HttpInterceptor {

    constructor(private tokenExtractor: HttpXsrfTokenExtractor) {
    }

    intercept(req: HttpRequest<any>, next: HttpHandler): Observable<HttpEvent<any>> {
        // send request with credential options in order to be able to read cross-origin cookies
        req = req.clone({ withCredentials: true });

        // return XSRF-TOKEN in each request's header (anti-CSRF security)
        const headerName = 'X-XSRF-TOKEN';
        let token = this.tokenExtractor.getToken() as string;
        if (token !== null && !req.headers.has(headerName)) {
            req = req.clone({ headers: req.headers.set(headerName, token) });
        }
        return next.handle(req);
    }
}

使用这个类,您可以向所有请求注入额外的内容。

第一部分req = req.clone({ withCredentials: true });是为了使用withCredentials: true选项发送每个请求所必需的。这实际上意味着会首先发送一个OPTION请求,以便在发送实际的POST/PUT/DELETE请求之前获取cookie和其中的授权令牌,这些请求需要附加在它们的标头中(在标头中),以便服务器验证和执行请求。

第二部分是专门处理所有请求的反CSRF令牌。在需要时从cookie中读取,并将其写入每个请求的标头中。

期望的结果如下:

response request


这个答案对现有的回答有什么补充? - Pim van der Heijden
8
一个实际的实现。我决定发布它的原因是,我花费了很多时间搜索相同的问题,并从各种帖子中添加片段来实现它。如果有这篇文章作为比较,对于其他人来说应该会更容易做到相同的事情。 - Stefanos Kargas
@CrossOrigin注解中设置allowCredentials = "true"对我很有帮助。 - ponder275
如@lennylip在他的回答中提到的那样,samesite和secure标志会显示错误。如何在本地主机服务器上实现而不使用安全标志。 - nirmal patel
1
这里有一个提示,Angular默认的CSRF拦截器实现不会起作用,使用提供的答案中的拦截器将会起作用,并且它也会在GET调用中附加cookie。 - ExploreEv

6

升级您的express库到最新的稳定版本4.17.1。然后:

在CorsOption中:将origin设置为您的本地主机URL或前端生产URL,将credentials设置为true,例如:

  const corsOptions = {
    origin: config.get("origin"),
    credentials: true,
  };

我使用 config npm模块 动态设置了我的源。

res.cookie 中:

对于本地主机:您根本不需要设置 sameSite 和 secure 选项,您可以将 httpOnly 设置为 true,以防止 XSS 攻击和其他有用的选项 ,具体取决于您的用例。

对于生产环境,您需要将 sameSite 设置为 none,以进行跨域请求,并将 secure 设置为 true。请记住,sameSite 仅适用于最新版本的 express,而最新版本的 Chrome 只会通过 https 设置 cookie,因此需要安全选项。

以下是我如何使其动态设置的。

 res
    .cookie("access_token", token, {
      httpOnly: true,
      sameSite: app.get("env") === "development" ? true : "none",
      secure: app.get("env") === "development" ? false : true,
    })

对我来说,SameSite 不是 sameSite(大写 S)。 - Adel Benyahia

4

Pim的答案很有帮助。在我的情况下,我必须使用

Expires / Max-Age: "Session"

如果是日期时间,即使没有过期,它仍然不会将 cookie 发送到后端。
Expires / Max-Age: "Thu, 21 May 2020 09:00:34 GMT"

希望这对未来可能遇到相同问题的人有所帮助。

2
  1. 前端

    `await axios.post(`你的API地址`, 数据,{
        withCredentials:true,
    })
    await axios.get(`你的API地址`,{
            withCredentials:true,
        });`
    
  2. 后端

    var  corsOptions  = {
     origin: 'http://localhost:3000', //前端URL
     credentials: true}
    
    
    app.use(cors(corsOptions));
    const token=jwt.sign({_id:user_id},process.env.JWT_SECRET,{expiresIn:"7d"});
    res.cookie("token",token,{httpOnly:true});
    
    
    
    希望它能正常工作。
    

2
仅有代码而没有任何解释很少有帮助。Stack Overflow 是关于学习的,而不是提供盲目复制粘贴的片段。请编辑您的问题并解释它如何回答特定的问题。请参阅 如何回答 - Sfili_81

1

经过一天多的尝试你们所有的建议和更多的尝试,我放弃了。 Chrome在本地主机上根本不接受我的跨域cookie。 没有错误,只是默默地被忽略了。 我想要使用http only cookie来更安全地存储令牌。 所以对于本地主机来说,代理似乎是解决这个问题的最佳方法。我还没有真正尝试过。

我最终做的事情,也许能帮助到别人。

后端(node/express/typescript)

像平常一样设置cookie。

res.status(200).cookie("token", token, cookieOptions)

为本地主机创建一个解决方案

// if origin localhost
response.setHeader("X-Set-Cookie", response.getHeader("set-cookie") ?? "");

允许在CORS中使用x-set-cookie头

app.use(cors({
    //...
    exposedHeaders: [
        "X-Set-Cookie",
        //... 
    ]
}));

前端(Axios)

在 Axios 响应中, 删除 domain= 使其默认。 拆分多个 cookie 并将它们存储在本地。

// Localhost cookie work around
const xcookies = response.headers?.["x-set-cookie"];
if(xcookies !== undefined){
    xcookies
        .replace(/\s+Domain=[^=\s;]+;/g, "")
        .split(/,\s+(?=[^=\s]+=[^=\s]+)/)
        .forEach((cookie:string) => {
            document.cookie = cookie.trim();
    });
}

不是很理想,但我可以继续我的生活。

总的来说,我认为这只是变得过于复杂了 :-(

更新我的用例,也许我们可以解决它?

这是一个具有自定义域名的Heroku服务器。 根据这篇文章应该没问题 https://devcenter.heroku.com/articles/cookies-and-herokuapp-com

我进行了孤立的测试,但仍然无法使用。 我相当确定在FireFox中曾经看到过它工作,但目前除了我的丑陋的解决方法外,似乎什么都不起作用。

服务器端

app.set("trust proxy", 1);

app.get("/cors-cookie", (request: Request, response: Response) => {

    // http://localhost:3000
    console.log("origin", request.headers?.["origin"]);

    const headers = response.getHeaders();
    Object.keys(headers).forEach(x => {
        response.removeHeader(x);
        
        console.log("remove header ", x, headers[x]);
    });
    console.log("headers", response.getHeaders());

    const expiryOffset = 1*24*60*60*1000; // +1 day

    const cookieOptions:CookieOptions = {
        path: "/",
        httpOnly: true,
        sameSite: "none",
        secure: true,
        domain: "api.xxxx.nl",
        expires: new Date(Date.now() + expiryOffset)
    }

    return response
        .status(200)
        .header("Access-Control-Allow-Credentials", "true")
        .header("Access-Control-Allow-Origin", "http://localhost:3000")
        .header("Access-Control-Allow-Methods", "GET,HEAD,OPTIONS,POST,PUT")
        .header("Access-Control-Allow-Headers", "Origin, X-Requested-With, Content-Type, Accept")
        .cookie("test-1", "_1_", cookieOptions)
        .cookie("test-2", "_2_", {...cookieOptions, ...{ httpOnly: false }})
        .cookie("test-3", "_3_", {...cookieOptions, ...{ domain: undefined }})
        .cookie("test-4", "_4_", {...cookieOptions, ...{ domain: undefined, httpOnly: false }})
        .cookie("test-5", "_5_", {...cookieOptions, ...{ domain: undefined, sameSite: "lax" }})
        .cookie("test-6", "_6_", {...cookieOptions, ...{ domain: undefined, httpOnly: false, sameSite: "lax" }})
        .cookie("test-7", "_7_", {...cookieOptions, ...{ domain: "localhost"}}) // Invalid domain
        .cookie("test-8", "_8_", {...cookieOptions, ...{ domain: ".localhost"}}) // Invalid domain
        .cookie("test-9", "_9_", {...cookieOptions, ...{ domain: "http://localhost:3000"}}) // Invalid domain
        .json({
            message: "cookie"
        });
});

客户端

const response = await axios("https://api.xxxx.nl/cors-cookie", {
    method: "get",
    withCredentials: true,
    headers: {
        "Accept": "application/json",
        "Content-Type": "application/json",                
    }
});

这将产生以下响应

enter image description here

我在“Network > request > cookies”选项卡中看到了cookies。
但是在“Application > Storage > Cookies”或者“document.cookie”下没有cookies。

我认为你可以使用常规的“Set-Cookie”头使其工作。 - Pim van der Heijden
我也希望如此,但我没能做到。 我会发布一些细节,也许其他人可以告诉我我错过了什么? - Lode Michels
cookieOptions 变量在这里非常关键。该 cookie 有哪些选项?此外,您的浏览器设置也可能会产生重大影响。 - Pim van der Heijden
@PimHeijden 我已经更新了我的帖子,并提供了一个独立的案例。 - Lode Michels
尝试查看网络选项卡中的每个请求 cookie 子选项卡。在那里,您可以获取有关为什么设置或未设置 cookie 的更多信息。 - Pim van der Heijden

1
在最新的Chrome标准中,如果CORS请求需要携带cookies,则必须开启samesite = none和secure,并且后端域名必须开启HTTPS。

你不能在没有Secure的情况下从HTTP网站调用HTTPS时包含SameSite=None的cookies。 - Pim van der Heijden

0

这是对上面“Lode Michels”的回答,关于在Heroku服务器(以及其他云提供商,如AWS)使用CORS cookie的问题。

你的CORS cookie无法设置的原因是因为Heroku在负载均衡器上剥离了SSL证书,所以当你尝试在服务器上设置“secure”cookie时,它会失败,因为它不再来自安全连接。

你可以明确指定连接是否安全,而不是让cookie模块检查请求。 https://github.com/pillarjs/cookies

使用koa,添加以下内容:

ctx.cookies.secure = true;

编辑:由于声望低于50,我无法直接评论该答案。


0

这段代码对我有效

在后端

在corsOptions中将凭据设置为true:

const corsOptions = {
 credentials: true,
  };

在发送请求之前设置cookies:

res.cookie('token', 'xxx-xxx-xxx', { 
maxAge: 24*60*60*1000, httpOnly: true, 
SameSite:"None" })

在前端

浏览器中的请求(使用axios):

axios.post('uri/signin', 
JSON.stringify({ username: 'userOne', 
password: '123456'}),. 
{withCredentials:true})
.the(result 
=>console.log(result?.data))
.catch(err => console.log(err))

网页内容由stack overflow 提供, 点击上面的
可以查看英文原文,
原文链接