邮箱验证链接API返回400错误,Postman正常前端异常求助
问题描述
后端通过Postman测试邮箱验证API运行正常,但前端调用时返回400 (Bad Request)错误。后端服务运行在5111端口,前端服务在3000端口且已配置代理,相关代码如下:
后端代码(/backend/routes/userRoutes.js)
userRouter.post( '/signup', expressAsyncHandler(async (req, res) => { try { const newUser = new User({ name: req.body.name, email: req.body.email, password: bcrypt.hashSync(req.body.password), }); const user = await newUser.save(); const verifyToken = await new VerificationToken({ userId: user._id, token: crypto.randomBytes(32).toString('hex'), }).save(); const url = `${process.env.BASE_URL}/${verifyToken._id}/verify/${verifyToken.token}`; await mailTransport(user.email, 'Verify Email', url); res.status(201).send({ message: 'A verification link has been sent to your account, please verify ', }); } catch (error) { res.status(500).send({ message: 'Internal Server Error' }); } }) ); userRouter.get('/:id/verify/:token', async (req, res) => { try { const user = await User.findOne({ _id: req.params.id }); if (!user) return res.status(400).send({ message: 'Invalid Link' }); const verificationToken = await VerificationToken.findOne({ userId: user._id, token: req.params.token, }); if (!verificationToken) return res.status(400).send({ message: 'Invalid link' }); await User.updateOne({ verified: true }); res.status(200).send({ _id: user._id, name: user.name, email: user.email, isAdmin: user.isAdmin, verified: user.verified, token: generateToken(user), message: 'Email verified successfully', }); await verificationToken.remove(); // res.status(200).send({ message: 'Email verified successfully' }); } catch (error) { res.status(500).send({ message: 'Internal Server Error' }); console.log(error); } });
前端注册页面(/frontend/src/screens/SignupScreen.js)
import React, { useContext, useState, useEffect } from 'react'; import Container from 'react-bootstrap/Container'; import Form from 'react-bootstrap/Form'; import Button from 'react-bootstrap/Button'; import { Helmet } from 'react-helmet-async'; import { Link, useLocation, useNavigate } from 'react-router-dom'; import axios from 'axios'; import { Store } from '../Store'; import { toast } from 'react-toastify'; import { getError} from '../utils'; export default function SignupScreen() { const navigate = useNavigate(); const { search } = useLocation(); const redirectInUrl = new URLSearchParams(search).get('redirect'); const redirect = redirectInUrl ? redirectInUrl : '/signup'; const [name, setName] = useState(''); const [email, setEmail] = useState(''); const [password, setPassword] = useState(''); const [confirmPassword, setConfirmPassword] = useState(''); const [error, setError] = useState(''); const [msg, setMsg] = useState(''); const { state } = useContext(Store); const { userInfo } = state; const submitHandler = async (e) => { e.preventDefault(); if (password !== confirmPassword) { toast.error('Passwords do not match'); return; } try { const { data } = await axios.post(`/api/users/signup`, { name, email, password, confirmPassword, }); setMsg(data.message); setError(data.error); } catch (err) { toast.error(getError(err)); } }; useEffect(() => { if (userInfo) { navigate(redirect); } }, [navigate, redirect, userInfo]); return ( <Container className="small-container"> <Helmet> <title>Sign Up</title> </Helmet> <h1 className="my-3">Sign Up</h1> <Form onSubmit={submitHandler}> <Form.Group className="mb-3" controlId="name"> <Form.Label>Name</Form.Label> <Form.Control type="name" onChange={(e) => setName(e.target.value)} required /> </Form.Group> <Form.Group className="mb-3" controlId="email"> <Form.Label>Email</Form.Label> <Form.Control type="email" onChange={(e) => setEmail(e.target.value)} required /> </Form.Group> <Form.Group className="mb-3" onChange={(e) => setPassword(e.target.value)} controlId="password" > <Form.Label>Password</Form.Label> <Form.Control type="password" required /> </Form.Group> <Form.Group className="mb-3" controlId="confirmPassword"> <Form.Label>Confirm Password</Form.Label> <Form.Control type="password" onChange={(e) => setConfirmPassword(e.target.value)} required /> </Form.Group> {msg && <div className="mb-3">{msg}</div>} {error && <div className="mb-3">{error}</div>} <div className="mb-3"> <Button type="submit">Sign Up</Button> </div> <div className="mb-3"> New customer? <Link to={`/signin?redirect=${redirect}`}>Sign In</Link> </div> </Form> </Container> ); }
前端邮箱验证页面(/frontend/src/screens/VerifyEmailScreen.js)
import React, { useContext, useState, useEffect } from 'react'; import Container from 'react-bootstrap/Container'; import Button from 'react-bootstrap/Button'; import { Helmet } from 'react-helmet-async'; import { Link, useLocation, useNavigate, useParams } from 'react-router-dom'; import axios from 'axios'; import { Store } from '../Store'; import { toast } from 'react-toastify'; import { getError, API_URL } from '../utils'; axios.defaults.withCredentials = true; export default function VerifyEmailScreen() { const navigate = useNavigate(); const { search } = useLocation(); const params = useParams(); const redirectInUrl = new URLSearchParams(search).get('redirect'); const redirect = redirectInUrl ? redirectInUrl : '/:id/verify/:token'; const [validUrl, setValidUrl] = useState(''); const { state, dispatch: ctxDispatch } = useContext(Store); const { userInfo } = state; const [msg, setMsg] = useState(''); useEffect(() => { const verifyEmailUrl = async () => { try { const url = await axios.get( `/api/users/${params.id}/verify/${params.token}` ); console.log(url); ctxDispatch({ type: 'USER_SIGNIN', payload: url }); localStorage.setItem('userInfo', JSON.stringify(url)); setMsg(url.message); setValidUrl(true); } catch (error) { getError(error); setValidUrl(false); } }; verifyEmailUrl(); }); return ( <Container className="small-container"> <Helmet> <title>Sign Up</title> </Helmet> {validUrl ? ( <div className="d-flex justify-content-center flex-column text-center"> <h1 className="mb-5">Email Verified successfully</h1> {msg && <div className="mb-3">{msg}. Please Login below</div>} <Link to="/login"> <Button>Login</Button> </Link> </div> ) : ( <h1>404 Not Found</h1> )} </Container> ); }
前端路由配置(App.js)
<Route path="/:id/verify/:token" element={<VerifyEmailScreen />} />
问题排查与修复方案
核心错误点
- 验证链接参数不匹配:后端生成验证链接时用了
verifyToken._id,但验证接口却用该ID查询User表,导致查不到用户直接返回400。 - 用户更新逻辑错误:
User.updateOne({ verified: true })没有指定筛选条件,会把所有用户的verified字段改为true,且返回的用户数据中verified仍为false。 - 前端useEffect无限循环:
VerifyEmailScreen的useEffect无依赖数组,会反复调用验证API。 - 前端响应数据处理错误:直接把axios返回的整个响应对象存入本地存储,而非后端返回的用户数据。
修复步骤
1. 修正后端验证逻辑
修改注册接口的链接生成
把验证链接中的ID改为用户ID:
// 原代码 const url = `${process.env.BASE_URL}/${verifyToken._id}/verify/${verifyToken.token}`; // 修改后 const url = `${process.env.BASE_URL}/${user._id}/verify/${verifyToken.token}`;
修改验证接口的更新逻辑
确保只更新当前用户,并同步用户对象的verified字段:
userRouter.get('/:id/verify/:token', async (req, res) => { try { const user = await User.findOne({ _id: req.params.id }); if (!user) return res.status(400).send({ message: 'Invalid Link' }); const verificationToken = await VerificationToken.findOne({ userId: user._id, token: req.params.token, }); if (!verificationToken) return res.status(400).send({ message: 'Invalid link' }); // 只更新当前用户的verified字段 await User.updateOne({ _id: user._id }, { verified: true }); // 更新user对象的字段,确保返回数据正确 user.verified = true; res.status(200).send({ _id: user._id, name: user.name, email: user.email, isAdmin: user.isAdmin, verified: user.verified, token: generateToken(user), message: 'Email verified successfully', }); await verificationToken.remove(); } catch (error) { res.status(500).send({ message: 'Internal Server Error' }); console.log(error); } });
2. 修正前端验证页面逻辑
修复useEffect无限循环
添加依赖数组,确保验证逻辑只执行一次:
useEffect(() => { const verifyEmailUrl = async () => { try { const response = await axios.get( `/api/users/${params.id}/verify/${params.token}` ); const userData = response.data; ctxDispatch({ type: 'USER_SIGNIN', payload: userData }); localStorage.setItem('userInfo', JSON.stringify(userData)); setMsg(userData.message); setValidUrl(true); } catch (error) { toast.error(getError(error)); setValidUrl(false); } }; verifyEmailUrl(); }, [params.id, params.token, ctxDispatch]); // 添加依赖数组
修正redirect默认值
const redirect = redirectInUrl ? redirectInUrl : '/';
修正响应数据处理
从response.data中获取后端返回的用户数据,而非整个axios响应对象。
3. 前端注册接口优化
后端不需要confirmPassword参数,可从请求体中移除:
const { data } = await axios.post(`/api/users/signup`, { name, email, password, });
内容的提问来源于stack exchange,提问作者Chidimma Nworah
相关产品推荐
相关产品推荐

