React SPA/CRA+Express电商站点安全与CSRF问题解决方案问询
解决React SPA + Express的API安全与CSRF防护问题
我来帮你一步步梳理问题,先解决API访问权限的顾虑,再重点修复你遇到的CSRF配置错误。
一、API访问权限控制:不是任何人都能随便访问
首先明确:CSRF防护是防止跨站请求伪造,但不能替代身份认证来控制API的访问权限。你需要区分两种API:
1. 公开API(如商品列表、分类)
这类API可以开放给所有用户,但建议通过CORS严格限制来源(你已经设置了origin: "http://localhost:3000",这一步是对的),避免恶意网站直接调用。
2. 私有API(如购物车、订单、用户信息)
必须添加身份认证中间件,只有登录用户才能访问:
- 如果你用Passport.js,可以在路由里添加
passport.authenticate('session')(或对应策略)作为中间件 - 也可以自己实现JWT验证中间件,要求请求头携带有效token
举个例子,修改你的路由配置:
// 私有API需要先验证用户是否登录 const requireAuth = (req, res, next) => { if (req.isAuthenticated()) { return next(); } res.status(401).json({ error: "Unauthorized" }); }; app.use('/api/', ensureSession, [ CategoryRoutes, // 公开API ProductRoutes, // 公开API // 私有API添加身份验证 CartRoutes, AuthRoutes, TrackRoutes, ].map(router => requireAuth(router)));
二、CSRF防护的正确配置:修复"Invalid CSRF token"错误
你的代码里有几个关键错误,导致了token无效或undefined的问题,下面是修正方案:
1. 服务端配置修正(Server.js)
核心问题:
- 拼写错误+中间件顺序混乱
- csurf配置错误,没有把token存在客户端可读取的cookie中
- CORS没有开启
credentials: true,导致客户端无法发送cookie
修正后的关键代码:
import express from 'express'; import bodyParser from 'body-parser'; import passport from 'passport'; import connection from './config/conn'; import { /* 你的路由 */ } from './modules'; import session from 'express-session'; import csrf from 'csurf' import cors from 'cors' import cookieParser from 'cookie-parser' import passportSetup from './config/passport-setup'; import keys from './config/keys'; import uuidv4 from 'uuid/v4'; import UAparser from 'ua-parser-js'; import { ensureSession } from './config/sessionCheck'; const app = express(); // 1. 先解析cookie app.use(cookieParser()); // 2. 配置session app.use(session({ genid: (req) => uuidv4(), name: keys.session.name, secret: keys.session.secret, resave: false, saveUninitialized: false, // 禁止自动创建未初始化的session,更安全 rolling: true, cookie: { secure: process.env.NODE_ENV === 'production', // 生产环境开启HTTPS才设为true httpOnly: true, maxAge: keys.session.maxAge, sameSite: 'strict', // 严格限制跨站cookie } })); // 3. Passport初始化 app.use(passport.initialize()); app.use(passport.session()); // 4. CORS配置:必须开启credentials,允许客户端发送cookie app.use(cors({ origin: keys.origin.url, credentials: true })); // 5. CSRF防护配置:把token存在客户端可读取的cookie中 const csrfProtection = csrf({ cookie: { key: 'csrf_token', // 指定cookie名称,方便客户端读取 httpOnly: false, // 允许客户端JS读取这个cookie sameSite: 'strict', maxAge: 3600000, // 1小时有效期 secure: process.env.NODE_ENV === 'production' } }); app.use(csrfProtection); // 6. 安全响应头 + 可选:把token放到响应头,方便客户端读取 app.use((req, res, next)=>{ res.header('X-XSS-Protection', '1; mode=block'); res.header('X-Frame-Options','deny'); res.header('X-Content-Type-Options','nosniff'); res.header('X-CSRF-Token', req.csrfToken()); // 可选,和cookie二选一即可 next(); }); // 新增:可选的CSRF token获取接口,方便客户端初始化时获取 app.get('/api/csrf-token', (req, res) => { res.json({ csrfToken: req.csrfToken() }); }); // 7. 解析请求体 app.use(bodyParser.urlencoded({ extended: false })); app.use(bodyParser.json()); // 8. 路由配置:私有API记得加身份认证 app.use('/api/', ensureSession, csrfProtection, [ CategoryRoutes, ProductRoutes, CartRoutes, AuthRoutes, TrackRoutes, AddressRoutes, ShippingRoutes, UIRoutes, LookbookRoutes, CheckoutRoutes ]); const port = process.env.PORT || 5000; app.listen(port, (err) => { if(err){ console.log(err); }else{ console.log(`Server running on port ${port}!`); } });
2. 客户端配置修正(Redux + Axios)
核心问题:
- 你把cookie名称拼错了:服务端设的是
csrf_token,客户端写的是csrt_token(少了一个f) - 没有全局配置axios自动携带CSRF token,每个请求都要手动写,容易出错
修正后的代码:
首先全局配置axios(建议在项目入口文件如index.js里配置):
import axios from 'axios'; import Cookies from 'js-cookie'; // 配置axios基础路径 axios.defaults.baseURL = 'http://localhost:5000'; // 允许axios发送cookie到服务端 axios.defaults.withCredentials = true; // 请求拦截器:自动给所有POST/PUT/DELETE请求添加CSRF token axios.interceptors.request.use(config => { const csrfToken = Cookies.get('csrf_token'); if (csrfToken) { config.headers['x-csrf-token'] = csrfToken; } return config; }, error => Promise.reject(error));
然后你的Redux action可以简化,不用手动加headers了:
export const fetchCartList = (value) => dispatch => { dispatch(loadingCart()); axios.post('/api/v1/track/cart') .then(res => { const dataType = typeof res.data.cart_list; if (typeof res.data.token_c !== "undefined") { localStorage.setItem("hammerstout_c", res.data.token_c); } if (dataType !== "undefined") { dispatch({ type: FETCH_CART_LIST, payload: res.data.cart_list }) } else { dispatch(stopLoadingCart()); localStorage.removeItem("hammerstout_c"); } }).catch(err => { dispatch({ type: GET_ERRORS, payload: err.response.data }); if (err.response.data.error) { dispatch(stopLoadingCart()); } }) }
三、为什么之前会出错?
- 拼写错误:客户端读取的cookie名称和服务端设置的不一致,直接导致
CSRF is undefined - 中间件顺序错误:csrf中间件必须在cookieParser和session之后,否则无法读取session/cookie来验证token
- CORS配置缺失:没有开启
credentials: true,客户端不会把cookie发送给服务端,导致服务端无法验证CSRF token - csurf配置错误:一开始设置
cookie: false,意味着token存在session里,但客户端无法读取session,所以必须把token存在可读取的cookie中
四、额外注意事项
- 生产环境必须开启HTTPS,同时把
cookie.secure设为true,避免cookie被明文传输 - 不要把CSRF token存在localStorage里,容易被XSS攻击窃取,用cookie(
sameSite: strict)更安全 - 首次加载时,如果第一个API请求出现token无效,可以先调用
/api/csrf-token接口获取token后再发起请求
内容的提问来源于stack exchange,提问作者user10583820
相关产品推荐
相关产品推荐

