[스프링 부트 + 스프링 시큐리티 + JPA] 회원가입

jyleever·2022년 4월 19일
1

Spring Security 와 OAuth 2.0 를 이용하여 회원가입을 할 수 있다.

  1. 사용자가 뷰 화면에서 회원 정보를 입력하고 서버에 회원 정보를 전송한다.
  2. 서버는 회원 정보를 받고 아래와 같은 작업을 수행한다.
    • 중복된 아이디가 존재하는지, 가입 조건에 맞는지 등 유효성 검사를 실행한다.
    • 위 유효성 검사를 통과하지 못 하면 해당 사항에 대한 에러 메시지를 화면에 전달한다.
    • 유효성 검사를 통과하면 비밀번호를 암호화하여 데이터베이스에 회원 정보를 저장한다.
  3. 회원 가입이 성공하면 전달 받은 데이터를 데이터베이스에 추가 한다.

build.gradle

implementation 'org.springframework.boot:spring-boot-starter-security'

SecurityConfig

  • 로그인하지 않은 유저도 접근할 수 있도록, /auth/** 경로에 대해 권한 없이 접근 가능하도록 설정한다. 회원가입 페이지는 '/auth/join' url와 매핑되어있다.
	@Override
	protected void configure(HttpSecurity http) throws Exception {
		http
			...
			.antMatchers("/", "/auth/**", "/post/search/**").permitAll()

UserController

회원 가입 폼을 반환

	/* 회원 가입 폼으로 이동 */
	@GetMapping("/auth/join")
	public String join() {
		return "/user/user-join";
	}

회원 가입 정보 검증 및 회원 가입 실패/완료 처리

	@PostMapping("/auth/joinProc")
	public String joinProc(@Valid UserDto.RequestUserDto userDto, BindingResult bindingResult, Model model) {
		
		/* 검증 */
		if(bindingResult.hasErrors()) {
			/* 회원가입 실패 시 입력 데이터 값 유지 */
			model.addAttribute("userDto", userDto);
			
			/* 유효성 검사를 통과하지 못 한 필드와 메시지 핸들링 */
			Map<String, String> errorMap = new HashMap<>();
			
			for(FieldError error : bindingResult.getFieldErrors()) {
				errorMap.put("valid_"+error.getField(), error.getDefaultMessage());
				log.info("회원가입 실패 ! error message : "+error.getDefaultMessage());
			}
			
			/* Model에 담아 view resolve */
			for(String key : errorMap.keySet()) {
				model.addAttribute(key, errorMap.get(key));
			}

			/* 회원가입 페이지로 리턴 */
			return "/user/user-join";
		}
		
		// 회원가입 성공 시
		userService.userJoin(userDto);
		log.info("회원가입 성공");
		return "redirect:/auth/login";
	}
  • 회원 가입이 실패하면 어떤 오류 때문에 실패했는지 메시지를 보여준다.
  • 회원 가입이 성공하면 UserService의 userJoin 메소드를 호출하여 회원가입 로직을 진행시킨다.

회원가입 유효성 검사에 관한 포스팅은 링크

UserService

회원가입

비밀번호를 암호화하여 dto에 저장하고 해당 dto를 엔티티로 저장하여 DB에 저장하는 등 회원가입 로직을 진행한다.

	private final BCryptPasswordEncoder encoder;
		
	/* 회원가입 */
	@Override
	public Long userJoin(RequestUserDto dto) {
		/* 비밀번호 암호화 */
		dto.encryptPassword(encoder.encode(dto.getPassword()));
		
		User user = dto.toEntity();
		userRepository.save(user);
		log.info("DB에 회원 저장 성공");
		
		return user.getId();
	}

BCryptPasswordEncoder

Spring Security에서 제공하는 비밀번호 암호화 객체
비밀번호를 암호화해서 사용할 수 있도록 Bean으로 등록한다.

SecurityConfig에서 @Bean으로 등록한다.

public class SecurityConfig extends WebSecurityConfigurerAdapter{

