即使 cookie 在开发人员工具中列出且 httpOnly 标志设置为 false,访问 document.cookie 也会返回空字符串

dav*_*sto 8 javascript django cookies httponly reactjs

有时*,在登录页面访问时,document.cookie我会得到一个空字符串,即使:

  1. cookie 列在 Chrome 和 Firefox 开发者工具中,
  2. 我感兴趣的 cookie 的 httpOnly 标志设置为 false,
  3. 我感兴趣的 cookie 路径设置为“/”。

期望的行为

我的 React 单页应用程序 (SPA) 有一个登录页面,其中包含一个<form />用于将登录凭据发送到后端的元素。当收到后端的响应并且身份验证成功时,我检查身份验证 cookie 是否已正确设置。如果是这种情况,将触发重定向,显示登录用户的内容。

实际行为

不幸的是,大约15%的登录尝试document.cookie会返回一个空字符串,这会阻止重定向并将用户保留在登录页面上。按 并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)

有关的:

登录页面 (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>
      );
    };
Run Code Online (Sandbox Code Playgroud)

路由 (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>
    );
Run Code Online (Sandbox Code Playgroud)

* I know, that's probably not how a post should start...

str*_*str 3

您遇到了SameSite cookie的问题。有关解释,请参阅SameSite cookie解释:

如果您将 SameSite 设置为 Strict,您的 cookie 将仅在第一方上下文中发送。[...] 当用户访问您的网站时,cookie 将按预期随请求一起发送。然而,当通过链接进入您的网站时,例如从另一个网站或通过朋友的电子邮件,在该初始请求中,将不会发送 cookie。当您拥有与始终位于初始导航后面的功能相关的 cookie(例如更改密码或进行购买)时,这很好,但对于 promo_shown 来说限制性太大。如果您的读者通过链接进入网站,他们希望发送 cookie,以便应用他们的偏好。

现在你至少有两个选择:

  • 建议:保留samesite=strict并重构您的客户端代码。前端根本不需要访问身份验证cookie,因此您可以设置httponly=True。然后引入一个后端 API,根据客户端代码的请求验证 cookie。这为您提供了额外的优势,不易受到 XSS 攻击,因为前端代码无法访问身份验证 cookie。
  • 不推荐:设置samesitenonelax