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

React实现注册自动登录及手动登录生成JWT存Cookie

MERN栈聊天应用:注册自动登录与JWT Cookie处理实现方案

问题背景

基于MERN栈开发聊天应用,后端已实现注册/登录时将JWT令牌存入HttpOnly Cookie,且注册后自动登录。需在React前端完成以下需求:

  • 注册成功后自动登录(利用后端返回的Cookie)
  • 手动登录时触发后端生成新JWT并写入Cookie
  • 前端识别登录状态,实现路由保护

后端现有实现(authController.js)

后端已通过createSendToken函数处理JWT生成与Cookie设置,注册和登录接口均调用该函数完成令牌下发。

import jwt from 'jsonwebtoken'
import path from 'path'
import { fileURLToPath } from 'url'

import User from '../models/userModel.js'
import catchAsync from '../utils/catchAsync.js'
import AppError from '../utils/appError.js'

const __filename = fileURLToPath(import.meta.url)
const __dirname = path.dirname(__filename)

const signToken = id => {
    return jwt.sign({ id }, process.env.JWT_SECRET, {
        expiresIn: process.env.JWT_EXPIRES_IN
    })
}

const createSendToken = (user, statusCode, req, res) => {
    const token = signToken(user._id)

    const cookieOptions = {
        expires: new Date(Date.now() + process.env.JWT_COOKIE_EXPIRES_IN * 24 * 60 * 60 * 1000),
        httpOnly: true  // 防止XSS攻击,禁止前端JS访问Cookie
    }

    // 生产环境下仅通过HTTPS发送Cookie
    if (process.env.NODE_ENV === 'production') cookieOptions.secure = true

    // 发送Cookie
    res.cookie('jwt', token, cookieOptions)

    // 返回结果中隐藏密码
    user.password = undefined

    res.status(statusCode).json({
        status: 'success',
        token,
        data: {
            user
        }
    })
}

export const signup = catchAsync(async (req, res) => {
    let { profilePhoto } = req.body
    const randomImageNumber = Math.floor(Math.random() * 5) + 1
    profilePhoto = `img-${randomImageNumber}.jpg`
    const newUser = await User.create({ ...req.body, profilePhoto })

    createSendToken(newUser, 201, req, res)
})

export const login = catchAsync(async (req, res, next) => {
    const { userName, password } = req.body

    if (!userName || !password) {
        return next(new AppError('请提供用户名和密码', 400))
    }

    const user = await User.findOne({ userName }).select('+password')

    if (!user || !(await user.correctPassword(password, user.password))) {
        return next(new AppError('用户名或密码错误', 401))
    }

    createSendToken(user, 200, req, res)
})

前端实现步骤

1. 全局登录状态管理(AuthContext.js)

创建全局上下文管理用户登录状态,方便各组件获取登录信息:

import { createContext, useContext, useState, useEffect } from 'react';

const AuthContext = createContext();

export const AuthProvider = ({ children }) => {
    const [user, setUser] = useState(null);
    const [loading, setLoading] = useState(true);

    // 初始化时验证用户是否已登录(调用后端验证接口)
    useEffect(() => {
        const checkAuth = async () => {
            try {
                const res = await fetch('/api/v1/auth/me', {
                    credentials: 'include' // 携带Cookie发送请求
                });
                if (res.ok) {
                    const data = await res.json();
                    setUser(data.data.user);
                }
            } catch (err) {
                console.log('未登录或令牌失效');
            } finally {
                setLoading(false);
            }
        };

        checkAuth();
    }, []);

    // 登录成功后更新用户状态
    const login = (userData) => {
        setUser(userData);
    };

    // 登出时清除用户状态(后端需清除Cookie)
    const logout = async () => {
        try {
            await fetch('/api/v1/auth/logout', { credentials: 'include' });
            setUser(null);
        } catch (err) {
            console.error('登出失败');
        }
    };

    return (
        <AuthContext.Provider value={{ user, login, logout, loading }}>
            {children}
        </AuthContext.Provider>
    );
};

export const useAuth = () => useContext(AuthContext);

2. 配置全局上下文(main.jsx)

在入口文件中包裹AuthProvider:

import React from "react";
import ReactDOM from "react-dom/client";
import App from "./App.jsx";
import "./index.css";
import { BrowserRouter } from "react-router-dom";
import { AuthProvider } from "./context/AuthContext"; // 引入上下文

ReactDOM.createRoot(document.getElementById("root")).render(
  <React.StrictMode>
    <BrowserRouter>
      <AuthProvider>
        <App />
      </AuthProvider>
    </BrowserRouter>
  </React.StrictMode>
);

3. 完善注册逻辑(useSignup.js)

注册成功后跳转首页,并更新全局用户状态:

import { useState } from "react";
import toast from "react-hot-toast";
import { useNavigate } from "react-router-dom";
import { useAuth } from "../context/AuthContext";

