05.회원 가입 로직
데이터베이스 종류와 ORM
- 회원정보를 저장하기 위한 데이터베이스는 MySQL 엔진의 데이터베이스를 사용한다
- 그리고 접근은 Spring Data JPA를 사용한다
회원 가입 로직
- 회원정보를 통해 인증 인가 작업을 진행하기 때문에 사용자로부터 회원가입을 진행한 뒤 데이터베이스에 회원정보를 저장해야 한다.
회원가입 페이지
1
2
3
4
5
<form action="/joinProc" method="post" name="joinForm">
<input type="text" name="username" placeholder="Username"/>
<input type="password" name="password" placeholder="Password"/>
<input type="submit" value="Join"/>
</form>
JoinDTO
- form 양식으로 넘어온 데이터를 이동시킬 객체
1
2
3
4
5
6
7
8
9
import lombok.Getter;
import lombok.Setter;
@Setter
@Getter
public class JoinDTO {
private String username;
private String password;
}
JoinController
- 회원가입 로직을 시작하는 컨트롤러
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
import com.example.testsecurity.dto.JoinDTO;
import com.example.testsecurity.service.JoinService;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.stereotype.Controller;
import org.springframework.web.bind.annotation.GetMapping;
import org.springframework.web.bind.annotation.PostMapping;
@Controller
public class JoinController {
@Autowired
private JoinService joinService;
@GetMapping("/join")
public String joinP() {
return "join";
}
@PostMapping("/joinProc")
public String joinProcess(JoinDTO joinDTO) {
joinService.joinProcess(joinDTO);
return "redirect:/login";
}
}
- joinP() 회원가입 페이지 호출
- joinProcess() 회원가입 로직 진행
- 현재는 편의상 의존성 주입 방식을 필드 주입 방식으로 하였지만, 실무에서는 final 키워드를 달고 생성자 주입방식으로 하는 것을 권장함
JoinService
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
import com.example.testsecurity.dto.JoinDTO;
import com.example.testsecurity.entity.UserEntity;
import com.example.testsecurity.repository.UserRepository;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.security.crypto.bcrypt.BCryptPasswordEncoder;
import org.springframework.stereotype.Service;
@Service
public class JoinService {
@Autowired
private UserRepository userRepository;
@Autowired
private BCryptPasswordEncoder bCryptPasswordEncoder;
public void joinProcess(JoinDTO joinDTO) {
//db에 이미 동일한 username을 가진 회원이 존재하는지?
UserEntity data = new UserEntity();
data.setUsername(joinDTO.getUsername());
data.setPassword(bCryptPasswordEncoder.encode(joinDTO.getPassword()));
data.setRole("ROLE_USER");
userRepository.save(data);
}
}
- 서비스에서 동일한 아이디 조회하는 로직도 짜도록 해보자
- setRole()을 통해 강제적으로 역할을 세팅하도록 하고, 나중에 필요하면 ENUM을 만들어서 불러와도 좋고, 로직을 추가해도 좋다.
- setPassword()의 경우는 단방향 해시 암호화를 하여 저장해야 함으로 bCryptPasswordEncoder를 통해 암호화 하여 저장하도록 하자
UserEntity
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
import jakarta.persistence.Entity;
import jakarta.persistence.GeneratedValue;
import jakarta.persistence.GenerationType;
import jakarta.persistence.Id;
import lombok.Getter;
import lombok.Setter;
@Entity
@Setter
@Getter
public class UserEntity {
@Id
@GeneratedValue(strategy = GenerationType.IDENTITY)
private int id;
private String username;
private String password;
private String role;
}
- Entity를 만들고 Hibernate 설정을 통해 테이블 생성을 세팅해두자
UserRepository
1
2
3
4
5
6
import com.example.testsecurity.entity.UserEntity;
import org.springframework.data.jpa.repository.JpaRepository;
public interface UserRepository extends JpaRepository<UserEntity, Integer> {
}
- JPA를 사용하기 위해 인터페이스로 만들고 JpaRepository를 상속 받도록 하자
SecurityConfig 접근권한
1
2
3
4
5
6
7
http
.authorizeHttpRequests((auth) -> auth
.requestMatchers("/", "/login", "/loginProc", "/join", "/joinProc").permitAll()
.requestMatchers("/admin").hasRole("ADMIN")
.requestMatchers("/my/**").hasAnyRole("ADMIN", "USER")
.anyRequest().authenticated()
);
- requestMatchers(“/”, “/login”, “/loginProc”, “/join”, “/joinProc”).permitAll() 를 통해 모두 접근 가능한 경로를 추가하자
회원 중복 검증 및 처리
- username에 대해서 중복된 가입이 발생하면 서비스에서 아주 치명적인 문제가 발생하기 때문에 백엔드단에서 중복 검증과 중복 방지 로직을 작성해야 한다.
UserEntity Unique 설정
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
import jakarta.persistence.*;
import lombok.Getter;
import lombok.Setter;
@Entity
@Setter
@Getter
public class UserEntity {
@Id
@GeneratedValue(strategy = GenerationType.IDENTITY)
private int id;
@Column(unique = true)
private String username;
private String password;
private String role;
}
JoinService 중복 검증
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
import com.example.testsecurity.dto.JoinDTO;
import com.example.testsecurity.entity.UserEntity;
import com.example.testsecurity.repository.UserRepository;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.security.crypto.bcrypt.BCryptPasswordEncoder;
import org.springframework.stereotype.Service;
@Service
public class JoinService {
@Autowired
private UserRepository userRepository;
@Autowired
private BCryptPasswordEncoder bCryptPasswordEncoder;
public void joinProcess(JoinDTO joinDTO) {
//db에 이미 동일한 username을 가진 회원이 존재하는지?
boolean isUser = userRepository.existsByUsername(joinDTO.getUsername());
if (isUser) {
return;
}
UserEntity data = new UserEntity();
data.setUsername(joinDTO.getUsername());
data.setPassword(bCryptPasswordEncoder.encode(joinDTO.getPassword()));
data.setRole("ROLE_USER");
userRepository.save(data);
}
}
- existsByUsername()의 경우 개발자가 커스텀하여 메소드를 만들어준다.
UserRepository
1
2
3
4
5
6
import com.example.testsecurity.entity.UserEntity;
import org.springframework.data.jpa.repository.JpaRepository;
public interface UserRepository extends JpaRepository<UserEntity, Integer> {
boolean existsByUsername(String username);
}
front
- httpXMLRequest 메소드를 통해 백엔드에 미리 구현해둔 API에 이미 존재하는 username인지 검증하는 로직을 추가해야 한다.
- 또한 정규식 처리를 통해 올바른 형식의 입력값을 받아야 한다
- 아이디의 자리수
- 아이디의 특수문자 포함 불가
- admin과 같은 아이디 사용 불가
- 비밀번호 자리수
- 비밀번호 특수문자 포함 필수
트러블 슈팅
- 난 MSA 연습으로 프로젝트를 구성하였다.
- MSA이기 때문에 gateway에서 유저 서버를 호출하여 회원가입을 진행하려고 했고, 회원에 시큐리티가 걸려있었다.
- 또한, 컨트롤러역시 RestController로 설정하였기 때문에 일반적인 Controller로 하여 html 파일로 호출하는 것이 안되었다.
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
@RestController
@RequiredArgsConstructor
@RequestMapping("/api/users")
public class UserController {
private final UserService userService;
private ModelAndView mav;
// 회원가입 페이지
@GetMapping("/join")
public ModelAndView joinP(){
ModelAndView mav = new ModelAndView("join");
return mav;
}
// 회원가입
@PostMapping("/joinProc")
public ModelAndView joinProccess(JoinDto joinDto){
userService.joinProccess(joinDto);
mav = new ModelAndView("login");
return mav;
}
}
- 문제점1
- 페이지 이동이 안됨. Controller 처럼 반환값을 String으로 하여 페이지 이동이 안됨.
- 해결
- ModelAndView 객체를 이용하여 해결
- 문제점1-1
- Circular view path [join]: would dispatch back to the current handler URL [/api/users/join] again. Check your ViewResolver setup! 에러가 발생하였다
- 해결
- 이것은 join.html을 만들어서 resource의 static 패키지에 넣어놨는데 파일을 인식하지 못하여서 발생하는 문제였다.
- 부트에서 따로 html을 인식하게 하는것이 있는 줄은 모르겠는데
- 내가 찾아본 정보들은 기본적으로 String을 반환타입으로 하여 join.html로 보내면 static 폴더에서 찾아서 연결되는 듯 했다.
- 하지만 나는 RestController였고 String을 반환타입으로 하면 String이 그대로 반환되었다.
- ModelAndView를 이용하여 “join.html”로 하여도 되지 않았다.
- mustache를 의존성에 추가하고, templates 패키지에 join.mustache 파일을 추가 하였다.
- 문제점2
- 회원가입 페이지까지는 열렸으나 그 다음에 회원가입 로직이 진행이 되지 않았다.
- 해결
- 문제는 회원가입하고 form을 날리는 url을 /joinProc로 해놨는데, /joinProc는 /api/users/joinProc 였으므로 form action 값을 변경하였다.
- 또한 SecurityConfig에서 requestMatchers에 permitAll()을 해놓은 값에도 requestMatchers(“/”, “/api/users/login”, “/api/users/join” , “/api/users/joinProc”).permitAll()를 추가하였다.