3-06 요약 (회원 가입 코드 리뷰, 비밀번호 암호화 빈 등록하기, 회원가입 시 오류 처리)
회원 엔티티
package com.study.board.siteUser;
import jakarta.persistence.Column;
import jakarta.persistence.Entity;
import jakarta.persistence.GeneratedValue;
import jakarta.persistence.GenerationType;
import jakarta.persistence.Id;
import lombok.Getter;
import lombok.Setter;
@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;
}
회원 엔티티 이름은 User.java 가 아닌 SiteUser.java 로 했다.
왜냐하면 스프링 시큐리티에 이미 User 클래스가 있기 때문이다.
물론 패키지가 달라서 User 라고 사용할 수 있지만 패키지 오용을 방지하기 위해 SiteUser 로 만들었다.
회원 리포지터리
package com.study.board.siteUser;
import java.util.Optional;
import org.springframework.data.jpa.repository.JpaRepository;
public interface UserRepository extends JpaRepository<SiteUser, Long>{
//회원조회(사용자id(username)으로 조회)
Optional<SiteUser> findByusername(String username);
}
findByusername 메서드는 로그인 시 사용할 메서드이다.
회원 서비스
package com.study.board.siteUser;
import java.util.Optional;
//import org.springframework.security.crypto.bcrypt.BCryptPasswordEncoder;
import org.springframework.security.crypto.password.PasswordEncoder;
import org.springframework.stereotype.Service;
import com.study.board.DataNotFoundException;
import lombok.RequiredArgsConstructor;
@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 passEncoder = new BCryptPasswordEncoder();
user.setPassword(passEncoder.encode(password));
*/
user.setPassword(passwordEncoder.encode(password));
this.userRepository.save(user);
return user;
}
//로그인한 유저 정보 조회
public SiteUser getUSer(String username) {
Optional<SiteUser> siteUser = this.userRepository.findByusername(username);
if (siteUser.isPresent()) {
return siteUser.get();
} else {
throw new DataNotFoundException("siteuser not found");
}
}
}
create 메서드
- 로그인id, 이메일, 비밀번호를 받아 회원 데이터를 생성한다.
- 이때, 비밀번호는 입력한 값 그대로가 아닌 암호화 해서 저장해야 한다.
* 서비스 단에서 BCryptPasswordEncoder 객체를 new 하여 생성하여 사용하지 말고,
먼저 PasswordEncoder 객체를 빈으로 등록하고, 서비스 단에서는 객체를 주입받아 사용하는 것이 좋다.
나중에 암호화 방식을 변경하게 되면 BCryptPasswordEncoder 를 사용한 모든 프로그램을 수정해야 하기 때문이다.
SecurityConfig 파일
package com.study.board;
(... 생략 ...)
import org.springframework.security.crypto.bcrypt.BCryptPasswordEncoder;
import org.springframework.security.crypto.password.PasswordEncoder;
(... 생략 ...)
@Configuration
@EnableWebSecurity
@EnableMethodSecurity(prePostEnabled = true)
public class SecurityConfig {
@Bean
SecurityFilterChain filterChain(HttpSecurity http) throws Exception {
(..생략..)
return http.build();
}
@Bean
PasswordEncoder passwordEncoder() {
return new BCryptPasswordEncoder();
}
}
빈을 만드는 가장 쉬운 방법은 @Configuration 이 적용된 파일에 @Bean 메서드를 추가하는 것이다.
BCryptPasswordEncoder 객체를 빈으로 등록하여 서비스에서 사용하기 위해,
SecurityConfig 파일에 @Bean 어노테이션을 달린 passwordEncoder 메서드를 작성하였다.
회원폼
회원가입 화면에서 submit 시 체크할 항목을 작성한 폼 클래스이다.
package com.study.board.siteUser;
import jakarta.validation.constraints.Email;
import jakarta.validation.constraints.NotEmpty;
import jakarta.validation.constraints.Size;
import lombok.Getter;
import lombok.Setter;
@Getter
@Setter
public class UserForm {
@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;
}
@Email 어노테이션 : 해당 속성의 값이 이메일 형식과 일치하는지 검증
회원 컨트롤러
package com.study.board.siteUser;
import org.springframework.dao.DataIntegrityViolationException;
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 jakarta.validation.Valid;
import lombok.RequiredArgsConstructor;
@RequiredArgsConstructor
@Controller
public class UserController {
private final UserService userService;
//회원가입 화면 진입
@GetMapping("/user/signup")
public String getSignup(UserForm userForm) {
return "signupForm";
}
//회원가입 실행
@PostMapping("/user/signup")
public String signup(@Valid UserForm userForm, BindingResult bindingResult) {
//@Valid 애너테이션이 달린 폼 검증 후, 오류있을 때 bindingResult에 반환
if(bindingResult.hasErrors()) {
return "signupForm";
}
if(! userForm.getPassword1().equals(userForm.getPassword2())) {
//rejectValue 메서드 매개변수: 필드명, 오류코드, 오류메세지
bindingResult.rejectValue("password2", "passwordInCorrect", "2개의 패스워드가 일치하지 않습니다.");
return "signupForm";
}
try {
//user insert
this.userService.create(userForm.getUsername(), userForm.getEmail(), userForm.getPassword1());
//unique키(사용자id, 이메일)이 이미 존재하는 값일 경우 catch
}catch(DataIntegrityViolationException e) {
e.printStackTrace();
bindingResult.reject("signupFailed", "이미 등록된 사용자 입니다.");
return "signupForm";
}catch(Exception e) {
e.printStackTrace();
bindingResult.reject("signupFailed", e.getMessage());
return "signupForm";
}
return "redirect:/";
}
//로그인 화면 진입
@GetMapping("/user/login")
public String getLogin() {
return "loginForm";
}
//로그인 진행
// /user/login에 대한 postmapping은 스프링시큐리티가 대신처리함
// 코드 작성하지 않아도 된다.
}
회원 엔티티에서 유저id 와 이메일을 unique=true 로 설정했다.
즉. 테이블에 유저id와 이메일은 중복으로 입력될 수 없다.
만약 회원가입 화면에서 기존에 등록된 유저id나 이메일로 회원가입신청이 됐을 경우 화면에 알려주기 위해
catch(DataIntegrityViolationException e) 를 추가했다.
회원가입 템플릿 singupForm.html
<html layout:decorate="~{common/layout}">
<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="${userForm}" method="post">
<div th:replace="~{common/formErrors :: 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="password1" class="form-label">비밀번호</label>
<input type="password" th:field="*{password1}" class="form-control">
<!--type="password"이면 회원가입 오류시 값이 다시 세팅되지않음-->
</div>
<div class="mb-3">
<label for="password2" 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>
@GetMapping("/user/signup") 에서 매개변수로 UserForm 을 넘겨주고 있다.
userForm.html 파일이 실행이 될 때, form 태그의 th:object="${userForm}" 에 해당하는 userForm 객체가 필요하기 때문이다.
만약 겟매핑 컨트롤러의 매개변수에서 UserForm 을 넘겨주지 않으면, 화면 렌더링 시 오류가 발생한다.
회원가입 버튼 클릭 시,
@PostMapping("/user/signup") 이 실행이 되고,
@Valid 어노테이션이 붙은 UserForm 클래스의 검증을 한다.
이때 검증 대상은, th:object 선언이 되었던 form 태그에 입력된 값들이다.