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

SpringBoot+React(TypeScript)注册报错:rawPassword cannot be null

React+SpringBoot Todo应用注册API报错:rawPassword cannot be null 排查解决

我正在学习Java SpringBoot,使用React(TypeScript)与SpringBoot开发Todo应用。前端运行在localhost:3000,后端运行在localhost:8080。调用注册API时,SpringBoot抛出错误:rawPassword cannot be null。已通过浏览器开发者工具确认前端能获取并保存密码值,也尝试在package.json中配置代理PROXY=http://localhost:8080,但问题仍未解决。


相关代码

Signup.tsx

import React, { useState } from 'react'
import { Link } from "react-router-dom";
import { Flex,Heading,Input,Button,InputGroup,Stack,InputLeftElement,chakra,Box,Avatar,FormControl,InputRightElement} from "@chakra-ui/react";
import { FaUserAlt, FaLock } from "react-icons/fa";
import axios from 'axios';
import { BASE_URL } from './helper/BASE_URL';

const CFaUserAlt = chakra(FaUserAlt);
const CFaLock = chakra(FaLock);

const Signup = () => {
    const [showPassword, setShowPassword] = useState(false);
    const [inputEmail, setInputEmail] = useState("")
    const [inputPassword, setInputPassword] = useState("");
    const handleShowClick = () => setShowPassword(!showPassword);

    const handleInputEmailChange = (event: React.ChangeEvent<HTMLInputElement>) => setInputEmail(event.target.value);
    const handleInputPasswordChange = (event: React.ChangeEvent<HTMLInputElement>) => setInputPassword(event.target.value);
    const isError = inputEmail === ""

    const onClickSignup = (event: any) => {
        event.preventDefault(); 
        axios.post(BASE_URL + "/signup", {
            email: inputEmail,
            password: inputPassword
        }).then((response) => {
            console.log(response);
        })
    }
    
    return (
        <div>
            <Flex
            flexDirection="column"
            width="100wh"
            height="100vh"
            backgroundColor="blue.500"
            justifyContent="center"
            alignItems="center"
            >
            <Stack
            flexDir="column"
            mb="2"
            justifyContent="center"
            alignItems="center"
            >
            <Avatar bg="teal.500" />
            <Heading color="white">新規登録</Heading>
            <Box minW={{ base: "90%", md: "468px" }}>
                <form onSubmit={onClickSignup}>
                    <Stack
                    spacing={4}
                    p="1rem"
                    backgroundColor="whiteAlpha.900"
                    boxShadow="md"
                >
                    <FormControl isInvalid={isError}>
                        <InputGroup>
                            <InputLeftElement
                            pointerEvents="none"
                            children={<CFaUserAlt color="gray.300" />}
                            />
                                <Input type="email" id="email" name='email' value={inputEmail} onChange={handleInputEmailChange} placeholder="メールアドレス" />
                        </InputGroup>
                    </FormControl>
                    <FormControl isInvalid={isError}>
                        <InputGroup>
                            <InputLeftElement
                            pointerEvents="none"
                            color="gray.300"
                            children={<CFaLock color="gray.300" />}
                            />
                            <Input
                            type={showPassword ? "text" : "password"}
                            id="password"
                            name="password"
                            placeholder="パスワードは6文字以上の英数字で入力"
                            value={inputPassword} onChange={handleInputPasswordChange}
                            />
                                <InputRightElement width="4.5rem">
                                    <Button h="1.75rem" size="sm" onClick={handleShowClick}>
                                    {showPassword ? "Hide" : "Show"}
                                    </Button>
                                </InputRightElement>
                        </InputGroup>
                    </FormControl>
                    <Button
                    borderRadius={0}
                    type="submit"
                    variant="solid"
                    colorScheme="blue"
                    width="full"
                    >
                        登録
                    </Button>
                    </Stack>
                </form>
            </Box>
        </Stack>
        <Box pt="3">
            <Link style={{color: "white"}} to={'/'}>ホームへ</Link>
        </Box>
    </Flex>
            <Link to="/">Homeへ</Link>
        </div>
    )
}

export default Signup

UserDetailsServiceImpl.java

package com.portfolio.todoapp.asoviewtodo.Service;

