背景
我有一个安静的后端,React + Redux 前端,我正在努力防止 CSRF 和 XSS 攻击。
前端从 API 请求 CSRF 令牌。API 响应在 HttpOnly cookie 和响应正文中设置 CSRF 令牌。redux reducer 将令牌(来自响应正文)保存到 redux 存储。
如果我在主容器中请求令牌componentDidMount()
,一切正常,但问题是这是一次性的。相反,由于对 API 的请求通过自定义中间件,我希望中间件在本地不存在 CSRF 令牌时请求它。
问题
流程如下(在 Chrome 50 和 Firefox 47 上测试):
- 已请求 CSRF 令牌。存储在 HttpOnly cookie 和 redux 存储中的令牌
- 请求带有
X-CSRF-Token
标头集的原始 API 调用。cookie 未发送 - 由于缺少 cookie,从 API 接收 403。API 以新的 HttpOnly cookie响应。Javascript 看不到这个 cookie,所以 redux 存储没有更新。
- 使用步骤 2 中的 X-CSRF-Token 标头和步骤 3 中的 cookie 请求的其他 API 调用。
- 由于 cookie 与 X-CSRF-Token 不匹配而收到 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);
});
});
};