React 로그인 Modal 만들기 5 - 회원 가입 Back end 구현

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.addAllowedOrigin("*");
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

Share