You need to enable JavaScript to run this app.
优惠活动
大模型
产品
解决方案
定价
更多

邮箱验证链接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 />}
/>

问题排查与修复方案

核心错误点

  1. 验证链接参数不匹配:后端生成验证链接时用了verifyToken._id,但验证接口却用该ID查询User表,导致查不到用户直接返回400。
  2. 用户更新逻辑错误:User.updateOne({ verified: true })没有指定筛选条件,会把所有用户的verified字段改为true,且返回的用户数据中verified仍为false。
  3. 前端useEffect无限循环:VerifyEmailScreen的useEffect无依赖数组,会反复调用验证API。
  4. 前端响应数据处理错误:直接把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

相关产品推荐
方舟 Agent Plan

超全模态模型 × Harness 升级,最新支持 Deepseek-V4.1-Flash、GLM-5.3 系列、Doubao-Seedream-5.0-pro、Kimi-K3 (部分), 限时 9.9 元起

最近更新时间:2026.08.02 10:01:44