Flutter调用Express登录API返回Token为Null问题求助
问题:Flutter调用Express登录API返回Token为Null,但Postman正常
我基于Express为Flutter应用开发登录功能,Postman调用登录API时能正常生成并返回Token,但在Flutter中调用同一API,返回的Token始终为Null。相关代码如下:
auth_service.dart 代码
// services/auth_service.dart import 'dart:convert'; import 'package:http/http.dart' as http; import 'package:manunited_trivia/models/User.dart'; import 'package:shared_preferences/shared_preferences.dart'; class AuthService { final String baseUrl = "http://192.168.1.6:8080/api/auth"; Future<String?> register(String email, String password) async { try { final response = await http.post( Uri.parse('$baseUrl/register'), headers: {'Content-Type': 'application/json'}, body: jsonEncode({'email': email, 'password': password}), ); if (response.statusCode == 200) { final data = jsonDecode(response.body); return data['token']; } else { return null; } } catch (e) { print('Error: $e'); return null; } } Future<String?> login(String email, String password) async { try { final response = await http.post( Uri.parse('$baseUrl/login'), headers: {'Content-Type': 'application/json'}, body: jsonEncode({'email': email, 'password': password}), ); if (response.statusCode == 200) { final data = jsonDecode(response.body); return data['token']; } else { return null; } } catch (e) { print('Error: $e'); return null; } } Future<User?> getUser(String token) async { try { final response = await http.get( Uri.parse('$baseUrl/user'), headers: { 'Content-Type': 'application/json', 'x-auth-token': token, }, ); if (response.statusCode == 200) { final data = jsonDecode(response.body); return User.fromJson(data); } else { return null; } } catch (e) { print('Error: $e'); return null; } } }
登录页面代码
import 'package:firebase_auth/firebase_auth.dart'; import 'package:flutter/material.dart'; import 'package:manunited_trivia/constants/ColorsToUse.dart'; import 'package:manunited_trivia/screens/RegisterScreen.dart'; import 'package:manunited_trivia/services/auth_service.dart'; import 'package:quickalert/models/quickalert_type.dart'; import 'package:quickalert/widgets/quickalert_dialog.dart'; import 'package:shared_preferences/shared_preferences.dart'; class LoginScreen extends StatefulWidget { const LoginScreen({super.key}); @override State<LoginScreen> createState() => _LoginScreenState(); } class _LoginScreenState extends State<LoginScreen> { bool obscure = true; final TextEditingController _emailController = TextEditingController(); final TextEditingController _passwordController = TextEditingController(); final AuthService authService = AuthService(); void _login(BuildContext context) async { String username = _emailController.text.trim(); String password = _passwordController.text.trim(); String? token = await authService.login(username, password); if (token != null) { // Store token in shared preferences for persistent login SharedPreferences prefs = await SharedPreferences.getInstance(); prefs.setString('token', token); // Navigate to home screen Navigator.pushReplacementNamed(context, '/homeScreen'); } else { print(token); // Handle login failure ScaffoldMessenger.of(context).showSnackBar( const SnackBar(content: Text('Login failed')), ); } } @override Widget build(BuildContext context) { return Scaffold( backgroundColor: Colors.white, // Background color body: Stack( children: <Widget>[ Image.asset( 'assets/images/header_bg.jpg', // Custom background image width: double.infinity, height: double.infinity, // opacity: const AlwaysStoppedAnimation(.85), fit: BoxFit.cover, ), Center( child: SingleChildScrollView( padding: const EdgeInsets.all(20.0), child: Column( mainAxisAlignment: MainAxisAlignment.center, crossAxisAlignment: CrossAxisAlignment.stretch, children: <Widget>[ Align( alignment: Alignment.bottomCenter, child: Text( 'Welcome to', style: TextStyle( fontFamily: "AldotheApache", color: Colors.white, fontSize: 24.0, fontWeight: FontWeight.bold, shadows: [ Shadow( blurRadius: 5.0, color: Colors.black.withOpacity(0.5), offset: Offset(2.0, 2.0), ), ], ), ), ), const SizedBox(height: 10.0), Text( 'The Theatre of Quiz', style: TextStyle( fontFamily: "AldotheApache", color: ColorsToUse().unitedRed, fontSize: 36.0, fontWeight: FontWeight.bold, letterSpacing: 2.0, shadows: [ Shadow( blurRadius: 5.0, color: Colors.black.withOpacity(0.5), offset: Offset(2.0, 2.0), ), ], ), textAlign: TextAlign.center, ), const SizedBox(height: 30.0), TextField( controller: _emailController, decoration: InputDecoration( filled: true, fillColor: Colors.white.withOpacity(0.9), hintText: 'Enter your email', prefixIcon: const Icon(Icons.email), border: OutlineInputBorder( borderRadius: BorderRadius.circular(10.0), ), ), ), const SizedBox(height: 20.0), TextField( controller: _passwordController, obscureText: obscure, decoration: InputDecoration( filled: true, fillColor: Colors.white.withOpacity(0.9), hintText: 'Enter your password', prefixIcon: const Icon(Icons.lock), suffixIcon: IconButton( onPressed: () { setState(() { obscure = !obscure; }); }, icon: const Icon( Icons.remove_red_eye, size: 20, )), border: OutlineInputBorder( borderRadius: BorderRadius.circular(10.0), ), ), ), const SizedBox(height: 20.0), ElevatedButton( onPressed: () { _login(context); }, style: ElevatedButton.styleFrom( backgroundColor: Colors.white, padding: const EdgeInsets.symmetric(vertical: 15.0), shape: RoundedRectangleBorder( borderRadius: BorderRadius.circular(10.0), ), shadowColor: Colors.black, elevation: 5, ), child: const Text( 'Login', style: TextStyle(fontSize: 18.0), ), ), const SizedBox( height: 20, ), ElevatedButton( onPressed: () { Navigator.of(context).push(MaterialPageRoute( builder: (context) => RegisterScreen())); }, style: ElevatedButton.styleFrom( backgroundColor: Colors.white, // Manchester United's primary color padding: const EdgeInsets.symmetric(vertical: 15.0), shape: RoundedRectangleBorder( borderRadius: BorderRadius.circular(10.0), ), shadowColor: Colors.black, elevation: 5, ), child: const Text( 'Register', style: TextStyle(fontSize: 18.0), ), ), ], ), ), ), ], ), ); } }
核心问题:String? token = await authService.login(username, password);返回Null,需要排查原因。
排查与解决步骤
1. 打印完整API响应,定位问题根源
修改login方法,添加响应状态码和响应体的打印,明确Flutter实际收到的内容:
Future<String?> login(String email, String password) async { try { final response = await http.post( Uri.parse('$baseUrl/login'), headers: {'Content-Type': 'application/json'}, body: jsonEncode({'email': email, 'password': password}), ); // 新增打印,查看真实响应 print('登录响应状态码: ${response.statusCode}'); print('登录响应内容: ${response.body}'); if (response.statusCode == 200) { final data = jsonDecode(response.body); return data['token']; } else { return null; } } catch (e) { print('请求异常: $e'); return null; } }
通过打印可以确认:
- 状态码是否为200?如果不是,说明后端返回了错误(比如401未授权)
- 响应体是否和Postman一致?字段名是否为
token(注意大小写)
2. 核对请求参数与后端要求
- 登录页面中,变量名
username实际存储的是邮箱地址,确认后端是否接受邮箱作为登录凭证,还是需要用户名 - 检查Postman中的请求体键名,是否和Flutter代码中的
email、password完全一致,比如后端是否要求username而非email
3. 解决跨域与网络连通问题
- 确认Flutter设备(模拟器/真机)和Express服务器在同一局域网,IP地址
192.168.1.6是否正确,服务器是否正常运行 - Express后端必须配置CORS中间件,否则Flutter请求会被拦截:
// Express后端添加CORS支持 const cors = require('cors'); app.use(cors());
4. 验证JSON解析与字段匹配
如果Postman返回的Token字段不是token(比如access_token、authToken),则data['token']会返回Null,需根据实际字段名调整代码。
5. 完善异常处理
当前catch块仅打印基础错误,可补充JSON解析异常的判断,比如:
catch (e) { if (e is FormatException) { print('JSON解析错误: $e'); } else { print('请求异常: $e'); } return null; }
内容的提问来源于stack exchange,提问作者Aayush Pandey
相关产品推荐
相关产品推荐

