ExpressJS + ReactJS SPA - 在每个请求中生成不同的 CSRF 令牌
ExpressJS + ReactJS SPA - Different CSRF token generated in every request
我已经设置了 csurf
节点模块来为我的 ExpressJS 应用程序添加 CSRF 保护。前端是 ReactJS 单页应用程序。问题是每次 POST 请求时我都会不断收到此错误:ForbiddenError: invalid csrf token
。我发现 CSRF 令牌在请求之间以某种方式发生变化并且不会持续存在。我该如何解决这个问题?这是我的 Express 服务器代码:
import * as dotenv from "dotenv";
import express, { Express } from 'express';
import morgan from 'morgan';
import helmet from 'helmet';
import cors from 'cors';
import cookieSession from 'cookie-session';
import cookieParser from 'cookie-parser';
import csrf from 'csurf';
import passport from 'passport';
import fs from 'fs';
import config from '../config.json';
dotenv.config({ path: `.env.${config.NODE_ENV}` });
const app: Express = express();
/************************************************************************************
* Basic Express Middlewares
***********************************************************************************/
app.set('json spaces', 4);
app.use(express.json());
app.use(express.urlencoded({ extended: true }));
app.use(cookieParser());
const useSecureCookies: boolean = (process.env.NODE_ENV !== 'development' && process.env.NODE_ENV !== 'test');
app.use(cookieSession({
name: 'myapp',
keys: [process.env.COOKIE_SECRET],
secure: useSecureCookies,
maxAge: 24 * 60 * 60 * 1000 // 24 hours
}));
// Handle logs in console during development
if (process.env.NODE_ENV === 'development' || config.NODE_ENV === 'development') {
app.use(morgan('dev'));
app.use(cors());
}
// Handle security and origin in production
if (process.env.NODE_ENV === 'production' || config.NODE_ENV === 'production') {
app.use(helmet());
}
/***********************************
* CSRF Protection *
***********************************/
app.use(csrf({
cookie: {
secure: useSecureCookies,
httpOnly: true
}
}));
// Add the CSRF token to every request
// Source:
app.use((req: express.Request, res: express.Response, next: express.NextFunction) => {
res.cookie("X-CSRF-TOKEN", req.csrfToken());
next();
});
/***********************************
* Authentication *
***********************************/
app.use(passport.initialize());
app.use(passport.session());
...
export default app;
我在 app/routes/session.ts
中创建了一条新路线:
import { Router } from 'express';
import passport from 'passport';
import { BASE_ENDPOINT } from "../../constants/endpoint";
import User from '../../db/models/user';
export const router: Router = Router();
router.get(`${BASE_ENDPOINT}/sessions/sign_in`, async (req, res) => {
res.status(200).send({
token: req.csrfToken(),
session: req.session?.csrfSecret
});
});
router.post(`${BASE_ENDPOINT}/sessions/sign_in`,
passport.authenticate('local'),
async (req, res) => {
res.status(200).send({
user: (req.user as User).toJSON()
});
}
);
我正在使用 Ruby 控制台发出 GET 和 POST 请求来测试我服务器的 API:
- 我向 /sessions/sign_in 发出 GET 请求以获取 CSRF 令牌
- 我使用用户的电子邮件和密码向 /sessions/sign_in 发出 POST 请求。我尝试在 POST body 中包含一个带有令牌的
_csrf
字段,并在令牌中包含一个 X-CSRF-TOKEN
header,但是 none的工作。
任何关于如何保留令牌的想法都将不胜感激。
我明白了。这是因为我的浏览器和服务器没有发送 cookie。这是我修复它的方法:
客户端
- 将
withCredentials: true
选项添加到我的 axios 实例中:
const axiosInstance = axios.create({
baseURL: process.env.SERVER_API_BASE_URL,
withCredentials: true
});
服务器
- 更新 CORS 以向客户端发送凭据。在
server.ts
:
import cors from 'cors';
...
app.use(cors({
credentials: true,
origin: "http://localhost:8080" // The host name of the client
}));
...
现在我要做的就是将 CSRF 令牌从服务器发送到客户端,然后将其包含在我的请求中。
我已经设置了 csurf
节点模块来为我的 ExpressJS 应用程序添加 CSRF 保护。前端是 ReactJS 单页应用程序。问题是每次 POST 请求时我都会不断收到此错误:ForbiddenError: invalid csrf token
。我发现 CSRF 令牌在请求之间以某种方式发生变化并且不会持续存在。我该如何解决这个问题?这是我的 Express 服务器代码:
import * as dotenv from "dotenv";
import express, { Express } from 'express';
import morgan from 'morgan';
import helmet from 'helmet';
import cors from 'cors';
import cookieSession from 'cookie-session';
import cookieParser from 'cookie-parser';
import csrf from 'csurf';
import passport from 'passport';
import fs from 'fs';
import config from '../config.json';
dotenv.config({ path: `.env.${config.NODE_ENV}` });
const app: Express = express();
/************************************************************************************
* Basic Express Middlewares
***********************************************************************************/
app.set('json spaces', 4);
app.use(express.json());
app.use(express.urlencoded({ extended: true }));
app.use(cookieParser());
const useSecureCookies: boolean = (process.env.NODE_ENV !== 'development' && process.env.NODE_ENV !== 'test');
app.use(cookieSession({
name: 'myapp',
keys: [process.env.COOKIE_SECRET],
secure: useSecureCookies,
maxAge: 24 * 60 * 60 * 1000 // 24 hours
}));
// Handle logs in console during development
if (process.env.NODE_ENV === 'development' || config.NODE_ENV === 'development') {
app.use(morgan('dev'));
app.use(cors());
}
// Handle security and origin in production
if (process.env.NODE_ENV === 'production' || config.NODE_ENV === 'production') {
app.use(helmet());
}
/***********************************
* CSRF Protection *
***********************************/
app.use(csrf({
cookie: {
secure: useSecureCookies,
httpOnly: true
}
}));
// Add the CSRF token to every request
// Source:
app.use((req: express.Request, res: express.Response, next: express.NextFunction) => {
res.cookie("X-CSRF-TOKEN", req.csrfToken());
next();
});
/***********************************
* Authentication *
***********************************/
app.use(passport.initialize());
app.use(passport.session());
...
export default app;
我在 app/routes/session.ts
中创建了一条新路线:
import { Router } from 'express';
import passport from 'passport';
import { BASE_ENDPOINT } from "../../constants/endpoint";
import User from '../../db/models/user';
export const router: Router = Router();
router.get(`${BASE_ENDPOINT}/sessions/sign_in`, async (req, res) => {
res.status(200).send({
token: req.csrfToken(),
session: req.session?.csrfSecret
});
});
router.post(`${BASE_ENDPOINT}/sessions/sign_in`,
passport.authenticate('local'),
async (req, res) => {
res.status(200).send({
user: (req.user as User).toJSON()
});
}
);
我正在使用 Ruby 控制台发出 GET 和 POST 请求来测试我服务器的 API:
- 我向 /sessions/sign_in 发出 GET 请求以获取 CSRF 令牌
- 我使用用户的电子邮件和密码向 /sessions/sign_in 发出 POST 请求。我尝试在 POST body 中包含一个带有令牌的
_csrf
字段,并在令牌中包含一个X-CSRF-TOKEN
header,但是 none的工作。
任何关于如何保留令牌的想法都将不胜感激。
我明白了。这是因为我的浏览器和服务器没有发送 cookie。这是我修复它的方法:
客户端
- 将
withCredentials: true
选项添加到我的 axios 实例中:
const axiosInstance = axios.create({
baseURL: process.env.SERVER_API_BASE_URL,
withCredentials: true
});
服务器
- 更新 CORS 以向客户端发送凭据。在
server.ts
:
import cors from 'cors';
...
app.use(cors({
credentials: true,
origin: "http://localhost:8080" // The host name of the client
}));
...
现在我要做的就是将 CSRF 令牌从服务器发送到客户端,然后将其包含在我的请求中。