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

React+Redux Toolkit调用Node.js认证API无数据返回问题排查

用户认证功能报错排查与修复

我正在实现用户认证功能,Node.js端的POST认证接口通过Postman测试可正常完成用户认证,但在React项目中使用Redux Toolkit实现登录功能时,提交表单出现400 Bad Request错误,API接收为空对象,数据未被正确传递。

Node.js认证接口

router.post('/', [
    check('email', 'Please include a valid email').isEmail(),
    check('password', 'Password is required').exists()
], 
async (req, res) => {
    const errors = validationResult(req);
    if(!errors.isEmpty()){
        return res.status(400).json({ errors:errors.array()}); //400 is for bad requests
    }

    const { email, password } = req.body;

    try{
        //See if user exists
        let user = await User.findOne({ email });

        if(!user){
            return res.status(400).json({ errors: [{ msg:'Invalid credentials' }] });
        }

        //Compare the input password, plain text, to the encrypted password.
        const isMatch = await bcrypt.compare(password, user.password);
        
        if(!isMatch){
            return res.status(400).json({ errors: [{ msg:'Invalid credentials' }] });
        }
    
        //Return jsonwebtoken -> this for users to be logged in right after registration
        const payload = {
            user:{
                id: user.id
            }
        }

        jwt.sign(
            payload,
            config.get('jwtSecret'),
            {expiresIn: 360000}, //change to 3600 for production
            (err, token) => {
                if(err) throw err;
                res.json({ token });
            }
        )

    }catch(err){
        console.error(err.message);
        res.status(500).send('Server Error');
    }
});

Token设置工具函数

import axios from 'axios'

const setAuthToken = token => {
    if(token){
        axios.defaults.headers.common['x-auth-token'] = token;
    } else {
        delete axios.defaults.headers.common['x-atuh-token'];
    }
}

export default setAuthToken

Redux Toolkit 用户切片

import { createSlice, createAsyncThunk } from '@reduxjs/toolkit'
import setAuthToken from '../../utils/setAuthToken';
import axios from 'axios';

const initialState = {
    token: localStorage.getItem('item'),
    isAuthenticated: false,
    loading: true,
    user: null,
    error: null
}

export const loadUser = createAsyncThunk(
  'user/loadUser',
  async ({ rejectWithValue }) => {
    if(localStorage.token){
      setAuthToken(localStorage.token);
    }

    try {
      const res = axios.get('http://localhost:3000/api/auth');
    } catch (error) {
      return rejectWithValue(error.response.data);
    }
  }
)

export const loginUser = createAsyncThunk('auth/login', async ({ email, password }, { dispatch, rejectWithValue }) => {
  const body = { email, password };

  try {
    const res = await axios.post('http://localhost:3000/api/auth', body);
    dispatch(loadUser());
    return res.data;
  } catch (err) {
    return rejectWithValue(err.response.data);
  }
});

export const userSlice = createSlice({
  name: 'user',
  initialState,
  reducers: {},
  extraReducers: {
    [loadUser.pending]: (state) => {
      state.loading = true;
      state.error = null;
    },
    [loadUser.fulfilled]: (state, action) => {
      state.isAuthenticated = true;
      state.user = action.payload;
      state.loading = false;
      state.error = false;
    },
    [loadUser.rejected]: (state, action) => {
      state.loading = false;
      state.isAuthenticated = false;
      state.error = action.payload;
    },
    [loginUser.pending]: (state) => {
      state.loading = true;
      state.error = null;
    },
    [loginUser.fulfilled]: (state, action) => {
      state.token = localStorage.setItem('token', action.payload.token);
      state.isAuthenticated = true;
      state.loading = false;
      state.user = action.payload;
    },
    [loginUser.rejected]: (state, action) => {
      state.isAuthenticated = false;
      state.loading = false;
      state.error = action.payload;
    }
  }
})

export const {  } = userSlice.actions
export default userSlice.reducer

React登录组件