import com.portfolio.todoapp.asoviewtodo.Repositopry.UserRepository;
import com.portfolio.todoapp.asoviewtodo.UserDetailsImpl;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.jdbc.core.JdbcTemplate;
import org.springframework.security.core.GrantedAuthority;
import org.springframework.security.core.authority.SimpleGrantedAuthority;
import org.springframework.security.core.userdetails.UserDetails;
import org.springframework.security.core.userdetails.UserDetailsService;
import org.springframework.security.core.userdetails.UsernameNotFoundException;
import org.springframework.security.crypto.password.PasswordEncoder;
import org.springframework.stereotype.Service;
import org.springframework.transaction.annotation.Transactional;

import java.util.*;

@Service
public class UserDetailsServiceImpl implements UserDetailsService {

    @Autowired
    PasswordEncoder passwordEncoder;

    @Autowired
    JdbcTemplate jdbcTemplate;

    @Autowired
    UserRepository userRepository;

    @Override
    public UserDetails loadUserByUsername(String email) throws UsernameNotFoundException {
        try {
            String sql = "SELECT * FROM asoviewtodo.users WHERE email = ?;";
            Map<String, Object> map = jdbcTemplate.queryForMap(sql, email);
            String password = (String)map.get("password");
            Collection<GrantedAuthority> authorities = new ArrayList<>();
            authorities.add(new SimpleGrantedAuthority((String)map.get("authority")));
            return new UserDetailsImpl(email, password, authorities);
        }catch (Exception e) {
            throw new UsernameNotFoundException("user not found", e);
        }
    }

    @Transactional
    public void register(String email, String password, String authority) {
        String sql = "INSERT INTO asoviewtodo.users (email, password, authority) VALUES (?,?,?)";
        jdbcTemplate.update(sql, email, passwordEncoder.encode(password), authority);
    }

}

WebSecurityConfig.java

package com.portfolio.todoapp.asoviewtodo.Config;

import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.springframework.security.config.annotation.web.builders.HttpSecurity;
import org.springframework.security.config.annotation.web.configuration.EnableWebSecurity;
import org.springframework.security.crypto.bcrypt.BCryptPasswordEncoder;
import org.springframework.security.crypto.password.PasswordEncoder;
import org.springframework.security.web.SecurityFilterChain;
import org.springframework.web.cors.CorsConfiguration;
import org.springframework.web.cors.CorsConfigurationSource;
import org.springframework.web.cors.UrlBasedCorsConfigurationSource;

import java.util.List;

@Configuration
@EnableWebSecurity
public class WebSecurityConfig {

    @Bean
    public SecurityFilterChain securityFilterChain(HttpSecurity http) throws Exception {
        http.authorizeRequests().antMatchers("/signup").permitAll().anyRequest().authenticated().and().csrf().
                disable().cors();
        http.formLogin(login -> login.loginPage("http://localhost:3000/login").loginProcessingUrl("/login").
                defaultSuccessUrl("http://localhost:3000/").failureUrl("/login/error").permitAll()).
                logout(logout -> logout.logoutUrl("http://localhost:3000/login"));
         return http.build();
    }

    @Bean
    public CorsConfigurationSource corsConfigurationSource() {
        final CorsConfiguration configuration = new CorsConfiguration();
        configuration.setAllowedOrigins(List.of("http://localhost:3000"));
        configuration.setAllowedMethods(List.of("GET", "POST", "PUT", "DELETE", "OPTIONS", "PATCH"));
        configuration.setAllowedHeaders(List.of("Authorization", "Cache-Control", "Content-Type"));
        configuration.setAllowCredentials(true);
        configuration.setExposedHeaders(List.of("Authorization"));
        final UrlBasedCorsConfigurationSource source = new UrlBasedCorsConfigurationSource();
        source.registerCorsConfiguration("/**", configuration);
        return source;
    }

    @Bean
    public PasswordEncoder passwordEncoder() {
        return new BCryptPasswordEncoder();
    }
}

UserController.java

package com.portfolio.todoapp.asoviewtodo.Controller;

