访问 document.cookie returns 空字符串,即使在开发人员工具中列出了 cookie 且 httpOnly 标志设置为 false
Accessing document.cookie returns empty string even though cookies are listed in developer tools with httpOnly flag set to false
有时*,在登录页面访问 document.cookie
时,我得到一个空字符串,即使:
- cookie 列在 Chrome 和 Firefox 开发人员工具中,
- 我感兴趣的 cookie 的 httpOnly 标志设置为 false,
- 我感兴趣的 cookie 路径设置为“/”。
期望的行为
我的 React 单页应用程序 (SPA) 有一个登录页面,其中包含一个 <form />
元素,用于将登录凭据发送到后端。当收到来自后端的响应并且身份验证成功时,我检查身份验证 cookie 是否已正确设置。如果是这种情况,将触发重定向以显示登录用户的内容。
实际行为
不幸的是,在大约 15% 的登录尝试中,document.cookie
returns 一个空字符串会阻止重定向并使用户保持登录状态页。按 F5
并不能解决问题,但是在成功登录请求后手动替换 url 的路径时(例如,将 'www.website.tld/login' 更新为 'www.website.tld/start'),用户将被转发到仅供登录用户使用的所需页面。
我无法手动重现错误。这似乎是随机发生的。但是当它发生时,我查看了开发者控制台,我可以看到来自后端的所有 cookie(设置正确)。
附加信息
- django 服务器 运行 在后端
- 所需的 cookie 设置为
response.set_cookie('key', 'value', secure=False httponly=False, samesite='strict')
- JS 库(axios、react-router)
相关:
- Can't access cookies from document.cookie in JS, but browser shows cookies exist(仅限 http)
- Can't access a cookie using document.cookie in JS(仅限 http)
登录页面 (JSX)
import React, { useState } from "react";
import { Redirect } from "react-router-dom";
import axios from "axios";
/**
* We're using cookies.js to read cookies.
* Source: https://github.com/madmurphy/cookies.js
*/
function hasItem(sKey) {
return new RegExp(
"(?:^|;\s*)" +
encodeURIComponent(sKey).replace(/[\-\.\+\*]/g, "\$&") +
"\s*\="
).test(document.cookie);
}
export const LoginPage = () => {
const [isAuthenticated, setIsAuthenticated] = useState(false);
const [username, setUsername] = useState("");
const [password, setPassword] = useState("");
function handleSubmit(e) {
e.preventDefault();
function onSuccess(response) {
// handle response
// [...]
// sometimes console.log(document.cookie) returns empty string
if (hasItem("auth_cookie")) {
setIsAuthenticated(true);
} else {
console.warn("Cookie not found!");
}
}
function onFailure(error) {
// handle error
}
const conf = {
headers: new Headers({
"Content-Type": "application/json; charset=UTF-8",
Origin: window.location.origin
})
};
axios
.post("/api/login/", { username, password }, conf)
.then(response => {
onSuccess(response);
})
.catch(error => {
onFailure(error);
});
}
if (isAuthenticated) {
return <Redirect to="/start" />;
}
return (
<div className="login-page">
<form
name="login-form"
method="post"
onSubmit={e => handleSubmit(e)}
action="api/login"
target="hiddenFrame"
>
<iframe className="invisible-frame" src="" name="hiddenFrame" />
<div>
<label htmlFor="username">Email</label>
<input
name="username"
type="text"
onChange={e => setUsername(e.target.value)}
/>
</div>
<div>
<label htmlFor="password">Password</label>
<input
name="password"
type="password"
onChange={e => setPassword(e.target.value)}
/>
</div>
<button type="submit">Submit</button>
</form>
</div>
);
};
路由 (JSX)
import React from "react";
import { Route, Redirect } from "react-router-dom";
const RootLayout = () => {
return (
<div className="root-layout">
<Switch>
<PublicRoute path="/login" component={LoginPage} />
<PrivateRoute path="/" component={App} />
</Switch>
</div>
);
};
/**
* Component that handles redirection when user is logged in already
*/
const PublicRoute = ({ component: ChildComponent, ...remainingProps }) => {
let isAuthenticated = hasItem("auth_cookie");
return (
<Route
render={props =>
isAuthenticated ? <Redirect to="/" /> : <ChildComponent {...props} />
}
{...remainingProps}
/>
);
};
/**
* Component that handles redirection when user has been logged out.
* E.g. when authentication cookie expires.
*/
const PrivateRoute = ({ component: ChildComponent, ...remainingProps }) => {
let isAuthenticated = hasItem("auth_cookie");
return (
<Route
render={props =>
!isAuthenticated ? (
<Redirect to="/login" />
) : (
<ChildComponent {...props} />
)
}
{...remainingProps}
/>
);
};
const App = () => (
<Switch>
<Route exact path="/" render={() => <Redirect to="/start" />} />
<Route exact path="/start" component={StartPage} />
<Route exact path="/blog" component={BlogPage} />
{/*...*/}
</Switch>
);
* I know, that's probably not how a post should start...
您 运行 遇到了 sameSite cookie 的问题。参见 SameSite cookies explained 的解释:
If you set SameSite to Strict, your cookie will only be sent in a first-party context. [...] When the user is on your site, then the cookie will be sent with the request as expected. However when following a link into your site, say from another site or via an email from a friend, on that initial request the cookie will not be sent. This is good when you have cookies relating to functionality that will always be behind an initial navigation, such as changing a password or making a purchase, but is too restrictive for promo_shown. If your reader follows the link into the site, they want the cookie sent so their preference can be applied.
现在你至少有两个选择:
- 建议:保留
samesite=strict
并重构您的客户端代码。前端根本不需要访问 auth cookie,因此您可以设置 httponly=True
。然后引入一个后端 API,它根据来自客户端代码的请求来验证 cookie。由于前端代码无法访问 auth cookie,这为您提供了不易受到 XSS 攻击的额外优势。
- 不推荐:将
samesite
设置为 none
或 lax
。
有时*,在登录页面访问 document.cookie
时,我得到一个空字符串,即使:
- cookie 列在 Chrome 和 Firefox 开发人员工具中,
- 我感兴趣的 cookie 的 httpOnly 标志设置为 false,
- 我感兴趣的 cookie 路径设置为“/”。
期望的行为
我的 React 单页应用程序 (SPA) 有一个登录页面,其中包含一个 <form />
元素,用于将登录凭据发送到后端。当收到来自后端的响应并且身份验证成功时,我检查身份验证 cookie 是否已正确设置。如果是这种情况,将触发重定向以显示登录用户的内容。
实际行为
不幸的是,在大约 15% 的登录尝试中,document.cookie
returns 一个空字符串会阻止重定向并使用户保持登录状态页。按 F5
并不能解决问题,但是在成功登录请求后手动替换 url 的路径时(例如,将 'www.website.tld/login' 更新为 'www.website.tld/start'),用户将被转发到仅供登录用户使用的所需页面。
我无法手动重现错误。这似乎是随机发生的。但是当它发生时,我查看了开发者控制台,我可以看到来自后端的所有 cookie(设置正确)。
附加信息
- django 服务器 运行 在后端
- 所需的 cookie 设置为
response.set_cookie('key', 'value', secure=False httponly=False, samesite='strict')
- JS 库(axios、react-router)
相关:
- Can't access cookies from document.cookie in JS, but browser shows cookies exist(仅限 http)
- Can't access a cookie using document.cookie in JS(仅限 http)
登录页面 (JSX)
import React, { useState } from "react";
import { Redirect } from "react-router-dom";
import axios from "axios";
/**
* We're using cookies.js to read cookies.
* Source: https://github.com/madmurphy/cookies.js
*/
function hasItem(sKey) {
return new RegExp(
"(?:^|;\s*)" +
encodeURIComponent(sKey).replace(/[\-\.\+\*]/g, "\$&") +
"\s*\="
).test(document.cookie);
}
export const LoginPage = () => {
const [isAuthenticated, setIsAuthenticated] = useState(false);
const [username, setUsername] = useState("");
const [password, setPassword] = useState("");
function handleSubmit(e) {
e.preventDefault();
function onSuccess(response) {
// handle response
// [...]
// sometimes console.log(document.cookie) returns empty string
if (hasItem("auth_cookie")) {
setIsAuthenticated(true);
} else {
console.warn("Cookie not found!");
}
}
function onFailure(error) {
// handle error
}
const conf = {
headers: new Headers({
"Content-Type": "application/json; charset=UTF-8",
Origin: window.location.origin
})
};
axios
.post("/api/login/", { username, password }, conf)
.then(response => {
onSuccess(response);
})
.catch(error => {
onFailure(error);
});
}
if (isAuthenticated) {
return <Redirect to="/start" />;
}
return (
<div className="login-page">
<form
name="login-form"
method="post"
onSubmit={e => handleSubmit(e)}
action="api/login"
target="hiddenFrame"
>
<iframe className="invisible-frame" src="" name="hiddenFrame" />
<div>
<label htmlFor="username">Email</label>
<input
name="username"
type="text"
onChange={e => setUsername(e.target.value)}
/>
</div>
<div>
<label htmlFor="password">Password</label>
<input
name="password"
type="password"
onChange={e => setPassword(e.target.value)}
/>
</div>
<button type="submit">Submit</button>
</form>
</div>
);
};
路由 (JSX)
import React from "react";
import { Route, Redirect } from "react-router-dom";
const RootLayout = () => {
return (
<div className="root-layout">
<Switch>
<PublicRoute path="/login" component={LoginPage} />
<PrivateRoute path="/" component={App} />
</Switch>
</div>
);
};
/**
* Component that handles redirection when user is logged in already
*/
const PublicRoute = ({ component: ChildComponent, ...remainingProps }) => {
let isAuthenticated = hasItem("auth_cookie");
return (
<Route
render={props =>
isAuthenticated ? <Redirect to="/" /> : <ChildComponent {...props} />
}
{...remainingProps}
/>
);
};
/**
* Component that handles redirection when user has been logged out.
* E.g. when authentication cookie expires.
*/
const PrivateRoute = ({ component: ChildComponent, ...remainingProps }) => {
let isAuthenticated = hasItem("auth_cookie");
return (
<Route
render={props =>
!isAuthenticated ? (
<Redirect to="/login" />
) : (
<ChildComponent {...props} />
)
}
{...remainingProps}
/>
);
};
const App = () => (
<Switch>
<Route exact path="/" render={() => <Redirect to="/start" />} />
<Route exact path="/start" component={StartPage} />
<Route exact path="/blog" component={BlogPage} />
{/*...*/}
</Switch>
);
* I know, that's probably not how a post should start...
您 运行 遇到了 sameSite cookie 的问题。参见 SameSite cookies explained 的解释:
If you set SameSite to Strict, your cookie will only be sent in a first-party context. [...] When the user is on your site, then the cookie will be sent with the request as expected. However when following a link into your site, say from another site or via an email from a friend, on that initial request the cookie will not be sent. This is good when you have cookies relating to functionality that will always be behind an initial navigation, such as changing a password or making a purchase, but is too restrictive for promo_shown. If your reader follows the link into the site, they want the cookie sent so their preference can be applied.
现在你至少有两个选择:
- 建议:保留
samesite=strict
并重构您的客户端代码。前端根本不需要访问 auth cookie,因此您可以设置httponly=True
。然后引入一个后端 API,它根据来自客户端代码的请求来验证 cookie。由于前端代码无法访问 auth cookie,这为您提供了不易受到 XSS 攻击的额外优势。 - 不推荐:将
samesite
设置为none
或lax
。