React+Node.js+MariaDB会话过期重登录认证回退及数据库离线兼容问题
社交应用数据库离线回退逻辑实现及问题排查
现有代码片段
前端Login.jsx登录请求代码
fetch(getApiUrl('/auth/login'), { method: 'POST', headers: { 'Content-Type': 'application/json', }, body: JSON.stringify(values), credentials: 'include', }) .then(res => { const jsn = res.json(); if (res.ok) { handleAuthLogin(navigate, revalidate)(jsn); } else if (jsn.message) { toast.error(<div className='text-lg'>{jsn.message}</div>); } }) .catch((err) => { console.error(err); if (err.message === 'Failed to fetch') { navigate('/service-unavailable'); } else if (err.message === 'Invalid token' || err.message === 'Unauthorized') { navigate('/login'); } else { toast.error(<div className='text-lg'>Failed to connect to the server. Please check your connection.</div>); } });
后端index.js代码片段
/** * Middleware for handling authentication routes. * All routes starting with '/auth' will be handled by the authRouter. */ app.use(checkToken); app.use('/auth', authRouter); app.use(userRouter); // Set up the /users endpoint app.get('/users', async (req, res) => { const { name } = req.query.name; // Query the database using Prisma const users = await prisma.user.findMany({ where: { name: { contains: name, }, }, }); // Send the result back to the client res.json(users); });
checkToken中间件代码
const checkToken = (req, res, next) => { const token = req.headers['authorization']; console.log('token: ', token); if (!token) { return res.status(403).send({ message: 'No token provided.' }); } try { const decoded = decodeJwt(token); req.userId = decoded.id; next(); } catch (error) { return res.status(401).send({ message: 'Unauthorized! Session expired.' }); } };
表单验证代码
const formVerification = (req, res, action) => { // Extract the data from the request body const data = req.body; console.log(data); // Validate the data against the schema schema .validate(data) .catch(err => { // If validation fails, send a 422 status code and the validation error messages res.status(422).json({ status: 422, message: err.errors }); }) .then(valid => { if (valid) { const cred = { usr: data.username, pwd: data.password }; if (action === "login") { // Login handle handleLogin(cred, req, res); } else if (action === "signup") { // Signup handle handleSignup(cred, req, res); } else { throw new Error("Invalid action"); } } else { res.status(400).json({ status: 400, message: "Invalid request" }); } }); }
handleLogin及JWT相关代码
const hashPwd = (pwd) => { return createHash("md5").update(pwd).digest("hex"); }; const encodeJwt = ({ id, name }) => { return sign({ id, name }, secret, { expiresIn: "1h" }); }; const decodeJwt = (jwt) => { try { return verify(jwt, secret); } catch (e) { throw new Error('Failed to decode JWT: ' + e.message); } }; const handleLogin = ({ usr, pwd }, req, res) => { console.log('usr: ', usr, 'pwd: ', pwd); getUser(usr, (err, user) => { if (err) { console.error(err); return res.status(500).json({ status: 500, message: "Database is offline" }); } console.log('user: ', user); if (!user) { return res.status(404).json({ status: 404, message: "User not found" }); } else if (user.credentials.hash !== hashPwd(pwd)) { return res.status(401).json({ status: 401, message: "Invalid password" }); } else { return res.status(200).json({ status: 200, jwt: encodeJwt(user), user: { id: user.id, name: user.name } }); } }, true); };
遇到的问题
- 应用挂载时触发
user.js:29 GET http://localhost:5000/user/@me 401 (Unauthorized)错误,对应代码:
// No token stored in local store const NO_TOKEN_ERR = "no_token"; const UserContext = createContext(null); const UserProvider = ({ children }) => { const [user, setUser] = useState(null); const [fetching, setFetching] = useState(false); const [error, setError] = useState(null); // Obtain new data const revalidate = () => { // Obtain local JWT val const jwt = localStorage.getItem("jwt"); if (!jwt) { setError(NO_TOKEN_ERR); return; } const init = { method: "GET", headers: { 'Authorization': `Bearer ${jwt}` } } // Fetch that thing fetch(getApiUrl("/user/@me"), init) // line 29 .then(res => res.json()) .then(({ user }) => setUser(user)) .catch((e) => setError(e.message)) .finally(() => setFetching(false)) } // rest of the code }
- 提交登录表单时触发
POST http://localhost:5000/auth/login 403 (Forbidden)错误,提示token未定义,但应用挂载时本地存储存在哈希后的token。
需求
- 数据库离线时自动跳转至
/service-unavailable页面 - 数据库正常时,完成表单验证、登录等常规社交应用功能
解决方案
问题1:401 Unauthorized 错误修复
- 调整后端中间件顺序:
checkToken中间件不应拦截登录/注册路由,修改后代码:
// 修改后 app.use('/auth', authRouter); // 登录/注册路由无需token校验 app.use(checkToken); // 其他需要校验的路由放在中间件之后 app.use(userRouter);
- 前端
revalidate函数补充响应状态校验:
fetch(getApiUrl("/user/@me"), init) .then(res => { if (!res.ok) { if (res.status === 401) { localStorage.removeItem("jwt"); setError("Unauthorized"); // 可在此添加跳转登录页逻辑 } throw new Error(`HTTP error! status: ${res.status}`); } return res.json(); }) .then(({ user }) => setUser(user)) .catch((e) => setError(e.message)) .finally(() => setFetching(false))
问题2:登录请求403 Forbidden 错误修复
按照上述中间件顺序调整后,/auth路由不再被checkToken拦截,即可解决该问题。
数据库离线回退逻辑实现
- 前端处理后端返回的数据库离线标识:修复Login.jsx中
res.json()的Promise解析问题,补充500状态处理:
.then(res => { res.json().then(data => { if (res.ok) { handleAuthLogin(navigate, revalidate)(data); } else { if (data.status === 500 && data.message === "Database is offline") { navigate('/service-unavailable'); } else if (data.message) { toast.error(<div className='text-lg'>{data.message}</div>); } } }); })
- 全局封装fetch工具函数:统一处理网络错误和数据库离线情况:
const fetchWithFallback = (url, options = {}) => { return fetch(url, options) .then(res => { if (res.status === 500) { return res.json().then(data => { if (data.message === "Database is offline") { throw new Error("Database offline"); } return res; }); } return res; }) .catch(err => { if (err.message === "Failed to fetch" || err.message === "Database offline") { window.location.href = '/service-unavailable'; } throw err; }); }
所有请求替换为该工具函数,实现统一跳转。
3. 配置服务不可用页面:确保前端路由存在/service-unavailable页面,展示数据库离线提示信息。
内容的提问来源于stack exchange,提问作者Patrik Bajzík
相关产品推荐
相关产品推荐