import com.portfolio.todoapp.asoviewtodo.SignupForm;
import com.portfolio.todoapp.asoviewtodo.Service.UserDetailsServiceImpl;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.dao.DataAccessException;
import org.springframework.security.authentication.AnonymousAuthenticationToken;
import org.springframework.security.core.Authentication;
import org.springframework.security.core.context.SecurityContext;
import org.springframework.security.core.context.SecurityContextHolder;
import org.springframework.stereotype.Controller;
import org.springframework.ui.Model;
import org.springframework.validation.BindingResult;
import org.springframework.validation.annotation.Validated;
import org.springframework.web.bind.annotation.CrossOrigin;
import org.springframework.web.bind.annotation.GetMapping;
import org.springframework.web.bind.annotation.PostMapping;
import org.springframework.web.bind.annotation.RequestMapping;

import javax.servlet.ServletException;
import javax.servlet.http.HttpServletRequest;

@Controller
@RequestMapping("/")
@CrossOrigin("http://localhost:3000")
public class TestController {
    @Autowired
    UserDetailsServiceImpl userDetailsServiceImpl;

    @GetMapping("/login")
    public String login() {
        return "login";
    }

    @GetMapping("/signup")
    public String newSignup(SignupForm signupForm) {
        return "signup";
    }

    @PostMapping("/signup")
    public String signup(@Validated SignupForm signupForm, BindingResult result , Model model, HttpServletRequest request) {
        try {
            userDetailsServiceImpl.register(signupForm.getEmail(), signupForm.getPassword(), "ROLE_USER");
        }catch (DataAccessException e) {
            model.addAttribute("signupError", "fail to signup");
        }

        result.hasErrors();
        if(result.hasErrors()) {
            return "signup";
        }

        SecurityContext context = SecurityContextHolder.getContext();
        Authentication authentication = context.getAuthentication();

        if(authentication instanceof AnonymousAuthenticationToken == false) {
            SecurityContextHolder.clearContext();
        }
        try {
            request.login(signupForm.getEmail(), signupForm.getPassword());
        } catch(ServletException e) {
            e.printStackTrace();
        }

        return "redirect:/";
    }
}

问题根源

  1. 请求格式不匹配:前端axios默认发送application/json格式的请求体,但后端@Controller默认处理表单提交格式(application/x-www-form-urlencoded),导致Spring无法将JSON参数绑定到SignupForm对象,signupForm.getPassword()返回null。
  2. 控制器类型错误:后端用@Controller是为服务器端渲染设计的,返回视图名,而前后端分离场景应该用@RestController返回JSON响应。

解决方案

1. 修改后端控制器为RestController并支持JSON绑定

  • 将@Controller改为@RestController,确保返回JSON而非视图
  • 在SignupForm参数前添加@RequestBody注解,告诉Spring从JSON请求体中绑定参数
  • 调整方法返回值为ResponseEntity,适配前后端分离的响应格式

修改后的TestController.java核心代码:

@RestController
@RequestMapping("/")
@CrossOrigin("http://localhost:3000")
public class TestController {
    @Autowired
    UserDetailsServiceImpl userDetailsServiceImpl;

    @PostMapping("/signup")
    public ResponseEntity<?> signup(@Validated @RequestBody SignupForm signupForm, BindingResult result, HttpServletRequest request) {
        if(result.hasErrors()) {
            return ResponseEntity.badRequest().body("参数格式错误");
        }
        try {
            userDetailsServiceImpl.register(signupForm.getEmail(), signupForm.getPassword(), "ROLE_USER");
        }catch (DataAccessException e) {
            return ResponseEntity.status(500).body("注册失败,邮箱可能已存在");
        }

        try {
            request.login(signupForm.getEmail(), signupForm.getPassword());
            return ResponseEntity.ok("注册并登录成功");
        } catch(ServletException e) {
            e.printStackTrace();
            return ResponseEntity.status(500).body("注册成功,但登录失败");
        }
    }
}

2. 确保SignupForm类有正确的Getter/Setter

SignupForm必须包含与前端JSON键名一致的字段,以及对应的Getter/Setter方法:

public class SignupForm {
    private String email;
    private String password;

    public String getEmail() {
        return email;
    }

    public void setEmail(String email) {
        this.email = email;
    }

    public String getPassword() {
        return password;
    }

    public void setPassword(String password) {
        this.password = password;
    }
}

内容的提问来源于stack exchange,提问作者funeral of tech

相关产品推荐
方舟 Agent Plan

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

最近更新时间:2026.08.09 14:45:27