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

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);
        });
    })
}

问题根源
  1. Spring Security表单认证逻辑冲突:loginProcessingUrl("/admin/login")是让Spring Security接管该地址的认证处理,但你同时自定义了@PostMapping("/admin/login")的Controller接口。由于Security过滤器优先级高于Controller,你的自定义校验逻辑完全没被执行,而Security默认认证逻辑因未配置UserDetailsService无法正确校验用户,导致任意用户都能登录、始终返回OK。
  2. 请求格式不匹配:前端用FormData发送multipart/form-data类型请求,但Controller用@RequestBody User user接收——@RequestBody仅支持JSON格式请求体,无法解析FormData,导致Spring返回HTML错误页面,前端解析JSON时遇到<字符报错。
  3. 密码校验逻辑不符合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

相关产品推荐
方舟 Agent Plan

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

最近更新时间:2026.08.18 06:30:55