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:/"; } }
问题根源
- 请求格式不匹配:前端axios默认发送
application/json格式的请求体,但后端@Controller默认处理表单提交格式(application/x-www-form-urlencoded),导致Spring无法将JSON参数绑定到SignupForm对象,signupForm.getPassword()返回null。 - 控制器类型错误:后端用
@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
相关产品推荐
相关产品推荐

