httponly cookie 在后续的 xhr 请求中不可用

httponly cookie not available in subsequent xhr requests

背景

我有一个 restful 后端,React + Redux 前端,我正在尝试防止 CSRF 和 XSS 攻击。

前端从 API 请求 CSRF 令牌。 API 响应在 HttpOnly cookie 和响应 body 中设置 CSRF 令牌。 redux reducer 将令牌(来自响应 body)保存到 redux store。

如果我在主容器的 componentDidMount() 中请求令牌,一切正常,但问题是这是一次性的。相反,由于对 API 的请求通过自定义中间件,如果本地不存在,我更希望中间件请求 CSRF 令牌。

问题

流程如下(在Chrome 50和Firefox 47上测试):

  1. 已请求 CSRF 令牌。令牌存储在 HttpOnly cookie 和 redux store
  2. 原始 API 呼叫请求 X-CSRF-Token header 集。 cookie 未发送
  3. 由于缺少 cookie,从 API 收到 403。 API 使用 新的 HttpOnly cookie 进行响应。 Javascript 看不到这个 cookie,所以 redux store 没有更新。
  4. 额外的 API 请求请求来自步骤 2 的 X-CSRF-Token header 和来自步骤 3 的 cookie。
  5. 由于 cookie 与 X-CSRF-Token
  6. 不匹配而收到 403

如果我在第 2 步之前使用 window.setTimeout 添加延迟,cookie 仍未发送,所以我 认为 它不是浏览器的竞争条件没有足够的时间保存 cookie?

动作创作者

const login = (credentials) => {

    return {
        type: AUTH_LOGIN,
        payload: {
            api: {
                method: 'POST',
                url: api.v1.auth.login,
                data: credentials
            }
        }
    };
};

中间件

/**
 * Ensure the crumb and JWT authentication token are wrapped in all requests to the API.
 */
export default (store) => (next) => (action) => {

    if (action.payload && action.payload.api) {

        store.dispatch({ type: `${action.type}_${PENDING}` });
        return ensureCrumb(store)
            .then((crumb) => {

                const state = store.getState();
                const requestConfig = {
                    ...action.payload.api,
                    withCredentials: true,
                    xsrfCookieName: 'crumb',
                    xsrfHeaderName: 'X-CSRF-Token',
                    headers: {
                        'X-CSRF-Token': crumb
                    }
                };

                if (state.auth.token) {
                    requestConfig.headers = { ...requestConfig.headers, Authorization: `Bearer ${state.auth.token}` };
                }

                return axios(requestConfig);
            })
            .then((response) => store.dispatch({ type:`${action.type}_${SUCCESS}`, payload: response.data }))
            .catch((response) => store.dispatch({ type: `${action.type}_${FAILURE}`, payload: response.data }));
    }

    return next(action);
};

/**
 * Return the crumb if it exists, otherwise requests a crumb
 * @param store - The current redux store
 * @returns Promise - crumb token
 */
const ensureCrumb = (store) => {

    const state = store.getState();
    return new Promise((resolve, reject) => {

        if (state.crumb.token) {
            return resolve(state.crumb.token);
        }

        store.dispatch({ type: CRUMB_PENDING });
        axios.get(api.v1.crumb)
            .then((response) => {

                store.dispatch({ type: CRUMB_SUCCESS, payload: { token: response.data.crumb } });
                window.setTimeout(() => resolve(response.data.crumb), 10000);
                // return resolve(response.data.crumb);
            })
            .catch((error) => {

                store.dispatch({ type: CRUMB_FAILURE });
                return reject(error);
            });
    });
};

这是因为我在每个请求上都创建了一个新的 axios 客户端,如果我对所有 API 请求重复使用相同的 axios 客户端,cookie 会正确保存并在后续请求中使用。