잡담
캘린더 프로젝트에 회원을 추가해서 회원마다의 일정을 사용할 수 있도록 회원가입, 로그인에 대해 공부하고 배운 내용들을 정리해서 올려보고자 합니다. 작성된 내용들이 회원가입 서비스를 만들기 위해서 배우고 있는 개발자분들에게 도움이 되었으면합니다. 다만 저 또한 공부하고 작성하는 코드들이여서 부족한 내용이 있을 수도 있으니 참고 정도로 살펴보셨으면 합니다.
회원 가입 만들기
1. 이메일 검증
//Controller
@PostMapping("/register/users")
public String createUser(@Validated @ModelAttribute UserDto.RegisterRequest userRequest,
RedirectAttributes redirectAttributes, Model model) {
try {
userService.createUser(userRequest);
} catch (IllegalArgumentException ex) {
model.addAttribute("userRequest", userRequest);
model.addAttribute("errorMessage", ex.getMessage());
return "register";
}
redirectAttributes.addFlashAttribute("message", "회원가입이 성공적으로 완료되었습니다.");
return "redirect:/login";
}
폼 형식의 데이터를 받아오고 되돌려줄 때의 편의성을 위해 @RestController 대신 @Controller를 사용했습니다. 검증에 실패했을 경우, 어떤 정보가 잘못 입력되었는지를 반환하고, 사용자가 원래 입력했던 값들을 모델에 담아 뷰에서 사용할 수 있도록 합니다. 또한, 일시적으로 회원가입 메시지를 띄운 후, 입력한 폼을 새로고침했을 때 다시 POST 요청이 발생하지 않도록 하기 위해, 검증에 성공한 경우에는 로그인 페이지로 리다이렉트합니다.
@ModelAttribute를 사용할 때 @RestController가 아닌 @Controller를 선택한 이유는?
@ModelAttribute는 폼 데이터를 객체로 변환하여 서버에서 처리하는 데 사용되며, 이렇게 생성된 객체는 뷰에 전달되어 화면에서 사용할 수 있습니다. 반면, @RestController는 데이터를 JSON 형식으로 반환하므로 뷰를 렌더링하지 않습니다.
결론적으로, 폼 데이터를 처리하고 뷰를 렌더링할 때는 @Controller를 사용하고, 데이터만 전달할 때는 @RestController를 사용해야 합니다.
<!-- html -->
<div class="form-container">
<h2>Sign up</h2>
<form id="register-form" th:action="@{/register/users}" th:object="${userRequest}" method="post">
<input type="email" th:field="*{email}" placeholder="Email" autocomplete="email" required />
<input type="password" th:field="*{password}" placeholder="Password" autocomplete="new-password" required>
<input type="password" id="confirm-password" placeholder="Confirm Password" autocomplete="new-password" required>
<input type="text" th:field="*{nickname}" placeholder="Nickname" autocomplete="nickname" required/>
<small id="password-error" style="color:red; display:none;">Passwords do not match</small>
<button type="submit" class="signup-btn">Sign Up</button>
</form>
<button onclick="location.href='/login'" class="signin" style="background-color: #ff5733; color: white;">Back to Sign In</button>
<div th:if="${errorMessage}" style="color: red;">
<p th:text="${errorMessage}"></p>
</div>
</div>
검증에 실패했을 경우 반환 받은 값들은 Thymeleaf의 태그들을 사용해서 다시 입력필드에 채워줍니다.
//Service
private final PasswordEncoder passwordEncoder;
@Transactional
public void createUser(UserDto.RegisterRequest requestUser) {
if(userRepository.existsByEmail(requestUser.getEmail())) {
throw new IllegalArgumentException("이미 해당 \"이메일\"을 가진 사용자가 존재합니다.");
}
if(userRepository.existsByNickname(requestUser.getNickname())) {
throw new IllegalArgumentException("이미 해당 \"닉네임\"을 가진 사용자가 존재합니다.");
}
ScheduleUtility.validateEmail(requestUser.getEmail());
userRepository.save(new UserEntity(requestUser, passwordEncoder));
}
먼저 해당 이메일이 있는지, 닉네임이 있는지 체크를 해줍니다.
이메일의 형식과 유효성을 검증하기 위해서 apache의 EmailValidator를 사용했습니다.
username@domain 구조의 이메일 형식, 올바른 도메인 형식인지를 확인해줍니다. 실제로 존재하는 이메일인지는 판단하기 위해서는 따로 이메일에 메일을 보내 확인을 주고 받는 검증이 추가적으로 필요합니다.
2. 비밀번호 암호화
<script>
const password = document.getElementById('password');
const confirmPassword = document.getElementById('confirm-password');
const errorMessage = document.getElementById('password-error');
const form = document.getElementById('register-form');
confirmPassword.addEventListener('input', function() {
if (password.value !== confirmPassword.value) {
errorMessage.style.display = 'block';
} else {
errorMessage.style.display = 'none';
}
});
form.addEventListener('submit', function (event) {
if (password.value !== confirmPassword.value) {
event.preventDefault();
errorMessage.style.display = 'block';
alert('Passwords do not match.');
}
});
</script>
비밀번호를 유저가 정확하게 적었는지 확인하기 위해서 비밀번호 확인을 시킵니다.
@Configuration
@EnableWebSecurity
public class SecurityConfig {
@Bean
public PasswordEncoder passwordEncoder() {
return new BCryptPasswordEncoder(); // BCrypt 알고리즘을 사용하는 PasswordEncoder
}
}
SecurityConfig 클래스는 Spring Security의 전반적인 설정을 관리합니다. 이 클래스 내에 @Bean으로 PasswordEncoder를 정의함으로써, Spring의 컨테이너에서 해당 객체를 생성하고 관리할 수 있게 됩니다. 이를 통해 Spring Security는 비밀번호를 암호화하거나 검증할 때 사용할 수 있는 PasswordEncoder 인스턴스를 제공받게 됩니다.
BCryptPasswordEncoder를 사용하면 비밀번호를 해시 형태로 암호화하여 데이터베이스에 저장할 수 있습니다. 이 과정에서 단방향 암호화가 적용되므로, 암호화된 비밀번호는 원래 비밀번호로 복원할 수 없게 되어 보안이 강화됩니다. BCrypt는 Blowfish 알고리즘을 기반으로 하는 해시 함수로, 민감한 데이터를 안전하게 저장하기 위해 Blowfish의 암호화 메커니즘을 사용하여 단방향 해시를 생성합니다.
//Entity
public UserEntity(UserDto.RegisterRequest request, PasswordEncoder passwordEncoder) {
this.email = request.getEmail();
this.password = passwordEncoder.encode(request.getPassword());
this.nickname = request.getNickname();
}
public boolean checkPassword(String plainPassword, PasswordEncoder passwordEncoder) {
return passwordEncoder.matches(plainPassword, this.password);
}
엔티티의 생성자에서 passwordEncode를 사용하여 비밀번호를 암호화 해줍니다.
그렇게 암호화된 비밀번호를 userRepository의 save를 통해 데이터베이스에 저장해줍니다.
3. 정리
- 회원 가입 폼을 만든다.
- Controller에서 해당 폼에 대한 데이터들을 받아준다.
- Service에서 해당 데이터를 검증한다.
- 이메일의 형식에 대한 검증은 EmailValidator으로 검증하고 동일한 이메일이 있는지 확인은 데이터베이스에서 찾아본다.
- 닉네임이 있는지 확인은 데이터베이스에서 찾아본다.
- 비밀번호는 BCryptPasswordEncoder를 사용해서 단방향으로 암호화하여 데이터베이스에 저장한다.
마치며
처음에 회원가입을 만들면서 로그인과 회원가입을 책임을 분리해서 사용해야겠다고 생각해서 작업을 회원가입 먼저 시작했습니다. 캘린더 프로젝트 특성상 아이디는 이메일로 하는게 좋아보여 이메일에 대한 검증, 비밀번호에 대한 암호화를 키워드로 검색을 시작해서 공부를 했습니다.
이메일은 /^[A-Za-z0-9_\.\-]+@[A-Za-z0-9\-]+\.[A-za-z0-9\-]+/ 같은 특정 키워드들이 주로 검색이 되었고 해당 검증을 해주는 것들을 찾아서 해결했습니다.
비밀번호에 대한 암호화를 검색하면 단방향, 양방향 암호화에 대한 정보들을 얻을 수 있었고 회원들을 관리할 때 단뱡향으로 암호를 저장해서 복호화할 수 없게 해서 저장하고 유저가 비밀번호를 까먹었다면 비밀번호 초기화를 시켜주는 방법을 사용하는 것을 알게 되었습니다.
이렇게 필요한 내용들을 단계별로 하나씩 익히면서 만드는 과정이 꽤나 재밌었던 것 같습니다.
'Backend Programming' 카테고리의 다른 글
HTTPS에 대해서 (0) | 2024.09.29 |
---|---|
JWT을 사용하여 로그인 서비스 만들기 (2) | 2024.09.22 |
DTO에서 NotNull설정이 되어 있을 때 PATCH를 어떻게 해줘야할까? (1) | 2024.08.26 |
RESTful API란? (0) | 2024.07.03 |
서블릿이란 무엇일까? (0) | 2024.06.30 |