Spring Boot自定义表单认证失效及JSON解析错误求助
问题描述
我基于Spring Boot和原生Java实现表单认证,用户分为ADMIN和USER两种角色。配置自定义登录地址.loginProcessingUrl("/admin/login")时出现认证失效问题:
- 始终返回HttpStatus OK
- 未注册用户也能登录
注释掉该配置使用Spring默认登录页时,认证正常。已在启动时注入测试用户(1个ADMIN、2个USER),不确定是User实体未存储Session ID,还是JS代码存在问题。同时调用登录接口时出现语法错误:SyntaxError: Unexpected token '<', "<!DOCTYPE "... is not valid JSON"。
相关代码如下:
1. SecurityConfig类中的SecurityFilterChain配置
@Bean public SecurityFilterChain filterChain1(HttpSecurity httpSecurity) throws Exception { return httpSecurity .cors() .and() .csrf(AbstractHttpConfigurer::disable) .authorizeHttpRequests((auth) -> auth .antMatchers("/admin/**", "/secured/**") .hasRole(ADMIN.name()) ) .formLogin() .loginProcessingUrl("/admin/login") .and() // logout TODO // .logout() // .logoutUrl("/admin/logout") // .and() .build(); }
2. 全局@RequestMapping("/admin")的Admin登录Controller
@PostMapping(path = "/login") public ResponseEntity<?> login(@Valid @RequestBody User user) { System.out.println("Status " + userDTOService.confirmUser(user)); if (!userDTOService.confirmUser(user)) { return new ResponseEntity<>(!userDTOService.confirmUser(user), BAD_REQUEST); } return new ResponseEntity<>(userDTOService.confirmUser(user), FOUND); }
3. 用户校验Service类的confirmUser方法
public Boolean confirmUser(User user) { /* * Check if username exist in the database * then check if the password provided equals password in database * Then check if user is an admin * */ System.out.println(user); String userName = user.getUserName(); String password = user.getPassword(); Optional<User> findUser = userRepository.findUserByUserName(userName); return findUser .stream() .anyMatch(param -> param.getPassword().equals(password) && param.getRole().equals(ADMIN) ); }
4. 原生JS登录代码
const signIn = () => { formElement.addEventListener("submit", (event) => { event.preventDefault(); const formD = new FormData(event.target); fetch(LOGIN, { method: "POST", body: formD }).then(async (response) => { if (response.ok) { // window.location.href = "../static/new.html"; console.log("Success"); return response.json(); } const body = await response.json(); throw new Error(body.message); }) .catch((error) => { console.log(error); }); }) }
问题根源
- Spring Security表单认证逻辑冲突:
loginProcessingUrl("/admin/login")是让Spring Security接管该地址的认证处理,但你同时自定义了@PostMapping("/admin/login")的Controller接口。由于Security过滤器优先级高于Controller,你的自定义校验逻辑完全没被执行,而Security默认认证逻辑因未配置UserDetailsService无法正确校验用户,导致任意用户都能登录、始终返回OK。 - 请求格式不匹配:前端用
FormData发送multipart/form-data类型请求,但Controller用@RequestBody User user接收——@RequestBody仅支持JSON格式请求体,无法解析FormData,导致Spring返回HTML错误页面,前端解析JSON时遇到<字符报错。 - 密码校验逻辑不符合Security规范:Service中直接对比明文密码,而Spring Security默认要求密码加密存储,这也是默认登录页正常、自定义配置失效的潜在原因。
解决方案
1. 移除冲突的自定义登录Controller
删除@PostMapping("/admin/login")的Controller方法,让Spring Security完全接管登录请求的认证流程。
2. 实现UserDetailsService对接数据库校验
创建自定义UserDetailsService,让Security能从数据库获取并校验用户信息:
@Service public class CustomUserDetailsService implements UserDetailsService { private final UserRepository userRepository; public CustomUserDetailsService(UserRepository userRepository) { this.userRepository = userRepository; } @Override public UserDetails loadUserByUsername(String username) throws UsernameNotFoundException { User user = userRepository.findUserByUserName(username) .orElseThrow(() -> new UsernameNotFoundException("用户不存在: " + username)); // 转换为Security的UserDetails对象,角色需加ROLE_前缀(hasRole默认会拼接该前缀) return org.springframework.security.core.userdetails.User.builder() .username(user.getUserName()) .password(user.getPassword()) // 确保密码是加密后的,需配合密码编码器 .roles(user.getRole().name()) .build(); } }
3. 完善SecurityConfig配置
添加密码编码器,并关联UserDetailsService:
@Bean public PasswordEncoder passwordEncoder() { // 生产环境推荐使用BCryptPasswordEncoder return new BCryptPasswordEncoder(); } @Bean public SecurityFilterChain filterChain1(HttpSecurity httpSecurity, UserDetailsService userDetailsService) throws Exception { return httpSecurity .cors() .and() .csrf(AbstractHttpConfigurer::disable) .authorizeHttpRequests(auth -> auth .antMatchers("/admin/**", "/secured/**").hasRole(ADMIN.name()) .anyRequest().permitAll() // 放行登录页面等非授权请求 ) .formLogin(form -> form .loginProcessingUrl("/admin/login") // Security接管的登录接口地址 .loginPage("/admin/login-page") // 自定义登录页面地址(按需配置) .defaultSuccessUrl("/admin/dashboard", true) // 登录成功跳转地址 .failureUrl("/admin/login-page?error=true") // 登录失败跳转地址 ) .userDetailsService(userDetailsService) .passwordEncoder(passwordEncoder()) .build(); }
注意:启动注入测试用户时,密码需用passwordEncoder().encode("明文密码")加密后存储。
4. 调整前端请求格式
若需保留前端自定义逻辑,将请求改为JSON格式:
const signIn = () => { formElement.addEventListener("submit", (event) => { event.preventDefault(); // 将FormData转为JSON对象 const formData = new FormData(event.target); const user = Object.fromEntries(formData.entries()); fetch(LOGIN, { method: "POST", headers: { "Content-Type": "application/json" }, body: JSON.stringify(user) }).then(async (response) => { if (response.ok) { window.location.href = "../static/new.html"; console.log("Success"); return; } const errorMsg = await response.text(); throw new Error(errorMsg); }).catch((error) => { console.log(error); }); }) }
若使用Spring Security默认的表单认证逻辑,前端直接提交FormData即可,无需额外处理。
5. Session ID说明
Spring Security会自动管理Session,无需在User实体中存储Session ID。认证成功后,Security会自动创建Session并在响应中返回JSESSIONID Cookie。
内容的提问来源于stack exchange,提问作者iTchTheRightSpot
相关产品推荐
相关产品推荐