const useSignup = () => {
    const [loading, setLoading] = useState(false);
    const navigate = useNavigate();
    const { login } = useAuth();

    const signup = async ({ name, userName, email, password, passwordConfirm, gender }) => {
        const success = handleInputErrors({ name, userName, email, password, passwordConfirm, gender });
        if (!success) return;

        setLoading(true)
        try {
            const res = await fetch("/api/v1/auth/signup", {
                method: "POST",
                headers: { "Content-Type": "application/json" },
                body: JSON.stringify({ name, userName, email, password, passwordConfirm, gender }),
                credentials: 'include' // 确保携带Cookie
            });

            const data = await res.json();
            if (data.status !== 'success') {
                throw new Error(data.message || '注册失败')
            }

            // 更新全局用户状态
            login(data.data.user);
            // 跳转首页
            navigate('/');
            toast.success('注册成功');

        } catch (error) {
            toast.error(error.message)
        } finally {
            setLoading(false);
        }
    };

    return { loading, signup };
};

export default useSignup;

function handleInputErrors({ name, userName, email, password, passwordConfirm, gender }) {
    if (!name || !userName || !email || !password || !passwordConfirm || !gender) {
        toast.error("请填写所有字段");
        return false;
    }
    if (password !== passwordConfirm) {
        toast.error("两次输入的密码不一致");
        return false;
    }
    return true;
}

4. 实现登录功能(useLogin.js)

创建登录钩子,处理登录请求并更新状态:

import { useState } from "react";
import toast from "react-hot-toast";
import { useNavigate } from "react-router-dom";
import { useAuth } from "../context/AuthContext";

const useLogin = () => {
    const [loading, setLoading] = useState(false);
    const navigate = useNavigate();
    const { login } = useAuth();

    const loginUser = async ({ userName, password }) => {
        if (!userName || !password) {
            toast.error("请填写用户名和密码");
            return;
        }

        setLoading(true);
        try {
            const res = await fetch("/api/v1/auth/login", {
                method: "POST",
                headers: { "Content-Type": "application/json" },
                body: JSON.stringify({ userName, password }),
                credentials: 'include'
            });

            const data = await res.json();
            if (data.status !== 'success') {
                throw new Error(data.message || '登录失败');
            }

            login(data.data.user);
            navigate('/');
            toast.success('登录成功');

        } catch (error) {
            toast.error(error.message);
        } finally {
            setLoading(false);
        }
    };

    return { loading, loginUser };
};

export default useLogin;

5. 登录页面(Login.jsx)

实现登录表单:

import { Link } from "react-router-dom";
import { useState } from "react";
import useLogin from "../../hooks/useLogin";

const Login = () => {
    const [inputs, setInputs] = useState({
        userName: "",
        password: "",
    });
    const { loading, loginUser } = useLogin();

    const handleSubmit = async (e) => {
        e.preventDefault();
        await loginUser(inputs);
    };

    return (
        <div className="flex flex-col items-center justify-center min-w-96 mx-auto">
            <div className="h-full w-full bg-white-400 rounded-2xl bg-clip-padding backdrop-filter backdrop-blur-xl bg-opacity-30 border border-gray-100 p-5">
                <h1 className="text-3xl font-semibold text-center text-gray-200">
                    登录 <span className="text-blue-500">ChatApp</span>
                </h1>

                <form onSubmit={handleSubmit}>
                    <div>
                        <label className="label pb-0">
                            <span className="text-base text-gray-50 label-text">
                                用户名
                            </span>
                        </label>
                        <input
                            type="text"
                            placeholder="输入用户名"
                            className="input input-bordered input-info w-full h-8"
                            value={inputs.userName}
                            onChange={(e) =>
                                setInputs({ ...inputs, userName: e.target.value })
                            }
                            disabled={loading}
                        />
                    </div>

                    <div className="mt-4">
                        <label className="label pb-0">
                            <span className="text-base text-gray-50 label-text">
                                密码
                            </span>
                        </label>
                        <input
                            type="password"
                            placeholder="输入密码"
                            className="input input-bordered input-info w-full h-8"
                            value={inputs.password}
                            onChange={(e) =>
                                setInputs({ ...inputs, password: e.target.value })
                            }
                            disabled={loading}
                        />
                    </div>

                    <div className="text-center mt-2">
                        <Link to="/signup" className="text-sm text-gray-50 hover:underline hover:text-blue-600 inline-block">
                            还没有账号?立即注册
                        </Link>
                    </div>

                    <div className="text-center py-3">
                        <button 
                            className="btn min-h-0 h-8 btn-outline btn-info"
                            disabled={loading}
                        >           
                            {loading ? '登录中...' : '登录'}
                        </button>
                    </div>
                </form>
            </div>
        </div>
    );
};

