React调用Express用户Profile接口时401 Unauthorized问题求助
我用React(运行于3000端口)和Express(运行于3001端口)开发应用,Express端采用JWT实现认证功能。注册、登录接口在Postman和React中均可正常工作,但React调用/users/profile获取用户数据时,控制台返回GET http://localhost:3001/users/profile 401 (Unauthorized)错误,无法渲染数据;而该接口在Postman中可正常返回JSON数据。
我确认React端已存储JWT cookie,但不清楚如何在React端完成认证流程。此前仅用Express结合hbs渲染页面,这是首次尝试React+JWT认证,已尝试调整React组件和Express路由,但仍未解决问题。以下是相关代码:
auth.js服务文件
const jwt = require('jsonwebtoken'); const models = require('../models'); const bcrypt = require('bcryptjs'); var authService = { signUser: function (user) { const token = jwt.sign({ Username: user.Username, UserId: user.UserId }, 'secretkey', { expiresIn: '1h' } ); return token; }, verifyUser: function (token) { try { let decoded = jwt.verify(token, 'secretkey'); return models.users.findByPk(decoded.UserId); } catch (err) { console.log(err); return null; } }, hashPassword: function (plainTextPassword) { let salt = bcrypt.genSaltSync(10); let hash = bcrypt.hashSync(plainTextPassword, salt); return hash; }, comparePasswords: function (plainTextPassword, hashedPassword) { return bcrypt.compareSync(plainTextPassword, hashedPassword); } } module.exports = authService;
注册路由
router.post('/signup', function (req, res, next) { models.users.findOrCreate({ where: { Username: req.body.username }, defaults: { FirstName: req.body.firstName, LastName: req.body.lastName, Email: req.body.email, Password: authService.hashPassword(req.body.password) } }) .spread(function (result, created) { if (created) { res.redirect("http://localhost:3000/login"); } else { res.send('This user already exist') } }); });
注册功能在Postman和React中均可正常使用。
登录路由
router.post('/login', function (req, res, next) { models.users.findOne({ where: { Username: req.body.username } }).then(user => { if (!user) { console.log('User not found') return res.status(401).json({ message: "Login Failed" }); } else { let passwordMatch = authService.comparePasswords(req.body.password, user.Password); if (passwordMatch) { let token = authService.signUser(user); res.cookie('jwt', token); res.redirect('http://localhost:3001/users/profile'); } else { console.log('Wrong Password'); } } }); });
登录功能在Postman和React中均可正常使用。
Profile路由
router.get('/profile', function (req, res, next) { let token = req.cookies.jwt; if (token) { authService.verifyUser(token).then(user => { if (user) { res.setHeader('Content-Type', 'application/json'); res.send(JSON.stringify(user)); } else { res.status(401); res.send('Invalid authentication token'); } }); } else { res.status(401); res.send('Invalid authentication token'); } });
该接口仅在Postman中正常返回数据,React调用时返回401错误。
React侧Profile组件
import React from 'react'; import axios from 'axios'; class UserProfile extends React.Component { constructor(props) { super(props); this.state = { profileData: [] } }; fetchProfileData = () => { var encodedURI = window.encodeURI(this.props.uri); return axios.get(encodedURI).then(response => { this.setState(() => { return { profileData: response.data }; }); }); }; componentDidMount() { this.fetchProfileData(); } render() { console.log(this.state.profileData); if (this.state.profileData.length === 0) { return <div>Failed to fetch data from server</div> } const profile = this.state.profileData.map(user => ( <div key={user.UserId}>Hello world</div> )); return <div>{profile}</div> } } export default UserProfile;
组件调用方式:
<UserProfile uri="http://localhost:3001/users/profile" />
调用后页面渲染“Failed to fetch data from server”,控制台输出401错误。
Express的app.js文件
var createError = require('http-errors'); var express = require('express'); var path = require('path'); var cookieParser = require('cookie-parser'); var logger = require('morgan'); var models = require('./models'); var cors = require('cors'); var indexRouter = require('./routes/index'); var usersRouter = require('./routes/users'); var app = express(); // view engine setup app.set('views', path.join(__dirname, 'views')); app.set('view engine', 'hbs'); app.use(function (req, res, next) { res.header('Access-Control-Allow-Origin', 'http://localhost:3000'); res.header('Access-Control-Allow-Headers', 'Origin, X-Requested-With, Content-Type, Accept'); next(); }); app.use(logger('dev')); app.use(express.json()); app.use(express.urlencoded({ extended: false })); app.use(cookieParser()); app.use(express.static(path.join(__dirname, 'public'))); app.use(cors()); app.use('/', indexRouter); app.use('/users', usersRouter); // catch 404 and forward to error handler app.use(function (req, res, next) { next(createError(404)); }); // error handler app.use(function (err, req, res, next) { // set locals, only providing error in development res.locals.message = err.message; res.locals.error = req.app.get('env') === 'development' ? err : {}; // render the error page res.status(err.status || 500); res.render('error'); }); models.sequelize.sync().then(function () { console.log("DB Synced Up"); }); module.exports = app;
问题原因及解决办法
1. 跨域请求未携带Cookie
React和Express属于跨域场景,默认axios不会自动携带Cookie到跨域接口,同时Express端CORS配置未允许凭证传递。
- React端修改:在axios请求中添加
withCredentials: true,确保请求携带Cookie:
fetchProfileData = () => { var encodedURI = window.encodeURI(this.props.uri); return axios.get(encodedURI, { withCredentials: true }).then(response => { this.setState(() => { return { profileData: response.data }; }); }); };
- Express端修改:删除手动设置的CORS Header,替换为带凭证允许的CORS配置:
// 删掉原手动设置Header的代码 // app.use(function (req, res, next) { // res.header('Access-Control-Allow-Origin', 'http://localhost:3000'); // res.header('Access-Control-Allow-Headers', 'Origin, X-Requested-With, Content-Type, Accept'); // next(); // }); // 替换为配置好的cors中间件 app.use(cors({ origin: 'http://localhost:3000', credentials: true }));
2. 登录路由重定向逻辑错误
当前登录成功后重定向到Express的/profile接口,而非由React前端发起请求。建议返回JSON结果,由React前端控制页面跳转:
// 登录路由修改 if (passwordMatch) { let token = authService.signUser(user); res.cookie('jwt', token, { httpOnly: true, sameSite: 'lax' }); // 增加安全配置 res.status(200).json({ message: 'Login success', redirectUrl: '/profile' }); }
React登录请求拿到响应后,通过路由跳转(如history.push)到自身的/profile页面,再发起数据请求。
3. React组件状态处理错误
接口返回单个用户对象,而非数组,但组件初始状态设为数组,导致length判断失效。修改状态初始化和渲染逻辑:
constructor(props) { super(props); this.state = { profileData: null // 初始化为null } }; render() { console.log(this.state.profileData); if (!this.state.profileData) { // 判断是否获取到数据 return <div>Loading...</div> } // 直接渲染单个用户数据 return <div>Hello {this.state.profileData.Username}</div> }
内容的提问来源于stack exchange,提问作者Nolboy

