React 로그인 Modal 만들기 5 - 회원 가입 Back end 구현 회원 데이터를 저장할 UserInfo 정의 회원 가입을 위해 사용자 id, username, email, password와 사용자 별로 권한을 관리하기 위한 UserInfo 객체를 관리하도록 정의
@Entity @NoArgsConstructor @AllArgsConstructor @Getter @Builder public class UserInfo { @Id @GeneratedValue(strategy = GenerationType.IDENTITY) private Long id; @Column(nullable = false) private String username; @Column(nullable = false) private String email; @Column(nullable = false) private String password; @Enumerated(EnumType.STRING) private UserRole userRole; }
User 권한 목록
권한은 3가지로 일반사용자를 위한 USER 권한, 수정/변경 권한이 있는 MANAGER 권한과 관리자인 ADMIN 3가지 권한으로 분류 한다
public enum UserRole { USER("ROLE_USER" ), MANAGER("ROLE_MANAGER" ), ADMIN("ROLE_ADMIN" ); private String value; UserRole(String value){ this .value = value; } public String getValue () { return this .value; } }
사용자 이름과 Email을 이용해 사용자 정보를 가져올 수 있도록 메소드를 추가한다.
@Repository public interface UserInfoRepository extends JpaRepository <UserInfo, String> { Optional<UserInfo> findByUsername (String useranme) ; Optional<UserInfo> findByEmail (String email) ; }
회원 가입을 위해 사용하는 DTO 객체를 정의한다.
@Data public class UserInfoDto { private String username; private String email; private String password; }
UserDetailsService 정의
UserDetailsService 에는 사용자 정보를 가져오기 위한 loadUserByUsername 만 있어 새로운 사용자를 저장하기 위해 UserDetailsService 를 상속한 UserInfoService 에 saveUserInfo 메소드를 추가했다
public interface UserInfoService extends UserDetailsService { public UserInfo saveUserInfo (SignupDto signupDto) ; }
loadUserByUsername 메소드는 전달 받은 Email을 이용해 사용자 정보를 가져온 후 UserDetails 객체를 만들어 준다.
saveUserInfo 메소드에서는 전달 받은 SignupDto 객체로부터 새로운 UserInfo 객체를 생성할 때 PasswordEncoder 를 이용해 Password를 암호화 한 뒤 저장한다.
@RequiredArgsConstructor public class UserInfoServiceImpl implements UserInfoService { private final UserInfoRepository userInfoRepository; private final PasswordEncoder passwordEncoder; @Override public UserDetails loadUserByUsername (String email) throws UsernameNotFoundException { Optional<UserInfo> userInfoOptional = userInfoRepository.findByEmail(email); if (userInfoOptional.isPresent()){ UserInfo userInfo = userInfoOptional.get(); List<GrantedAuthority> authorities = AuthorityUtils.createAuthorityList(userInfo.getUserRole().getValue()); return new User (userInfo.getEmail(), userInfo.getPassword(), authorities); }else { return null ; } } public UserInfo saveUserInfo (SignupDto signupDto) { UserInfo userInfo = UserInfo .builder() .username(signupDto.getUsername()) .email(signupDto.getEmail()) .password(passwordEncoder.encode(signupDto.getPassword())) .userRole(UserRole.USER).build(); return userInfoRepository.save(userInfo); } }
회원 가입을 위한 Controller 추가 @RestController @RequiredArgsConstructor public class UserInfoController { private final UserInfoService userInfoService; @GetMapping("/hello") public String hello () { return "Hello World" ; } @PostMapping("/api/signup") public ResponseEntity signUp (@RequestBody UserInfoDto userInfoDto) { UserInfo userInfo = UserInfo.builder().username(userInfoDto.getUsername()).email(userInfoDto.getEmail()) .password(userInfoDto.getPassword()).userRole(UserRole.USER).build(); UserInfo createdUser = userInfoService.saveUserInfo(userInfo); URI uri = ServletUriComponentsBuilder.fromCurrentContextPath().path("/{id}" ).buildAndExpand(createdUser.getId()) .toUri(); return ResponseEntity.created(uri).body(createdUser); } }
Spring Security 설정하기 configure(WebSecurity web) 를 이용해 정적 자원에 대한 접근이 가능하도록 하고, configure(HttpSecurity http) 를 이용해 개발 URL 별로 접근을 설정한다. 테스트를 진행하면서 Ajax를 이용해 Spring Security에 접그하기 위해서 CORS 문제가 발생하지 않도록 하기 위해 CORS에 대한 설정도 추가해 준다.
@Configuration @EnableWebSecurity @Order(2) public class SecurityConfig extends WebSecurityConfigurerAdapter { @Override public void configure (WebSecurity web) { web .ignoring() .requestMatchers(PathRequest.toStaticResources().atCommonLocations()); } @Override protected void configure (HttpSecurity http) throws Exception { http .authorizeRequests() .antMatchers("/api/signup" ).permitAll() .anyRequest().authenticated(); http .csrf().disable() .cors().configurationSource(corsConfigurationSource()); http .formLogin().disable(); } @Bean public CorsConfigurationSource corsConfigurationSource () { CorsConfiguration configuration = new CorsConfiguration (); configuration.addAllowedOriginPattern("*" ); configuration.addAllowedHeader("*" ); configuration.setAllowedMethods(Arrays.asList("GET" , "POST" , "OPTIONS" , "DELETE" , "PUT" , "PATCH" )); configuration.setAllowCredentials(true ); UrlBasedCorsConfigurationSource source = new UrlBasedCorsConfigurationSource (); source.registerCorsConfiguration("/**" , configuration); return source; } }
Spring Security에 CORS를 적용하면서 발생했던 문제
java.lang.IllegalArgumentException: When allowCredentials is true, allowedOrigins cannot contain the special value “*” since that cannot be set on the “Access-Control-Allow-Origin” response header. To allow credentials to a set of origins, list them explicitly or consider using “allowedOriginPatterns” instead.
Spring Seurity에 CORS 설정을 진행하면서 위와 같음 문제가 계속 발생했다. 해당 원인에 대해 찾아보니 CorsConfiguration 에 대한 설정으로 setAllowCredentials(true) 와 addAllowedOrigin(“*”) 를 동시에 설정해서 발생하는 문제였다.
해결 방법은 addAllowedOrigin(“*”) 대신 addAllowedOriginPattern(“*”) 를 사용하면 문제가 해결 된다.
참조 https://code-delivery.me/posts/2021-03-16/ https://github.com/spring-projects/spring-framework/issues/26111