점프투스프링부트(스프링부트+JPA)

3-06 요약 (회원 가입 코드 리뷰, 비밀번호 암호화 빈 등록하기, 회원가입 시 오류 처리)

여행자0 2024. 12. 17. 17:31

회원 엔티티

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 태그에 입력된 값들이다.