엔티티명을 User가 아닌 SiteUser로 한 이유는 스프링 시큐리티에 이미 User 클래스가 있기 때문이다. 패키지명이 다르기 때문에 User라는 이름을 사용할 수는 있지만 패키지 오용으로 인한 오류를 방지하기 위해 다르게 명명한다.
SiteUser.java
import lombok.Getter;
import lombok.Setter;
import javax.persistence.*;
@Getter
@Setter
@Entity
public class SiteUser {
@Id
@GeneratedValue(strategy = GenerationType.IDENTITY)
private Long id;
@Column(unique = true)
private String username;
private String password;
@Column(unique = true)
private String email;
}
import com.example.sbb.domain.SiteUser;
import org.springframework.data.jpa.repository.JpaRepository;
public interface UserRepository extends JpaRepository<SiteUser, Long> {
}
import com.example.sbb.domain.SiteUser;
import com.example.sbb.repository.UserRepository;
import lombok.RequiredArgsConstructor;
import org.springframework.security.crypto.bcrypt.BCryptPasswordEncoder;
import org.springframework.security.crypto.password.PasswordEncoder;
import org.springframework.stereotype.Service;
@RequiredArgsConstructor
@Service
public class UserService {
private final UserRepository userRepository;
private final PasswordEncoder passwordEncoder;
public SiteUser create(String username, String email, String password){
SiteUser user = new SiteUser();
user.setUsername(username);
user.setEmail(email);
//BCryptPasswordEncoder passwordEncoder = new BCryptPasswordEncoder();
user.setPassword(passwordEncoder.encode(password));
this.userRepository.save(user);
return user;
}
BCryptPasswordEncoder 객체를 직접 new로 생성하는 방식도 있지만 PasswordEncoder 빈으로 등록해서 사용하는 것이 더 바람직하다. 암호화 방식을 변경하면 BCryptPasswordEncoder를 사용한 모든 프로그램을 일일이 수정해야 하기 때문이다.
+) PasswordEncoder는 BCryptPasswordEncoder의 인터페이스이다.
PasswordEncoder 빈을 만드는 가장 쉬운 방법은 @Configuration이 적용된 SecurityConfig에 @Bean 메서드를 생성하는 것이다.
@Configuration
@EnableWebSecurity
public class SecurityConfig {
...
@Bean
public PasswordEncoder passwordEncoder(){
return new BCryptPasswordEncoder();
}
}
회원가입을 위한 폼 클래스
UserCreateForm.java
import lombok.Getter;
import lombok.Setter;
import javax.validation.constraints.Email;
import javax.validation.constraints.NotEmpty;
import javax.validation.constraints.Size;
@Getter
@Setter
public class UserCreateForm {
@Size(min = 3, max = 25)
@NotEmpty(message = "사용자 ID는 필수항목입니다.")
private String username;
@NotEmpty(message = "비밀번호는 필수항목입니다.")
private String password1;
@NotEmpty(message = "비밀번호 확인은 필수항목입니다.")
private String password2;
@NotEmpty(message = "이메일은 필수항목입니다.")
@Email
private String email;
}
UserController.java
import com.example.sbb.domain.UserCreateForm;
import com.example.sbb.service.UserService;
import lombok.RequiredArgsConstructor;
import org.springframework.stereotype.Controller;
import org.springframework.validation.BindingResult;
import org.springframework.web.bind.annotation.GetMapping;
import org.springframework.web.bind.annotation.PostMapping;
import org.springframework.web.bind.annotation.RequestMapping;
import javax.validation.Valid;
@RequiredArgsConstructor
@Controller
@RequestMapping("/user")
public class UserController {
private final UserService userService;
@GetMapping("/signup")
public String signup(UserCreateForm userCreateForm){
return "signup_form";
}
@PostMapping("/signup")
public String signup(@Valid UserCreateForm userCreateForm, BindingResult bindingResult){
if(bindingResult.hasErrors()){
return "signup_form";
}
if(!userCreateForm.getPassword1().equals(userCreateForm.getPassword2())){
bindingResult.rejectValue("password2", "passwordInCorrect",
"2개의 패스워드가 일치하지 않습니다.");
return "signup_form";
}
userService.create(userCreateForm.getUsername(), userCreateForm.getEmail(),
userCreateForm.getPassword1());
return "redirect:/";
}
}
비밀번호 1
과비밀번호 2
가 일치하지 않는 경우,bindingResult.rejectValue(필드명, 오류코드, 에러메시지)
로 인해 오류가 발생하게 된다.
signup_form.html
<html layout:decorate="~{layout}" xmlns:th="http://www.thymeleaf.org" xmlns:layout="http://www.w3.org/1999/xhtml">
<div layout:fragment="content" class="container my-3">
<div class="my-3 border-bottom">
<div>
<h4>회원가입</h4>
</div>
</div>
<form th:action="@{/user/signup}" th:object="${userCreateForm}" method="post">
<div th:replace="form_errors :: formErrorsFragment"></div>
<div class="mb-3">
<label for="username" class="form-label">사용자ID</label>
<input type="text" th:field="*{username}" class="form-control">
</div>
<div class="mb-3">
<label for="password" class="form-label">비밀번호</label>
<input type="password" th:field="*{password1}" class="form-control">
</div>
<div class="mb-3">
<label for="password" class="form-label">비밀번호 확인</label>
<input type="password" th:field="*{password2}" class="form-control">
</div>
<div class="mb-3">
<label for="email" class="form-label">이메일</label>
<input type="email" th:field="*{email}" class="form-control">
</div>
<button type="submit" class="btn btn-primary">회원가입</button>
</form>
</div>
</html>
navbar.html
<nav th:fragment="navbarFragment" class="navbar navbar-expand-lg
navbar-light bg-light border-bottom" xmlns:th="http://www.thymeleaf.org">
<div class="container-fluid">
<a class="navbar-brand" href="/">SBB</a>
<button class="navbar-toggler" type="button"
data-bs-toggle="collapse" data-bs-target="#navbarSupportedContent"
aria-controls="navbarSupportedContent" aria-expanded="false" aria-label="Toggle navigation">
<span class="navbar-toggler-icon"></span>
</button>
<div class="collapse navbar-collapse" id="navbarSupportedContent">
<ul class="navbar-nav me-auto mb-2 mb-lg-0">
<li class="nav-item">
<a class="nav-link" href="#">로그인</a>
</li>
<!--추가된 부분-->
<li class="nav-item">
<a class="nav-link" th:href="@{/user/signup}">회원가입</a>
</li>
<!--추가된 부분 end-->
</ul>
</div>
</div>
이미 가입한 사용자 id 또는 이메일 주소로 회원가입을 진행하면 다음과 같은 오류가 발생한다.
SiteUser
엔티티에서 unique=true
설정으로 인해 중복 가입이 허용되지 않기 때문이다. 그러나 화면에 500 오류를 그대로 보여주는 것은 좋지 않으므로 다음과 같이 처리해준다.
UserController.java
...
@PostMapping("/signup")
public String signup(@Valid UserCreateForm userCreateForm, BindingResult bindingResult){
if(bindingResult.hasErrors()){
return "signup_form";
}
if(!userCreateForm.getPassword1().equals(userCreateForm.getPassword2())){
bindingResult.rejectValue("password2", "passwordInCorrect",
"2개의 패스워드가 일치하지 않습니다.");
return "signup_form";
}
/* 수정된 부분 */
try{
userService.create(userCreateForm.getUsername(), userCreateForm.getEmail(),
userCreateForm.getPassword1());
}catch (DataIntegrityViolationException e){
e.printStackTrace();
bindingResult.reject("signupFailed", "이미 등록된 사용자입니다.");
return "signup_form";
}catch (Exception e){
e.printStackTrace();
bindingResult.reject("signupFailed", e.getMessage());
return "signup_form";
}
/* 수정된 부분 end */
return "redirect:/";
}
...
- 사용자 ID 또는 이메일 주소가 동일할 경우 DataIntegrityViolationException이 발생하고, "이미 등록된 사용자입니다."라는 오류를 화면에 표시한다.
- 다른 오류의 경우에는 해당 오류의 메시지(e.getMessage())를 출력한다.
bindingResult.reject(오류코드, 오류메시지)
는 특정 필드의 오류가 아닌 일반적인 오류를 등록할 때 사용한다.