	// 해당 메서드의 리턴되는 오브젝트는 IoC로 등록해준다
	@Bean
	public BCryptPasswordEncoder encodePwd() {
		return new BCryptPasswordEncoder();
	}

서비스 로직에서 encoder 메소드를 이용해 dto를 통해 입력 받은 비밀번호를 해쉬 암호화 한 후, 다시 그 dto 객체에 저장하여 레포지토리에 저장하도록 한다.

user-join.html

{{>layout/header}}
<div id="post_list">
    <div class="container col-md-4">
        <form action="/auth/joinProc" method="post">
        	<input type="hidden" name="_csrf" value="{{_csrf.token}}"/>
            <div class="form-group">
                <label>아이디</label>
                <input type="text" name="username" value="{{#userDto}}{{userDto.username}}{{/userDto}}" class="form-control" placeholder="아이디를 입력해주세요"/>
                {{#valid_username}} <span id="valid">{{valid_username}}</span> {{/valid_username}}
            </div>

            <div class="form-group">
                <label>비밀번호</label>
                <input type="password" name="password" value="{{#userDto}}{{userDto.password}}{{/userDto}}" class="form-control" placeholder="비밀번호를 입력해주세요"/>
                {{#valid_password}} <span id="valid">{{valid_password}}</span> {{/valid_password}}
            </div>

            <div class="form-group">
                <label>닉네임</label>
                <input type="text" name="nickname" value="{{#userDto}}{{userDto.nickname}}{{/userDto}}" class="form-control" placeholder="닉네임을 입력해주세요"/>
                {{#valid_nickname}} <span id="valid">{{valid_nickname}}</span> {{/valid_nickname}}
                
            </div>

            <div class="form-group">
                <label>이메일</label>
                <input type="email" name="email" value="{{#userDto}}{{userDto.email}}{{/userDto}}" class="form-control" placeholder="이메일을 입력해주세요"/>
                {{#valid_email}} <span id="valid">{{valid_email}}</span> {{/valid_email}}
            </div>

            <button class="btn btn-primary bi bi-person"> 가입</button>
            <a href="/" role="button" class="btn btn-info bi bi-arrow-return-left"> 목록</a>
        </form>
    </div>
</div>
{{>layout/footer}}
  • form action="/auth/joinProc" 을 통해 회원 가입 버튼을 누르면 /auth/joinProc 페이지에 post 방식으로 이동.
  • 유효성 검사에서 실패한 필드 목록 Map에서 에러 메시지가 [valid_에러 필드] 변수로 저장되어있다. 따라서 유효성 검사에서 실패한 경우 해당 변수를 화면에 보여준다.

User , UserDto, UserRepository

User

public class User extends BaseTimeEntity{
	@Id
	@GeneratedValue(strategy = GenerationType.IDENTITY)
	private Long id;
	
	/* 로그인할 회원 아이디 */
	@Column(nullable = false, length = 30, unique = true)
	private String username;
	
	@Column(length = 100)
	private String password;
	
	@Column(nullable = false, length = 50, unique = true)
	private String nickname;
	
	@Column(nullable = false, length = 50, unique = true)
	private String email;
	
	@Enumerated(EnumType.STRING)
	@Column(nullable = false)
	private Role role;
    ...

UserDto - RequestDto

public class UserDto {
	
	/* 회원 서비스 요청 RequestDTO 클래스 */
	@AllArgsConstructor
	@NoArgsConstructor
	@Getter
	@Builder
	@Setter
	public static class RequestUserDto{
		private Long id;
		
		@NotBlank(message = "아이디는 필수 입력값입니다.")
		@Pattern(regexp = "^[a-z0-9]{4,20}$", message = "아이디는 영어 소문자와 숫자만 사용하여 4~20자리여야 합니다.")
		private String username;
		
		@Pattern(regexp = "^(?=.*[A-Za-z])(?=.*\\d)(?=.*[$@$!%*#?&])[A-Za-z\\d$@$!%*#?&]{8,16}$", message = "비밀번호는 8~16자리수여야 합니다. 영문 대소문자, 숫자, 특수문자를 1개 이상 포함해야 합니다.")
		private String password;
		
		@NotBlank(message = "닉네임은 필수 입력값입니다.")
		@Pattern(regexp = "^[가-힣a-zA-Z0-9]{2,10}$" , message = "닉네임은 특수문자를 포함하지 않은 2~10자리여야 합니다.")
		private String nickname;
		
		@NotBlank(message = "이메일은 필수 입력값입니다.")
		@Email(message = "이메일 형식이 올바르지 않습니다.")
		private String email;
		
		private Role role;
	
		/* 암호화된 password */
		public void encryptPassword(String BCryptpassword) {
			this.password = BCryptpassword;
		}
		
		/* DTO -> Entity */
		public User toEntity() {
			User user = User.builder()
						.id(id)
						.username(username)
						.password(password)
						.nickname(nickname)
						.email(email)
						.role(role.USER)
						.build();
			return user;
		}
	}
	
}
  • 암호화 처리 한 비밀번호를 저장하기 위해 encryptPassword 메소드 추가

UserRepository

public interface UserRepository extends JpaRepository<User, Integer> {

}
  • JpaRepository가 기본적인 CURD 기능을 제공하므로 JpaRepository를 extends한 UserRepository 인터페이스를 선언하여 회원가입 과정을 통해 받은 User 정보를 DB에 저장할 수 있도록 한다.

0개의 댓글