Spring Boot集成Flutter登录时遇302重定向错误求助
Spring Boot与Flutter集成登录时302重定向错误排查与解决
问题背景
在Spring Boot与Flutter集成开发的登录流程中,触发302重定向错误,导致登录失败无法继续。
问题详情
- 现象:登录请求返回302重定向,流程被阻断,无法完成认证
- 预期:登录成功完成用户认证,返回用户信息并跳转至对应页面
- 实际:未完成登录,收到302响应,流程终止
请求协助
需要解决302重定向错误的指导,包括Spring Boot/Flutter端的排查步骤、配置修改建议。
附相关代码
Spring Boot SecurityConfig
package com.example.//.config; import com.example.//.Service.UserDetailService; import com.fasterxml.jackson.databind.ObjectMapper; import lombok.RequiredArgsConstructor; import org.springframework.context.annotation.Bean; import org.springframework.context.annotation.Configuration; import org.springframework.security.authentication.AuthenticationManager; import org.springframework.security.authentication.ProviderManager; import org.springframework.security.authentication.dao.DaoAuthenticationProvider; import org.springframework.security.config.annotation.web.builders.HttpSecurity; import org.springframework.security.config.annotation.web.configuration.EnableWebSecurity; import org.springframework.security.config.annotation.web.configurers.AbstractHttpConfigurer; import org.springframework.security.crypto.bcrypt.BCryptPasswordEncoder; import org.springframework.security.web.SecurityFilterChain; import org.springframework.security.web.authentication.AuthenticationSuccessHandler; @Configuration @EnableWebSecurity @RequiredArgsConstructor public class SecurityConfig { private final UserDetailService userDetailService; private final ObjectMapper objectMapper; @Bean public SecurityFilterChain filterChain(HttpSecurity http) throws Exception { http .cors(cors -> cors .configurationSource(CorsConfig.corsConfigurationSource()) ) .csrf(AbstractHttpConfigurer::disable) .authorizeHttpRequests(auth -> auth .requestMatchers("/user/create", "/user/login", "/user/mypage").permitAll() .requestMatchers("/swagger-ui/**", "/v3/api-docs/**").permitAll() ) .formLogin(form -> form .loginPage("/user/login").permitAll() .usernameParameter("nickname") .passwordParameter("password") .successHandler(authenticationSuccessHandler()) ) .logout(logout -> logout .logoutUrl("/user/logout") .invalidateHttpSession(true) ); return http.build(); } @Bean public BCryptPasswordEncoder passwordEncoder() { return new BCryptPasswordEncoder(); } @Bean public AuthenticationManager authenticationManager() { DaoAuthenticationProvider daoAuthenticationProvider = new DaoAuthenticationProvider(); daoAuthenticationProvider.setUserDetailsService(userDetailService); daoAuthenticationProvider.setPasswordEncoder(passwordEncoder()); return new ProviderManager(daoAuthenticationProvider); } @Bean public AuthenticationSuccessHandler authenticationSuccessHandler() { return (request, response, auth) -> response.sendRedirect("/friends/page"); } }
Spring Boot CorsConfig
package com.example.blogback.config; import lombok.AccessLevel; import lombok.NoArgsConstructor; import org.springframework.http.HttpHeaders; import org.springframework.web.cors.CorsConfiguration; import org.springframework.web.cors.CorsConfigurationSource; import org.springframework.web.cors.UrlBasedCorsConfigurationSource; import java.util.ArrayList; import java.util.Collections; import java.util.List; @NoArgsConstructor(access = AccessLevel.PRIVATE) public class CorsConfig { public static CorsConfigurationSource corsConfigurationSource() { CorsConfiguration configuration = new CorsConfiguration(); ArrayList<String> allowedOriginPatterns = new ArrayList<>(); allowedOriginPatterns.add("http://localhost:"); allowedOriginPatterns.add("http://localhost:*"); configuration.setAllowedOrigins(allowedOriginPatterns); ArrayList<String> allowedHttpMethods = new ArrayList<>(); allowedHttpMethods.add("GET"); allowedHttpMethods.add("POST"); allowedHttpMethods.add("PUT"); allowedHttpMethods.add("DELETE"); allowedHttpMethods.add("OPTIONS"); configuration.setAllowedMethods(allowedHttpMethods); configuration.setAllowedHeaders(Collections.singletonList("*")); configuration.setAllowedHeaders(List.of(HttpHeaders.AUTHORIZATION, HttpHeaders.CONTENT_TYPE)); configuration.setAllowCredentials(true); UrlBasedCorsConfigurationSource source = new UrlBasedCorsConfigurationSource(); source.registerCorsConfiguration("/**", configuration); return source; } }
Flutter Login页面
import 'package:flutter/material.dart'; import 'package:flutter_application_blog/page/mainPage.dart'; import 'package:flutter_application_blog/page/mypage.dart'; import 'package:flutter_application_blog/page/create_account.dart'; import 'package:http/http.dart' as http; import 'dart:convert'; import 'package:flutter_secure_storage/flutter_secure_storage.dart'; import 'package:dio/dio.dart'; class Login extends StatefulWidget { const Login({super.key}); @override _LoginState createState() => _LoginState(); } class _LoginState extends State<Login> { final TextEditingController _nicknameController = TextEditingController(); final TextEditingController _passwordController = TextEditingController(); final _formKey = GlobalKey<FormState>(); static final storage = FlutterSecureStorage(); dynamic userInfo; @override void initState() { super.initState(); _checkLoginStatus(); } Future<void> _checkLoginStatus() async { userInfo = await storage.read(key: 'login'); if (userInfo != null) { Navigator.pushNamed(context, '/main'); } } Future<bool> loginAction(String nickname, String password) async { try { var dio = Dio(BaseOptions( baseUrl: "http://localhost:8081", headers: {"Accept": "application/json"}, )); var param = {'nickname': nickname, 'password': password}; Response response = await dio.post('/user/login', data: param); if (response.statusCode == 200) { final userId = response.data['user_id']; await storage.write(key: 'login', value: jsonEncode(userId)); print('Login successful!'); return true; } else { print('Login failed: ${response.statusCode}'); return false; } } catch (e) { print('Error occurred: $e'); return false; } } @override Widget build(BuildContext context) { return Scaffold( body: _buildBody(), ); } Widget _buildBody() { return Container( color: const Color(0xffFEFCEB), child: SingleChildScrollView( child: Column( children: [ Container( width: 150, height: 150, margin: const EdgeInsets.only(top: 45, bottom: 20), child: Image.asset("logo.png"), ), Container( width: 330, height: 250, decoration: BoxDecoration( borderRadius: BorderRadius.circular(10), color: Colors.white, border: Border.all(width: 0.5, color: const Color(0xff1A495D)), ), child: Form( key: _formKey, child: Column( children: [ _buildTextField( controller: _nicknameController, label: 'Nickname', ), _buildTextField( controller: _passwordController, label: 'Password', obscureText: true, ), ], ), ), ), const SizedBox(height: 20), _buildLoginButton(), const SizedBox(height: 10), _buildSignUpPrompt(), ], ), ), ); } Widget _buildLoginButton() { return Container( width: 100, height: 50, margin: const EdgeInsets.all(20), decoration: BoxDecoration( borderRadius: BorderRadius.circular(40), color: const Color(0xffB5DDDB), ), child: Center( child: GestureDetector( onTap: () async { if (_formKey.currentState!.validate()) { bool success = await loginAction( _nicknameController.text, _passwordController.text); if (success) { print('로그인 성공'); Navigator.of(context).push( MaterialPageRoute( builder: (context) => MainPage()), // CreateAccount로 수정 ); } else { print('로그인 실패'); } } }, child: const Text( 'Login', style: TextStyle( color: Colors.black, fontSize: 18, fontWeight: FontWeight.bold, ), ), ), ), ); } Widget _buildSignUpPrompt() { return Column( children: [ const Text( "계정이 없으신가요?", style: TextStyle(color: Colors.grey, fontSize: 15), ), TextButton( onPressed: () { Navigator.of(context).push( MaterialPageRoute( builder: (context) => create_account()), // CreateAccount로 수정 ); }, child: const Text( "회원가입", style: TextStyle( decoration: TextDecoration.underline, color: Colors.black, ), ), ), ], ); } Widget _buildTextField({ required TextEditingController controller, required String label, bool obscureText = false, }) { return Padding( padding: const EdgeInsets.only(top: 20), child: Column( crossAxisAlignment: CrossAxisAlignment.start, children: [ Text( label, style: const TextStyle(fontSize: 22), textAlign: TextAlign.left, ), Container( width: 250, margin: const EdgeInsets.only(top: 10), decoration: BoxDecoration( border: Border.all(width: 2, color: const Color(0xffB5DDDD)), borderRadius: BorderRadius.circular(10), ), child: TextField( controller: controller, obscureText: obscureText, decoration: InputDecoration( border: InputBorder.none, hintText: " ", contentPadding: const EdgeInsets.only(left: 10, top: 12), ), ), ), ], ), ); } }
问题根源分析
302错误的核心原因是Spring Security的formLogin组件与前后端分离场景不兼容:
- formLogin是为传统服务端渲染页面设计的,登录成功后默认触发页面重定向(你的代码中
successHandler明确调用了sendRedirect("/friends/page")) - Flutter作为前端应用,需要JSON格式的登录结果而非页面跳转,302响应会被Dio默认拦截,导致前端无法正确获取登录状态
Spring Boot端修改方案
1. 移除formLogin,改用自定义API认证
修改SecurityConfig,删除formLogin相关配置,避免重定向机制:
@Configuration @EnableWebSecurity @RequiredArgsConstructor public class SecurityConfig { private final UserDetailService userDetailService; private final ObjectMapper objectMapper; @Bean public SecurityFilterChain filterChain(HttpSecurity http) throws Exception { http .cors(cors -> cors.configurationSource(CorsConfig.corsConfigurationSource())) .csrf(AbstractHttpConfigurer::disable) .authorizeHttpRequests(auth -> auth .requestMatchers("/user/create", "/user/login", "/user/mypage").permitAll() .requestMatchers("/swagger-ui/**", "/v3/api-docs/**").permitAll() .anyRequest().authenticated() ) .logout(logout -> logout .logoutUrl("/user/logout") .invalidateHttpSession(true) ); return http.build(); } @Bean public BCryptPasswordEncoder passwordEncoder() { return new BCryptPasswordEncoder(); } @Bean public AuthenticationManager authenticationManager() { DaoAuthenticationProvider daoAuthenticationProvider = new DaoAuthenticationProvider(); daoAuthenticationProvider.setUserDetailsService(userDetailService); daoAuthenticationProvider.setPasswordEncoder(passwordEncoder()); return new ProviderManager(daoAuthenticationProvider); } }
2. 编写自定义登录接口
创建UserController,实现登录逻辑并返回JSON结果:
@RestController @RequestMapping("/user") @RequiredArgsConstructor public class UserController { private final AuthenticationManager authenticationManager; @PostMapping("/login") public ResponseEntity<?> login(@RequestBody LoginRequest request) { try { Authentication authentication = authenticationManager.authenticate( new UsernamePasswordAuthenticationToken(request.getNickname(), request.getPassword()) ); SecurityContextHolder.getContext().setAuthentication(authentication); UserDetails userDetails = (UserDetails) authentication.getPrincipal(); return ResponseEntity.ok(Map.of("user_id", userDetails.getUsername())); } catch (AuthenticationException e) { return ResponseEntity.status(HttpStatus.UNAUTHORIZED).body("用户名或密码错误"); } } public static class LoginRequest { private String nickname; private String password; // Getters and Setters public String getNickname() { return nickname; } public void setNickname(String nickname) { this.nickname = nickname; } public String getPassword() { return password; } public void setPassword(String password) { this.password = password; } } }
3. 修复CorsConfig的重复配置问题
移除重复的allowedHeaders设置,确保CORS规则正确:
@NoArgsConstructor(access = AccessLevel.PRIVATE) public class CorsConfig { public static CorsConfigurationSource corsConfigurationSource() { CorsConfiguration configuration = new CorsConfiguration(); configuration.setAllowedOriginPatterns(List.of("http://localhost:*")); configuration.setAllowedMethods(List.of("GET", "POST", "PUT", "DELETE", "OPTIONS")); configuration.setAllowedHeaders(List.of("*")); configuration.setAllowCredentials(true); UrlBasedCorsConfigurationSource source = new UrlBasedCorsConfigurationSource(); source.registerCorsConfiguration("/**", configuration); return source; } }
Flutter端调整
1. 优化登录请求逻辑
添加401状态码处理,并开启withCredentials(如果后端使用Session认证):
Future<bool> loginAction(String nickname, String password) async { try { var dio = Dio(BaseOptions( baseUrl: "http://localhost:8081", headers: {"Accept": "application/json"}, withCredentials: true, // Session认证需开启 )); var param = {'nickname': nickname, 'password': password}; Response response = await dio.post('/user/login', data: param); if (response.statusCode == 200) { final userId = response.data['user_id']; await storage.write(key: 'login', value: jsonEncode(userId)); print('Login successful!'); return true; } else if (response.statusCode == 401) { print('用户名或密码错误'); return false; } else { print('Login failed: ${response.statusCode}'); return false; } } catch (e) { print('Error occurred: $e'); return false; } }
通用排查步骤
- 查看请求详情:用浏览器DevTools或Flutter日志查看登录请求的完整响应头,确认302跳转目标
- 检查Spring Security配置:确认是否存在强制重定向规则(如
successHandler、defaultSuccessUrl) - 验证CORS配置:确保允许Flutter的域名/端口,且
allowCredentials设置正确 - 独立测试接口:用Postman直接调用登录接口,验证是否返回预期JSON结果而非302
内容的提问来源于stack exchange,提问作者최수진
相关产品推荐
相关产品推荐

