[SpringBoot] 스프링 시큐리티+타임리프로 회원가입/로그인/로그아웃 구현하기 - (2)

최가희·2022년 7월 31일
0

SpringBoot

목록 보기
8/13
post-thumbnail

SiteUser 엔티티

엔티티명을 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;
}



User 리포지토리와 서비스

1. UserRepository.java

import com.example.sbb.domain.SiteUser;
import org.springframework.data.jpa.repository.JpaRepository;

public interface UserRepository extends JpaRepository<SiteUser, Long> {
}

2. UserService.java

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 메서드를 생성하는 것이다.

3. SecurityConfig.java

@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(오류코드, 오류메시지)는 특정 필드의 오류가 아닌 일반적인 오류를 등록할 때 사용한다.


참고

점프 투 스프링

0개의 댓글