如果我是面试官,我会优先看:你做过"真实产品级"的闭环(登录-聊天-消息一致性),以及你是否能把难点做对并讲清楚工程化取舍。你这个项目已经有"注册登录 + 聊天 + socket.io 实时推送"的雏形了,接下来最能"眼前一亮"的改进,我建议从下面这些方向挑 2-3 个重点做深(别全堆,做精更值钱)。
一眼能加分的技术亮点(推荐优先级从高到低)
- JWT 鉴权 + 受保护接口(注册/登录后给 access token)
- 后端给 /api/messages/*、/api/auth/setAvatar 做鉴权中间件,未登录直接 401。
- 前端用 token(最好是 httpOnly cookie)后,聊天接口不再依赖 localStorage 的明文用户对象。
- 面试时你可以讲清楚:安全性提升、前后端怎么协同、如何防止越权访问。
- socket.io "房间化"与可靠投递(Rooms + ACK)
- 现在是用 onlineUsers Map(userId -> socketId) 直推,容易有边界问题。
- 升级成:按 chatRoomId 或 sortedUserPair 组成 room,发送时 io.to(room).emit(...)。
- 加 ack 回执:发送方收到服务器确认才把消息标为 "已发送",失败就回滚/提示。
- 这是实习面试里非常硬核的"实时一致性"点。
- 消息状态:已读/未读 + "对方正在输入..."(typing indicator)
- 当接收方打开聊天页:把对应消息更新为 readAt/seenBy。
- typing:前端每隔 1s 发送一次 typing 事件,后端转发给对方展示"对方正在输入"。
- 这两个点能显著提升"产品感",面试官通常会追问你如何避免刷屏(节流/防抖)。
- 消息分页/历史加载(Infinite Scroll)
- 别只拉全部消息:后端支持 GET /messages?roomId=&cursor= 或 page/limit。
- 前端实现"向上滚动加载更早消息",并用时间戳索引(createdAt / updatedAt)优化查询。
- 这是工程能力 + 性能优化的体现。
- 前端工程化:把 socket 和请求做成可复用 hook/状态管理
- 把 socket 逻辑封装成 useChatSocket()(自动连、重连、事件绑定/解绑、toast 提示)。
- 数据请求用 react-query(或至少封装 axios),并实现乐观更新。
- 代码结构清晰后,你讲起来会特别像"能维护的同学"。
- TypeScript + 组件规范化(可选但很加分)
- 把消息结构、用户结构、socket event payload 都类型化。
- 组件层做统一的 UI(你现在用 styled-components + 组件化是好的方向)。
- 测试与持续集成(强烈建议至少做后端测试)
- 用 jest + supertest 测:register/login、setAvatar、addMessage、getMessages。
- GitHub Actions 做 CI:每次 push 自动跑测试和 lint。
- 面试官会很在意"可验证性",这能让你从"写出能跑的项目"升级到"写出可交付的项目"。
二选一的"质变"路线(我建议你这样打磨)
- 路线 A(实时一致性路线):JWT + socket rooms + ACK + 已读/typing + 分页
- 路线 B(工程化路线):TypeScript + react-query + socket hook + 测试 + CI/CD(把可靠性讲清楚)
你现在项目最值得先修的"讲故事点"
面试时你可以坦诚地说你怎么发现问题并修复(这会显得你有 debug 能力):
- 你现在 socket/消息那块已经有真实修复经历(非常好讲)
- 建议把"你修复过哪些坑、为什么会出现、最后怎么验证"整理成面试可复述的 1-2 分钟故事
🔥接入 JWT 鉴权 + httpOnly Cookie + 受保护接口
我会完全从零教你,不用任何前置知识,直接复制代码就能用,适配你的现有项目(Express + MongoDB + React)。
原理
- JWT = 登录成功后,后端给前端发一个加密令牌(token)
- httpOnly Cookie = 把 token 存在浏览器里,前端 JS 读不到,超级安全(不会被盗)
- 鉴权中间件 = 后端写一个 "门卫",访问
/api/messages/*/api/auth/setAvatar必须带 token,否则直接 401 拒绝 - 改造后:前端不再用 localStorage 存明文用户,全部靠 token 认证
1.后端安装依赖
npm install jsonwebtoken cookie-parser
jsonwebtoken:生成 / 验证 JWTcookie-parser:让 Express 能读写 Cookie
2.后端主文件配置(server.js)
javascript
const express = require("express");
const mongoose = require("mongoose");
const cors = require("cors");
const cookieParser = require("cookie-parser"); // 加这个
require("dotenv").config();
const app = express();
// 中间件
app.use(express.json());
app.use(cookieParser()); // 加这个
app.use(
cors({
origin: "http://localhost:5173", // 你的前端地址
credentials: true, // 必须开,才能传递 cookie
})
);
// 数据库连接
mongoose.connect(process.env.MONGO_URI);
// 路由
app.use("/api/auth", require("./routes/authRoutes"));
app.use("/api/messages", require("./routes/messageRoutes"));
app.listen(5000, () => console.log("Server running on port 5000"));
3.创建 JWT 工具函数(生成 token)
新建 utils/generateToken.js
javascript
const jwt = require("jsonwebtoken");
const generateToken = (userId, res) => {
// 生成 JWT token,有效期 7 天
const token = jwt.sign({ id: userId }, process.env.JWT_SECRET, {
expiresIn: "7d",
});
// 把 token 存入 httpOnly cookie(最安全)
res.cookie("jwt", token, {
httpOnly: true, // 前端 JS 无法读取,防 XSS
secure: process.env.NODE_ENV === "production", // 生产环境开启 HTTPS
sameSite: "strict", // 防 CSRF
maxAge: 7 * 24 * 60 * 60 * 1000, // 7天
});
};
module.exports = generateToken;
4.创建鉴权中间件(门卫)
新建 middleware/authMiddleware.js
javascript
const jwt = require("jsonwebtoken");
const User = require("../models/userModel");
// 鉴权中间件:检查是否登录
const protectRoute = async (req, res, next) => {
try {
// 从 cookie 中获取 jwt
const token = req.cookies.jwt;
if (!token) {
return res.status(401).json({ message: "未登录,请先登录" });
}
// 验证 token 是否有效
const decoded = jwt.verify(token, process.env.JWT_SECRET);
// 把用户信息挂载到 req,后续接口可用
const user = await User.findById(decoded.id).select("-password");
req.user = user;
next(); // 放行
} catch (err) {
res.status(401).json({ message: "无效的 token,登录已过期" });
}
};
module.exports = protectRoute;
5.修改登录 / 注册接口(下发 token)
打开你的 authRoutes.js
1. 注册成功下发 token
javascript
const generateToken = require("../utils/generateToken");
router.post("/register", async (req, res) => {
try {
const { username, email, password } = req.body;
const user = await User.create({ username, email, password });
// 👇 关键:生成 token 并写入 cookie
generateToken(user._id, res);
res.status(201).json({
_id: user._id,
username: user.username,
email: user.email,
isAvatarImageSet: user.isAvatarImageSet,
});
} catch (err) {
res.status(400).json({ message: err.message });
}
});
2. 登录成功下发 token
javascript
router.post("/login", async (req, res) => {
try {
const { email, password } = req.body;
const user = await User.findOne({ email });
// 密码验证...
// 👇 关键:生成 token 并写入 cookie
generateToken(user._id, res);
res.json({
_id: user._id,
username: user.username,
email: user.email,
isAvatarImageSet: user.isAvatarImageSet,
});
} catch (err) {
res.status(400).json({ message: err.message });
}
});
6.给接口加保护(必须登录才能访问)
1. 保护 /api/auth/setAvatar
javascript
const protectRoute = require("../middleware/authMiddleware");
// 👇 加中间件 protectRoute
router.post("/setAvatar", protectRoute, async (req, res) => {
// 你的设置头像逻辑
// 可以直接用 req.user._id 获取当前登录用户
});
2. 保护 /api/messages/* 所有接口
javascript
const express = require("express");
const router = express.Router();
const protectRoute = require("../middleware/authMiddleware");
// 所有消息接口必须登录
router.use(protectRoute);
router.post("/", async (req, res) => { ... });
router.get("/:chatId", async (req, res) => { ... });
module.exports = router;
7.前端改造(axios 自动带 cookie)
你所有的 axios 请求都要加 withCredentials: true,这样 cookie 会自动发送给后端。
在 src/utils/APIRoutes.js 同级新建 axiosInstance.js
javascript
import axios from "axios";
const axiosInstance = axios.create({
baseURL: "http://localhost:5000",
withCredentials: true, // 关键:自动携带 cookie
});
export default axiosInstance;
以后所有请求都用这个 axios
javascript
// 以前
import axios from "axios";
// 现在
import axios from "../utils/axiosInstance";
8.前端不再依赖 localStorage
你现在的 Chat.jsx 是读 localStorage,改造后完全不用:
前端获取当前用户 → 靠后端鉴权接口
新建接口 /api/auth/me
javascript
router.get("/me", protectRoute, async (req, res) => {
res.json(req.user); // 返回当前登录用户
});
前端使用
javascript
useEffect(() => {
const getUser = async () => {
const { data } = await axios.get("/api/auth/me");
setCurrentUser(data);
};
getUser();
}, []);
✅ 最终效果
- ✅ JWT 鉴权 完成
- ✅ httpOnly cookie 存储 token(超级安全)
- ✅ 受保护接口:
/api/messages/*、/api/auth/setAvatar - ✅ 未登录直接返回 401
- ✅ 前端不再使用 localStorage 明文存用户
- ✅ 自动登录、自动过期、安全防攻击