import React, { useState } from 'react'
import { useDispatch } from 'react-redux';
import { loginUser } from '../reducers/auth/authSlice';

//Material UI
import {Grid, Container, Box, Button, FormControl, InputLabel, Input, FormHelperText } from '@mui/material'

function Login({ isAuthenticated }) {
  const dispatch = useDispatch();
  const [formData, setFormData] = useState({ 
    email: '',
    password: ''
  });

  const { email, password } = formData;

  const onChange = e => setFormData({
      ...formData, [e.target.name]:e.target.value
  });

  const onSubmit = async e => {
    e.preventDefault();
    dispatch(loginUser(email, password))
  }

  return (
    <Container>
        <Box p={4}>
            <Grid container>
                <form onSubmit={e => onSubmit(e)}>
                    <Grid item md={12}>
                        <FormControl>
                            <InputLabel htmlFor="email">Email</InputLabel>
                            <Input 
                            id='email' 
                            type='email' 
                            name='email'
                            value={email}
                            onChange={e => onChange(e)}
                            />
                            <FormHelperText>Insert your email</FormHelperText>
                        </FormControl>
                    </Grid>
                    <Grid item md={12}>    
                        <FormControl>
                            <InputLabel htmlFor="password">Password</InputLabel>
                            <Input 
                            id="password" 
                            type='password' 
                            name='password'
                            value={password}
                            onChange={e => onChange(e)}
                            />
                            <FormHelperText>Insert your password (must have at least 6 characters)</FormHelperText>
                        </FormControl>
                    </Grid>
                    <Grid item md={12}>    
                        <Button type='submit' variant="contained" color="primary">
                            Login
                        </Button>
                    </Grid>
                </form>
            </Grid>
        </Box>
    </Container>
  )
}

export default Login

核心问题与修复方案

  • 登录组件参数传递错误
    错误点:登录组件调用loginUser时传入了两个独立参数email和password,但loginUser异步thunk定义的是接收包含这两个字段的对象参数。
    修复:修改onSubmit中的dispatch调用:

    dispatch(loginUser({ email, password }))
    
  • setAuthToken函数拼写错误
    错误点:删除请求头时将x-auth-token误写为x-atuh-token,导致登出时无法正确移除认证头。
    修复:修正拼写:

    delete axios.defaults.headers.common['x-auth-token'];
    
  • loadUser异步thunk存在两处错误

    1. 参数解构错误:loadUser的第一个参数是payload,不需要解构rejectWithValue,应该从第二个参数(thunkAPI)中解构。
    2. 未返回响应数据:发送GET请求后没有返回数据,导致fulfilled阶段无法获取用户信息。
      修复后的loadUser:
    export const loadUser = createAsyncThunk(
      'user/loadUser',
      async (_, { rejectWithValue }) => {
        if(localStorage.getItem('token')){
          setAuthToken(localStorage.getItem('token'));
        }
    
        try {
          const res = await axios.get('http://localhost:3000/api/auth');
          return res.data;
        } catch (error) {
          return rejectWithValue(error.response.data);
        }
      }
    )
    
  • 初始状态localStorage键名错误
    错误点:初始状态中读取localStorage.getItem('item'),但实际存储token的键是token,导致初始化时无法读取已保存的token。
    修复:修改初始状态:

    const initialState = {
        token: localStorage.getItem('token'),
        // ... 其他状态
    }
    
  • 额外优化:loginUser.fulfilled中的token赋值错误
    错误点:localStorage.setItem返回undefined,直接赋值给state.token会导致token状态变为undefined。
    修复:先存储token,再赋值:

    [loginUser.fulfilled]: (state, action) => {
      localStorage.setItem('token', action.payload.token);
      state.token = action.payload.token;
      state.isAuthenticated = true;
      state.loading = false;
      state.user = action.payload;
    },
    

内容的提问来源于stack exchange,提问作者tukitooo1998

相关产品推荐
方舟 Agent Plan

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

最近更新时间:2026.07.25 19:02:54