如何基于Express.js和Firebase构建安全的服务端认证系统?
问题根源
你的代码仅调用Firebase的signInWithEmailAndPassword完成认证,但没有在服务端为每个用户建立独立、隔离的会话,且未对会话做安全配置,导致同一网络下的设备能共享未受保护的认证状态。
安全服务端认证系统构建方案
1. 基于Cookie的会话管理(推荐)
核心逻辑
认证成功后,为每个用户生成唯一会话标识,绑定用户信息存储在服务端,并通过安全Cookie传递会话标识,确保每个设备的会话独立隔离。
示例代码
const { v4: uuidv4 } = require('uuid'); const redis = require('redis'); const client = redis.createClient(); // 登录接口 app.post('/logIn', async (req, res) => { const context = { error: false }; let userCredential; try { userCredential = await signInWithEmailAndPassword(auth, req.body.email, req.body.password); } catch (err) { context.error = true; return res.render('auth/logIn.ejs', context); } // 生成唯一会话ID const sessionId = uuidv4(); // 将用户ID与会话ID绑定,存入Redis并设置2小时过期时间 await client.setEx(sessionId, 7200, userCredential.user.uid); // 设置安全Cookie res.cookie('sessionId', sessionId, { httpOnly: true, // 禁止前端JS读取,防范XSS攻击 secure: process.env.NODE_ENV === 'production', // 生产环境强制HTTPS传输 sameSite: 'Strict', // 防范CSRF攻击 maxAge: 7200000 // 会话有效期2小时 }); return res.redirect('/'); })
2. 会话验证中间件
对需要认证的路由,添加中间件校验会话有效性,拦截未认证请求:
async function authenticate(req, res, next) { const sessionId = req.cookies.sessionId; if (!sessionId) { return res.redirect('/logIn'); } // 从Redis获取绑定的用户ID const userId = await client.get(sessionId); if (!userId) { res.clearCookie('sessionId'); return res.redirect('/logIn'); } // 将用户ID挂载到req对象,供后续路由使用 req.userId = userId; next(); } // 示例:需要认证的首页路由 app.get('/', authenticate, (req, res) => { res.render('home.ejs'); });
3. 安全登出逻辑
登出时删除服务端会话记录,并清除客户端Cookie:
app.post('/logOut', async (req, res) => { const sessionId = req.cookies.sessionId; if (sessionId) { await client.del(sessionId); } res.clearCookie('sessionId'); res.redirect('/logIn'); });
4. 无状态替代方案:JWT认证
若偏好无状态架构,可使用JWT(JSON Web Token)实现认证,同样通过HttpOnly Cookie传输:
const jwt = require('jsonwebtoken'); // 登录时生成JWT app.post('/logIn', async (req, res) => { const context = { error: false }; let userCredential; try { userCredential = await signInWithEmailAndPassword(auth, req.body.email, req.body.password); } catch (err) { context.error = true; return res.render('auth/logIn.ejs', context); } // 生成JWT,有效期2小时 const token = jwt.sign( { userId: userCredential.user.uid }, process.env.JWT_SECRET, { expiresIn: '2h' } ); // 设置安全Cookie存储JWT res.cookie('token', token, { httpOnly: true, secure: process.env.NODE_ENV === 'production', sameSite: 'Strict', maxAge: 7200000 }); return res.redirect('/'); }) // JWT验证中间件 function authenticateJWT(req, res, next) { const token = req.cookies.token; if (!token) { return res.redirect('/logIn'); } jwt.verify(token, process.env.JWT_SECRET, (err, user) => { if (err) { res.clearCookie('token'); return res.redirect('/logIn'); } req.userId = user.userId; next(); }); }
5. 额外安全加固措施
- 生产环境强制使用HTTPS,避免明文传输认证信息
- 为登录接口添加速率限制,防范暴力破解
- 用户修改密码时,强制失效原有会话/令牌
- 若使用自定义用户系统,必须对密码进行哈希存储(Firebase已自动处理此步骤)
内容的提问来源于stack exchange,提问作者galdinotrashaj
相关产品推荐
相关产品推荐