export default Login;

6. 路由保护与App.jsx优化

修改App.jsx,实现未登录时重定向到登录页:

import { Navigate, Route, Routes } from "react-router-dom";
import "./App.css";
import Home from "./pages/home/Home";
import Login from "./pages/login/Login";
import SignUp from "./pages/signup/SignUp";
import { Toaster } from "react-hot-toast";
import { useAuth } from "./context/AuthContext";

function App() {
    const { user, loading } = useAuth();

    // 加载状态时显示加载提示
    if (loading) {
        return (
            <div className="h-screen flex items-center justify-center">
                <div className="text-xl text-gray-200">加载中...</div>
            </div>
        );
    }

    // 路由保护组件
    const ProtectedRoute = ({ children }) => {
        if (!user) {
            return <Navigate to="/login" replace />;
        }
        return children;
    };

    return (
        <div className="p-4 h-screen flex items-center justify-center overflow-auto">
            <Routes>
                <Route 
                    path="/" 
                    element={
                        <ProtectedRoute>
                            <Home />
                        </ProtectedRoute>
                    } 
                />
                <Route path="/login" element={user ? <Navigate to="/" replace /> : <Login />} />
                <Route path="/signup" element={user ? <Navigate to="/" replace /> : <SignUp />} />
            </Routes>
            <Toaster />
        </div>
    );
}

export default App;

7. 注册页面优化(SignUp.jsx)

添加按钮加载状态禁用:

import { Link } from "react-router-dom";
import GenderCheckbox from "./GenderCheckbox";
import { useState } from "react";
import useSignup from "../../hooks/useSignup";

const SignUp = () => {
  const [inputs, setInputs] = useState({
    name: "",
    userName: "",
    email: "",
    password: "",
    passwordConfirm: "",
    gender: "",
  });

  const { loading, signup } = useSignup();

  const handleCheckboxChange = (gender) => {
    setInputs({ ...inputs, gender });
  };

  const handleSubmit = async (e) => {
    e.preventDefault();
    await signup(inputs);
  };

  return (
    <div className="flex flex-col items-center justify-center min-w-96 mx-auto">
      <div className="h-full w-full bg-white-400 rounded-2xl bg-clip-padding backdrop-filter backdrop-blur-xl bg-opacity-30 border border-gray-100 p-5">
        <h1 className="text-3xl font-semibold text-center text-gray-200">
          注册 <span className="text-blue-500"> ChatApp</span>
        </h1>

        <form onSubmit={handleSubmit}>
          {/* 姓名输入框 */}
          <div>
            <label className="label pb-0">
              <span className="text-base text-gray-50 label-text">
                姓名
              </span>
            </label>
            <input
              type="text"
              placeholder="输入姓名"
              className="input input-bordered input-info w-full h-8"
              value={inputs.name}
              onChange={(e) =>
                setInputs({ ...inputs, name: e.target.value })
              }
              disabled={loading}
            />
          </div>

          <div className="mt-4">
            <label className="label pb-0">
              <span className="text-base text-gray-50 label-text">
                用户名
              </span>
            </label>
            <input
              type="text"
              placeholder="输入用户名"
              className="input input-bordered input-info w-full h-8"
              value={inputs.userName}
              onChange={(e) =>
                setInputs({ ...inputs, userName: e.target.value })
              }
              disabled={loading}
            />
          </div>

          <div className="mt-4">
            <label className="label pb-0">
              <span className="text-base text-gray-50 label-text">
                邮箱
              </span>
            </label>
            <input
              type="email"
              placeholder="输入邮箱"
              className="input input-bordered input-info w-full h-8"
              value={inputs.email}
              onChange={(e) =>
                setInputs({ ...inputs, email: e.target.value })
              }
              disabled={loading}
            />
          </div>

          <div className="mt-4">
            <label className="label pb-0">
              <span className="text-base text-gray-50 label-text">
                密码
              </span>
            </label>
            <input
              type="password"
              placeholder="输入密码"
              className="input input-bordered input-info w-full h-8"
              value={inputs.password}
              onChange={(e) =>
                setInputs({ ...inputs, password: e.target.value })
              }
              disabled={loading}
            />
          </div>

          <div className="mt-4">
            <label className="label pb-0">
              <span className="text-base text-gray-50 label-text">
                确认密码
              </span>
            </label>
            <input
              type="password"
              placeholder="确认密码"
              className="input input-bordered input-info w-full h-8"
              value={inputs.passwordConfirm}
              onChange={(e) =>
                setInputs({ ...inputs, passwordConfirm: e.target.value })
              }
              disabled={loading}
            />
          </div>

          <GenderCheckbox
            onCheckboxChange={handleCheckboxChange}
相关产品推荐
方舟 Agent Plan

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

最近更新时间:2026.06.22 02:24:33